Skip to content

Commit 20b3bfa

Browse files
committed
Support @NativeQuery for syntax highlighting and validation
1 parent d89fd14 commit 20b3bfa

File tree

5 files changed

+171
-6
lines changed

5 files changed

+171
-6
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public class Annotations {
4848

4949
public static final String DATA_QUERY_META_ANNOTATION = "org.springframework.data.annotation.QueryAnnotation";
5050
public static final String DATA_JPA_QUERY = "org.springframework.data.jpa.repository.Query";
51+
public static final String DATA_JPA_NATIVE_QUERY = "org.springframework.data.jpa.repository.NativeQuery";
5152

5253
public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
5354
public static final String QUALIFIER = "org.springframework.beans.factory.annotation.Qualifier";

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024 Broadcom, Inc.
2+
* Copyright (c) 2024, 2025 Broadcom, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -27,11 +27,15 @@ public class JdtQueryVisitorUtils {
2727

2828
private static final String QUERY = "Query";
2929
private static final String NAMED_QUERY = "NamedQuery";
30+
private static final String NATIVE_QUERY = "NativeQuery";
3031

3132
public record EmbeddedQueryExpression(EmbeddedLanguageSnippet query, boolean isNative) {};
3233

3334
public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchies annotationHierarchies, SingleMemberAnnotation a) {
34-
if (isQueryAnnotation(annotationHierarchies, a)) {
35+
if (isNativeQueryAnnotation(annotationHierarchies, a)) {
36+
EmbeddedLanguageSnippet expression = EmbeddedLangAstUtils.extractEmbeddedExpression(a.getValue());
37+
return expression == null ? null : new EmbeddedQueryExpression(expression, true);
38+
} else if (isQueryAnnotation(annotationHierarchies, a)) {
3539
EmbeddedLanguageSnippet expression = EmbeddedLangAstUtils.extractEmbeddedExpression(a.getValue());
3640
return expression == null ? null : new EmbeddedQueryExpression(expression, false);
3741
}
@@ -41,7 +45,22 @@ public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchi
4145
public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchies annotationHierarchies, NormalAnnotation a) {
4246
Expression queryExpression = null;
4347
boolean isNative = false;
44-
if (isQueryAnnotation(annotationHierarchies, a)) {
48+
if (isNativeQueryAnnotation(annotationHierarchies, a)) {
49+
for (Object value : a.values()) {
50+
if (value instanceof MemberValuePair) {
51+
MemberValuePair pair = (MemberValuePair) value;
52+
String name = pair.getName().getFullyQualifiedName();
53+
if (name != null) {
54+
switch (name) {
55+
case "value":
56+
queryExpression = pair.getValue();
57+
isNative = true;
58+
break;
59+
}
60+
}
61+
}
62+
}
63+
} else if (isQueryAnnotation(annotationHierarchies, a)) {
4564
for (Object value : a.values()) {
4665
if (value instanceof MemberValuePair) {
4766
MemberValuePair pair = (MemberValuePair) value;
@@ -119,5 +138,15 @@ static boolean isNamedQueryAnnotation(AnnotationHierarchies annotationHierarchie
119138
}
120139
return false;
121140
}
141+
142+
static boolean isNativeQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) {
143+
if (NATIVE_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.DATA_JPA_NATIVE_QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
144+
IAnnotationBinding type = a.resolveAnnotationBinding();
145+
if (type != null) {
146+
return annotationHierarchies.isAnnotatedWith(type, Annotations.DATA_JPA_NATIVE_QUERY);
147+
}
148+
}
149+
return false;
150+
}
122151

123152
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ public boolean visit(NormalAnnotation node) {
7272
public boolean visit(SingleMemberAnnotation node) {
7373
EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node);
7474
if (q != null) {
75-
getQueryReconciler(project).reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector());
75+
Optional<Reconciler> reconcilerOpt = q.isNative() ? getSqlReconciler(project) : Optional.of(getQueryReconciler(project));
76+
reconcilerOpt.ifPresent(r -> r.reconcile(q.query().getText(), q.query()::toSingleJavaRange, context.getProblemCollector()));
7677
}
7778
return super.visit(node);
7879
}

headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ default void findByLastName(EntityManager manager) {
292292
}
293293

294294
@Test
295-
void nativeQuery() throws Exception {
295+
void nativeQueryAttribute() throws Exception {
296296
String source = """
297297
package my.package
298298
@@ -357,7 +357,141 @@ public interface OwnerRepository {
357357
assertThat(token).isEqualTo(new SemanticTokenData(167, 168, "number", new String[0]));
358358

359359
}
360+
361+
@Test
362+
void nativeQuery_1() throws Exception {
363+
String source = """
364+
package my.package
365+
366+
import org.springframework.data.jpa.repository.NativeQuery;
367+
368+
public interface OwnerRepository {
369+
370+
@NativeQuery(value = "SELECT * FROM USERS u WHERE u.status = 1")
371+
void findByLastName();
372+
}
373+
""";
374+
375+
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
376+
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
377+
378+
assertThat(cu).isNotNull();
379+
380+
List<SemanticTokenData> tokens = computeTokens(cu);
381+
382+
SemanticTokenData token = tokens.get(0);
383+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("SELECT");
384+
assertThat(token).isEqualTo(new SemanticTokenData(140, 146, "keyword", new String[0]));
385+
386+
token = tokens.get(1);
387+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("*");
388+
assertThat(token).isEqualTo(new SemanticTokenData(147, 148, "operator", new String[0]));
389+
390+
token = tokens.get(2);
391+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("FROM");
392+
assertThat(token).isEqualTo(new SemanticTokenData(149, 153, "keyword", new String[0]));
393+
394+
token = tokens.get(3);
395+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("USERS");
396+
assertThat(token).isEqualTo(new SemanticTokenData(154, 159, "variable", new String[0]));
397+
398+
token = tokens.get(4);
399+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u");
400+
assertThat(token).isEqualTo(new SemanticTokenData(160, 161, "variable", new String[0]));
401+
402+
token = tokens.get(5);
403+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("WHERE");
404+
assertThat(token).isEqualTo(new SemanticTokenData(162, 167, "keyword", new String[0]));
405+
406+
token = tokens.get(6);
407+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u");
408+
assertThat(token).isEqualTo(new SemanticTokenData(168, 169, "variable", new String[0]));
409+
410+
token = tokens.get(7);
411+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo(".");
412+
assertThat(token).isEqualTo(new SemanticTokenData(169, 170, "operator", new String[0]));
413+
414+
token = tokens.get(8);
415+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("status");
416+
assertThat(token).isEqualTo(new SemanticTokenData(170, 176, "property", new String[0]));
417+
418+
token = tokens.get(9);
419+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("=");
420+
assertThat(token).isEqualTo(new SemanticTokenData(177, 178, "operator", new String[0]));
360421

422+
token = tokens.get(10);
423+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("1");
424+
assertThat(token).isEqualTo(new SemanticTokenData(179, 180, "number", new String[0]));
425+
426+
}
427+
428+
@Test
429+
void nativeQuery_2() throws Exception {
430+
String source = """
431+
package my.package
432+
433+
import org.springframework.data.jpa.repository.NativeQuery;
434+
435+
public interface OwnerRepository {
436+
437+
@NativeQuery("SELECT * FROM USERS u WHERE u.status = 1")
438+
void findByLastName();
439+
}
440+
""";
441+
442+
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
443+
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
444+
445+
assertThat(cu).isNotNull();
446+
447+
List<SemanticTokenData> tokens = computeTokens(cu);
448+
449+
SemanticTokenData token = tokens.get(0);
450+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("SELECT");
451+
assertThat(token).isEqualTo(new SemanticTokenData(132, 138, "keyword", new String[0]));
452+
453+
token = tokens.get(1);
454+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("*");
455+
assertThat(token).isEqualTo(new SemanticTokenData(140, 141, "operator", new String[0]));
456+
457+
token = tokens.get(2);
458+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("FROM");
459+
assertThat(token).isEqualTo(new SemanticTokenData(142, 146, "keyword", new String[0]));
460+
461+
token = tokens.get(3);
462+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("USERS");
463+
assertThat(token).isEqualTo(new SemanticTokenData(147, 152, "variable", new String[0]));
464+
465+
token = tokens.get(4);
466+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u");
467+
assertThat(token).isEqualTo(new SemanticTokenData(153, 154, "variable", new String[0]));
468+
469+
token = tokens.get(5);
470+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("WHERE");
471+
assertThat(token).isEqualTo(new SemanticTokenData(155, 160, "keyword", new String[0]));
472+
473+
token = tokens.get(6);
474+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("u");
475+
assertThat(token).isEqualTo(new SemanticTokenData(161, 162, "variable", new String[0]));
476+
477+
token = tokens.get(7);
478+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo(".");
479+
assertThat(token).isEqualTo(new SemanticTokenData(162, 163, "operator", new String[0]));
480+
481+
token = tokens.get(8);
482+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("status");
483+
assertThat(token).isEqualTo(new SemanticTokenData(163, 169, "property", new String[0]));
484+
485+
token = tokens.get(9);
486+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("=");
487+
assertThat(token).isEqualTo(new SemanticTokenData(170, 171, "operator", new String[0]));
488+
489+
token = tokens.get(10);
490+
assertThat(source.substring(token.getStart(), token.getEnd())).isEqualTo("1");
491+
assertThat(token).isEqualTo(new SemanticTokenData(172, 173, "number", new String[0]));
492+
493+
}
494+
361495
@Test
362496
void nativeQueryWithSpel() throws Exception {
363497
String source = """

headless-services/spring-boot-language-server/src/test/resources/test-projects/boot-mysql/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>3.2.2</version>
8+
<version>3.4.4</version>
99
<relativePath />
1010
</parent>
1111

0 commit comments

Comments
 (0)