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/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/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/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 4c74bad512..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() {
- return new DataRepositoryAotMetadataService();
+ 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..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,9 @@
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;
import org.springframework.ide.vscode.boot.java.cron.CronExpressionsInlayHintsProvider;
@@ -228,4 +231,17 @@ public class JdtConfig {
@Bean JdtCodeActionHandler jdtCodeActionHandler(CompilationUnitCache cuCache, Collection providers) {
return new JdtCodeActionHandler(cuCache, providers);
}
+
+ @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/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/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..7ab0b4759e
--- /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";
+
+ 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();
+ 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 -> {
+ 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/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/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..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,13 +10,56 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
-public record DataRepositoryAotMetadata (String name, String type, String module, DataRepositoryAotMetadataMethod[] methods) {
+import java.util.Objects;
+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;
+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 Optional findMethod(IMethodBinding method) {
+ String methodName = method.getName();
+
+ if (methodName != null) {
+ for (IDataRepositoryAotMethodMetadata methodMetadata : methods()) {
+
+ 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();
}
-
- 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..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
@@ -35,8 +35,11 @@
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.java.IJavaProject;
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;
@@ -57,12 +60,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
@@ -85,40 +98,28 @@ static boolean isValidMethodBinding(IMethodBinding methodBinding) {
|| methodBinding.getDeclaringClass().getBinaryName() == null) {
return false;
}
+
+ if (!methodBinding.getDeclaringClass().isInterface()) {
+ return false;
+ }
+
+ if (ASTUtils.findInTypeHierarchy(methodBinding.getDeclaringClass(), Set.of(Constants.REPOSITORY_TYPE)) == null) {
+ return false;
+ }
+
return true;
}
-
-// 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 Optional.ofNullable(dataRepositoryAotMetadataService.getRepositoryMetadata(project, repositoryClass));
- }
-
- static Optional getMethodMetadata(DataRepositoryAotMetadataService dataRepositoryAotMetadataService, DataRepositoryAotMetadata metadata, IMethodBinding methodBinding) {
- final IMethodBinding method = methodBinding.getMethodDeclaration();
-
- return Optional.ofNullable(dataRepositoryAotMetadataService.findMethod(metadata, 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 || !QueryMethodCodeActionProvider.isValidProject(project)) {
return;
}
@@ -127,13 +128,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 +142,43 @@ 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 (mb != null && hierarchyAnnot != null) {
-
- 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 (!isQueryAnnotated) {
- String queryStatement = methodMetadata.get().getQueryStatement(metadata);
- if (queryStatement != null) {
- Command queryTitle = new Command();
- queryTitle.setTitle(queryStatement);
- codeLenses.add(new CodeLens(range, queryTitle, null));
+ 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);
+
+ 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) {
+ String queryStatement = methodMetadata.getQueryStatement();
+ if (queryStatement != null) {
+ Command queryTitle = new Command();
+ queryTitle.setTitle(queryStatement);
+ codeLenses.add(new CodeLens(range, queryTitle, null));
+ }
+ }
+ }));
+
+ if (ProjectBuild.MAVEN_PROJECT_TYPE.equals(project.getProjectBuild().getType())) {
+ 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));
}
+
}
} catch (BadLocationException e) {
log.error("bad location while calculating code lens for data repository query method", e);
@@ -179,18 +186,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, DataRepositoryModule module, 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", 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) {
@@ -203,7 +210,5 @@ private static List createAttributeList(Map 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..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
@@ -10,106 +10,180 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data;
-import java.io.File;
-import java.io.FileReader;
+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;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
-import org.eclipse.jdt.core.dom.IMethodBinding;
-import org.eclipse.jdt.core.dom.ITypeBinding;
+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.java.parser.JLRMethodParser;
-import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser.JLRMethod;
+import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
+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;
+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;
/**
+ * Service for loading and caching Spring Data repository AOT metadata.
+ *
+ * Provides caching of metadata loaded from JSON files with automatic cache invalidation
+ * when files are created, modified, or deleted.
+ *
* @author Martin Lippert
*/
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);
+
+ 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<>();
+
+ private final Gson gson = new GsonBuilder().registerTypeAdapter(DataRepositoryAotMetadata.class, new JsonDeserializer() {
- public DataRepositoryAotMetadata getRepositoryMetadata(IJavaProject project, String repositoryType) {
- try {
- String metadataFilePath = repositoryType.replace('.', File.separatorChar);
-
- Optional metadataFile = IClasspathUtil.getOutputFolders(project.getClasspath())
- .map(outputFolder -> new File(outputFolder.getParentFile(), "spring-aot/main/resources/" + metadataFilePath + ".json"))
- .filter(file -> file.exists())
- .findFirst();
-
- if (metadataFile.isPresent()) {
- return readMetadataFile(metadataFile.get());
+ @Override
+ public DataRepositoryAotMetadata deserialize(JsonElement json, Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+ JsonObject o = json.getAsJsonObject();
+ JsonElement e = o.get(MODULE_JSON_PROP);
+ if (e.isJsonPrimitive()) {
+ String module = e.getAsString();
+ 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));
+ case JPA:
+ return new DataRepositoryAotMetadata(name, type, moduleType, context.deserialize(o.get(METHODS), JpaAotMethodMetadata[].class));
+ }
}
-
- } catch (Exception e) {
- log.error("error finding spring data repository definition metadata file", e);
+ return null;
}
- return null;
+ }).create();
+
+ 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<>();
+ for (String fileUri : changedFiles) {
+ URI uri = URI.create(fileUri);
+ Path path = Paths.get(uri);
+ Optional removed = metadataCache.remove(path);
+ if (removed != null) {
+ 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);
+ }
+ });
+ 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);
+ }
+ }
+ });
+ }
}
- private DataRepositoryAotMetadata readMetadataFile(File file) {
+ public Optional getRepositoryMetadata(IJavaProject project, String repositoryType) {
+ String metadataFilePath = repositoryType.replace('.', '/') + ".json";
- try (FileReader reader = new FileReader(file)) {
- Gson gson = new Gson();
- DataRepositoryAotMetadata result = gson.fromJson(reader, DataRepositoryAotMetadata.class);
-
- return result;
- }
- catch (IOException e) {
- return null;
- }
+ 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));
}
-
- public String getQueryStatement(DataRepositoryAotMetadata metadata, IMethodBinding method) {
- DataRepositoryAotMetadataMethod methodMetadata = findMethod(metadata, method);
- return methodMetadata.getQueryStatement(metadata);
+
+ 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);
+ }
+ }
+ return Optional.empty();
}
- 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;
+ 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()).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");
}
+ goal.add("org.springframework.boot:spring-boot-maven-plugin:process-aot");
+ return buildCmds.executeMavenGoal(jp, String.join(" ", goal));
}
-
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;
+
+ 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/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..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;
}
- String queryStatement = repositoryMetadataService.getQueryStatement(repositoryMetadata, methodBinding);
- return queryStatement;
+ 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/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..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
@@ -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;
@@ -45,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)
@@ -72,13 +75,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);
@@ -89,9 +90,9 @@ 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.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..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
@@ -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(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 5292733d04..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
@@ -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(1).getCommand().getTitle());
+ 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/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..f53e3a1420
--- /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 event handling 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 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/DataRepositoryAotMetadataServiceTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataServiceTest.java
index 92ed93f4d4..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;
@@ -31,8 +33,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;
@@ -51,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;
@@ -69,29 +73,26 @@ public void setup() throws Exception {
}
@Test
- void testBasicRepositoryAotMetadataLookuo() throws Exception {
- DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService();
-
- DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository");
+ void testBasicRepositoryAotMetadataLookup() throws Exception {
+ DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null);
assertNotNull(metadata);
assertEquals("example.springdata.aot.UserRepository", metadata.name());
- assertEquals("JPA", metadata.module());
+ assertEquals(DataRepositoryModule.JPA, metadata.module());
}
@Test
void testRepositoryMethodsIntMetadata() throws Exception {
- DataRepositoryAotMetadataService service = new DataRepositoryAotMetadataService();
- DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository");
- DataRepositoryAotMetadataMethod[] methods = metadata.methods();
+ DataRepositoryAotMetadata metadata = service.getRepositoryMetadata(testProject, "example.springdata.aot.UserRepository").orElse(null);
+ 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());
@@ -103,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 -> {
@@ -112,14 +112,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).orElse(null);
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;
@@ -130,5 +130,27 @@ else if (method.name().equals("findUserByLastnameStartingWith") && binding.getPa
});
}
+
+ @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);
+ }
}
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/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/.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 100755
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..a8198912c0
--- /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-RC2
+
+
+
+ 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
+
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
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