initial work on using aot information for spring data repositories

This commit is contained in:
Martin Lippert
2025-04-17 08:39:12 +02:00
parent ff5bb3d9fe
commit 68cc248ccb
9 changed files with 296 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2018, 2024 Pivotal, Inc.
* Copyright (c) 2018, 2025 Pivotal, 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
@@ -53,6 +53,7 @@ import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnBeanDefinitionProvider;
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnResourceDefinitionProvider;
import org.springframework.ide.vscode.boot.java.copilot.util.ResponseModifier;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
@@ -428,6 +429,11 @@ public class BootLanguageServerBootApp {
return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, config);
}
@Bean
DataRepositoryAotMetadataService dataAotMetadataService() {
return new DataRepositoryAotMetadataService();
}
@Bean ResponseModifier responseModifier() {
return new ResponseModifier();
}

View File

@@ -19,6 +19,7 @@ import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.ConfigurationPropertiesSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.FeignClientSymbolProvider;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService;
import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvider;
import org.springframework.ide.vscode.boot.java.events.EventListenerSymbolProvider;
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
@@ -29,14 +30,14 @@ import org.springframework.ide.vscode.boot.java.utils.RestrictedDefaultSymbolPro
public class SpringSymbolIndexerConfig {
@Bean
AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache) {
AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache, DataRepositoryAotMetadataService repositoryMetadataService) {
AnnotationHierarchyAwareLookup<SymbolProvider> providers = new AnnotationHierarchyAwareLookup<>();
RequestMappingSymbolProvider requestMappingSymbolProvider = new RequestMappingSymbolProvider();
BeansSymbolProvider beansSymbolProvider = new BeansSymbolProvider();
ComponentSymbolProvider componentSymbolProvider = new ComponentSymbolProvider();
ConfigurationPropertiesSymbolProvider configPropsSymbolProvider = new ConfigurationPropertiesSymbolProvider();
DataRepositorySymbolProvider dataRepositorySymbolProvider = new DataRepositorySymbolProvider();
DataRepositorySymbolProvider dataRepositorySymbolProvider = new DataRepositorySymbolProvider(repositoryMetadataService);
EventListenerSymbolProvider eventListenerSymbolProvider = new EventListenerSymbolProvider();
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();

View File

@@ -33,6 +33,8 @@ import org.springframework.ide.vscode.boot.java.beans.QualifierReferencesProvide
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider;
import org.springframework.ide.vscode.boot.java.copilot.CopilotAgentCommandHandler;
import org.springframework.ide.vscode.boot.java.copilot.util.ResponseModifier;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataCodeLensProvider;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService;
import org.springframework.ide.vscode.boot.java.events.EventReferenceProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine;
@@ -123,6 +125,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private JdtSemanticTokensHandler semanticTokensHandler;
private JdtInlayHintsHandler inlayHintsHandler;
private SpelSemanticTokens spelSemanticTokens;
private DataRepositoryAotMetadataService dataRepositoryAotMetadataService;
public BootJavaLanguageServerComponents(ApplicationContext appContext) {
this.server = appContext.getBean(SimpleLanguageServer.class);
@@ -177,8 +180,8 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
new LiveAppURLSymbolProvider(liveDataProvider)));
spelSemanticTokens = appContext.getBean(SpelSemanticTokens.class);
codeLensHandler = createCodeLensEngine(springIndex, projectFinder, server, spelSemanticTokens);
dataRepositoryAotMetadataService = appContext.getBean(DataRepositoryAotMetadataService.class);
codeLensHandler = createCodeLensEngine(springIndex, projectFinder, server, spelSemanticTokens, dataRepositoryAotMetadataService);
highlightsEngine = createDocumentHighlightEngine(appContext);
documents.onDocumentHighlight(highlightsEngine);
@@ -312,10 +315,13 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
return new BootJavaReferencesHandler(this, cuCache, projectFinder, specificProviders, unspecificProviders);
}
protected BootJavaCodeLensEngine createCodeLensEngine(SpringMetamodelIndex springIndex, JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
protected BootJavaCodeLensEngine createCodeLensEngine(SpringMetamodelIndex springIndex, JavaProjectFinder projectFinder, SimpleLanguageServer server,
SpelSemanticTokens spelSemanticTokens, DataRepositoryAotMetadataService repositoryAotMetadataService) {
Collection<CodeLensProvider> codeLensProvider = new ArrayList<>();
codeLensProvider.add(new WebfluxHandlerCodeLensProvider(springIndex));
codeLensProvider.add(new CopilotCodeLensProvider(projectFinder, server, spelSemanticTokens));
codeLensProvider.add(new DataRepositoryAotMetadataCodeLensProvider(projectFinder, repositoryAotMetadataService));
return new BootJavaCodeLensEngine(this, codeLensProvider);
}

View File

@@ -0,0 +1,14 @@
/*******************************************************************************
* 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;
public record DataRepositoryAotMetadata (String name, String type, DataRepositoryAotMetadataMethod[] methods) {
}

View File

@@ -0,0 +1,112 @@
/*******************************************************************************
* 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.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* @author Martin Lippert
*/
public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvider {
private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataCodeLensProvider.class);
private final DataRepositoryAotMetadataService repositoryMetadataService;
private final JavaProjectFinder projectFinder;
public DataRepositoryAotMetadataCodeLensProvider(JavaProjectFinder projectFinder, DataRepositoryAotMetadataService repositoryMetadataService) {
this.projectFinder = projectFinder;
this.repositoryMetadataService = repositoryMetadataService;
}
@Override
public void provideCodeLenses(CancelChecker cancelToken, TextDocument document, CompilationUnit cu, List<CodeLens> resultAccumulator) {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
provideCodeLens(cancelToken, node, document, resultAccumulator);
return super.visit(node);
}
});
}
protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node, TextDocument document, List<CodeLens> resultAccumulator) {
cancelToken.checkCanceled();
IMethodBinding methodBinding = node.resolveBinding();
if (methodBinding == null || methodBinding.getDeclaringClass() == null
|| methodBinding.getMethodDeclaration() == null
|| methodBinding.getDeclaringClass().getBinaryName() == null
|| methodBinding.getMethodDeclaration().toString() == null) {
return;
}
cancelToken.checkCanceled();
final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim();
final IMethodBinding method = methodBinding.getMethodDeclaration();
IJavaProject project = projectFinder.find(document.getId()).get();
if (project == null) {
return;
}
DataRepositoryAotMetadata metadata = repositoryMetadataService.getRepositoryMetadata(project, repositoryClass);
if (metadata == null) {
return;
}
cancelToken.checkCanceled();
String queryStatement = repositoryMetadataService.getQueryStatement(metadata, method);
if (queryStatement == null) {
return;
}
CodeLens codeLens = createCodeLens(node, document, queryStatement);
resultAccumulator.add(codeLens);
}
private CodeLens createCodeLens(MethodDeclaration node, TextDocument document, String queryStatement) {
try {
Command cmd = new Command();
cmd.setTitle(queryStatement);
CodeLens codeLens = new CodeLens();
codeLens.setRange(document.toRange(node.getName().getStartPosition(), node.getName().getLength()));
codeLens.setCommand(cmd);
return codeLens;
} catch (BadLocationException e) {
log.error("bad location while calculating code lens for data repository query method", e);
return null;
}
}
}

View File

@@ -0,0 +1,14 @@
/*******************************************************************************
* 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;
public record DataRepositoryAotMetadataMethod(String name, String signature, DataRepositoryAotMetadataQuery query) {
}

View File

@@ -0,0 +1,15 @@
/*******************************************************************************
* 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;
public record DataRepositoryAotMetadataQuery(String query) {
}

View File

@@ -0,0 +1,100 @@
/*******************************************************************************
* 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.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser;
import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser.JLRMethod;
import com.google.gson.Gson;
/**
* @author Martin Lippert
*/
public class DataRepositoryAotMetadataService {
private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataService.class);
public DataRepositoryAotMetadata getRepositoryMetadata(IJavaProject project, String repositoryType) {
try {
String metadataFilePath = repositoryType.replace('.', File.separatorChar);
IClasspathUtil.getOutputFolders(project.getClasspath()).forEach(System.out::println);
Optional<File> metadataFile = IClasspathUtil.getOutputFolders(project.getClasspath())
.map(outputFolder -> new File(outputFolder.getParentFile(), "spring-aot/main/resources/" + metadataFilePath + ".json"))
.filter(file -> file.exists())
.findFirst();
if (metadataFile.isPresent()) {
return readMetadataFile(metadataFile.get());
}
} catch (Exception e) {
log.error("error finding spring data repository definition metadata file", e);
}
return null;
}
private DataRepositoryAotMetadata readMetadataFile(File file) {
try (FileReader reader = new FileReader(file)) {
Gson gson = new Gson();
DataRepositoryAotMetadata result = gson.fromJson(reader, DataRepositoryAotMetadata.class);
return result;
}
catch (IOException e) {
return null;
}
}
public String getQueryStatement(DataRepositoryAotMetadata metadata, IMethodBinding method) {
DataRepositoryAotMetadataMethod methodMetadata = findMethod(metadata, method);
return methodMetadata != null ? methodMetadata.query().query() : null;
}
private DataRepositoryAotMetadataMethod findMethod(DataRepositoryAotMetadata metadata, IMethodBinding method) {
String name = method.getName();
for (DataRepositoryAotMetadataMethod methodMetadata : metadata.methods()) {
// TODO: This check needs more exact method signature matching - which is a little more complicated
// due to runtime Method.toGenericString() output needs to be compared to IMethodBinding source level method information
if (methodMetadata.name() != null && methodMetadata.name().equals(name)) {
String signature = methodMetadata.signature();
JLRMethod parsedMethodSignature = JLRMethodParser.parse(signature);
String methodName = parsedMethodSignature.getMethodName();
String[] parameters = parsedMethodSignature.getParameters();
String returnType = parsedMethodSignature.getReturnType();
parsedMethodSignature.getFQClassName();
return methodMetadata;
}
}
return null;
}
}

View File

@@ -16,6 +16,7 @@ import java.util.List;
import java.util.Set;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
@@ -57,7 +58,13 @@ import reactor.util.function.Tuples;
public class DataRepositorySymbolProvider implements SymbolProvider {
private static final Logger log = LoggerFactory.getLogger(DataRepositorySymbolProvider.class);
private final DataRepositoryAotMetadataService repositoryMetadataService;
public DataRepositorySymbolProvider(DataRepositoryAotMetadataService repositoryMetadataService) {
this.repositoryMetadataService = repositoryMetadataService;
}
@Override
public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
// this checks spring data repository beans that are defined as extensions of the repository interface
@@ -115,7 +122,7 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
Range range = doc.toRange(nodeRegion);
if (methodName != null) {
String queryString = identifyQueryString(method, annotationHierarchies);
String queryString = identifyQueryString(method, annotationHierarchies, context);
String methodSignature = identifyMethodSignature(method);
beanDefinition.addChild(new QueryMethodIndexElement(methodSignature, queryString, range));
}
@@ -174,8 +181,9 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
return result;
}
private String identifyQueryString(MethodDeclaration method, AnnotationHierarchies annotationHierarchies) {
private String identifyQueryString(MethodDeclaration method, AnnotationHierarchies annotationHierarchies, SpringIndexerJavaContext context) {
// lookup query annotation on the method first
EmbeddedQueryExpression queryExpression = null;
Collection<Annotation> annotations = ASTUtils.getAnnotations(method);
@@ -191,12 +199,22 @@ public class DataRepositorySymbolProvider implements SymbolProvider {
}
}
}
if (queryExpression != null) {
return queryExpression.query().getText();
}
// second option: lookup repository metadata service to see if there is a matching enty
IMethodBinding methodBinding = method.resolveBinding();
final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim();
return null;
DataRepositoryAotMetadata repositoryMetadata = this.repositoryMetadataService.getRepositoryMetadata(context.getProject(), repositoryClass);
if (repositoryMetadata == null) {
return null;
}
String queryStatement = repositoryMetadataService.getQueryStatement(repositoryMetadata, methodBinding);
return queryStatement;
}
protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) {