diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index c78a049cd..ca07edcb0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -48,6 +48,7 @@ public class Annotations { public static final String DATA_QUERY_META_ANNOTATION = "org.springframework.data.annotation.QueryAnnotation"; public static final String DATA_JPA_QUERY = "org.springframework.data.jpa.repository.Query"; + public static final String DATA_JPA_NATIVE_QUERY = "org.springframework.data.jpa.repository.NativeQuery"; public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"; public static final String QUALIFIER = "org.springframework.beans.factory.annotation.Qualifier"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java index 1c4525e4e..18392715d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Broadcom, Inc. + * Copyright (c) 2024, 2025 Broadcom, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -27,11 +27,15 @@ public class JdtQueryVisitorUtils { private static final String QUERY = "Query"; private static final String NAMED_QUERY = "NamedQuery"; + private static final String NATIVE_QUERY = "NativeQuery"; public record EmbeddedQueryExpression(EmbeddedLanguageSnippet query, boolean isNative) {}; public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchies annotationHierarchies, SingleMemberAnnotation a) { - if (isQueryAnnotation(annotationHierarchies, a)) { + if (isNativeQueryAnnotation(annotationHierarchies, a)) { + EmbeddedLanguageSnippet expression = EmbeddedLangAstUtils.extractEmbeddedExpression(a.getValue()); + return expression == null ? null : new EmbeddedQueryExpression(expression, true); + } else if (isQueryAnnotation(annotationHierarchies, a)) { EmbeddedLanguageSnippet expression = EmbeddedLangAstUtils.extractEmbeddedExpression(a.getValue()); return expression == null ? null : new EmbeddedQueryExpression(expression, false); } @@ -41,7 +45,22 @@ public class JdtQueryVisitorUtils { public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchies annotationHierarchies, NormalAnnotation a) { Expression queryExpression = null; boolean isNative = false; - if (isQueryAnnotation(annotationHierarchies, a)) { + if (isNativeQueryAnnotation(annotationHierarchies, a)) { + for (Object value : a.values()) { + if (value instanceof MemberValuePair) { + MemberValuePair pair = (MemberValuePair) value; + String name = pair.getName().getFullyQualifiedName(); + if (name != null) { + switch (name) { + case "value": + queryExpression = pair.getValue(); + isNative = true; + break; + } + } + } + } + } else if (isQueryAnnotation(annotationHierarchies, a)) { for (Object value : a.values()) { if (value instanceof MemberValuePair) { MemberValuePair pair = (MemberValuePair) value; @@ -119,5 +138,15 @@ public class JdtQueryVisitorUtils { } return false; } + + static boolean isNativeQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) { + if (NATIVE_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.DATA_JPA_NATIVE_QUERY.equals(a.getTypeName().getFullyQualifiedName())) { + IAnnotationBinding type = a.resolveAnnotationBinding(); + if (type != null) { + return annotationHierarchies.isAnnotatedWith(type, Annotations.DATA_JPA_NATIVE_QUERY); + } + } + return false; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java index f9f2293cb..ff8ddbcaf 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java @@ -72,7 +72,8 @@ public class QueryJdtAstReconciler implements JdtAstReconciler { public boolean visit(SingleMemberAnnotation node) { EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node); if (q != null) { - getQueryReconciler(project).reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector()); + Optional reconcilerOpt = q.isNative() ? getSqlReconciler(project) : Optional.of(getQueryReconciler(project)); + reconcilerOpt.ifPresent(r -> r.reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector())); } return super.visit(node); } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java index 25069ed9f..6551733f8 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java @@ -292,7 +292,7 @@ public class JdtDataQuerySemanticTokensProviderTest { } @Test - void nativeQuery() throws Exception { + void nativeQueryAttribute() throws Exception { String source = """ package my.package @@ -357,7 +357,141 @@ public class JdtDataQuerySemanticTokensProviderTest { assertThat(token).isEqualTo(new SemanticTokenData(167, 168, "number", new String[0])); } + + @Test + void nativeQuery_1() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.NativeQuery; + + public interface OwnerRepository { + + @NativeQuery(value = "SELECT * FROM USERS u WHERE u.status = 1") + void findByLastName(); + } + """; + + String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString(); + CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp); + + assertThat(cu).isNotNull(); + + List tokens = computeTokens(cu); + + SemanticTokenData token = tokens.get(0); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("SELECT"); + assertThat(token).isEqualTo(new SemanticTokenData(140, 146, "keyword", new String[0])); + + token = tokens.get(1); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("*"); + assertThat(token).isEqualTo(new SemanticTokenData(147, 148, "operator", new String[0])); + + token = tokens.get(2); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("FROM"); + assertThat(token).isEqualTo(new SemanticTokenData(149, 153, "keyword", new String[0])); + + token = tokens.get(3); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("USERS"); + assertThat(token).isEqualTo(new SemanticTokenData(154, 159, "variable", new String[0])); + + token = tokens.get(4); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u"); + assertThat(token).isEqualTo(new SemanticTokenData(160, 161, "variable", new String[0])); + + token = tokens.get(5); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("WHERE"); + assertThat(token).isEqualTo(new SemanticTokenData(162, 167, "keyword", new String[0])); + + token = tokens.get(6); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u"); + assertThat(token).isEqualTo(new SemanticTokenData(168, 169, "variable", new String[0])); + + token = tokens.get(7); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("."); + assertThat(token).isEqualTo(new SemanticTokenData(169, 170, "operator", new String[0])); + + token = tokens.get(8); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("status"); + assertThat(token).isEqualTo(new SemanticTokenData(170, 176, "property", new String[0])); + token = tokens.get(9); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("="); + assertThat(token).isEqualTo(new SemanticTokenData(177, 178, "operator", new String[0])); + + token = tokens.get(10); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("1"); + assertThat(token).isEqualTo(new SemanticTokenData(179, 180, "number", new String[0])); + + } + + @Test + void nativeQuery_2() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.NativeQuery; + + public interface OwnerRepository { + + @NativeQuery("SELECT * FROM USERS u WHERE u.status = 1") + void findByLastName(); + } + """; + + String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString(); + CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp); + + assertThat(cu).isNotNull(); + + List tokens = computeTokens(cu); + + SemanticTokenData token = tokens.get(0); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("SELECT"); + assertThat(token).isEqualTo(new SemanticTokenData(132, 138, "keyword", new String[0])); + + token = tokens.get(1); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("*"); + assertThat(token).isEqualTo(new SemanticTokenData(140, 141, "operator", new String[0])); + + token = tokens.get(2); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("FROM"); + assertThat(token).isEqualTo(new SemanticTokenData(142, 146, "keyword", new String[0])); + + token = tokens.get(3); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("USERS"); + assertThat(token).isEqualTo(new SemanticTokenData(147, 152, "variable", new String[0])); + + token = tokens.get(4); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u"); + assertThat(token).isEqualTo(new SemanticTokenData(153, 154, "variable", new String[0])); + + token = tokens.get(5); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("WHERE"); + assertThat(token).isEqualTo(new SemanticTokenData(155, 160, "keyword", new String[0])); + + token = tokens.get(6); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u"); + assertThat(token).isEqualTo(new SemanticTokenData(161, 162, "variable", new String[0])); + + token = tokens.get(7); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("."); + assertThat(token).isEqualTo(new SemanticTokenData(162, 163, "operator", new String[0])); + + token = tokens.get(8); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("status"); + assertThat(token).isEqualTo(new SemanticTokenData(163, 169, "property", new String[0])); + + token = tokens.get(9); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("="); + assertThat(token).isEqualTo(new SemanticTokenData(170, 171, "operator", new String[0])); + + token = tokens.get(10); + assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("1"); + assertThat(token).isEqualTo(new SemanticTokenData(172, 173, "number", new String[0])); + + } + @Test void nativeQueryWithSpel() throws Exception { String source = """ diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/boot-mysql/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/boot-mysql/pom.xml index 5f57f051c..d76592e5b 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/boot-mysql/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/boot-mysql/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.2 + 3.4.4