diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index d281462c2..b56b6b0f1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -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(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java index 94150c528..f3f275409 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndexerConfig.java @@ -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 symbolProviders(IndexCache cache) { + AnnotationHierarchyAwareLookup symbolProviders(IndexCache cache, DataRepositoryAotMetadataService repositoryMetadataService) { AnnotationHierarchyAwareLookup 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(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index 5af5dfd97..eb38e2b9d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -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 = 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); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadata.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadata.java new file mode 100644 index 000000000..fd10191a2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadata.java @@ -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) { +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java new file mode 100644 index 000000000..539c7b81b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java @@ -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 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 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; + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataMethod.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataMethod.java new file mode 100644 index 000000000..935df8600 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataMethod.java @@ -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) { +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataQuery.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataQuery.java new file mode 100644 index 000000000..a0c876a68 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataQuery.java @@ -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) { + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataService.java new file mode 100644 index 000000000..70e4fbe52 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataService.java @@ -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 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; + } + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java index c49ff5b0e..581777487 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java @@ -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 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) {