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