Skip to content

Commit e7eb27c

Browse files
committed
GH-1491: index query methods and show them as child nodes for repository document symbols, incl. query strings when defined
1 parent dc29abe commit e7eb27c

File tree

8 files changed

+322
-15
lines changed

8 files changed

+322
-15
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* @author Kris De Volder
2020
*/
2121
public class Annotations {
22+
2223
public static final String BEAN = "org.springframework.context.annotation.Bean";
2324
public static final String PROFILE = "org.springframework.context.annotation.Profile";
2425
public static final String CONDITIONAL = "org.springframework.context.annotation.Conditional";
@@ -40,7 +41,9 @@ public class Annotations {
4041
public static final String JPA_JAVAX_ID_CLASS = "javax.persistence.IdClass";
4142
public static final String JPA_JAKARTA_NAMED_QUERY = "jakarta.persistence.NamedQuery";
4243
public static final String JPA_JAVAX_NAMED_QUERY = "javax.persistence.NamedQuery";
43-
public static final String DATA_QUERY = "org.springframework.data.jpa.repository.Query";
44+
45+
public static final String DATA_QUERY_META_ANNOTATION = "org.springframework.data.annotation.QueryAnnotation";
46+
public static final String DATA_JPA_QUERY = "org.springframework.data.jpa.repository.Query";
4447

4548
public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
4649
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/DataRepositorySymbolProvider.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,25 @@
1616

1717
import org.eclipse.jdt.core.dom.Annotation;
1818
import org.eclipse.jdt.core.dom.ITypeBinding;
19+
import org.eclipse.jdt.core.dom.MethodDeclaration;
20+
import org.eclipse.jdt.core.dom.Modifier;
21+
import org.eclipse.jdt.core.dom.NormalAnnotation;
22+
import org.eclipse.jdt.core.dom.SimpleName;
23+
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
1924
import org.eclipse.jdt.core.dom.TypeDeclaration;
2025
import org.eclipse.lsp4j.Location;
26+
import org.eclipse.lsp4j.Range;
2127
import org.eclipse.lsp4j.SymbolKind;
2228
import org.eclipse.lsp4j.WorkspaceSymbol;
2329
import org.eclipse.lsp4j.jsonrpc.messages.Either;
2430
import org.slf4j.Logger;
2531
import org.slf4j.LoggerFactory;
32+
import org.springframework.ide.vscode.boot.java.Annotations;
33+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
2634
import org.springframework.ide.vscode.boot.java.beans.BeanUtils;
2735
import org.springframework.ide.vscode.boot.java.beans.CachedBean;
36+
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils;
37+
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression;
2838
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
2939
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
3040
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
@@ -62,6 +72,9 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
6272
SymbolKind.Interface,
6373
Either.forLeft(location));
6474

75+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
76+
77+
// index elements
6578
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc);
6679

6780
ITypeBinding concreteBeanTypeBindung = typeDeclaration.resolveBinding();
@@ -75,8 +88,8 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
7588
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);
7689

7790
Bean beanDefinition = new Bean(beanName, concreteRepoType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
91+
indexQueryMethods(beanDefinition, typeDeclaration, context, doc);
7892

79-
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
8093
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
8194

8295
} catch (BadLocationException e) {
@@ -85,6 +98,59 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
8598
}
8699
}
87100

101+
private void indexQueryMethods(Bean beanDefinition, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
102+
MethodDeclaration[] methods = typeDeclaration.getMethods();
103+
if (methods == null) return;
104+
105+
for (MethodDeclaration method : methods) {
106+
int modifiers = method.getModifiers();
107+
SimpleName nameNode = method.getName();
108+
109+
if (nameNode != null && (modifiers & Modifier.DEFAULT) == 0) {
110+
String methodName = nameNode.getFullyQualifiedName();
111+
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, method);
112+
113+
try {
114+
Range range = doc.toRange(nodeRegion);
115+
116+
if (methodName != null) {
117+
String queryString = identifyQueryString(method);
118+
beanDefinition.addChild(new QueryMethodIndexElement(methodName, queryString, range));
119+
}
120+
121+
} catch (BadLocationException e) {
122+
log.error("query method range computation failed", e);
123+
}
124+
}
125+
}
126+
}
127+
128+
private String identifyQueryString(MethodDeclaration method) {
129+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(method);
130+
131+
EmbeddedQueryExpression queryExpression = null;
132+
133+
Collection<Annotation> annotations = ASTUtils.getAnnotations(method);
134+
for (Annotation annotation : annotations) {
135+
ITypeBinding typeBinding = annotation.resolveTypeBinding();
136+
137+
if (typeBinding != null && annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.DATA_QUERY_META_ANNOTATION)) {
138+
if (annotation instanceof SingleMemberAnnotation) {
139+
queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (SingleMemberAnnotation)annotation);
140+
}
141+
else if (annotation instanceof NormalAnnotation) {
142+
queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (NormalAnnotation)annotation);
143+
}
144+
}
145+
}
146+
147+
if (queryExpression != null) {
148+
return queryExpression.query().getText();
149+
}
150+
151+
return null;
152+
}
153+
88154
protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) {
89155
StringBuilder symbolLabel = new StringBuilder();
90156
symbolLabel.append("@+");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.data;
12+
13+
import java.util.List;
14+
15+
import org.eclipse.lsp4j.DocumentSymbol;
16+
import org.eclipse.lsp4j.Range;
17+
import org.eclipse.lsp4j.SymbolKind;
18+
import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement;
19+
import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement;
20+
21+
public class QueryMethodIndexElement extends AbstractSpringIndexElement implements SymbolElement {
22+
23+
private final String methodName;
24+
private final String queryString;
25+
private final Range range;
26+
27+
public QueryMethodIndexElement(String methodName, String queryString, Range range) {
28+
this.methodName = methodName;
29+
this.queryString = queryString;
30+
this.range = range;
31+
}
32+
33+
public String getMethodName() {
34+
return methodName;
35+
}
36+
37+
public String getQueryString() {
38+
return queryString;
39+
}
40+
41+
@Override
42+
public DocumentSymbol getDocumentSymbol() {
43+
DocumentSymbol symbol = new DocumentSymbol();
44+
45+
symbol.setName(methodName);
46+
symbol.setKind(SymbolKind.Method);
47+
symbol.setRange(range);
48+
symbol.setSelectionRange(range);
49+
50+
if (queryString != null) {
51+
DocumentSymbol querySymbol = new DocumentSymbol();
52+
querySymbol.setName(queryString);
53+
querySymbol.setKind(SymbolKind.Constant);
54+
querySymbol.setRange(range);
55+
querySymbol.setSelectionRange(range);
56+
57+
symbol.setChildren(List.of(querySymbol));
58+
}
59+
60+
return symbol;
61+
}
62+
63+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ public static EmbeddedQueryExpression extractQueryExpression(MethodInvocation m)
102102
}
103103

104104
static boolean isQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) {
105-
if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
106-
return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_QUERY);
105+
if (Annotations.DATA_JPA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
106+
return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_JPA_QUERY);
107107
}
108108
return false;
109109
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.data.test;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
15+
import java.io.File;
16+
import java.util.List;
17+
import java.util.concurrent.CompletableFuture;
18+
import java.util.concurrent.TimeUnit;
19+
20+
import org.apache.commons.lang3.ArrayUtils;
21+
import org.eclipse.lsp4j.TextDocumentIdentifier;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
28+
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
29+
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
30+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
31+
import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement;
32+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
33+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
34+
import org.springframework.ide.vscode.commons.protocol.spring.DocumentElement;
35+
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
36+
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
37+
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
38+
import org.springframework.test.context.junit.jupiter.SpringExtension;
39+
40+
/**
41+
* @author Martin Lippert
42+
*/
43+
@ExtendWith(SpringExtension.class)
44+
@BootLanguageServerTest
45+
@Import(SymbolProviderTestConf.class)
46+
public class DataRepositoryIndexElementsTest {
47+
48+
@Autowired private BootLanguageServerHarness harness;
49+
@Autowired private JavaProjectFinder projectFinder;
50+
@Autowired private SpringSymbolIndex indexer;
51+
@Autowired private SpringMetamodelIndex springIndex;
52+
53+
private File directory;
54+
55+
@BeforeEach
56+
public void setup() throws Exception {
57+
harness.intialize(null);
58+
59+
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-data-symbols/").toURI());
60+
String projectDir = directory.toURI().toString();
61+
62+
// trigger project creation
63+
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
64+
65+
CompletableFuture<Void> initProject = indexer.waitOperation();
66+
initProject.get(5, TimeUnit.SECONDS);
67+
}
68+
69+
@Test
70+
void testSimpleRepositoryElements() throws Exception {
71+
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
72+
73+
DocumentElement document = springIndex.getDocument(docUri);
74+
List<SpringIndexElement> children = document.getChildren();
75+
Bean repositoryElement = (Bean) children.get(0);
76+
assertEquals("customerRepository", repositoryElement.getName());
77+
assertEquals(1, children.size());
78+
79+
Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository");
80+
assertEquals(1, repoBean.length);
81+
assertEquals("customerRepository", repoBean[0].getName());
82+
assertEquals("org.test.CustomerRepository", repoBean[0].getType());
83+
84+
Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository");
85+
assertEquals(3, matchingBeans.length);
86+
ArrayUtils.contains(matchingBeans, repoBean[0]);
87+
}
88+
89+
@Test
90+
void testSimpleQueryMethodElements() throws Exception {
91+
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
92+
93+
DocumentElement document = springIndex.getDocument(docUri);
94+
List<SpringIndexElement> children = document.getChildren();
95+
Bean repositoryElement = (Bean) children.get(0);
96+
97+
List<SpringIndexElement> queryMethods = repositoryElement.getChildren();
98+
assertEquals(1, queryMethods.size());
99+
100+
QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0);
101+
assertEquals("findByLastName", queryMethod.getMethodName());
102+
}
103+
104+
@Test
105+
void testQueryMethodElementWithQueryString() throws Exception {
106+
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString();
107+
108+
DocumentElement document = springIndex.getDocument(docUri);
109+
List<SpringIndexElement> children = document.getChildren();
110+
Bean repositoryElement = (Bean) children.get(0);
111+
112+
List<SpringIndexElement> queryMethods = repositoryElement.getChildren();
113+
assertEquals(1, queryMethods.size());
114+
115+
QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0);
116+
assertEquals("findPetTypes", queryMethod.getMethodName());
117+
assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryMethod.getQueryString());
118+
119+
}
120+
121+
}

0 commit comments

Comments
 (0)