From cae1a57d298df8aad2ec2eb20489ee0dc46a42e0 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Mon, 10 Nov 2025 18:07:47 -0800 Subject: [PATCH 01/13] Data Repo Aot Optimizations Signed-off-by: BoykoAlex --- ...stractDataRepositoryAotMethodMetadata.java | 32 ++++++ .../java/data/DataRepositoryAotMetadata.java | 50 +++++++- ...RepositoryAotMetadataCodeLensProvider.java | 14 +-- .../data/DataRepositoryAotMetadataMethod.java | 108 ------------------ .../DataRepositoryAotMetadataService.java | 83 +++++--------- ...taQuery.java => DataRepositoryModule.java} | 16 ++- .../data/DataRepositorySymbolProvider.java | 4 +- .../IDataRepositoryAotMethodMetadata.java | 25 ++++ .../boot/java/data/JpaAotMethodMetadata.java | 36 ++++++ .../java/data/MongoAotMethodMetadata.java | 93 +++++++++++++++ .../data/QueryMethodCodeActionProvider.java | 4 +- .../DataRepositoryAotMetadataServiceTest.java | 27 ++--- 12 files changed, 293 insertions(+), 199 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/AbstractDataRepositoryAotMethodMetadata.java delete mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataMethod.java rename headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/{DataRepositoryAotMetadataQuery.java => DataRepositoryModule.java} (50%) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/IDataRepositoryAotMethodMetadata.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/JpaAotMethodMetadata.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/MongoAotMethodMetadata.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/AbstractDataRepositoryAotMethodMetadata.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/AbstractDataRepositoryAotMethodMetadata.java new file mode 100644 index 0000000000..59b699565d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/AbstractDataRepositoryAotMethodMetadata.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +abstract class AbstractDataRepositoryAotMethodMetadata implements IDataRepositoryAotMethodMetadata { + + private String name; + + private String signature; + + public AbstractDataRepositoryAotMethodMetadata(String name, String signature) { + this.name = name; + this.signature = signature; + } + + public String getName() { + return name; + } + + public String getSignature() { + return signature; + } + +} 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 index 6dec3a166f..d64df9cd9f 100644 --- 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 @@ -10,13 +10,51 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.data; -public record DataRepositoryAotMetadata (String name, String type, String module, DataRepositoryAotMetadataMethod[] methods) { +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser; +import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser.JLRMethod; + +public record DataRepositoryAotMetadata (String name, String type, DataRepositoryModule module, IDataRepositoryAotMethodMetadata[] methods) { - public boolean isJPA() { - return module != null && module.toLowerCase().equals("jpa"); + public IDataRepositoryAotMethodMetadata findMethod(IMethodBinding method) { + String name = method.getName(); + + for (IDataRepositoryAotMethodMetadata methodMetadata : methods()) { + + if (methodMetadata.getName() != null && methodMetadata.getName().equals(name)) { + + String signature = methodMetadata.getSignature(); + JLRMethod parsedMethodSignature = JLRMethodParser.parse(signature); + + if (parsedMethodSignature.getFQClassName().equals(name()) + && parsedMethodSignature.getMethodName().equals(method.getName()) + && parsedMethodSignature.getReturnType().equals(method.getReturnType().getQualifiedName()) + && parameterMatches(parsedMethodSignature, method)) { + return methodMetadata; + } + } + } + + return null; } - - public boolean isMongoDb() { - return module != null && module.toLowerCase().equals("mongodb"); + + private boolean parameterMatches(JLRMethod parsedMethodSignature, IMethodBinding method) { + String[] parsedParameeterTypes = parsedMethodSignature.getParameters(); + ITypeBinding[] methodParameters = method.getParameterTypes(); + + if (parsedParameeterTypes == null || methodParameters == null || parsedParameeterTypes.length != methodParameters.length) { + return false; + } + + for (int i = 0; i < parsedParameeterTypes.length; i++) { + String qualifiedName = methodParameters[i].getQualifiedName(); + if (qualifiedName != null && !qualifiedName.equals(parsedParameeterTypes[i])) { + return false; + } + } + + return true; } + } 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 index f99706b3a1..5f0b2627e5 100644 --- 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 @@ -108,10 +108,10 @@ static Optional getMetadata(DataRepositoryAotMetadata return Optional.ofNullable(dataRepositoryAotMetadataService.getRepositoryMetadata(project, repositoryClass)); } - static Optional getMethodMetadata(DataRepositoryAotMetadataService dataRepositoryAotMetadataService, DataRepositoryAotMetadata metadata, IMethodBinding methodBinding) { + static Optional getMethodMetadata(DataRepositoryAotMetadataService dataRepositoryAotMetadataService, DataRepositoryAotMetadata metadata, IMethodBinding methodBinding) { final IMethodBinding method = methodBinding.getMethodDeclaration(); - return Optional.ofNullable(dataRepositoryAotMetadataService.findMethod(metadata, method)); + return Optional.ofNullable(metadata.findMethod(method)); } protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node, TextDocument document, List resultAccumulator) { @@ -143,7 +143,7 @@ private List createCodeLenses(MethodDeclaration node, TextDocument doc Range range = new Range(startPos, endPos); AnnotationHierarchies hierarchyAnnot = AnnotationHierarchies.get(node); - Optional methodMetadata = getMethodMetadata(repositoryMetadataService, metadata, mb); + Optional methodMetadata = getMethodMetadata(repositoryMetadataService, metadata, mb); if (mb != null && hierarchyAnnot != null && methodMetadata.isPresent()) { @@ -165,7 +165,7 @@ private List createCodeLenses(MethodDeclaration node, TextDocument doc codeLenses.add(new CodeLens(range, impl, null)); if (!isQueryAnnotated) { - String queryStatement = methodMetadata.get().getQueryStatement(metadata); + String queryStatement = methodMetadata.get().getQueryStatement(); if (queryStatement != null) { Command queryTitle = new Command(); queryTitle.setTitle(queryStatement); @@ -179,18 +179,18 @@ private List createCodeLenses(MethodDeclaration node, TextDocument doc return codeLenses; } - static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryAotMetadata metadata, DataRepositoryAotMetadataMethod methodMetadata) { + static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryAotMetadata metadata, IDataRepositoryAotMethodMetadata methodMetadata) { return new FixDescriptor(AddAnnotationOverMethod.class.getName(), List.of(docUri), "Turn into `@Query`") .withRecipeScope(RecipeScope.FILE) .withParameters(Map.of( - "annotationType", metadata.isJPA() ? Annotations.DATA_JPA_QUERY : Annotations.DATA_MONGODB_QUERY, + "annotationType", metadata.module() == DataRepositoryModule.JPA ? Annotations.DATA_JPA_QUERY : Annotations.DATA_MONGODB_QUERY, "method", "%s %s(%s)".formatted(mb.getDeclaringClass().getQualifiedName(), mb.getName(), Arrays.stream(mb.getParameterTypes()) .map(pt -> pt.getQualifiedName()) .collect(Collectors.joining(", "))), - "attributes", createAttributeList(methodMetadata.getAttributesMap(metadata)))); + "attributes", createAttributeList(methodMetadata.getAttributesMap()))); } private static List createAttributeList(Map attributes) { 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 deleted file mode 100644 index 1c778b62c7..0000000000 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataMethod.java +++ /dev/null @@ -1,108 +0,0 @@ -/******************************************************************************* - * 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.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.util.StringUtils; - -public record DataRepositoryAotMetadataMethod(String name, String signature, DataRepositoryAotMetadataQuery query) { - - public String getQueryStatement(DataRepositoryAotMetadata repository) { - if (repository != null && repository.isJPA()) { - return getJpaQueryStatement(); - } - else if (repository != null && repository.isMongoDb()) { - return getMongoDbQueryStatement(); - } - else { - return null; - } - } - - private String getJpaQueryStatement() { - return query() != null ? query.query(): null; - } - - private String getMongoDbQueryStatement() { - List parts = new ArrayList<>(); - - if (query == null) return null; - - if (query().filter() != null) { - if (!StringUtils.hasText(query().sort()) - && !StringUtils.hasText(query().fields()) - && !StringUtils.hasText(query().projection()) - && !StringUtils.hasText(query().pipeline())) { - - parts.add(query().filter()); - } - else { - parts.add("filter = \"" + query().filter() + "\""); - } - } - - if (query().fields() != null) { - parts.add("fields = \"" + query().fields() + "\""); - } - - if (query().sort() != null) { - parts.add("sort = \"" + query().sort() + "\""); - } - - if (query().projection() != null) { - parts.add("projection = \"" + query().projection() + "\""); - } - - if (query().pipeline() != null) { - parts.add("pipeline = \"" + query().pipeline() + "\""); - } - - return String.join(", ", parts); - } - - public Map getAttributesMap(DataRepositoryAotMetadata metadata) { - if (metadata != null && metadata.isJPA()) { - return Map.of("value", getJpaQueryStatement()); - } - else if (metadata != null && metadata.isMongoDb()) { - if (query != null) { - return createMongoDbQueryAttributes(); - } - } - - return Map.of(); - } - - private Map createMongoDbQueryAttributes() { - Map result = new HashMap<>(); - - if (query.filter() != null) { - result.put("value", query.filter()); - } - - if (query().fields() != null) { - result.put("fields", query().fields()); - } - - if (query().sort() != null) { - result.put("sort", query().sort()); - } - - // TODO; what about projection and pipeline ? - - return result; - } - -} 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 index 6996219503..313a734422 100644 --- 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 @@ -13,18 +13,21 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.lang.reflect.Type; import java.util.Optional; -import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; 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; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; /** * @author Martin Lippert @@ -32,6 +35,30 @@ public class DataRepositoryAotMetadataService { private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataService.class); + + final private Gson gson = new GsonBuilder().registerTypeAdapter(DataRepositoryAotMetadata.class, new JsonDeserializer() { + + @Override + public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + JsonObject o = json.getAsJsonObject(); + JsonElement e = o.get("module"); + if (e.isJsonPrimitive()) { + String module = e.getAsString(); + String name = context.deserialize(o.get("name"), String.class); + String type = context.deserialize(o.get("type"), String.class); + DataRepositoryModule moduleType = DataRepositoryModule.valueOf(module.toUpperCase()); + switch (moduleType) { + case MONGODB: + return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get("methods"), MongoAotMethodMetadata[].class)); + case JPA: + return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get("methods"), JpaAotMethodMetadata[].class)); + } + } + return null; + } + + }).create(); public DataRepositoryAotMetadata getRepositoryMetadata(IJavaProject project, String repositoryType) { try { @@ -56,60 +83,12 @@ public DataRepositoryAotMetadata getRepositoryMetadata(IJavaProject project, Str 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.getQueryStatement(metadata); - } - public DataRepositoryAotMetadataMethod findMethod(DataRepositoryAotMetadata metadata, IMethodBinding method) { - String name = method.getName(); - - for (DataRepositoryAotMetadataMethod methodMetadata : metadata.methods()) { - - if (methodMetadata.name() != null && methodMetadata.name().equals(name)) { - - String signature = methodMetadata.signature(); - JLRMethod parsedMethodSignature = JLRMethodParser.parse(signature); - - if (parsedMethodSignature.getFQClassName().equals(metadata.name()) - && parsedMethodSignature.getMethodName().equals(method.getName()) - && parsedMethodSignature.getReturnType().equals(method.getReturnType().getQualifiedName()) - && parameterMatches(parsedMethodSignature, method)) { - return methodMetadata; - } - } - } - - return null; - } - - private boolean parameterMatches(JLRMethod parsedMethodSignature, IMethodBinding method) { - String[] parsedParameeterTypes = parsedMethodSignature.getParameters(); - ITypeBinding[] methodParameters = method.getParameterTypes(); - - if (parsedParameeterTypes == null || methodParameters == null || parsedParameeterTypes.length != methodParameters.length) { - return false; - } - - for (int i = 0; i < parsedParameeterTypes.length; i++) { - String qualifiedName = methodParameters[i].getQualifiedName(); - if (qualifiedName != null && !qualifiedName.equals(parsedParameeterTypes[i])) { - return false; - } - } - - return true; - } - - } 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/DataRepositoryModule.java similarity index 50% rename from headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataQuery.java rename to headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryModule.java index e96f60a1c5..4789196ffe 100644 --- 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/DataRepositoryModule.java @@ -1,20 +1,18 @@ /******************************************************************************* - * Copyright (c) 2025 Broadcom + * Copyright (c) 2025 Broadcom, 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 * https://www.eclipse.org/legal/epl-v10.html * * Contributors: - * Broadcom - initial API and implementation + * Broadcom, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.vscode.boot.java.data; -/** - * Details about the AOT generated query. - * - * query: For JPA-based repositories, this field contains the generated SQL query statememt for the query method - * filter, sort, projection, pipeline: Query details for MongoDB-based repository query methods - */ -public record DataRepositoryAotMetadataQuery(String query, String filter, String sort, String projection, String pipeline, String fields) { +public enum DataRepositoryModule { + + JPA, + MONGODB + } 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 1673a49c30..a04441e1fd 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 @@ -213,8 +213,8 @@ else if (annotation instanceof NormalAnnotation) { return null; } - String queryStatement = repositoryMetadataService.getQueryStatement(repositoryMetadata, methodBinding); - return queryStatement; + IDataRepositoryAotMethodMetadata aotMethod = repositoryMetadata.findMethod(methodBinding); + return aotMethod == null ? null : aotMethod.getQueryStatement(); } protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/IDataRepositoryAotMethodMetadata.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/IDataRepositoryAotMethodMetadata.java new file mode 100644 index 0000000000..81506841ee --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/IDataRepositoryAotMethodMetadata.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.Map; + +public interface IDataRepositoryAotMethodMetadata { + + String getName(); + + String getSignature(); + + String getQueryStatement(); + + Map getAttributesMap(); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/JpaAotMethodMetadata.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/JpaAotMethodMetadata.java new file mode 100644 index 0000000000..0391785bcf --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/JpaAotMethodMetadata.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.Map; + +class JpaAotMethodMetadata extends AbstractDataRepositoryAotMethodMetadata { + + record Query(String query) {} + + private Query query; + + public JpaAotMethodMetadata(String name, String signature, Query query) { + super(name, signature); + this.query = query; + } + + @Override + public String getQueryStatement() { + return query == null ? null : query.query(); + } + + @Override + public Map getAttributesMap() { + return Map.of("value", getQueryStatement()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/MongoAotMethodMetadata.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/MongoAotMethodMetadata.java new file mode 100644 index 0000000000..730c5a0adc --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/MongoAotMethodMetadata.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.StringUtils; + +class MongoAotMethodMetadata extends AbstractDataRepositoryAotMethodMetadata { + + record Query( String filter, String sort, String projection, String pipeline, String fields) {} + + private Query query; + + public MongoAotMethodMetadata(String name, String signature, Query query) { + super(name, signature); + this.query = query; + } + + @Override + public String getQueryStatement() { + List parts = new ArrayList<>(); + + if (query == null) return null; + + if (query.filter() != null) { + if (!StringUtils.hasText(query.sort()) + && !StringUtils.hasText(query.fields()) + && !StringUtils.hasText(query.projection()) + && !StringUtils.hasText(query.pipeline())) { + + parts.add(query.filter()); + } + else { + parts.add("filter = \"" + query.filter() + "\""); + } + } + + if (query.fields() != null) { + parts.add("fields = \"" + query.fields() + "\""); + } + + if (query.sort() != null) { + parts.add("sort = \"" + query.sort() + "\""); + } + + if (query.projection() != null) { + parts.add("projection = \"" + query.projection() + "\""); + } + + if (query.pipeline() != null) { + parts.add("pipeline = \"" + query.pipeline() + "\""); + } + + return String.join(", ", parts); + } + + @Override + public Map getAttributesMap() { + if (query != null) { + Map result = new HashMap<>(); + + if (query.filter() != null) { + result.put("value", query.filter()); + } + + if (query.fields() != null) { + result.put("fields", query.fields()); + } + + if (query.sort() != null) { + result.put("sort", query.sort()); + } + + // TODO; what about projection and pipeline ? + + return result; + } + return Map.of(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java index 5be4787008..1031f9b209 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java @@ -74,7 +74,7 @@ public boolean visit(MethodDeclaration node) { Optional metadata = DataRepositoryAotMetadataCodeLensProvider.getMetadata(repositoryMetadataService, project, binding); if (metadata.isPresent()) { - Optional methodMetadata = DataRepositoryAotMetadataCodeLensProvider.getMethodMetadata(repositoryMetadataService, metadata.get(), binding); + Optional methodMetadata = DataRepositoryAotMetadataCodeLensProvider.getMethodMetadata(repositoryMetadataService, metadata.get(), binding); methodMetadata .map(method -> createCodeAction(binding, docURI, metadata.get(), method)) .ifPresent(collector::accept); @@ -89,7 +89,7 @@ public boolean visit(MethodDeclaration node) { }; } - private CodeAction createCodeAction(IMethodBinding mb, URI docUri, DataRepositoryAotMetadata metadata, DataRepositoryAotMetadataMethod method) { + private CodeAction createCodeAction(IMethodBinding mb, URI docUri, DataRepositoryAotMetadata metadata, IDataRepositoryAotMethodMetadata method) { CodeAction ca = new CodeAction(); ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata, method))); ca.setTitle(TITLE); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java index 92ed93f4d4..2d49b3fa81 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java @@ -31,8 +31,9 @@ import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadata; -import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataMethod; import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService; +import org.springframework.ide.vscode.boot.java.data.DataRepositoryModule; +import org.springframework.ide.vscode.boot.java.data.IDataRepositoryAotMethodMetadata; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser; @@ -69,13 +70,13 @@ public void setup() throws Exception { } @Test - void testBasicRepositoryAotMetadataLookuo() throws Exception { + void testBasicRepositoryAotMetadataLookup() throws Exception { DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService(); DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository"); assertNotNull(metadata); assertEquals("example.springdata.aot.UserRepository", metadata.name()); - assertEquals("JPA", metadata.module()); + assertEquals(DataRepositoryModule.JPA, metadata.module()); } @Test @@ -83,15 +84,15 @@ void testRepositoryMethodsIntMetadata() throws Exception { DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService(); DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository"); - DataRepositoryAotMetadataMethod[] methods = metadata.methods(); + IDataRepositoryAotMethodMetadata[] methods = metadata.methods(); assertEquals(32, methods.length); - DataRepositoryAotMetadataMethod methodMetadata = Arrays.stream(methods).filter(method -> method.name().equals("countUsersByLastnameLike")).findFirst().get(); - assertEquals("countUsersByLastnameLike", methodMetadata.name()); - assertEquals("public abstract java.lang.Long example.springdata.aot.UserRepository.countUsersByLastnameLike(java.lang.String)", methodMetadata.signature()); + IDataRepositoryAotMethodMetadata methodMetadata = Arrays.stream(methods).filter(method -> method.getName().equals("countUsersByLastnameLike")).findFirst().get(); + assertEquals("countUsersByLastnameLike", methodMetadata.getName()); + assertEquals("public abstract java.lang.Long example.springdata.aot.UserRepository.countUsersByLastnameLike(java.lang.String)", methodMetadata.getSignature()); - JLRMethod parsedMethodSignature = JLRMethodParser.parse(methodMetadata.signature()); + JLRMethod parsedMethodSignature = JLRMethodParser.parse(methodMetadata.getSignature()); assertEquals("example.springdata.aot.UserRepository", parsedMethodSignature.getFQClassName()); assertEquals("java.lang.Long", parsedMethodSignature.getReturnType()); assertEquals("countUsersByLastnameLike", parsedMethodSignature.getMethodName()); @@ -112,14 +113,14 @@ void testRepositoryMethodsMatching() throws Exception { public boolean visit(MethodDeclaration node) { IMethodBinding binding = node.resolveBinding(); - DataRepositoryAotMetadataMethod method = service.findMethod(metadata, binding); + IDataRepositoryAotMethodMetadata method = metadata.findMethod(binding); assertNotNull(method); - if (method.name().equals("findUserByLastnameStartingWith") && binding.getParameterTypes().length == 1) { - assertEquals("public abstract java.util.List example.springdata.aot.UserRepository.findUserByLastnameStartingWith(java.lang.String)", method.signature()); + if (method.getName().equals("findUserByLastnameStartingWith") && binding.getParameterTypes().length == 1) { + assertEquals("public abstract java.util.List example.springdata.aot.UserRepository.findUserByLastnameStartingWith(java.lang.String)", method.getSignature()); } - else if (method.name().equals("findUserByLastnameStartingWith") && binding.getParameterTypes().length == 2) { - assertEquals("public abstract org.springframework.data.domain.Page example.springdata.aot.UserRepository.findUserByLastnameStartingWith(java.lang.String,org.springframework.data.domain.Pageable)", method.signature()); + else if (method.getName().equals("findUserByLastnameStartingWith") && binding.getParameterTypes().length == 2) { + assertEquals("public abstract org.springframework.data.domain.Page example.springdata.aot.UserRepository.findUserByLastnameStartingWith(java.lang.String,org.springframework.data.domain.Pageable)", method.getSignature()); } return true; From 46c74cebcaa86990d8df34ac973aa0aae9c8c096 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 12 Nov 2025 10:49:08 -0800 Subject: [PATCH 02/13] Polish Signed-off-by: BoykoAlex --- .../DataRepositoryAotMetadataService.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) 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 index 313a734422..168b90592a 100644 --- 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 @@ -14,6 +14,7 @@ import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Type; +import java.nio.file.Files; import java.util.Optional; import org.slf4j.Logger; @@ -34,6 +35,14 @@ */ public class DataRepositoryAotMetadataService { + private static final String MODULE_JSON_PROP = "module"; + + private static final String TYPE_JSON_PROP = "type"; + + private static final String NAME_JSON_PROP = "name"; + + private static final String METHODS = "methods"; + private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataService.class); final private Gson gson = new GsonBuilder().registerTypeAdapter(DataRepositoryAotMetadata.class, new JsonDeserializer() { @@ -42,17 +51,17 @@ public class DataRepositoryAotMetadataService { public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject o = json.getAsJsonObject(); - JsonElement e = o.get("module"); + JsonElement e = o.get(MODULE_JSON_PROP); if (e.isJsonPrimitive()) { String module = e.getAsString(); - String name = context.deserialize(o.get("name"), String.class); - String type = context.deserialize(o.get("type"), String.class); + String name = context.deserialize(o.get(NAME_JSON_PROP), String.class); + String type = context.deserialize(o.get(TYPE_JSON_PROP), String.class); DataRepositoryModule moduleType = DataRepositoryModule.valueOf(module.toUpperCase()); switch (moduleType) { case MONGODB: - return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get("methods"), MongoAotMethodMetadata[].class)); + return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get(METHODS), MongoAotMethodMetadata[].class)); case JPA: - return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get("methods"), JpaAotMethodMetadata[].class)); + return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get(METHODS), JpaAotMethodMetadata[].class)); } } return null; @@ -62,11 +71,12 @@ public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT, public DataRepositoryAotMetadata getRepositoryMetadata(IJavaProject project, String repositoryType) { try { - String metadataFilePath = repositoryType.replace('.', File.separatorChar); + String metadataFilePath = repositoryType.replace('.', '/') + ".json"; Optional metadataFile = IClasspathUtil.getOutputFolders(project.getClasspath()) - .map(outputFolder -> new File(outputFolder.getParentFile(), "spring-aot/main/resources/" + metadataFilePath + ".json")) - .filter(file -> file.exists()) + .map(outputFolder -> outputFolder.getParentFile().toPath().resolve("spring-aot/main/resources/").resolve(metadataFilePath)) + .filter(Files::isRegularFile) + .map(p -> p.toFile()) .findFirst(); if (metadataFile.isPresent()) { From 576a51fcc14d7b5d33e45abcb827b045ef30919c Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 12 Nov 2025 16:39:43 -0800 Subject: [PATCH 03/13] WIP Signed-off-by: BoykoAlex --- .../commons/languageserver/util/OS.java | 23 ++++ .../util/ParentProcessWatcher.java | 13 +- .../boot/app/BootLanguageServerBootApp.java | 4 +- .../java/data/DataRepositoryAotMetadata.java | 8 +- ...RepositoryAotMetadataCodeLensProvider.java | 93 +++++++------ .../DataRepositoryAotMetadataService.java | 126 ++++++++++++++---- .../data/DataRepositorySymbolProvider.java | 5 +- .../data/QueryMethodCodeActionProvider.java | 15 +-- ...oryAotMetadataCodeLensProviderJpaTest.java | 4 +- ...otMetadataCodeLensProviderMongoDbTest.java | 4 +- .../DataRepositoryAotMetadataServiceTest.java | 37 +++-- 11 files changed, 223 insertions(+), 109 deletions(-) create mode 100644 headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/OS.java diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/OS.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/OS.java new file mode 100644 index 0000000000..d4d5584dea --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/OS.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.languageserver.util; + +public class OS { + + public static boolean isWindows() { + String os = System.getProperty("os.name"); + if (os != null) { + return os.toLowerCase().indexOf("win") >= 0; + } + return false; + } + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java index d3579ad4fe..d19fe3f245 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ParentProcessWatcher.java @@ -71,7 +71,7 @@ private boolean parentProcessStillRunning() { return true; } String command; - if (isWindows()) { + if (OS.isWindows()) { command = "cmd /c \"tasklist /FI \"PID eq " + pid + "\" | findstr " + pid + "\""; } else { command = "kill -0 " + pid; @@ -85,7 +85,7 @@ private boolean parentProcessStillRunning() { process.destroy(); finished = process.waitFor(POLL_DELAY_SECS, TimeUnit.SECONDS); // wait for the process to stop } - if (isWindows() && finished && process.exitValue() > 1) { + if (OS.isWindows() && finished && process.exitValue() > 1) { // the tasklist command should return 0 (parent process exists) or 1 (parent process doesn't exist) logger.info("The tasklist command: '{}' returns {}", command, process.exitValue()); return true; @@ -103,7 +103,7 @@ private boolean parentProcessStillRunning() { // It is only closed when the Process object is garbage collected (in its finalize() method). // On Windows, when the Java LS is idle, we need to explicitly request a GC, // to prevent an accumulation of zombie processes, as finalize() will be called. - if (isWindows()) { + if (OS.isWindows()) { // Java >= 9 doesn't close the handle when the process is garbage collected // We need to close the opened streams if (!isJava1x) { @@ -134,11 +134,4 @@ public MessageConsumer apply(final MessageConsumer consumer) { }; } - private static boolean isWindows() { - String os = System.getProperty("os.name"); - if (os != null) { - return os.toLowerCase().indexOf("win") >= 0; - } - return false; - } } 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 4c74bad512..831d67a2a7 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 @@ -436,8 +436,8 @@ ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder p } @Bean - DataRepositoryAotMetadataService dataAotMetadataService() { - return new DataRepositoryAotMetadataService(); + DataRepositoryAotMetadataService dataAotMetadataService(SimpleLanguageServer server, FileObserver fileObserver, JavaProjectFinder projectFinder) { + return new DataRepositoryAotMetadataService(server, fileObserver, projectFinder); } @Bean 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 index d64df9cd9f..43a69f1aa7 100644 --- 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 @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.data; +import java.util.Optional; + import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser; @@ -17,7 +19,7 @@ public record DataRepositoryAotMetadata (String name, String type, DataRepositoryModule module, IDataRepositoryAotMethodMetadata[] methods) { - public IDataRepositoryAotMethodMetadata findMethod(IMethodBinding method) { + public Optional findMethod(IMethodBinding method) { String name = method.getName(); for (IDataRepositoryAotMethodMetadata methodMetadata : methods()) { @@ -31,12 +33,12 @@ public IDataRepositoryAotMethodMetadata findMethod(IMethodBinding method) { && parsedMethodSignature.getMethodName().equals(method.getName()) && parsedMethodSignature.getReturnType().equals(method.getReturnType().getQualifiedName()) && parameterMatches(parsedMethodSignature, method)) { - return methodMetadata; + return Optional.of(methodMetadata); } } } - return null; + return Optional.empty(); } private boolean parameterMatches(JLRMethod parsedMethodSignature, IMethodBinding method) { 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 index 5f0b2627e5..b20b9a6424 100644 --- 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 @@ -35,7 +35,10 @@ import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.commons.Version; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; import org.springframework.ide.vscode.commons.rewrite.java.AddAnnotationOverMethod; @@ -85,9 +88,19 @@ static boolean isValidMethodBinding(IMethodBinding methodBinding) { || methodBinding.getDeclaringClass().getBinaryName() == null) { return false; } + + if (ASTUtils.findInTypeHierarchy(methodBinding.getDeclaringClass(), Set.of(Constants.REPOSITORY_TYPE)) == null) { + return false; + } + return true; } + static boolean isValidProject(IJavaProject jp) { + Version version = SpringProjectUtil.getDependencyVersion(jp, "spring-data-commons"); + return version != null && version.compareTo(new Version(4, 0, 0, null)) >= 0; + } + // static Optional getDataQuery(DataRepositoryAotMetadataService repositoryMetadataService, IJavaProject project, IMethodBinding methodBinding) { // final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim(); // final IMethodBinding method = methodBinding.getMethodDeclaration(); @@ -104,21 +117,14 @@ static boolean isValidMethodBinding(IMethodBinding methodBinding) { static Optional getMetadata(DataRepositoryAotMetadataService dataRepositoryAotMetadataService, IJavaProject project, IMethodBinding methodBinding) { final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim(); - - return Optional.ofNullable(dataRepositoryAotMetadataService.getRepositoryMetadata(project, repositoryClass)); - } - - static Optional getMethodMetadata(DataRepositoryAotMetadataService dataRepositoryAotMetadataService, DataRepositoryAotMetadata metadata, IMethodBinding methodBinding) { - final IMethodBinding method = methodBinding.getMethodDeclaration(); - - return Optional.ofNullable(metadata.findMethod(method)); + return dataRepositoryAotMetadataService.getRepositoryMetadata(project, repositoryClass); } protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node, TextDocument document, List resultAccumulator) { cancelToken.checkCanceled(); IJavaProject project = projectFinder.find(document.getId()).get(); - if (project == null) { + if (project == null || !isValidProject(project)) { return; } @@ -127,13 +133,11 @@ protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node if (isValidMethodBinding(methodBinding)) { cancelToken.checkCanceled(); - getMetadata(repositoryMetadataService, project, methodBinding) - .map(metadata -> createCodeLenses(node, document, metadata)) - .ifPresent(cls -> cls.forEach(resultAccumulator::add)); + resultAccumulator.addAll(createCodeLenses(project, node, document)); } } - private List createCodeLenses(MethodDeclaration node, TextDocument document, DataRepositoryAotMetadata metadata) { + private List createCodeLenses(IJavaProject project, MethodDeclaration node, TextDocument document) { List codeLenses = new ArrayList<>(2); try { @@ -143,35 +147,38 @@ private List createCodeLenses(MethodDeclaration node, TextDocument doc Range range = new Range(startPos, endPos); AnnotationHierarchies hierarchyAnnot = AnnotationHierarchies.get(node); - Optional methodMetadata = getMethodMetadata(repositoryMetadataService, metadata, mb); - - if (mb != null && hierarchyAnnot != null && methodMetadata.isPresent()) { - - boolean isQueryAnnotated = hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_JPA_QUERY) - || hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_MONGODB_QUERY); - - - if (!isQueryAnnotated) { - codeLenses.add(new CodeLens(range, refactorings.createFixCommand(COVERT_TO_QUERY_LABEL, createFixDescriptor(mb, document.getUri(), metadata, methodMetadata.get())), null)); - } - - Command impl = new Command("Implementation", GenAotQueryMethodImplProvider.CMD_NAVIGATE_TO_IMPL, List.of(new GenAotQueryMethodImplProvider.GoToImplParams( - document.getId(), - mb.getDeclaringClass().getQualifiedName(), - mb.getName(), - Arrays.stream(mb.getParameterTypes()).map(p -> p.getQualifiedName()).toArray(String[]::new), - null - ))); - codeLenses.add(new CodeLens(range, impl, null)); + if (mb != null && hierarchyAnnot != null) { - if (!isQueryAnnotated) { - String queryStatement = methodMetadata.get().getQueryStatement(); - if (queryStatement != null) { - Command queryTitle = new Command(); - queryTitle.setTitle(queryStatement); - codeLenses.add(new CodeLens(range, queryTitle, null)); + getMetadata(repositoryMetadataService, project, mb).ifPresent(metadata -> metadata.findMethod(mb).ifPresent(methodMetadata -> { + boolean isQueryAnnotated = hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_JPA_QUERY) + || hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_MONGODB_QUERY); + + if (!isQueryAnnotated) { + codeLenses.add(new CodeLens(range, refactorings.createFixCommand(COVERT_TO_QUERY_LABEL, createFixDescriptor(mb, document.getUri(), metadata.module(), methodMetadata)), null)); } - } + + Command impl = new Command("Implementation", GenAotQueryMethodImplProvider.CMD_NAVIGATE_TO_IMPL, List.of(new GenAotQueryMethodImplProvider.GoToImplParams( + document.getId(), + mb.getDeclaringClass().getQualifiedName(), + mb.getName(), + Arrays.stream(mb.getParameterTypes()).map(p -> p.getQualifiedName()).toArray(String[]::new), + null + ))); + codeLenses.add(new CodeLens(range, impl, null)); + + if (!isQueryAnnotated && methodMetadata != null) { + String queryStatement = methodMetadata.getQueryStatement(); + if (queryStatement != null) { + Command queryTitle = new Command(); + queryTitle.setTitle(queryStatement); + codeLenses.add(new CodeLens(range, queryTitle, null)); + } + } + })); + + Command refreshCmd = new Command("Refresh", DataRepositoryAotMetadataService.CMD_REFESH_METADATA, List.of(project.getElementName())); + codeLenses.add(new CodeLens(range, refreshCmd, null)); + } } catch (BadLocationException e) { log.error("bad location while calculating code lens for data repository query method", e); @@ -179,13 +186,13 @@ private List createCodeLenses(MethodDeclaration node, TextDocument doc return codeLenses; } - static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryAotMetadata metadata, IDataRepositoryAotMethodMetadata methodMetadata) { + static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, DataRepositoryModule module, IDataRepositoryAotMethodMetadata methodMetadata) { return new FixDescriptor(AddAnnotationOverMethod.class.getName(), List.of(docUri), "Turn into `@Query`") .withRecipeScope(RecipeScope.FILE) .withParameters(Map.of( - "annotationType", metadata.module() == DataRepositoryModule.JPA ? Annotations.DATA_JPA_QUERY : Annotations.DATA_MONGODB_QUERY, + "annotationType", module == DataRepositoryModule.JPA ? Annotations.DATA_JPA_QUERY : Annotations.DATA_MONGODB_QUERY, "method", "%s %s(%s)".formatted(mb.getDeclaringClass().getQualifiedName(), mb.getName(), Arrays.stream(mb.getParameterTypes()) .map(pt -> pt.getQualifiedName()) @@ -203,7 +210,5 @@ private static List createAttributeList(Map() { + private static final Object MAVEN_LOCK = new Object(); + + // Cache: file path -> parsed metadata (Optional.empty() if file doesn't exist or failed to parse) + private final ConcurrentMap> metadataCache = new ConcurrentHashMap<>(); + + private final Gson gson = new GsonBuilder().registerTypeAdapter(DataRepositoryAotMetadata.class, new JsonDeserializer() { @Override public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT, @@ -69,36 +94,81 @@ public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT, }).create(); - public DataRepositoryAotMetadata getRepositoryMetadata(IJavaProject project, String repositoryType) { - try { - String metadataFilePath = repositoryType.replace('.', '/') + ".json"; - - Optional metadataFile = IClasspathUtil.getOutputFolders(project.getClasspath()) - .map(outputFolder -> outputFolder.getParentFile().toPath().resolve("spring-aot/main/resources/").resolve(metadataFilePath)) - .filter(Files::isRegularFile) - .map(p -> p.toFile()) - .findFirst(); - - if (metadataFile.isPresent()) { - return readMetadataFile(metadataFile.get()); - } - - } catch (Exception e) { - log.error("error finding spring data repository definition metadata file", e); + public DataRepositoryAotMetadataService(SimpleLanguageServer server, FileObserver fileObserver, JavaProjectFinder projectFinder) { + server.onCommand(CMD_REFESH_METADATA, params -> { + Object o = params.getArguments().get(0); + String projectName = o instanceof JsonPrimitive ? ((JsonPrimitive) o).getAsString() : o.toString(); + return projectFinder.all().stream().filter(jp -> projectName.equals(jp.getElementName())).findFirst() + .map(DataRepositoryAotMetadataService.this::regenerateAotMetadata) + .orElse(CompletableFuture.failedFuture( + new IllegalArgumentException("Cannot find project with name '%s'".formatted(projectName)))); + }); + if (fileObserver != null) { + fileObserver.onAnyChange(List.of("**/spring-aot/main/resources/**/*.json"), changedFiles -> { + List removedEntries = new ArrayList<>(); + for (String filePath : changedFiles) { + Path path = Path.of(filePath); + Optional removed = metadataCache.remove(path); + if (removed != null) { + removedEntries.add(path); + } + } + if (!removedEntries.isEmpty()) { + + } + }); } - - return null; } - private DataRepositoryAotMetadata readMetadataFile(File file) { + public Optional getRepositoryMetadata(IJavaProject project, String repositoryType) { + String metadataFilePath = repositoryType.replace('.', '/') + ".json"; - try (FileReader reader = new FileReader(file)) { - DataRepositoryAotMetadata result = gson.fromJson(reader, DataRepositoryAotMetadata.class); - return result; + return IClasspathUtil.getOutputFolders(project.getClasspath()) + .map(outputFolder -> outputFolder.getParentFile().toPath().resolve("spring-aot/main/resources/").resolve(metadataFilePath)) + .findFirst() + .flatMap(filePath -> metadataCache.computeIfAbsent(filePath, this::readMetadataFile)); + } + + private Optional readMetadataFile(Path filePath) { + if (Files.isRegularFile(filePath)) { + try (BufferedReader reader = Files.newBufferedReader(filePath)) { + return Optional.ofNullable(gson.fromJson(reader, DataRepositoryAotMetadata.class)); + } catch (IOException e) { + log.error("Failed to read metadata file: {}", filePath, e); + } } - catch (IOException e) { - return null; + return Optional.empty(); + } + + public CompletableFuture regenerateAotMetadata(IJavaProject jp) { + switch (jp.getProjectBuild().getType()) { + case ProjectBuild.MAVEN_PROJECT_TYPE: + return CompletableFuture.runAsync(() -> mavenRegenerateMetadata(jp)); } + return CompletableFuture.failedFuture(new IllegalStateException("Cannot generate AOT metadata")); } -} + private CompletableFuture mavenRegenerateMetadata(IJavaProject jp) { + synchronized(MAVEN_LOCK) { + List cmd = new ArrayList<>(); + Path projectPath = Paths.get(jp.getLocationUri()); + String mvnw = OS.isWindows() ? "mvnw.bat" : "./mvnw"; + cmd.add(Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"); + if (!IClasspathUtil.getOutputFolders(jp.getClasspath()).map(f -> f.toPath()).allMatch(Files::isDirectory)) { + // Check if source is compiled by checking that all output folders exist + // If not compiled then add `compile` goal + cmd.add("compile"); + } + cmd.add("org.springframework.boot:spring-boot-maven-plugin:process-aot"); + try { + return Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()]), null, projectPath.toFile()).onExit().thenAccept(process -> { + if (process.exitValue() != 0) { + throw new CompletionException("Failed to generate AOT metadata", new IllegalStateException("Errors running maven command: %s".formatted(String.join(" ", cmd)))); + } + }); + } catch (IOException e) { + throw new CompletionException(e); + } + } + } +} \ No newline at end of file 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 a04441e1fd..836b9e0b59 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 @@ -208,13 +208,12 @@ else if (annotation instanceof NormalAnnotation) { IMethodBinding methodBinding = method.resolveBinding(); final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim(); - DataRepositoryAotMetadata repositoryMetadata = this.repositoryMetadataService.getRepositoryMetadata(context.getProject(), repositoryClass); + DataRepositoryAotMetadata repositoryMetadata = this.repositoryMetadataService.getRepositoryMetadata(context.getProject(), repositoryClass).orElse(null); if (repositoryMetadata == null) { return null; } - IDataRepositoryAotMethodMetadata aotMethod = repositoryMetadata.findMethod(methodBinding); - return aotMethod == null ? null : aotMethod.getQueryStatement(); + return repositoryMetadata.findMethod(methodBinding).map(aotMethod -> aotMethod.getQueryStatement()).orElse(null); } protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java index 1031f9b209..732d9c1f6d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java @@ -11,7 +11,6 @@ package org.springframework.ide.vscode.boot.java.data; import java.net.URI; -import java.util.Optional; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; @@ -72,13 +71,11 @@ public boolean visit(MethodDeclaration node) { && !hierarchyAnnot.isAnnotatedWith(binding, Annotations.DATA_JPA_QUERY) && !hierarchyAnnot.isAnnotatedWith(binding, Annotations.DATA_MONGODB_QUERY)) { - Optional metadata = DataRepositoryAotMetadataCodeLensProvider.getMetadata(repositoryMetadataService, project, binding); - if (metadata.isPresent()) { - Optional methodMetadata = DataRepositoryAotMetadataCodeLensProvider.getMethodMetadata(repositoryMetadataService, metadata.get(), binding); - methodMetadata - .map(method -> createCodeAction(binding, docURI, metadata.get(), method)) - .ifPresent(collector::accept); - } + DataRepositoryAotMetadataCodeLensProvider + .getMetadata(repositoryMetadataService, project, binding) + .ifPresent(metadata -> metadata.findMethod(binding) + .map(method -> createCodeAction(binding, docURI, metadata, method)) + .ifPresent(collector::accept)); } } return super.visit(node); @@ -91,7 +88,7 @@ public boolean visit(MethodDeclaration node) { private CodeAction createCodeAction(IMethodBinding mb, URI docUri, DataRepositoryAotMetadata metadata, IDataRepositoryAotMethodMetadata method) { CodeAction ca = new CodeAction(); - ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata, method))); + ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), metadata.module(), method))); ca.setTitle(TITLE); ca.setKind(CodeActionKind.Refactor); return ca; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java index e83104fc74..524821be26 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java @@ -88,8 +88,10 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception { Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString()); List cls = editor.getCodeLenses("usersWithUsernamesStartingWith", 1); - assertEquals(1, cls.size()); + assertEquals(2, cls.size()); assertEquals("Implementation", cls.get(0).getCommand().getTitle()); assertEquals(1, cls.get(0).getCommand().getArguments().size()); + assertEquals("Refresh", cls.get(1).getCommand().getTitle()); + assertEquals(1, cls.get(1).getCommand().getArguments().size()); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java index 5292733d04..ec3f431c45 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java @@ -100,8 +100,10 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception { Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString()); List cls = editor.getCodeLenses("usersWithUsernamesStartingWith", 1); - assertEquals(1, cls.size()); + assertEquals(2, cls.size()); assertEquals("Implementation", cls.get(0).getCommand().getTitle()); assertEquals(1, cls.get(0).getCommand().getArguments().size()); + assertEquals("Refresh", cls.get(0).getCommand().getTitle()); + assertEquals(1, cls.get(1).getCommand().getArguments().size()); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java index 2d49b3fa81..d682519b04 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java @@ -12,6 +12,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import java.net.URI; import java.util.Arrays; @@ -52,6 +54,7 @@ public class DataRepositoryAotMetadataServiceTest { @Autowired private JavaProjectFinder projectFinder; @Autowired private SpringSymbolIndex indexer; @Autowired private CompilationUnitCache cuCache; + @Autowired private DataRepositoryAotMetadataService service; private IJavaProject testProject; @@ -71,9 +74,7 @@ public void setup() throws Exception { @Test void testBasicRepositoryAotMetadataLookup() throws Exception { - DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService(); - - DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository"); + DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null); assertNotNull(metadata); assertEquals("example.springdata.aot.UserRepository", metadata.name()); assertEquals(DataRepositoryModule.JPA, metadata.module()); @@ -81,9 +82,8 @@ void testBasicRepositoryAotMetadataLookup() throws Exception { @Test void testRepositoryMethodsIntMetadata() throws Exception { - DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService(); - DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository"); + DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null); IDataRepositoryAotMethodMetadata[] methods = metadata.methods(); assertEquals(32, methods.length); @@ -104,8 +104,7 @@ void testRepositoryMethodsIntMetadata() throws Exception { @Test void testRepositoryMethodsMatching() throws Exception { - DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService(); - DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository"); + DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null); URI docUri = testProject.getLocationUri().resolve("src/main/java/example/springdata/aot/UserRepository.java"); cuCache.withCompilationUnit(testProject, docUri, cu -> { @@ -113,7 +112,7 @@ void testRepositoryMethodsMatching() throws Exception { public boolean visit(MethodDeclaration node) { IMethodBinding binding = node.resolveBinding(); - IDataRepositoryAotMethodMetadata method = metadata.findMethod(binding); + IDataRepositoryAotMethodMetadata method = metadata.findMethod(binding).orElse(null); assertNotNull(method); if (method.getName().equals("findUserByLastnameStartingWith") && binding.getParameterTypes().length == 1) { @@ -131,5 +130,27 @@ else if (method.getName().equals("findUserByLastnameStartingWith") && binding.ge }); } + + @Test + void testCachingFunctionality() throws Exception { + // First call should load and cache the metadata + DataRepositoryAotMetadata metadata1 = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null); + assertNotNull(metadata1); + + // Second call should return the same cached instance + DataRepositoryAotMetadata metadata2 = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null); + assertSame(metadata1, metadata2, "Should return the same cached instance"); + } + + @Test + void testNegativeCaching() throws Exception { + // First call to non-existent repository should return null and cache the negative result + DataRepositoryAotMetadata metadata1 = service.getRepositoryMetadata(testProject, "nonexistent.Repository").orElse(null); + assertNull(metadata1); + + // Second call should also return null (from cache, not file system check) + DataRepositoryAotMetadata metadata2 = service.getRepositoryMetadata(testProject, "nonexistent.Repository").orElse(null); + assertNull(metadata2); + } } From 3dacb48dcdbb32e4dd6e2f82060af1261351ad75 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 12 Nov 2025 20:30:47 -0800 Subject: [PATCH 04/13] WIP: Fix typo Signed-off-by: BoykoAlex --- .../DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java index ec3f431c45..7fe6d45dcb 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java @@ -103,7 +103,7 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception { assertEquals(2, cls.size()); assertEquals("Implementation", cls.get(0).getCommand().getTitle()); assertEquals(1, cls.get(0).getCommand().getArguments().size()); - assertEquals("Refresh", cls.get(0).getCommand().getTitle()); + assertEquals("Refresh", cls.get(1).getCommand().getTitle()); assertEquals(1, cls.get(1).getCommand().getArguments().size()); } } From 34dfc6d23afd1294e32c32fe5257d9e9f87df32e Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 12 Nov 2025 23:36:11 -0800 Subject: [PATCH 05/13] Typo correction Signed-off-by: BoykoAlex --- .../boot/java/data/DataRepositoryAotMetadataService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 index 651ed9cfeb..5d45d3154c 100644 --- 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 @@ -13,6 +13,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.lang.reflect.Type; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -23,6 +24,7 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -106,15 +108,15 @@ public DataRepositoryAotMetadataService(SimpleLanguageServer server, FileObserve if (fileObserver != null) { fileObserver.onAnyChange(List.of("**/spring-aot/main/resources/**/*.json"), changedFiles -> { List removedEntries = new ArrayList<>(); - for (String filePath : changedFiles) { - Path path = Path.of(filePath); + for (String fileUri : changedFiles) { + Path path = Paths.get(URI.create(fileUri)); Optional removed = metadataCache.remove(path); if (removed != null) { removedEntries.add(path); } } if (!removedEntries.isEmpty()) { - + log.info("Spring AOT Metadata refreshed: %s".formatted(removedEntries.stream().map(p -> p.toString()).collect(Collectors.joining(", ")))); } }); } From f7191bebb7d8358acfe16157b1f9998a5e6058c0 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 13 Nov 2025 18:13:01 -0800 Subject: [PATCH 06/13] Eclipse and VSCode command to execute maven goals Signed-off-by: BoykoAlex --- .../META-INF/MANIFEST.MF | 4 +- .../plugin.xml | 20 ++++ .../commands/ExecuteMavenGoalHandler.java | 101 ++++++++++++++++++ .../ls/commons/classpath/ClasspathUtil.java | 14 ++- .../boot/app/BootLanguageServerBootApp.java | 5 +- .../ide/vscode/boot/app/JdtConfig.java | 6 ++ .../BootJavaLanguageServerComponents.java | 2 +- .../boot/java/BuildCommandProvider.java | 20 ++++ .../boot/java/VSCodeBuildCommandProvider.java | 30 ++++++ ...RepositoryAotMetadataCodeLensProvider.java | 19 +++- .../DataRepositoryAotMetadataService.java | 78 ++++++-------- ...oryAotMetadataCodeLensProviderJpaTest.java | 2 +- ...otMetadataCodeLensProviderMongoDbTest.java | 2 +- .../vscode-spring-boot/package.json | 3 +- 14 files changed, 248 insertions(+), 58 deletions(-) create mode 100644 eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/ExecuteMavenGoalHandler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BuildCommandProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/VSCodeBuildCommandProvider.java diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF index c27268d6ac..d285908bee 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/META-INF/MANIFEST.MF @@ -24,7 +24,9 @@ Require-Bundle: org.eclipse.jdt.launching;bundle-version="3.8.0", org.springsource.ide.eclipse.commons.core, org.eclipse.e4.ui.css.swt.theme, org.eclipse.swt, - com.google.gson + com.google.gson, + org.eclipse.m2e.launching, + org.eclipse.m2e.core Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Export-Package: org.springframework.tooling.ls.eclipse.commons, diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml index 8dcb1abdc8..466b0b337c 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml @@ -107,6 +107,10 @@ class="org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor" commandId="org.springframework.tooling.ls.eclipse.commons.commands.OpenJarEntryInEditor"> + + @@ -195,6 +199,22 @@ optional="false"> + + + + + + diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/ExecuteMavenGoalHandler.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/ExecuteMavenGoalHandler.java new file mode 100644 index 0000000000..a547d55eab --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/ExecuteMavenGoalHandler.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.tooling.ls.eclipse.commons.commands; + +import java.nio.file.Paths; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationType; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.IDebugUIConstants; +import org.eclipse.debug.ui.RefreshTab; +import org.eclipse.lsp4e.LSPEclipseUtils; +import org.eclipse.lsp4e.command.LSPCommandHandler; +import org.eclipse.lsp4j.Command; +import org.eclipse.m2e.actions.MavenLaunchConstants; + +import org.eclipse.m2e.core.MavenPlugin; +import org.eclipse.m2e.core.internal.IMavenConstants; +import org.eclipse.m2e.core.project.IMavenProjectFacade; +import org.eclipse.m2e.core.project.IMavenProjectRegistry; +import org.eclipse.m2e.core.project.IProjectConfiguration; + +import com.google.gson.Gson; + +@SuppressWarnings("restriction") +public class ExecuteMavenGoalHandler extends AbstractHandler { + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + Command cmd = new Gson().fromJson(event.getParameter(LSPCommandHandler.LSP_COMMAND_PARAMETER_ID), Command.class); + if (cmd != null) { + try { + String pomPath = (String) cmd.getArguments().get(0); + String goal = (String) cmd.getArguments().get(1); + System.out.println("Project: '%s', goal: '%s'".formatted(pomPath, goal)); + + IResource pomFile = LSPEclipseUtils.findResourceFor(Paths.get(pomPath).toUri()); + ILaunchConfiguration launchConfig = createLaunchConfiguration(pomFile.getParent(), goal); + + DebugUITools.launch(launchConfig, ILaunchManager.RUN_MODE); + } catch (Exception e) { + throw new ExecutionException("Failed to execute Maven Goal command", e); + } + } + throw new ExecutionException("Maven Goal Execution command is invalid"); + } + + private ILaunchConfiguration createLaunchConfiguration(IContainer basedir, String goal) throws CoreException { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType launchConfigurationType = launchManager + .getLaunchConfigurationType(MavenLaunchConstants.LAUNCH_CONFIGURATION_TYPE_ID); + + String safeConfigName = launchManager.generateLaunchConfigurationName("Goal `%s`".formatted(goal)); + + ILaunchConfigurationWorkingCopy workingCopy = launchConfigurationType.newInstance(null, safeConfigName); + workingCopy.setAttribute(MavenLaunchConstants.ATTR_POM_DIR, basedir.getLocation().toOSString()); + workingCopy.setAttribute(MavenLaunchConstants.ATTR_GOALS, goal); + workingCopy.setAttribute(IDebugUIConstants.ATTR_PRIVATE, true); + workingCopy.setAttribute(RefreshTab.ATTR_REFRESH_SCOPE, "${project}"); //$NON-NLS-1$ + workingCopy.setAttribute(RefreshTab.ATTR_REFRESH_RECURSIVE, true); + + setProjectConfiguration(workingCopy, basedir); + + return workingCopy; + } + + private void setProjectConfiguration(ILaunchConfigurationWorkingCopy workingCopy, IContainer basedir) { + IMavenProjectRegistry projectManager = MavenPlugin.getMavenProjectRegistry(); + IFile pomFile = basedir.getFile(IPath.fromOSString(IMavenConstants.POM_FILE_NAME)); + IMavenProjectFacade projectFacade = projectManager.create(pomFile, false, new NullProgressMonitor()); + if (projectFacade != null) { + IProjectConfiguration configuration = projectFacade.getConfiguration(); + + String selectedProfiles = configuration.getSelectedProfiles(); + if (selectedProfiles != null && selectedProfiles.length() > 0) { + workingCopy.setAttribute(MavenLaunchConstants.ATTR_PROFILES, selectedProfiles); + } + } + } + +} diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java index 7e98eb04ca..75e489f2cd 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/src/org/springframework/tooling/jdt/ls/commons/classpath/ClasspathUtil.java @@ -15,7 +15,9 @@ import java.io.File; import java.net.MalformedURLException; +import java.net.URI; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -244,14 +246,14 @@ public static ProjectBuild createProjectBuild(IJavaProject jp, Logger logger) { // if (facade != null) { // return ProjectBuild.createMavenBuild(facade.getPom().getLocationURI().toASCIIString()); // } else { - return ProjectBuild.createMavenBuild(jp.getProject().getFile("pom.xml").getLocationURI().toASCIIString()); + return ProjectBuild.createMavenBuild(fromIFile(jp.getProject().getFile("pom.xml")).toASCIIString()); // } } else if (GradleProjectNature.isPresentOn(jp.getProject())) { IFile g = jp.getProject().getFile("build.gradle"); if (!g.exists()) { g = jp.getProject().getFile("build.gradle.kts"); } - return ProjectBuild.createGradleBuild(g.exists() ? g.getLocationURI().toASCIIString() : null); + return ProjectBuild.createGradleBuild(g.exists() ? fromIFile(g).toASCIIString() : null); } else { try { for (IClasspathEntry e : jp.getRawClasspath()) { @@ -271,7 +273,7 @@ public static ProjectBuild createProjectBuild(IJavaProject jp, Logger logger) { if (likelyMaven) { IFile f = jp.getProject().getFile("pom.xml"); if (f.exists()) { - return ProjectBuild.createMavenBuild(f.getLocationURI().toASCIIString()); + return ProjectBuild.createMavenBuild(fromIFile(f).toASCIIString()); } } else if (likelyGradle) { IFile g = jp.getProject().getFile("build.gradle"); @@ -279,7 +281,7 @@ public static ProjectBuild createProjectBuild(IJavaProject jp, Logger logger) { g = jp.getProject().getFile("build.gradle.kts"); } if (g.exists()) { - return ProjectBuild.createGradleBuild(g.getLocationURI().toASCIIString()); + return ProjectBuild.createGradleBuild(fromIFile(g).toASCIIString()); } } // } catch (JavaModelException e) { @@ -287,4 +289,8 @@ public static ProjectBuild createProjectBuild(IJavaProject jp, Logger logger) { // } return new ProjectBuild(null, null); } + + private static URI fromIFile(IFile f) { + return Paths.get(f.getLocationURI()).toUri(); + } } 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 831d67a2a7..e69c09293d 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 @@ -46,6 +46,7 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCache; import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDiscDeltaBased; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; +import org.springframework.ide.vscode.boot.java.BuildCommandProvider; import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.NamedDefinitionProvider; @@ -436,8 +437,8 @@ ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder p } @Bean - DataRepositoryAotMetadataService dataAotMetadataService(SimpleLanguageServer server, FileObserver fileObserver, JavaProjectFinder projectFinder) { - return new DataRepositoryAotMetadataService(server, fileObserver, projectFinder); + DataRepositoryAotMetadataService dataAotMetadataService(FileObserver fileObserver, JavaProjectFinder projectFinder, BuildCommandProvider buildCmds) { + return new DataRepositoryAotMetadataService(fileObserver, projectFinder, buildCmds); } @Bean diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index 410f5d643e..dc6b573311 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -18,6 +18,7 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.VSCodeBuildCommandProvider; import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider; import org.springframework.ide.vscode.boot.java.codeaction.JdtCodeActionHandler; import org.springframework.ide.vscode.boot.java.cron.CronExpressionsInlayHintsProvider; @@ -228,4 +229,9 @@ public class JdtConfig { @Bean JdtCodeActionHandler jdtCodeActionHandler(CompilationUnitCache cuCache, Collection providers) { return new JdtCodeActionHandler(cuCache, providers); } + + @Bean VSCodeBuildCommandProvider vsCodeBuildCommandProvider() { + return new VSCodeBuildCommandProvider(); + } + } 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 2c04082c6f..c21c830c82 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 @@ -327,7 +327,7 @@ protected BootJavaCodeLensEngine createCodeLensEngine(SpringMetamodelIndex sprin codeLensProvider.add(new WebfluxHandlerCodeLensProvider(springIndex)); codeLensProvider.add(new CopilotCodeLensProvider(projectFinder, server, spelSemanticTokens)); codeLensProvider.add(new RouterFunctionCodeLensProvider()); - codeLensProvider.add(new DataRepositoryAotMetadataCodeLensProvider(projectFinder, repositoryAotMetadataService, refactorings, config)); + codeLensProvider.add(new DataRepositoryAotMetadataCodeLensProvider(server, projectFinder, repositoryAotMetadataService, refactorings, config)); codeLensProvider.add(new WebConfigCodeLensProvider(projectFinder, springIndex, config)); return new BootJavaCodeLensEngine(this, codeLensProvider); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BuildCommandProvider.java new file mode 100644 index 0000000000..4b9cbb08e9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BuildCommandProvider.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java; + +import org.eclipse.lsp4j.Command; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public interface BuildCommandProvider { + + Command executeMavenGoal(IJavaProject project, String goal); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/VSCodeBuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/VSCodeBuildCommandProvider.java new file mode 100644 index 0000000000..842acac34a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/VSCodeBuildCommandProvider.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java; + +import java.nio.file.Paths; +import java.util.List; + +import org.eclipse.lsp4j.Command; +import org.springframework.ide.vscode.commons.java.IJavaProject; + +public class VSCodeBuildCommandProvider implements BuildCommandProvider { + + @Override + public Command executeMavenGoal(IJavaProject project, String goal) { + Command cmd = new Command(); + cmd.setCommand("maven.goal.custom"); + cmd.setTitle("Execute Maven Goal"); + cmd.setArguments(List.of(Paths.get(project.getProjectBuild().getBuildFile()).toFile().toString(), goal)); + return cmd; + } + +} 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 index b20b9a6424..e047c3bc2c 100644 --- 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 @@ -40,6 +40,7 @@ import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; import org.springframework.ide.vscode.commons.rewrite.java.AddAnnotationOverMethod; import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; @@ -60,12 +61,22 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid private final RewriteRefactorings refactorings; private final BootJavaConfig config; - public DataRepositoryAotMetadataCodeLensProvider(JavaProjectFinder projectFinder, DataRepositoryAotMetadataService repositoryMetadataService, + public DataRepositoryAotMetadataCodeLensProvider(SimpleLanguageServer server, JavaProjectFinder projectFinder, DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings, BootJavaConfig config) { this.projectFinder = projectFinder; this.repositoryMetadataService = repositoryMetadataService; this.refactorings = refactorings; this.config = config; + + listenForAotMetadataChanges(server); + } + + private void listenForAotMetadataChanges(SimpleLanguageServer server) { + repositoryMetadataService.addListener(files -> { + if (config.isEnabledCodeLensOverDataQueryMethods()) { + server.getClient().refreshCodeLenses(); + } + }); } @Override @@ -166,7 +177,7 @@ private List createCodeLenses(IJavaProject project, MethodDeclaration ))); codeLenses.add(new CodeLens(range, impl, null)); - if (!isQueryAnnotated && methodMetadata != null) { + if (!isQueryAnnotated) { String queryStatement = methodMetadata.getQueryStatement(); if (queryStatement != null) { Command queryTitle = new Command(); @@ -176,7 +187,9 @@ private List createCodeLenses(IJavaProject project, MethodDeclaration } })); - Command refreshCmd = new Command("Refresh", DataRepositoryAotMetadataService.CMD_REFESH_METADATA, List.of(project.getElementName())); + String refreshCmdTitle = codeLenses.isEmpty() ? "Show AOT-generated Implementation, Query, etc..." : "Refresh"; + Command refreshCmd = repositoryMetadataService.regenerateMetadataCommand(project); + refreshCmd.setTitle(refreshCmdTitle); codeLenses.add(new CodeLens(range, refreshCmd, null)); } 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 index 5d45d3154c..39cf64ac51 100644 --- 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 @@ -20,21 +20,21 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; import java.util.stream.Collectors; +import org.eclipse.lsp4j.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.BuildCommandProvider; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; -import org.springframework.ide.vscode.commons.languageserver.util.OS; -import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild; import org.springframework.ide.vscode.commons.util.FileObserver; +import org.springframework.ide.vscode.commons.util.ListenerList; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -43,7 +43,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; /** * Service for loading and caching Spring Data repository AOT metadata. @@ -55,8 +54,6 @@ */ public class DataRepositoryAotMetadataService { - static final String CMD_REFESH_METADATA = "sts/boot/aot/refresh-metadata"; - private static final String MODULE_JSON_PROP = "module"; private static final String TYPE_JSON_PROP = "type"; @@ -67,7 +64,9 @@ public class DataRepositoryAotMetadataService { private static final Logger log = LoggerFactory.getLogger(DataRepositoryAotMetadataService.class); - private static final Object MAVEN_LOCK = new Object(); + private ListenerList>> listeners; + + private BuildCommandProvider buildCmds; // Cache: file path -> parsed metadata (Optional.empty() if file doesn't exist or failed to parse) private final ConcurrentMap> metadataCache = new ConcurrentHashMap<>(); @@ -96,27 +95,23 @@ public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT, }).create(); - public DataRepositoryAotMetadataService(SimpleLanguageServer server, FileObserver fileObserver, JavaProjectFinder projectFinder) { - server.onCommand(CMD_REFESH_METADATA, params -> { - Object o = params.getArguments().get(0); - String projectName = o instanceof JsonPrimitive ? ((JsonPrimitive) o).getAsString() : o.toString(); - return projectFinder.all().stream().filter(jp -> projectName.equals(jp.getElementName())).findFirst() - .map(DataRepositoryAotMetadataService.this::regenerateAotMetadata) - .orElse(CompletableFuture.failedFuture( - new IllegalArgumentException("Cannot find project with name '%s'".formatted(projectName)))); - }); + public DataRepositoryAotMetadataService(FileObserver fileObserver, JavaProjectFinder projectFinder, BuildCommandProvider buildCmds) { + this.buildCmds = buildCmds; + this.listeners = new ListenerList<>(); if (fileObserver != null) { fileObserver.onAnyChange(List.of("**/spring-aot/main/resources/**/*.json"), changedFiles -> { - List removedEntries = new ArrayList<>(); + List removedEntries = new ArrayList<>(); for (String fileUri : changedFiles) { - Path path = Paths.get(URI.create(fileUri)); + URI uri = URI.create(fileUri); + Path path = Paths.get(uri); Optional removed = metadataCache.remove(path); if (removed != null) { - removedEntries.add(path); + removedEntries.add(uri); } } if (!removedEntries.isEmpty()) { log.info("Spring AOT Metadata refreshed: %s".formatted(removedEntries.stream().map(p -> p.toString()).collect(Collectors.joining(", ")))); + notify(removedEntries); } }); } @@ -142,35 +137,30 @@ private Optional readMetadataFile(Path filePath) { return Optional.empty(); } - public CompletableFuture regenerateAotMetadata(IJavaProject jp) { + public Command regenerateMetadataCommand(IJavaProject jp) { switch (jp.getProjectBuild().getType()) { - case ProjectBuild.MAVEN_PROJECT_TYPE: - return CompletableFuture.runAsync(() -> mavenRegenerateMetadata(jp)); - } - return CompletableFuture.failedFuture(new IllegalStateException("Cannot generate AOT metadata")); - } - - private CompletableFuture mavenRegenerateMetadata(IJavaProject jp) { - synchronized(MAVEN_LOCK) { - List cmd = new ArrayList<>(); - Path projectPath = Paths.get(jp.getLocationUri()); - String mvnw = OS.isWindows() ? "mvnw.bat" : "./mvnw"; - cmd.add(Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"); + case ProjectBuild.MAVEN_PROJECT_TYPE: + List goal = new ArrayList<>(); if (!IClasspathUtil.getOutputFolders(jp.getClasspath()).map(f -> f.toPath()).allMatch(Files::isDirectory)) { // Check if source is compiled by checking that all output folders exist // If not compiled then add `compile` goal - cmd.add("compile"); - } - cmd.add("org.springframework.boot:spring-boot-maven-plugin:process-aot"); - try { - return Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()]), null, projectPath.toFile()).onExit().thenAccept(process -> { - if (process.exitValue() != 0) { - throw new CompletionException("Failed to generate AOT metadata", new IllegalStateException("Errors running maven command: %s".formatted(String.join(" ", cmd)))); - } - }); - } catch (IOException e) { - throw new CompletionException(e); + goal.add("compile"); } + goal.add("org.springframework.boot:spring-boot-maven-plugin:process-aot"); + return buildCmds.executeMavenGoal(jp, String.join(" ", goal)); } + return null; + } + + public void addListener(Consumer> listener) { + listeners.add(listener); + } + + public void removeListener(Consumer> listener) { + listeners.remove(listener); + } + + private void notify(List metadtaFiles) { + listeners.forEach(l -> l.accept(metadtaFiles)); } } \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java index 524821be26..b0897c7de6 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java @@ -92,6 +92,6 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception { assertEquals("Implementation", cls.get(0).getCommand().getTitle()); assertEquals(1, cls.get(0).getCommand().getArguments().size()); assertEquals("Refresh", cls.get(1).getCommand().getTitle()); - assertEquals(1, cls.get(1).getCommand().getArguments().size()); + assertEquals(2, cls.get(1).getCommand().getArguments().size()); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java index 7fe6d45dcb..d626d4bb60 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderMongoDbTest.java @@ -104,6 +104,6 @@ void noCodeLensOverMethodWithQueryAnnotation() throws Exception { assertEquals("Implementation", cls.get(0).getCommand().getTitle()); assertEquals(1, cls.get(0).getCommand().getArguments().size()); assertEquals("Refresh", cls.get(1).getCommand().getTitle()); - assertEquals(1, cls.get(1).getCommand().getArguments().size()); + assertEquals(2, cls.get(1).getCommand().getArguments().size()); } } diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index 33432044f3..9a3829fcba 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -1501,6 +1501,7 @@ "typescript": "^4.8.0" }, "extensionDependencies": [ - "redhat.java" + "redhat.java", + "vscjava.vscode-maven" ] } \ No newline at end of file From d44c6b23e90b925fafe1d82b19b6b76b8a1c1cec Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 14 Nov 2025 12:41:02 -0800 Subject: [PATCH 07/13] Tests Signed-off-by: BoykoAlex --- .../ide/vscode/boot/app/JdtConfig.java | 14 +- .../java/DefaultBuildCommandProvider.java | 81 ++++++ ...RepositoryAotMetadataCodeLensProvider.java | 40 +-- .../data/QueryMethodCodeActionProvider.java | 4 + ...sitoryAotMetadataCodeLensProviderTest.java | 103 +++++++ ...taRepositoryNoAotMetadataCodeLensTest.java | 83 ++++++ .../.mvn/wrapper/maven-wrapper.properties | 19 ++ .../data-repositories-jpa-4/mvnw | 259 ++++++++++++++++++ .../data-repositories-jpa-4/mvnw.cmd | 149 ++++++++++ .../data-repositories-jpa-4/pom.xml | 126 +++++++++ .../example/springdata/aot/AotJpaApp.java | 30 ++ .../main/java/example/springdata/aot/CLR.java | 124 +++++++++ .../java/example/springdata/aot/Post.java | 68 +++++ .../java/example/springdata/aot/User.java | 114 ++++++++ .../springdata/aot/UserProjection.java | 26 ++ .../springdata/aot/UserRepository.java | 57 ++++ .../src/main/resources/application.properties | 5 + 17 files changed, 1273 insertions(+), 29 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryNoAotMetadataCodeLensTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/.mvn/wrapper/maven-wrapper.properties create mode 100755 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/AotJpaApp.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/CLR.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/Post.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/User.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserProjection.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserRepository.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/resources/application.properties diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index dc6b573311..0a409aed86 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -18,6 +18,8 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.BuildCommandProvider; +import org.springframework.ide.vscode.boot.java.DefaultBuildCommandProvider; import org.springframework.ide.vscode.boot.java.VSCodeBuildCommandProvider; import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider; import org.springframework.ide.vscode.boot.java.codeaction.JdtCodeActionHandler; @@ -230,8 +232,16 @@ public class JdtConfig { return new JdtCodeActionHandler(cuCache, providers); } - @Bean VSCodeBuildCommandProvider vsCodeBuildCommandProvider() { - return new VSCodeBuildCommandProvider(); + @Bean BuildCommandProvider buildCommandProvider(SimpleLanguageServer server) { + switch(LspClient.currentClient()) { + case VSCODE: + case THEIA: + return new VSCodeBuildCommandProvider(); + case ECLIPSE: + return new VSCodeBuildCommandProvider(); + default: + return new DefaultBuildCommandProvider(server); + } } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java new file mode 100644 index 0000000000..821482aab3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import org.eclipse.lsp4j.Command; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.util.OS; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; + +import com.google.gson.JsonPrimitive; + +public class DefaultBuildCommandProvider implements BuildCommandProvider { + + private static final String CMD_EXEC_MAVEN_GOAL = "sts.maven.goal.custom"; + + private static final Object MAVEN_LOCK = new Object(); + + public DefaultBuildCommandProvider(SimpleLanguageServer server) { + server.onCommand(CMD_EXEC_MAVEN_GOAL, params -> { + String pomPath = extractString(params.getArguments().get(0)); + String goal = extractString(params.getArguments().get(1)); + return CompletableFuture.runAsync(() -> { + try { + mavenRegenerateMetadata(Paths.get(pomPath), goal.trim().split("\\s+")).get(); + } catch (Exception e) { + throw new CompletionException(e); + } + }); + }); + } + + @Override + public Command executeMavenGoal(IJavaProject project, String goal) { + Command cmd = new Command(); + cmd.setCommand(CMD_EXEC_MAVEN_GOAL); + cmd.setTitle("Execute Maven Goal"); + cmd.setArguments(List.of(Paths.get(project.getProjectBuild().getBuildFile()).toFile().toString(), goal)); + return cmd; + } + + private static String extractString(Object o) { + return o instanceof JsonPrimitive ? ((JsonPrimitive) o).getAsString() : o.toString(); + } + + private CompletableFuture mavenRegenerateMetadata(Path pom, String[] goal) { + synchronized(MAVEN_LOCK) { + String[] cmd = new String[1 + goal.length]; + Path projectPath = pom.getParent(); + String mvnw = OS.isWindows() ? "mvnw.bat" : "./mvnw"; + cmd[0] = Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"; + System.arraycopy(goal, 0, cmd, 1, goal.length); + try { + return Runtime.getRuntime().exec(cmd, null, projectPath.toFile()).onExit().thenAccept(process -> { + if (process.exitValue() != 0) { + throw new CompletionException("Failed to execute Maven goal", new IllegalStateException("Errors running maven command: %s".formatted(String.join(" ", cmd)))); + } + }); + } catch (IOException e) { + throw new CompletionException(e); + } + } + } + + +} 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 index e047c3bc2c..b487e6ed84 100644 --- 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 @@ -36,11 +36,10 @@ import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; -import org.springframework.ide.vscode.commons.Version; import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild; import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; import org.springframework.ide.vscode.commons.rewrite.java.AddAnnotationOverMethod; import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; @@ -100,32 +99,17 @@ static boolean isValidMethodBinding(IMethodBinding methodBinding) { return false; } + if (!methodBinding.getDeclaringClass().isInterface()) { + return false; + } + if (ASTUtils.findInTypeHierarchy(methodBinding.getDeclaringClass(), Set.of(Constants.REPOSITORY_TYPE)) == null) { return false; } return true; } - - static boolean isValidProject(IJavaProject jp) { - Version version = SpringProjectUtil.getDependencyVersion(jp, "spring-data-commons"); - return version != null && version.compareTo(new Version(4, 0, 0, null)) >= 0; - } - -// static Optional getDataQuery(DataRepositoryAotMetadataService repositoryMetadataService, IJavaProject project, IMethodBinding methodBinding) { -// final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim(); -// final IMethodBinding method = methodBinding.getMethodDeclaration(); -// -// DataRepositoryAotMetadata metadata = repositoryMetadataService.getRepositoryMetadata(project, repositoryClass); -// -// if (metadata != null) { -// return Optional.ofNullable(repositoryMetadataService.getQueryStatement(metadata, method)); -// } -// -// return Optional.empty(); -// -// } - + static Optional getMetadata(DataRepositoryAotMetadataService dataRepositoryAotMetadataService, IJavaProject project, IMethodBinding methodBinding) { final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim(); return dataRepositoryAotMetadataService.getRepositoryMetadata(project, repositoryClass); @@ -135,7 +119,7 @@ protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node cancelToken.checkCanceled(); IJavaProject project = projectFinder.find(document.getId()).get(); - if (project == null || !isValidProject(project)) { + if (project == null || !QueryMethodCodeActionProvider.isValidProject(project)) { return; } @@ -187,10 +171,12 @@ private List createCodeLenses(IJavaProject project, MethodDeclaration } })); - String refreshCmdTitle = codeLenses.isEmpty() ? "Show AOT-generated Implementation, Query, etc..." : "Refresh"; - Command refreshCmd = repositoryMetadataService.regenerateMetadataCommand(project); - refreshCmd.setTitle(refreshCmdTitle); - codeLenses.add(new CodeLens(range, refreshCmd, null)); + if (ProjectBuild.MAVEN_PROJECT_TYPE.equals(project.getProjectBuild().getType())) { + String refreshCmdTitle = codeLenses.isEmpty() ? "Show AOT-generated Implementation, Query, etc..." : "Refresh"; + Command refreshCmd = repositoryMetadataService.regenerateMetadataCommand(project); + refreshCmd.setTitle(refreshCmdTitle); + codeLenses.add(new CodeLens(range, refreshCmd, null)); + } } } catch (BadLocationException e) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java index 732d9c1f6d..462b45972f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java @@ -44,6 +44,10 @@ public QueryMethodCodeActionProvider(DataRepositoryAotMetadataService repository @Override public boolean isApplicable(IJavaProject project) { + return isValidProject(project); + } + + static boolean isValidProject(IJavaProject project) { Version springDataJpaVersion = SpringProjectUtil.getDependencyVersion(project, "spring-data-jpa"); Version springDataMongoDbVersion = SpringProjectUtil.getDependencyVersion(project, "spring-data-mongodb"); return (springDataJpaVersion != null && springDataJpaVersion.getMajor() >= 4) diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java new file mode 100644 index 0000000000..287aa9de90 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.CodeLens; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.ExecuteCommandParams; +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.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.Settings; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.google.gson.Gson; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class DataRepositoryAotMetadataCodeLensProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + + private IJavaProject testProject; + + @BeforeEach + public void setup() throws Exception { + testProject = ProjectsHarness.INSTANCE.mavenProject("data-repositories-jpa-4"); + harness.useProject(testProject); + harness.intialize(null); + + harness.changeConfiguration(new Settings(new Gson() + .toJsonTree(Map.of("boot-java", Map.of("java", Map.of("codelens-over-query-methods", true)))))); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(testProject.getLocationUri().toASCIIString())).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void generateMetadataCodeLens() throws Exception { + Path filePath = Paths.get(testProject.getLocationUri()) + .resolve("src/main/java/example/springdata/aot/UserRepository.java"); + Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString()); + + List cls = editor.getCodeLenses("findUserByUsername", 1); + assertEquals(1, cls.size()); + Command cmd = cls.get(0).getCommand(); + assertEquals("Show AOT-generated Implementation, Query, etc...", cmd.getTitle()); + + harness.getServer().getWorkspaceService().executeCommand(new ExecuteCommandParams(cmd.getCommand(), cmd.getArguments())).get(); + + Path metadataFile = Paths.get(testProject.getLocationUri()).resolve("target/spring-aot/main/resources/example/springdata/aot/UserRepository.json"); + + // Trigger file update and wait for the vene thandling to complete + assertTrue(Files.isRegularFile(metadataFile), "AOT mtadata JSON file should be generated"); + harness.createFile(metadataFile.toUri().toASCIIString()); + harness.getServer().getAsync().waitForAll(); + + cls = editor.getCodeLenses("findUserByUsername", 1); + assertTrue(cls.size() > 1); + assertEquals("Turn into @Query", cls.get(0).getCommand().getTitle()); + assertEquals("Implementation", cls.get(1).getCommand().getTitle()); + assertEquals("SELECT u FROM example.springdata.aot.User u WHERE u.username = :username", cls.get(2).getCommand().getTitle()); + assertEquals("Refresh", cls.get(3).getCommand().getTitle()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryNoAotMetadataCodeLensTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryNoAotMetadataCodeLensTest.java new file mode 100644 index 0000000000..0a16c3106c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryNoAotMetadataCodeLensTest.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data.test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.CodeLens; +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.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.Settings; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.google.gson.Gson; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class DataRepositoryNoAotMetadataCodeLensTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + + private IJavaProject testProject; + + @BeforeEach + public void setup() throws Exception { + testProject = ProjectsHarness.INSTANCE.mavenProject("test-spring-data-symbols"); + harness.useProject(testProject); + harness.intialize(null); + + harness.changeConfiguration(new Settings(new Gson() + .toJsonTree(Map.of("boot-java", Map.of("java", Map.of("codelens-over-query-methods", true)))))); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(testProject.getLocationUri().toASCIIString())).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void codeLensOverMethod() throws Exception { + Path filePath = Paths.get(testProject.getLocationUri()) + .resolve("src/main/java/org/test/CustomerRepository.java"); + Editor editor = harness.newEditor(LanguageId.JAVA, new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString()); + + List cls = editor.getCodeLenses("findByLastName", 1); + assertTrue(cls.isEmpty()); + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/.mvn/wrapper/maven-wrapper.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..2076438e03 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw new file mode 100755 index 0000000000..d7c358e5a2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd new file mode 100644 index 0000000000..6f779cff20 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml new file mode 100644 index 0000000000..d10cff02d1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.0-M3 + + + + org.example + data-repositories-jpa-4 + + + 21 + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.querydsl + querydsl-jpa + 5.1.0 + jakarta + + + com.querydsl + querydsl-apt + 5.1.0 + jakarta + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/AotJpaApp.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/AotJpaApp.java new file mode 100644 index 0000000000..5256d51e6c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/AotJpaApp.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.aot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Christoph Strobl + */ +@SpringBootApplication +public class AotJpaApp { + + public static void main(String[] args) { + SpringApplication.run(AotJpaApp.class, args); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/CLR.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/CLR.java new file mode 100644 index 0000000000..762f654add --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/CLR.java @@ -0,0 +1,124 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.aot; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Component; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Component +public class CLR implements CommandLineRunner { + + @Autowired UserRepository repository; + + @Override + public void run(String... args) throws Exception { + + User luke = new User("id-1", "luke"); + luke.setFirstname("Luke"); + luke.setLastname("Skywalker"); +// Post lukeP1 = new Post("I have a bad feeling about this."); +// em.persist(lukeP1); +// luke.setPosts(List.of(lukeP1)); + + User leia = new User("id-2", "leia"); + leia.setFirstname("Leia"); + leia.setLastname("Organa"); + + User han = new User("id-3", "han"); + han.setFirstname("Han"); + han.setLastname("Solo"); +// Post hanP1 = new Post("It's the ship that made the Kessel Run in less than 12 Parsecs."); +// em.persist(hanP1); +// han.setPosts(List.of(hanP1)); + + User chewbacca = new User("id-4", "chewbacca"); + User yoda = new User("id-5", "yoda"); + Post yodaP1 = new Post("Do. Or do not. There is no try."); + Post yodaP2 = new Post("Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered."); +// em.persist(yodaP1); +// em.persist(yodaP2); +// yoda.setPosts(List.of(yodaP1, yodaP2)); + + User vader = new User("id-6", "vader"); + vader.setFirstname("Anakin"); + vader.setLastname("Skywalker"); +// Post vaderP1 = new Post("I am your father"); +// em.persist(vaderP1); +// vader.setPosts(List.of(vaderP1)); + + User kylo = new User("id-7", "kylo"); + kylo.setFirstname("Ben"); + kylo.setLastname("Solo"); + + repository.saveAll(List.of(luke, leia, han, chewbacca, yoda, vader, kylo)); + + System.out.println("------- annotated multi -------"); + System.out.println(repository.usersWithUsernamesStartingWith("l")); + + System.out.println("------- derived single -------"); + System.out.println(repository.findUserByUsername("yoda")); + +// System.out.println("------- derived nested.path -------"); +// System.out.println(repository.findUserByPostsMessageLike("father")); + + System.out.println("------- derived optional -------"); + System.out.println(repository.findOptionalUserByUsername("yoda")); + + System.out.println("------- derived count -------"); + Long count = repository.countUsersByLastnameLike("Sky"); + System.out.println("user count " + count); + + System.out.println("------- derived exists -------"); + Boolean exists = repository.existsByUsername("vader"); + System.out.println("user exists " + exists); + + System.out.println("------- derived multi -------"); + System.out.println(repository.findUserByLastnameStartingWith("Sky")); + + System.out.println("------- derived sorted -------"); + System.out.println(repository.findUserByLastnameStartingWithOrderByFirstname("Sky")); + + System.out.println("------- derived page -------"); + Page page0 = repository.findUserByLastnameStartingWith("S", PageRequest.of(0, 2)); + System.out.println("page0: " + page0); + System.out.println("page0.content: " + page0.getContent()); + + Page page1 = repository.findUserByLastnameStartingWith("S", PageRequest.of(1, 2)); + System.out.println("page1: " + page1); + System.out.println("page1.content: " + page1.getContent()); + + System.out.println("------- derived slice -------"); + Slice slice0 = repository.findUserByUsernameAfter("luke", PageRequest.of(0, 2)); + System.out.println("slice0: " + slice0); + System.out.println("slice0.content: " + slice0.getContent()); + + System.out.println("------- derived top -------"); + System.out.println(repository.findTop2UsersByLastnameStartingWith("S")); + +// System.out.println("------- derived with fields -------"); +// System.out.println(repository.findJustUsernameBy()); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/Post.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/Post.java new file mode 100644 index 0000000000..e950f2aabf --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/Post.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.aot; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Random; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Entity +public class Post { + + @Id + @GeneratedValue + private Long id; + + private String message; + private Instant date; + + public Post() { + } + + public Post(String message) { + this.message = message; + this.date = Instant.now().minus(new Random().nextLong(100), ChronoUnit.MINUTES); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Instant getDate() { + return date; + } + + public void setDate(Instant date) { + this.date = date; + } + + @Override + public String toString() { + return message; + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/User.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/User.java new file mode 100644 index 0000000000..64cbf5319b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/User.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.aot; + +import java.time.Instant; +import java.util.List; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@Entity(name = "users") +public class User { + + @Id + private String id; + private String username; + + @Column(name = "first_name") String firstname; + @Column(name = "last_name") String lastname; + +// @OneToMany +// private List posts; + + Instant registrationDate; + Instant lastSeen; + + public User() { + } + + public User(String id, String username) { + this.id = id; + this.username = username; + } + + public String getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Instant getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(Instant registrationDate) { + this.registrationDate = registrationDate; + } + + public Instant getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Instant lastSeen) { + this.lastSeen = lastSeen; + } + +// public List getPosts() { +// return posts; +// } +// +// public void setPosts(List posts) { +// this.posts = posts; +// } + + @Override + public String toString() { + return "User{" + + "id='" + id + '\'' + + ", username='" + username + '\'' + + ", firstname='" + firstname + '\'' + + ", lastname='" + lastname + '\'' + + ", registrationDate=" + registrationDate + + ", lastSeen=" + lastSeen + +// ", posts=" + posts + + '}'; + } +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserProjection.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserProjection.java new file mode 100644 index 0000000000..8c7803383c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserProjection.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.aot; + +import java.time.Instant; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public record UserProjection(String username, Instant registrationDate) { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserRepository.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserRepository.java new file mode 100644 index 0000000000..5ae0e92a7a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/java/example/springdata/aot/UserRepository.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.aot; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserRepository extends CrudRepository, QuerydslPredicateExecutor { + + User findUserByUsername(String username); + + Optional findOptionalUserByUsername(String username); + + Long countUsersByLastnameLike(String lastname); + + Boolean existsByUsername(String username); + + List findUserByLastnameLike(String lastname); + + List findUserByLastnameStartingWithOrderByFirstname(String lastname); + + List findTop2UsersByLastnameStartingWith(String lastname); + + Slice findUserByUsernameAfter(String username, Pageable pageable); + + List findUserByLastnameStartingWith(String lastname); + + Page findUserByLastnameStartingWith(String lastname, Pageable page); + + @Query("SELECT u FROM example.springdata.aot.User u WHERE u.username LIKE ?1%") + List usersWithUsernamesStartingWith(String username); + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/resources/application.properties b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/resources/application.properties new file mode 100644 index 0000000000..a036d6f149 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.jpa.defer-datasource-initialization=true +spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false +spring.aot.repositories.enabled=true +#logging.level.org.springframework.data.repository.aot.generate.RepositoryContributor=trace + From ac01cb63346e61579d555a285481c68b4de407b2 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 14 Nov 2025 13:25:44 -0800 Subject: [PATCH 08/13] Fix test on win Signed-off-by: BoykoAlex --- .../ide/vscode/boot/java/DefaultBuildCommandProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java index 821482aab3..3c0dfe4ecc 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java @@ -62,7 +62,7 @@ private CompletableFuture mavenRegenerateMetadata(Path pom, String[] goal) synchronized(MAVEN_LOCK) { String[] cmd = new String[1 + goal.length]; Path projectPath = pom.getParent(); - String mvnw = OS.isWindows() ? "mvnw.bat" : "./mvnw"; + String mvnw = OS.isWindows() ? "mvnw.cmd" : "./mvnw"; cmd[0] = Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"; System.arraycopy(goal, 0, cmd, 1, goal.length); try { From a2133b8633d5c6ee1334de6fcfc1a71ce177f318 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 14 Nov 2025 14:12:27 -0800 Subject: [PATCH 09/13] Attempt to fix test on win Signed-off-by: BoykoAlex --- .../ide/vscode/boot/java/DefaultBuildCommandProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java index 3c0dfe4ecc..da428db8b7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java @@ -27,7 +27,7 @@ public class DefaultBuildCommandProvider implements BuildCommandProvider { - private static final String CMD_EXEC_MAVEN_GOAL = "sts.maven.goal.custom"; + private static final String CMD_EXEC_MAVEN_GOAL = "sts.maven.goal"; private static final Object MAVEN_LOCK = new Object(); @@ -62,7 +62,7 @@ private CompletableFuture mavenRegenerateMetadata(Path pom, String[] goal) synchronized(MAVEN_LOCK) { String[] cmd = new String[1 + goal.length]; Path projectPath = pom.getParent(); - String mvnw = OS.isWindows() ? "mvnw.cmd" : "./mvnw"; + String mvnw = OS.isWindows() ? "./mvnw.cmd" : "./mvnw"; cmd[0] = Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"; System.arraycopy(goal, 0, cmd, 1, goal.length); try { From f92af84dab00095590c4936d3700d758c50c6b8f Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 14 Nov 2025 16:06:49 -0800 Subject: [PATCH 10/13] Boot RC2 and attempt to fix the test Signed-off-by: BoykoAlex --- headless-services/commons/pom.xml | 2 +- .../java/DefaultBuildCommandProvider.java | 2 +- .../DataRepositoryAotMetadataService.java | 25 ++++++++++++++++++- ...oryAotMetadataCodeLensProviderJpaTest.java | 2 +- ...sitoryAotMetadataCodeLensProviderTest.java | 2 +- .../QueryMethodCodeActionProviderJpaTest.java | 2 +- .../aot-data-repositories-jpa/pom.xml | 2 +- .../aot-data-repositories-mongodb/pom.xml | 2 +- .../data-repositories-jpa-4/mvnw.cmd | 0 .../data-repositories-jpa-4/pom.xml | 2 +- .../test-projects/sf7-validation/pom.xml | 2 +- .../test-httpexchange-indexing/pom.xml | 2 +- .../test-web-config-support/pom.xml | 2 +- 13 files changed, 35 insertions(+), 12 deletions(-) mode change 100644 => 100755 headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd diff --git a/headless-services/commons/pom.xml b/headless-services/commons/pom.xml index ca3f119e6d..f5f8ed81ec 100644 --- a/headless-services/commons/pom.xml +++ b/headless-services/commons/pom.xml @@ -13,7 +13,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java index da428db8b7..a8974c3eeb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java @@ -62,7 +62,7 @@ private CompletableFuture mavenRegenerateMetadata(Path pom, String[] goal) synchronized(MAVEN_LOCK) { String[] cmd = new String[1 + goal.length]; Path projectPath = pom.getParent(); - String mvnw = OS.isWindows() ? "./mvnw.cmd" : "./mvnw"; + String mvnw = OS.isWindows() ? ".\\mvnw.cmd" : "./mvnw"; cmd[0] = Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"; System.arraycopy(goal, 0, cmd, 1, goal.length); try { 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 index 39cf64ac51..bbd5af46d6 100644 --- 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 @@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.lsp4j.Command; import org.slf4j.Logger; @@ -114,6 +115,22 @@ public DataRepositoryAotMetadataService(FileObserver fileObserver, JavaProjectFi notify(removedEntries); } }); + fileObserver.onFilesDeleted(List.of("**/spring-aot", "**/spring-aot/main", "**/spring-aot/main/resources"), changedFiles -> { + // If `spring-aot` folder is deleted VSCode would only notify about the folder deletion, no events for each contained file + for (String fileUri : changedFiles) { + URI uri = URI.create(fileUri); + Path path = Paths.get(uri); + List removedEntries = metadataCache.keySet().stream() + .filter(p -> p.startsWith(path)) + .filter(p -> metadataCache.remove(p).isPresent()) + .map(p -> p.toUri()) + .toList(); + if (!removedEntries.isEmpty()) { + log.info("Spring AOT Metadata refreshed: %s".formatted(removedEntries.stream().map(p -> p.toString()).collect(Collectors.joining(", ")))); + notify(removedEntries); + } + } + }); } } @@ -141,7 +158,13 @@ public Command regenerateMetadataCommand(IJavaProject jp) { switch (jp.getProjectBuild().getType()) { case ProjectBuild.MAVEN_PROJECT_TYPE: List goal = new ArrayList<>(); - if (!IClasspathUtil.getOutputFolders(jp.getClasspath()).map(f -> f.toPath()).allMatch(Files::isDirectory)) { + if (!IClasspathUtil.getOutputFolders(jp.getClasspath()).map(f -> f.toPath()).filter(Files::isDirectory).flatMap(d -> { + try { + return Files.walk(d); + } catch (IOException e) { + return Stream.empty(); + } + }).anyMatch(f -> Files.isRegularFile(f) && f.getFileName().toString().endsWith(".class"))) { // Check if source is compiled by checking that all output folders exist // If not compiled then add `compile` goal goal.add("compile"); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java index b0897c7de6..46c3e7cfbe 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderJpaTest.java @@ -78,7 +78,7 @@ void codeLensOverMethod() throws Exception { List cls = editor.getCodeLenses("findUserByUsername", 1); assertEquals("Turn into @Query", cls.get(0).getCommand().getTitle()); assertEquals("Implementation", cls.get(1).getCommand().getTitle()); - assertEquals("SELECT u FROM example.springdata.aot.User u WHERE u.username = :username", cls.get(2).getCommand().getTitle()); + assertEquals("SELECT u FROM users u WHERE u.username = :username", cls.get(2).getCommand().getTitle()); } @Test diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java index 287aa9de90..f67fec2fa3 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java @@ -96,7 +96,7 @@ void generateMetadataCodeLens() throws Exception { assertTrue(cls.size() > 1); assertEquals("Turn into @Query", cls.get(0).getCommand().getTitle()); assertEquals("Implementation", cls.get(1).getCommand().getTitle()); - assertEquals("SELECT u FROM example.springdata.aot.User u WHERE u.username = :username", cls.get(2).getCommand().getTitle()); + assertEquals("SELECT u FROM users u WHERE u.username = :username", cls.get(2).getCommand().getTitle()); assertEquals("Refresh", cls.get(3).getCommand().getTitle()); } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderJpaTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderJpaTest.java index f993fc6aa3..32a8a1e0d9 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderJpaTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderJpaTest.java @@ -85,7 +85,7 @@ void convertToQueryCodeAction() throws Exception { WorkspaceEdit edit = refactorings.createEdit((JsonElement) cmd.getArguments().get(1)).get(5, TimeUnit.SECONDS); TextDocumentEdit docEdit = edit.getDocumentChanges().get(0).getLeft(); assertEquals( - "@Query(\"SELECT u FROM example.springdata.aot.User u WHERE u.lastname LIKE :lastname ESCAPE '\\\\' ORDER BY u.firstname asc\")", + "@Query(\"SELECT u FROM users u WHERE u.lastname LIKE :lastname ESCAPE '\\\\' ORDER BY u.firstname asc\")", docEdit.getEdits().get(0).getNewText().trim()); assertEquals(filePath.toUri().toASCIIString(), docEdit.getTextDocument().getUri()); } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-jpa/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-jpa/pom.xml index 3e379f3cda..aee1755424 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-jpa/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-mongodb/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-mongodb/pom.xml index 2660b3e23c..b5e24a3f99 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-mongodb/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/aot-data-repositories-mongodb/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/mvnw.cmd old mode 100644 new mode 100755 diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml index d10cff02d1..a8198912c0 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/data-repositories-jpa-4/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml index 5ceec92ae3..ed927b8d7a 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/sf7-validation/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 com.example diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-httpexchange-indexing/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-httpexchange-indexing/pom.xml index c823b6df6c..efb9c94973 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-httpexchange-indexing/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-httpexchange-indexing/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 com.example.qualifier diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-web-config-support/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-web-config-support/pom.xml index ff4897beee..1ab9a4c8d2 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-web-config-support/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-web-config-support/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-M3 + 4.0.0-RC2 From 3a5207142fadf3b7dcd799e29b9df2ebe522a761 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Fri, 14 Nov 2025 21:40:06 -0500 Subject: [PATCH 11/13] Fix unit test on Windows Signed-off-by: BoykoAlex --- .../ide/vscode/boot/java/DefaultBuildCommandProvider.java | 4 ++-- .../test/DataRepositoryAotMetadataCodeLensProviderTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java index a8974c3eeb..7ab0b4759e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/DefaultBuildCommandProvider.java @@ -62,8 +62,8 @@ private CompletableFuture mavenRegenerateMetadata(Path pom, String[] goal) synchronized(MAVEN_LOCK) { String[] cmd = new String[1 + goal.length]; Path projectPath = pom.getParent(); - String mvnw = OS.isWindows() ? ".\\mvnw.cmd" : "./mvnw"; - cmd[0] = Files.isRegularFile(projectPath.resolve(mvnw)) ? mvnw : "mvn"; + Path mvnw = projectPath.resolve(OS.isWindows() ? "mvnw.cmd" : "mvnw"); + cmd[0] = Files.isRegularFile(mvnw) ? mvnw.toFile().toString() : "mvn"; System.arraycopy(goal, 0, cmd, 1, goal.length); try { return Runtime.getRuntime().exec(cmd, null, projectPath.toFile()).onExit().thenAccept(process -> { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java index f67fec2fa3..f53e3a1420 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java @@ -87,7 +87,7 @@ void generateMetadataCodeLens() throws Exception { Path metadataFile = Paths.get(testProject.getLocationUri()).resolve("target/spring-aot/main/resources/example/springdata/aot/UserRepository.json"); - // Trigger file update and wait for the vene thandling to complete + // Trigger file update and wait for the event handling to complete assertTrue(Files.isRegularFile(metadataFile), "AOT mtadata JSON file should be generated"); harness.createFile(metadataFile.toUri().toASCIIString()); harness.getServer().getAsync().waitForAll(); From deb4829e44eab10436cea286f51dab4667c0f281 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Mon, 17 Nov 2025 11:13:23 -0800 Subject: [PATCH 12/13] Workaround for lack of support in vscode-maven Signed-off-by: BoykoAlex --- .../org/springframework/ide/vscode/boot/app/JdtConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index 0a409aed86..48235088e3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -236,7 +236,8 @@ public class JdtConfig { switch(LspClient.currentClient()) { case VSCODE: case THEIA: - return new VSCodeBuildCommandProvider(); + return new DefaultBuildCommandProvider(server); +// return new VSCodeBuildCommandProvider(); case ECLIPSE: return new VSCodeBuildCommandProvider(); default: From da71d35cfa62db0bb4d4c9f3682b1fabb20e08c7 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 18 Nov 2025 11:23:59 -0800 Subject: [PATCH 13/13] Polish abd bit of fixing Signed-off-by: BoykoAlex --- .../ide/vscode/boot/app/JdtConfig.java | 3 +- .../java/data/DataRepositoryAotMetadata.java | 29 ++++++++++--------- ...RepositoryAotMetadataCodeLensProvider.java | 5 ++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index 48235088e3..0a409aed86 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -236,8 +236,7 @@ public class JdtConfig { switch(LspClient.currentClient()) { case VSCODE: case THEIA: - return new DefaultBuildCommandProvider(server); -// return new VSCodeBuildCommandProvider(); + return new VSCodeBuildCommandProvider(); case ECLIPSE: return new VSCodeBuildCommandProvider(); default: 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 index 43a69f1aa7..13ef32920d 100644 --- 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 @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.data; +import java.util.Objects; import java.util.Optional; import org.eclipse.jdt.core.dom.IMethodBinding; @@ -20,23 +21,25 @@ public record DataRepositoryAotMetadata (String name, String type, DataRepositoryModule module, IDataRepositoryAotMethodMetadata[] methods) { public Optional findMethod(IMethodBinding method) { - String name = method.getName(); + String methodName = method.getName(); - for (IDataRepositoryAotMethodMetadata methodMetadata : methods()) { - - if (methodMetadata.getName() != null && methodMetadata.getName().equals(name)) { - - String signature = methodMetadata.getSignature(); - JLRMethod parsedMethodSignature = JLRMethodParser.parse(signature); + if (methodName != null) { + for (IDataRepositoryAotMethodMetadata methodMetadata : methods()) { - if (parsedMethodSignature.getFQClassName().equals(name()) - && parsedMethodSignature.getMethodName().equals(method.getName()) - && parsedMethodSignature.getReturnType().equals(method.getReturnType().getQualifiedName()) - && parameterMatches(parsedMethodSignature, method)) { - return Optional.of(methodMetadata); + if (methodMetadata.getName() != null && methodMetadata.getName().equals(methodName)) { + + String signature = methodMetadata.getSignature(); + JLRMethod parsedMethodSignature = JLRMethodParser.parse(signature); + + if (Objects.equals(name(), parsedMethodSignature.getFQClassName()) + && methodName.equals(parsedMethodSignature.getMethodName()) + && Objects.equals(parsedMethodSignature.getReturnType(), method.getReturnType().getQualifiedName()) + && parameterMatches(parsedMethodSignature, method)) { + return Optional.of(methodMetadata); + } } } - } + } return Optional.empty(); } 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 index b487e6ed84..ff4c7ff97d 100644 --- 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 @@ -144,7 +144,8 @@ private List createCodeLenses(IJavaProject project, MethodDeclaration if (mb != null && hierarchyAnnot != null) { - getMetadata(repositoryMetadataService, project, mb).ifPresent(metadata -> metadata.findMethod(mb).ifPresent(methodMetadata -> { + Optional optMetadata = getMetadata(repositoryMetadataService, project, mb); + optMetadata.ifPresent(metadata -> metadata.findMethod(mb).ifPresent(methodMetadata -> { boolean isQueryAnnotated = hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_JPA_QUERY) || hierarchyAnnot.isAnnotatedWith(mb, Annotations.DATA_MONGODB_QUERY); @@ -172,7 +173,7 @@ private List createCodeLenses(IJavaProject project, MethodDeclaration })); if (ProjectBuild.MAVEN_PROJECT_TYPE.equals(project.getProjectBuild().getType())) { - String refreshCmdTitle = codeLenses.isEmpty() ? "Show AOT-generated Implementation, Query, etc..." : "Refresh"; + String refreshCmdTitle = optMetadata.map(m -> "Refresh").orElse("Show AOT-generated Implementation, Query, etc..."); Command refreshCmd = repositoryMetadataService.regenerateMetadataCommand(project); refreshCmd.setTitle(refreshCmdTitle); codeLenses.add(new CodeLens(range, refreshCmd, null));