Skip to content

Commit

Permalink
GH-1491: index query methods and show them as child nodes for reposit…
Browse files Browse the repository at this point in the history
…ory document symbols, incl. query strings when defined
  • Loading branch information
martinlippert committed Mar 4, 2025
1 parent dc29abe commit e7eb27c
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* @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";
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,25 @@

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;
Expand Down Expand Up @@ -62,6 +72,9 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
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();
Expand All @@ -75,8 +88,8 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
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) {
Expand All @@ -85,6 +98,59 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext
}
}

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("@+");
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ public static EmbeddedQueryExpression extractQueryExpression(MethodInvocation m)
}

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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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());

}

}
Loading

0 comments on commit e7eb27c

Please sign in to comment.