GH-1491: index query methods and show them as child nodes for repository document symbols, incl. query strings when defined

This commit is contained in:
Martin Lippert
2025-03-04 08:21:09 +01:00
parent dc29abedba
commit e7eb27c433
8 changed files with 322 additions and 15 deletions

View File

@@ -19,6 +19,7 @@ import java.util.Set;
* @author Kris De Volder
*/
public class Annotations {
public static final String BEAN = "org.springframework.context.annotation.Bean";
public static final String PROFILE = "org.springframework.context.annotation.Profile";
public static final String CONDITIONAL = "org.springframework.context.annotation.Conditional";
@@ -40,7 +41,9 @@ public class Annotations {
public static final String JPA_JAVAX_ID_CLASS = "javax.persistence.IdClass";
public static final String JPA_JAKARTA_NAMED_QUERY = "jakarta.persistence.NamedQuery";
public static final String JPA_JAVAX_NAMED_QUERY = "javax.persistence.NamedQuery";
public static final String DATA_QUERY = "org.springframework.data.jpa.repository.Query";
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 AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
public static final String QUALIFIER = "org.springframework.beans.factory.annotation.Qualifier";

View File

@@ -16,15 +16,25 @@ import java.util.Set;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.beans.BeanUtils;
import org.springframework.ide.vscode.boot.java.beans.CachedBean;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression;
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
@@ -62,6 +72,9 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
SymbolKind.Interface,
Either.forLeft(location));
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
// index elements
InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc);
ITypeBinding concreteBeanTypeBindung = typeDeclaration.resolveBinding();
@@ -75,8 +88,8 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);
Bean beanDefinition = new Bean(beanName, concreteRepoType, location, injectionPoints, supertypes, annotations, false, symbol.getName());
indexQueryMethods(beanDefinition, typeDeclaration, context, doc);
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
} catch (BadLocationException e) {
@@ -85,6 +98,59 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
}
}
private void indexQueryMethods(Bean beanDefinition, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
MethodDeclaration[] methods = typeDeclaration.getMethods();
if (methods == null) return;
for (MethodDeclaration method : methods) {
int modifiers = method.getModifiers();
SimpleName nameNode = method.getName();
if (nameNode != null && (modifiers & Modifier.DEFAULT) == 0) {
String methodName = nameNode.getFullyQualifiedName();
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, method);
try {
Range range = doc.toRange(nodeRegion);
if (methodName != null) {
String queryString = identifyQueryString(method);
beanDefinition.addChild(new QueryMethodIndexElement(methodName, queryString, range));
}
} catch (BadLocationException e) {
log.error("query method range computation failed", e);
}
}
}
}
private String identifyQueryString(MethodDeclaration method) {
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(method);
EmbeddedQueryExpression queryExpression = null;
Collection<Annotation> annotations = ASTUtils.getAnnotations(method);
for (Annotation annotation : annotations) {
ITypeBinding typeBinding = annotation.resolveTypeBinding();
if (typeBinding != null && annotationHierarchies.isAnnotatedWith(typeBinding, Annotations.DATA_QUERY_META_ANNOTATION)) {
if (annotation instanceof SingleMemberAnnotation) {
queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (SingleMemberAnnotation)annotation);
}
else if (annotation instanceof NormalAnnotation) {
queryExpression = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, (NormalAnnotation)annotation);
}
}
}
if (queryExpression != null) {
return queryExpression.query().getText();
}
return null;
}
protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) {
StringBuilder symbolLabel = new StringBuilder();
symbolLabel.append("@+");

View File

@@ -0,0 +1,63 @@
/*******************************************************************************
* Copyright (c) 2025 Broadcom
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
import java.util.List;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SymbolKind;
import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement;
import org.springframework.ide.vscode.commons.protocol.spring.SymbolElement;
public class QueryMethodIndexElement extends AbstractSpringIndexElement implements SymbolElement {
private final String methodName;
private final String queryString;
private final Range range;
public QueryMethodIndexElement(String methodName, String queryString, Range range) {
this.methodName = methodName;
this.queryString = queryString;
this.range = range;
}
public String getMethodName() {
return methodName;
}
public String getQueryString() {
return queryString;
}
@Override
public DocumentSymbol getDocumentSymbol() {
DocumentSymbol symbol = new DocumentSymbol();
symbol.setName(methodName);
symbol.setKind(SymbolKind.Method);
symbol.setRange(range);
symbol.setSelectionRange(range);
if (queryString != null) {
DocumentSymbol querySymbol = new DocumentSymbol();
querySymbol.setName(queryString);
querySymbol.setKind(SymbolKind.Constant);
querySymbol.setRange(range);
querySymbol.setSelectionRange(range);
symbol.setChildren(List.of(querySymbol));
}
return symbol;
}
}

View File

@@ -102,8 +102,8 @@ public class JdtQueryVisitorUtils {
}
static boolean isQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) {
if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_QUERY);
if (Annotations.DATA_JPA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) {
return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_JPA_QUERY);
}
return false;
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* Copyright (c) 2025 Broadcom
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.data.QueryMethodIndexElement;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.DocumentElement;
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Martin Lippert
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class DataRepositoryIndexElementsTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringSymbolIndex indexer;
@Autowired private SpringMetamodelIndex springIndex;
private File directory;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-data-symbols/").toURI());
String projectDir = directory.toURI().toString();
// trigger project creation
projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
}
@Test
void testSimpleRepositoryElements() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
DocumentElement document = springIndex.getDocument(docUri);
List<SpringIndexElement> children = document.getChildren();
Bean repositoryElement = (Bean) children.get(0);
assertEquals("customerRepository", repositoryElement.getName());
assertEquals(1, children.size());
Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository");
assertEquals(1, repoBean.length);
assertEquals("customerRepository", repoBean[0].getName());
assertEquals("org.test.CustomerRepository", repoBean[0].getType());
Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository");
assertEquals(3, matchingBeans.length);
ArrayUtils.contains(matchingBeans, repoBean[0]);
}
@Test
void testSimpleQueryMethodElements() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
DocumentElement document = springIndex.getDocument(docUri);
List<SpringIndexElement> children = document.getChildren();
Bean repositoryElement = (Bean) children.get(0);
List<SpringIndexElement> queryMethods = repositoryElement.getChildren();
assertEquals(1, queryMethods.size());
QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0);
assertEquals("findByLastName", queryMethod.getMethodName());
}
@Test
void testQueryMethodElementWithQueryString() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString();
DocumentElement document = springIndex.getDocument(docUri);
List<SpringIndexElement> children = document.getChildren();
Bean repositoryElement = (Bean) children.get(0);
List<SpringIndexElement> queryMethods = repositoryElement.getChildren();
assertEquals(1, queryMethods.size());
QueryMethodIndexElement queryMethod = (QueryMethodIndexElement) queryMethods.get(0);
assertEquals("findPetTypes", queryMethod.getMethodName());
assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryMethod.getQueryString());
}
}

View File

@@ -19,7 +19,8 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.lsp4j.DocumentSymbol;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.junit.jupiter.api.BeforeEach;
@@ -30,9 +31,7 @@ import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -48,7 +47,6 @@ public class DataRepositorySymbolProviderTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringSymbolIndex indexer;
@Autowired private SpringMetamodelIndex springIndex;
private File directory;
@@ -72,15 +70,40 @@ public class DataRepositorySymbolProviderTest {
List<? extends WorkspaceSymbol> symbols = indexer.getSymbols(docUri);
assertEquals(1, symbols.size());
assertTrue(containsSymbol(symbols, "@+ 'customerRepository' (Customer) Repository<Customer,Long>", docUri, 6, 17, 6, 35));
}
Bean[] repoBean = this.springIndex.getBeansWithName("test-spring-data-symbols", "customerRepository");
assertEquals(1, repoBean.length);
assertEquals("customerRepository", repoBean[0].getName());
assertEquals("org.test.CustomerRepository", repoBean[0].getType());
@Test
void testDocumentSymbolsForRepository() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepository.java").toUri().toString();
List<? extends DocumentSymbol> symbols = indexer.getDocumentSymbolsFromMetamodelIndex(docUri);
assertEquals(1, symbols.size());
assertTrue(containsDocumentSymbol(symbols, "@+ 'customerRepository' (Customer) Repository<Customer,Long>", docUri, 6, 17, 6, 35));
Bean[] matchingBeans = springIndex.getMatchingBeans("test-spring-data-symbols", "org.springframework.data.repository.CrudRepository");
assertEquals(2, matchingBeans.length);
ArrayUtils.contains(matchingBeans, repoBean[0]);
DocumentSymbol documentSymbol = symbols.get(0);
List<DocumentSymbol> children = documentSymbol.getChildren();
DocumentSymbol childSymbol = children.get(0);
assertEquals("findByLastName", childSymbol.getName());
assertEquals(1, children.size());
}
@Test
void testNestedDocumentSymbolsForRepositoryWithQuery() throws Exception {
String docUri = directory.toPath().resolve("src/main/java/org/test/CustomerRepositoryWithQuery.java").toUri().toString();
List<? extends DocumentSymbol> symbols = indexer.getDocumentSymbolsFromMetamodelIndex(docUri);
assertEquals(1, symbols.size());
assertTrue(containsDocumentSymbol(symbols, "@+ 'customerRepositoryWithQuery' (Customer) Repository<Customer,Long>", docUri, 7, 17, 7, 44));
DocumentSymbol documentSymbol = symbols.get(0);
List<DocumentSymbol> children = documentSymbol.getChildren();
DocumentSymbol queryMethodSymbol = children.get(0);
assertEquals("findPetTypes", queryMethodSymbol.getName());
assertEquals(1, children.size());
List<DocumentSymbol> queryChildren = queryMethodSymbol.getChildren();
DocumentSymbol queryStringSymbol = queryChildren.get(0);
assertEquals("SELECT ptype FROM PetType ptype ORDER BY ptype.name", queryStringSymbol.getName());
assertEquals(SymbolKind.Constant, queryStringSymbol.getKind());
}
private boolean containsSymbol(List<? extends WorkspaceSymbol> symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) {
@@ -100,4 +123,20 @@ public class DataRepositorySymbolProviderTest {
return false;
}
private boolean containsDocumentSymbol(List<? extends DocumentSymbol> symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) {
for (Iterator<? extends DocumentSymbol> iterator = symbols.iterator(); iterator.hasNext();) {
DocumentSymbol symbol = iterator.next();
if (symbol.getName().equals(name)
&& symbol.getRange().getStart().getLine() == startLine
&& symbol.getRange().getStart().getCharacter() == startCHaracter
&& symbol.getRange().getEnd().getLine() == endLine
&& symbol.getRange().getEnd().getCharacter() == endCharacter) {
return true;
}
}
return false;
}
}

View File

@@ -7,4 +7,7 @@ import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
default List<Customer> findByWhatever(String lastName) {return null;}
}

View File

@@ -0,0 +1,12 @@
package org.test;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepositoryWithQuery extends CrudRepository<Customer, Long> {
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
List<Object> findPetTypes();
}