From 52dd4d2ff51bdceae7045b578a6fe14e0f0121c3 Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 26 Feb 2024 10:33:17 -0500 Subject: [PATCH] IDE support for TestJars: maven projects in the workspace only --- .../DefaultLiveProcessCommandExecutor.java | 24 +- .../.classpath | 2 +- .../.settings/org.eclipse.jdt.core.prefs | 13 +- .../META-INF/MANIFEST.MF | 6 +- .../boot/launch/BootLaunchActivator.java | 10 +- .../boot/launch/BootLsCommandUtils.java | 51 +++++ .../boot/launch/TestJarLaunchListener.java | 210 ++++++++++++++++++ .../commons/gradle/GradleJavaProject.java | 4 +- .../ide/vscode/commons/java/IGav.java | 42 ++++ .../vscode/commons/java/IProjectBuild.java | 12 +- .../util/ExecuteCommandHandler.java | 6 +- .../ide/vscode/commons/protocol/java/Gav.java | 15 ++ .../commons/protocol/java/ProjectBuild.java | 35 +-- .../commons/maven/java/MavenJavaProject.java | 32 ++- .../maven/java/MavenProjectClasspath.java | 39 ++-- .../.classpath | 6 +- .../META-INF/MANIFEST.MF | 4 +- .../ls/commons/classpath/ClasspathUtil.java | 62 +++--- .../ide/vscode/boot/app/CommandsConfig.java | 26 +++ .../WorkspaceBootExecutableProjects.java | 89 ++++++++ .../vscode/boot/jdt/ls/JdtLsProjectCache.java | 4 +- .../WorkspaceBootExecutableProjectsTest.java | 119 ++++++++++ .../vscode-spring-boot/lib/Main.ts | 41 ++-- .../lib/debug-config-provider.ts | 152 +++++++++---- 24 files changed, 828 insertions(+), 176 deletions(-) create mode 100644 eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLsCommandUtils.java create mode 100644 eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java create mode 100644 headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IGav.java create mode 100644 headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Gav.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjects.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjectsTest.java diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java index 5ee35b2b4..f612b0702 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2023 Pivotal Software, Inc. + * Copyright (c) 2019, 2024 Pivotal Software, 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 @@ -16,11 +16,8 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import org.eclipse.lsp4e.LanguageServers; -import org.eclipse.lsp4e.LanguageServers.LanguageServerProjectExecutor; -import org.eclipse.lsp4e.LanguageServersRegistry; -import org.eclipse.lsp4j.ExecuteCommandOptions; import org.eclipse.lsp4j.ExecuteCommandParams; +import org.springframework.ide.eclipse.boot.launch.BootLsCommandUtils; import com.google.common.collect.ImmutableList; @@ -35,7 +32,7 @@ public final class DefaultLiveProcessCommandExecutor implements LiveProcessComma @SuppressWarnings("unchecked") @Override public Flux listCommands() { - List>> futures = getLanguageServers().computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( + List>> futures = BootLsCommandUtils.getLanguageServers(CMD_LIST_PROCESSES).computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( CMD_LIST_PROCESSES, ImmutableList.of() )).thenApply(o -> { @@ -54,21 +51,10 @@ public final class DefaultLiveProcessCommandExecutor implements LiveProcessComma @Override public Mono executeCommand(CommandInfo cmd) { - return Mono.fromFuture(getLanguageServers().collectAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( + return Mono.fromFuture(BootLsCommandUtils.getLanguageServers(cmd.command).collectAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( cmd.command, ImmutableList.of(cmd.info) - ))).thenAccept(null)); + ))).thenAccept(l -> {})); } - private LanguageServerProjectExecutor getLanguageServers() { - return LanguageServers.forProject(null).withFilter(cap -> { - ExecuteCommandOptions commandCap = cap.getExecuteCommandProvider(); - if (commandCap!=null) { - List supportedCommands = commandCap.getCommands(); - return supportedCommands!=null && supportedCommands.contains(CMD_LIST_PROCESSES); - } - return false; - }).withPreferredServer(LanguageServersRegistry.getInstance() - .getDefinition("org.eclipse.languageserver.languages.springboot")); - } } \ No newline at end of file diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.classpath b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.classpath index 43b986286..8d8612144 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.classpath +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.classpath @@ -1,6 +1,6 @@ - + diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs index 9f6ece88b..59fe89dce 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.settings/org.eclipse.jdt.core.prefs @@ -1,8 +1,15 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning org.eclipse.jdt.core.compiler.release=disabled -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.source=17 diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF index 3ef31d166..41fe5963d 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF @@ -29,7 +29,11 @@ Require-Bundle: org.eclipse.ui, org.eclipse.jface.text, org.eclipse.jdt.ui, org.eclipse.ui.genericeditor, - org.eclipse.ui.ide + org.eclipse.ui.ide, + org.eclipse.lsp4e, + org.eclipse.lsp4j, + com.google.gson, + org.eclipse.jdt.junit.core Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Export-Package: org.springframework.ide.eclipse.boot.launch, diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java index f17c71a7b..40eea70e9 100644 --- a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2015 Pivotal, Inc. + * Copyright (c) 2015, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,6 +15,7 @@ import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchManager; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.Bundle; @@ -37,7 +38,8 @@ public class BootLaunchActivator extends AbstractUIPlugin { @Override public void start(BundleContext context) throws Exception { super.start(context); - workspaceListener = new BootLaunchConfDeleter(ResourcesPlugin.getWorkspace(), DebugPlugin.getDefault().getLaunchManager()); + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + workspaceListener = new BootLaunchConfDeleter(ResourcesPlugin.getWorkspace(), launchManager); instance = this; IPreferenceStore myStore = instance.getPreferenceStore(); @@ -45,6 +47,8 @@ public class BootLaunchActivator extends AbstractUIPlugin { setPreference("org.eclipse.jdt.debug.ui", "org.eclipse.jdt.debug.ui.prompt_unable_to_install_breakpoint", false); myStore.setValue("cglib.breakpoint.warning.disabled", true); } + + launchManager.addLaunchListener(TestJarLaunchListener.getSingletonInstance()); } private void setPreference(String plugin, String key, boolean value) { @@ -70,6 +74,8 @@ public class BootLaunchActivator extends AbstractUIPlugin { if (workspaceListener!=null) { workspaceListener.dispose(); } + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + launchManager.removeLaunchListener(TestJarLaunchListener.getSingletonInstance()); super.stop(context); } diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLsCommandUtils.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLsCommandUtils.java new file mode 100644 index 000000000..e51285df8 --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLsCommandUtils.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2024 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.eclipse.boot.launch; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4e.LanguageServers; +import org.eclipse.lsp4e.LanguageServers.LanguageServerProjectExecutor; +import org.eclipse.lsp4e.LanguageServersRegistry; +import org.eclipse.lsp4j.ExecuteCommandOptions; +import org.eclipse.lsp4j.ExecuteCommandParams; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +@SuppressWarnings("restriction") +public class BootLsCommandUtils { + + private static Gson GSON = new Gson(); + + public static LanguageServerProjectExecutor getLanguageServers(String command) { + return LanguageServers.forProject(null).withFilter(cap -> { + ExecuteCommandOptions commandCap = cap.getExecuteCommandProvider(); + if (commandCap!=null) { + List supportedCommands = commandCap.getCommands(); + return supportedCommands!=null && supportedCommands.contains(command); + } + return false; + }).withPreferredServer(LanguageServersRegistry.getInstance() + .getDefinition("org.eclipse.languageserver.languages.springboot")); + } + + public static CompletableFuture> executeCommand(TypeToken resType, String cmd, Object... params) { + return getLanguageServers(cmd).computeFirst(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams( + cmd, + ImmutableList.of(params) + ))).thenApply(o -> o.map(v -> GSON.fromJson(GSON.toJsonTree(v), resType))); + } + +} diff --git a/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java new file mode 100644 index 000000000..3380b626c --- /dev/null +++ b/eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/TestJarLaunchListener.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 2024 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.eclipse.boot.launch; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; +import org.eclipse.debug.core.ILaunchDelegate; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.ILaunchesListener2; +import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants; +import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate; +import org.springsource.ide.eclipse.commons.livexp.util.Log; + +import com.google.gson.reflect.TypeToken; + +@SuppressWarnings("restriction") +public class TestJarLaunchListener implements ILaunchesListener2 { + + private static TestJarLaunchListener instance; + + private static final Pattern TESTJAR_PATTERN = Pattern.compile("^spring-boot-testjars-\\d+\\.\\d+\\.\\d+(.*)?.jar$"); + + private static final String TESTJAR_ARTIFACTS = "spring.boot.test-jar-artifacts"; + + public record ExecutableProject(String name, String uri, String gav, String mainClass, Collection classpath) {} + + public static TestJarLaunchListener getSingletonInstance() { + if (instance == null) { + instance = new TestJarLaunchListener(); + } + return instance; + } + + public void launchRemoved(ILaunch launch) { + clearTestJarWorkspaceProjectFiles(launch.getLaunchConfiguration()); + } + + private void clearTestJarWorkspaceProjectFiles(ILaunchConfiguration configuration) { + try { + if (JUnitLaunchConfigurationConstants.ID_JUNIT_APPLICATION.equals(configuration.getType().getIdentifier())) { + Map oldTestJarArtifacts = configuration.getAttribute(TESTJAR_ARTIFACTS, Collections.emptyMap()); + Map env = new HashMap<>(configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, Collections.emptyMap())); + for (Map.Entry testJarArtifactEnvEntry : oldTestJarArtifacts.entrySet()) { + env.remove(testJarArtifactEnvEntry.getKey()); + try { + Files.deleteIfExists(Paths.get(testJarArtifactEnvEntry.getValue())); + } catch (IOException e) { + Log.log(e); + } + } + ILaunchConfigurationWorkingCopy wc = configuration.getWorkingCopy(); + wc.removeAttribute(TESTJAR_ARTIFACTS); + if (env.isEmpty()) { + wc.removeAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES); + } else { + wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, env); + } + wc.doSave(); + } + } catch (CoreException e) { + Log.log(e); + } + } + + private void setupTestJarWorkspaceProjectFiles(ILaunchConfiguration configuration, Map testJarArtifactsEnvMap) { + try { + Map originalEnv = new HashMap<>(configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, Collections.emptyMap())); + Map oldTestJarArtifacts = configuration.getAttribute(TESTJAR_ARTIFACTS, Collections.emptyMap()); + for (Map.Entry envEntry : oldTestJarArtifacts.entrySet()) { + try { + Files.deleteIfExists(Paths.get(envEntry.getValue())); + } catch (IOException e) { + Log.log(e); + } + originalEnv.remove(envEntry.getKey()); + } + for (String envVar : originalEnv.keySet()) { + testJarArtifactsEnvMap.remove(envVar); + } + originalEnv.putAll(testJarArtifactsEnvMap); + ILaunchConfigurationWorkingCopy wc = configuration.getWorkingCopy(); + wc.setAttribute(TESTJAR_ARTIFACTS, testJarArtifactsEnvMap); + wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, originalEnv); + wc.doSave(); + } catch (CoreException e) { + e.printStackTrace(); + } + } + + public void launchAdded(ILaunch launch) { + try { + ILaunchConfiguration configuration = launch.getLaunchConfiguration(); + if (JUnitLaunchConfigurationConstants.ID_JUNIT_APPLICATION.equals(configuration.getType().getIdentifier())) { + ILaunchDelegate[] delegates = configuration.getType().getDelegates(Set.of(launch.getLaunchMode())); + if (delegates.length > 0) { + JUnitLaunchConfigurationDelegate delegate = (JUnitLaunchConfigurationDelegate) delegates[0].getDelegate(); + String[] classpath = delegate.getClasspathAndModulepath(configuration)[0]; + if (isTestJarsOnClasspath(classpath)) { + try { + getTestJarArtifactsMap() + .thenAccept(testJarArtifactsEnvMap -> setupTestJarWorkspaceProjectFiles(configuration, testJarArtifactsEnvMap)) + .get(3000000, TimeUnit.SECONDS); + } catch (Exception e) { + Log.log(e); + } + } + } + } + } catch (CoreException e) { + e.printStackTrace(); + } + } + + private static String createTestJarArtifactEnvKey(String gav) { + return "TESTJARS_ARTIFACT_%s".formatted(gav.replace(":", "_")); + } + + private static boolean isTestJarsOnClasspath(String[] classpath) { + for (String cpe : classpath) { + Path p = Paths.get(cpe); + if (Files.isRegularFile(p) && TESTJAR_PATTERN.matcher(p.getFileName().toString()).matches()) { + return true; + } + } + return false; + } + + private CompletableFuture> getTestJarArtifactsMap() { + try { + Optional opt = BootLsCommandUtils.executeCommand(TypeToken.getParameterized(List.class, ExecutableProject.class), "sts/spring-boot/executableBootProjects").get(3, TimeUnit.SECONDS); + if (opt.isPresent() && opt.get() instanceof List projects) { + Map gavToFile = new ConcurrentHashMap<>(); + CompletableFuture[] futures = projects.stream().map(ExecutableProject.class::cast).map(p -> CompletableFuture.runAsync(() -> { + try { + Path file = Files.createTempFile("%s_".formatted(p.gav().replace(":", "_")), UUID.randomUUID().toString()); + Files.write(file, List.of( + "# the main class to invoke", + "main=%s".formatted(p.mainClass()), + "# the classpath to use delimited by the OS specific delimiters", + "classpath=%s".formatted(String.join(File.pathSeparator, p.classpath()) + ))); + gavToFile.put(createTestJarArtifactEnvKey(p.gav()), file.toFile().toString()); + } catch (IOException e) { + Log.log(e); + } + })).toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures).thenApply(v -> gavToFile); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + Log.log(e); + } + + return CompletableFuture.completedFuture(Collections.emptyMap()); + } + + @Override + public void launchesRemoved(ILaunch[] launches) { + // nothing to do + } + + @Override + public void launchesAdded(ILaunch[] launches) { + for (ILaunch l : launches) { + launchAdded(l); + } + } + + @Override + public void launchesChanged(ILaunch[] launches) { + // nothing to do + } + + @Override + public void launchesTerminated(ILaunch[] launches) { + for (ILaunch l : launches) { + clearTestJarWorkspaceProjectFiles(l.getLaunchConfiguration()); + } + } + +} diff --git a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java index 3d1c06c41..13ed398ec 100644 --- a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java +++ b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2022 Pivotal, Inc. + * Copyright (c) 2017, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -35,7 +35,7 @@ public class GradleJavaProject extends LegacyJavaProject { private static final Logger log = LoggerFactory.getLogger(GradleJavaProject.class); private GradleJavaProject(FileObserver fileObserver, Path projectDataCache, IClasspath classpath, File gradleFile, JavadocService javadocService) { - super(fileObserver, gradleFile.getParentFile().toURI(), projectDataCache, classpath, javadocService, IProjectBuild.create(ProjectBuild.GRADLE_PROJECT_TYPE, gradleFile.toURI())); + super(fileObserver, gradleFile.getParentFile().toURI(), projectDataCache, classpath, javadocService, IProjectBuild.create(ProjectBuild.GRADLE_PROJECT_TYPE, gradleFile.toURI(), null)); } public static GradleJavaProject create(FileObserver fileObserver, GradleCore gradle, File gradleFile, Path projectDataCache, JavadocService javadocService) { diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IGav.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IGav.java new file mode 100644 index 000000000..e6ebc2ddb --- /dev/null +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IGav.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 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.java; + +import org.springframework.ide.vscode.commons.protocol.java.Gav; + +public interface IGav { + + String getGroupId(); + + String getArtifactId(); + + String getVersion(); + + static IGav create(Gav gav) { + return new IGav() { + + @Override + public String getVersion() { + return gav.version(); + } + + @Override + public String getGroupId() { + return gav.groupId(); + } + + @Override + public String getArtifactId() { + return gav.artifactId(); + } + }; + } +} diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IProjectBuild.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IProjectBuild.java index 5c73235c5..880bccbef 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IProjectBuild.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IProjectBuild.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 VMware, Inc. + * Copyright (c) 2022, 2024 VMware, 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 @@ -12,13 +12,17 @@ package org.springframework.ide.vscode.commons.java; import java.net.URI; +import org.springframework.ide.vscode.commons.protocol.java.Gav; + public interface IProjectBuild { String getType(); URI getBuildFile(); - static IProjectBuild create(String type, URI buildFile) { + IGav getGav(); + + static IProjectBuild create(String type, URI buildFile, Gav gav) { return new IProjectBuild() { @Override @@ -30,6 +34,10 @@ public interface IProjectBuild { public URI getBuildFile() { return buildFile; } + + public IGav getGav() { + return IGav.create(gav); + } }; } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ExecuteCommandHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ExecuteCommandHandler.java index 826f4da17..fceb2b74f 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ExecuteCommandHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/ExecuteCommandHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,6 +15,6 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.ExecuteCommandParams; @FunctionalInterface -public interface ExecuteCommandHandler { - CompletableFuture handle(ExecuteCommandParams params); +public interface ExecuteCommandHandler { + CompletableFuture handle(ExecuteCommandParams params); } diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Gav.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Gav.java new file mode 100644 index 000000000..6c2afbf66 --- /dev/null +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/Gav.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2024 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.protocol.java; + +public record Gav(String groupId, String artifactId, String version) { + +} diff --git a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/ProjectBuild.java b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/ProjectBuild.java index c2caa2a7b..adb015894 100644 --- a/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/ProjectBuild.java +++ b/headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/protocol/java/ProjectBuild.java @@ -10,39 +10,24 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.protocol.java; -public class ProjectBuild { +public record ProjectBuild(String type, String buildFile, Gav gav) { public static final String MAVEN_PROJECT_TYPE = "maven"; public static final String GRADLE_PROJECT_TYPE = "gradle"; - private String type; + public static ProjectBuild createMavenBuild(String buildFile, Gav gav) { + return new ProjectBuild(MAVEN_PROJECT_TYPE, buildFile, gav); + } - private String buildFile; + public static ProjectBuild createGradleBuild(String buildFile, Gav gav) { + return new ProjectBuild(GRADLE_PROJECT_TYPE, buildFile, gav); + } - public ProjectBuild(String type, String buildFile) { - this.type = type; - this.buildFile = buildFile; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getBuildFile() { - return buildFile; - } - - public void setBuildFile(String buildFile) { - this.buildFile = buildFile; - } - @Override public String toString() { - return "ProjectBuild [type=" + type + ", buildFile=" + buildFile + "]"; + return "ProjectBuild [type=" + type + ", buildFile=" + buildFile + "gav=" + gav + "]"; } + + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java index 51c9dfc6f..54e011879 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2022 Pivotal, Inc. + * Copyright (c) 2016, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.maven.java; import java.io.File; import java.nio.file.Path; +import org.apache.maven.project.MavenProject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.java.ClasspathFileBasedCache; @@ -22,9 +23,13 @@ import org.springframework.ide.vscode.commons.java.IProjectBuild; import org.springframework.ide.vscode.commons.java.LegacyJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavadocService; import org.springframework.ide.vscode.commons.maven.MavenCore; +import org.springframework.ide.vscode.commons.protocol.java.Gav; import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild; import org.springframework.ide.vscode.commons.util.FileObserver; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; + /** * Wrapper for Maven Core project * @@ -36,10 +41,13 @@ public class MavenJavaProject extends LegacyJavaProject { private static final Logger log = LoggerFactory.getLogger(MavenJavaProject.class); private final File pom; + + private Supplier gavSupplier; - private MavenJavaProject(FileObserver fileObserver, Path projectDataCache, IClasspath classpath, File pom, JavadocService javadocService) { - super(fileObserver, pom.getParentFile().toURI(), projectDataCache, classpath, javadocService, IProjectBuild.create(ProjectBuild.MAVEN_PROJECT_TYPE, pom.toURI())); + private MavenJavaProject(FileObserver fileObserver, Path projectDataCache, IClasspath classpath, File pom, JavadocService javadocService, Supplier gavSupplier) { + super(fileObserver, pom.getParentFile().toURI(), projectDataCache, classpath, javadocService, IProjectBuild.create(ProjectBuild.MAVEN_PROJECT_TYPE, pom.toURI(), null)); this.pom = pom; + this.gavSupplier = gavSupplier; } public static MavenJavaProject create(FileObserver fileObserver, MavenCore maven, File pom, Path projectDataCache, JavadocService javadocService) { @@ -51,7 +59,17 @@ public class MavenJavaProject extends LegacyJavaProject { () -> new MavenProjectClasspath(maven, pom), fileBasedCache ); - return new MavenJavaProject(fileObserver, projectDataCache, classpath, pom, javadocService); + + + return new MavenJavaProject(fileObserver, projectDataCache, classpath, pom, javadocService, Suppliers.memoize(() -> { + try { + MavenProject p = maven.readProject(pom, false); + return new Gav(p.getGroupId(), p.getArtifactId(), p.getVersion()); + } catch (Exception e) { + log.error("", e); + return null; + } + })); } public static MavenJavaProject create(FileObserver fileObserver, MavenCore maven, File pom, JavadocService javadocService) { @@ -83,4 +101,10 @@ public class MavenJavaProject extends LegacyJavaProject { public String toString() { return "MavenJavaProject("+getElementName()+")"; } + + @Override + public IProjectBuild getProjectBuild() { + return IProjectBuild.create(ProjectBuild.MAVEN_PROJECT_TYPE, pom.toURI(), gavSupplier.get()); + } + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java index 532c50a42..e3812f9fe 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectClasspath.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2019 Pivotal, Inc. + * Copyright (c) 2016, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -156,23 +156,24 @@ public class MavenProjectClasspath implements IClasspath { }); entries.add(cpe); } - { //test/java - File sourceFolder = new File(project.getBuild().getTestSourceDirectory()); - if (sourceFolder.exists()) { - File outputFolder = new File(project.getBuild().getTestOutputDirectory()); - CPE cpe = CPE.source(sourceFolder, outputFolder); - cpe.setOwn(true); - cpe.setTest(true); - cpe.setJavaContent(true); - safe(() -> { - String reportingDir = project.getModel().getReporting().getOutputDirectory(); - if (reportingDir!=null) { - File apidocs = new File(new File(reportingDir), "apidocs"); - cpe.setJavadocContainerUrl(apidocs.toURI().toURL()); - } - }); - entries.add(cpe); - } + { //test/java - duplicated down below without checking if folder exists + +// File sourceFolder = new File(project.getBuild().getTestSourceDirectory()); +// if (sourceFolder.exists()) { +// File outputFolder = new File(project.getBuild().getTestOutputDirectory()); +// CPE cpe = CPE.source(sourceFolder, outputFolder); +// cpe.setOwn(true); +// cpe.setTest(true); +// cpe.setJavaContent(true); +// safe(() -> { +// String reportingDir = project.getModel().getReporting().getOutputDirectory(); +// if (reportingDir!=null) { +// File apidocs = new File(new File(reportingDir), "apidocs"); +// cpe.setJavadocContainerUrl(apidocs.toURI().toURL()); +// } +// }); +// entries.add(cpe); +// } } { //main/resources for (Resource resource : project.getBuild().getResources()) { @@ -207,6 +208,8 @@ public class MavenProjectClasspath implements IClasspath { File outputFolder = new File(project.getBuild().getTestOutputDirectory()); CPE cpe = CPE.source(sourceFolder, outputFolder); cpe.setOwn(true); + cpe.setTest(true); + cpe.setJavaContent(true); safe(() -> { String reportingDir = project.getModel().getReporting().getOutputDirectory(); if (reportingDir!=null) { diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/.classpath b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/.classpath index 3e9b046bc..1ee696c63 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/.classpath +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/.classpath @@ -1,8 +1,8 @@ - - - + + + diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/META-INF/MANIFEST.MF b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/META-INF/MANIFEST.MF index 096852bda..1cd165ba5 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/META-INF/MANIFEST.MF +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons/META-INF/MANIFEST.MF @@ -13,7 +13,9 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.lsp4j, org.eclipse.lsp4j.jsonrpc, io.projectreactor.reactor-core, - org.reactivestreams.reactive-streams + org.reactivestreams.reactive-streams, + org.eclipse.m2e.core, + org.eclipse.buildship.core Export-Package: org.springframework.ide.vscode.commons.protocol, org.springframework.ide.vscode.commons.protocol.java, org.springframework.ide.vscode.commons.protocol.spring, 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 961d4e85e..836ff6be6 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 @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.buildship.core.internal.configuration.GradleProjectNature; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; @@ -36,16 +37,19 @@ import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.core.JavaProject; +import org.eclipse.m2e.core.MavenPlugin; +import org.eclipse.m2e.core.embedder.ArtifactKey; +import org.eclipse.m2e.core.project.IMavenProjectFacade; import org.springframework.ide.vscode.commons.protocol.java.Classpath; import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE; +import org.springframework.ide.vscode.commons.protocol.java.Gav; import org.springframework.ide.vscode.commons.protocol.java.ProjectBuild; import org.springframework.tooling.jdt.ls.commons.Logger; +@SuppressWarnings("restriction") public class ClasspathUtil { private static final Object JRE_CONTAINER_ID = "org.eclipse.jdt.launching.JRE_CONTAINER"; - private static final String GRADLE_CONTAINER_ID = "org.eclipse.buildship.core.gradleclasspathcontainer"; - private static final String MAVEN_CONTAINER_ID = "org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"; private static Set getSystemLibraryPaths(IJavaProject javaProject) { try { @@ -222,36 +226,40 @@ public class ClasspathUtil { } public static ProjectBuild createProjectBuild(IJavaProject jp) { - try { +// try { boolean likelyGradle = false; boolean likelyMaven = false; final Path home = System.getProperty("user.home") == null ? null : new File(System.getProperty("user.home")).toPath(); - for (IClasspathEntry e : jp.getRawClasspath()) { - switch (e.getPath().segment(0)) { - case MAVEN_CONTAINER_ID: - IFile f = jp.getProject().getFile("pom.xml"); - return new ProjectBuild(ProjectBuild.MAVEN_PROJECT_TYPE, f.exists() ? f.getLocationURI().toASCIIString() : null); - case GRADLE_CONTAINER_ID: - IFile g = jp.getProject().getFile("build.gradle"); - if (!g.exists()) { - g = jp.getProject().getFile("build.gradle.kts"); - } - return new ProjectBuild(ProjectBuild.GRADLE_PROJECT_TYPE, g.exists() ? g.getLocationURI().toASCIIString() : null); - default: - if (home != null && e.getPath() != null && e.getPath().toFile() != null) { - Path path = e.getPath().toFile().toPath(); - if (path.startsWith(home.resolve(".gradle"))) { - likelyGradle = true; - } else if (path.startsWith(home.resolve(".m2"))) { - likelyMaven = true; + if (MavenPlugin.isMavenProject(jp.getProject())) { + IMavenProjectFacade facade = MavenPlugin.getMavenProjectRegistry().getProject(jp.getProject()); + ArtifactKey artifact = facade.getArtifactKey(); + return ProjectBuild.createMavenBuild(facade.getPom().getLocationURI().toASCIIString(), new Gav(artifact.groupId(), artifact.artifactId(), artifact.version())); + } else if (GradleProjectNature.isPresentOn(jp.getProject())) { + IFile g = jp.getProject().getFile("build.gradle"); + if (!g.exists()) { + g = jp.getProject().getFile("build.gradle.kts"); + } + return new ProjectBuild(ProjectBuild.GRADLE_PROJECT_TYPE, g.exists() ? g.getLocationURI().toASCIIString() : null, null); + } else { + try { + for (IClasspathEntry e : jp.getRawClasspath()) { + if (home != null && e.getPath() != null && e.getPath().toFile() != null) { + Path path = e.getPath().toFile().toPath(); + if (path.startsWith(home.resolve(".gradle"))) { + likelyGradle = true; + } else if (path.startsWith(home.resolve(".m2"))) { + likelyMaven = true; + } } } + } catch (Exception e) { + // ignore } } if (likelyMaven) { IFile f = jp.getProject().getFile("pom.xml"); if (f.exists()) { - return new ProjectBuild(ProjectBuild.MAVEN_PROJECT_TYPE, f.getLocationURI().toASCIIString()); + return ProjectBuild.createMavenBuild(f.getLocationURI().toASCIIString(), null); } } else if (likelyGradle) { IFile g = jp.getProject().getFile("build.gradle"); @@ -259,12 +267,12 @@ public class ClasspathUtil { g = jp.getProject().getFile("build.gradle.kts"); } if (g.exists()) { - return new ProjectBuild(ProjectBuild.GRADLE_PROJECT_TYPE, g.getLocationURI().toASCIIString()); + return ProjectBuild.createGradleBuild(g.getLocationURI().toASCIIString(), null); } } - } catch (JavaModelException e) { - // ignore - } - return new ProjectBuild(null, null); +// } catch (JavaModelException e) { +// // ignore +// } + return new ProjectBuild(null, null, null); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java new file mode 100644 index 000000000..dd8492170 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/CommandsConfig.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 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.app; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ide.vscode.boot.java.commands.WorkspaceBootExecutableProjects; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; + +@Configuration(proxyBeanMethods = false) +public class CommandsConfig { + + @Bean WorkspaceBootExecutableProjects workspaceBootProjects(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringSymbolIndex symbolIndex) { + return new WorkspaceBootExecutableProjects(server, projectFinder, symbolIndex); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjects.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjects.java new file mode 100644 index 000000000..d5f1951fb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjects.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2024 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.commands; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.commons.java.IGav; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.protocol.java.Classpath; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.protocol.spring.BeansParams; + +public class WorkspaceBootExecutableProjects { + + public record ExecutableProject(String name, String uri, String gav, String mainClass, Collection classpath) {} + + final static String CMD = "sts/spring-boot/executableBootProjects"; + + private final static Logger log = LoggerFactory.getLogger(WorkspaceBootExecutableProjects.class); + + final private JavaProjectFinder projectFinder; + final private SpringSymbolIndex symbolIndex; + + public WorkspaceBootExecutableProjects(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringSymbolIndex symbolIndex) { + this.projectFinder = projectFinder; + this.symbolIndex = symbolIndex; + server.onCommand(CMD, params -> findExecutableProjects()); + } + + private CompletableFuture> mapToExecProject(IJavaProject project) { + BeansParams params = new BeansParams(); + params.setProjectName(project.getElementName()); + return symbolIndex.beans(params).thenApply(beans -> { + List bootAppBeans = beans.stream() + .filter(b -> Arrays.asList(b.getAnnotations()).contains(Annotations.BOOT_APP)) + .limit(2) + .collect(Collectors.toList()); + if (bootAppBeans.size() == 1) { + try { + Bean appBean = bootAppBeans.get(0); + Collection classpath = project.getClasspath().getClasspathEntries().stream() + .filter(cpe -> !cpe.isTest() && !cpe.isSystem()) + .map(cpe -> Classpath.isSource(cpe) ? cpe.getOutputFolder() : cpe.getPath()) + .collect(Collectors.toSet()); + IGav gav = project.getProjectBuild().getGav(); + String gavStr = "%s:%s:%s".formatted(gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); + return Optional.of(new ExecutableProject(project.getElementName(), project.getLocationUri().toASCIIString(), gavStr, appBean.getType(), classpath)); + } catch (Exception e) { + log.error("", e); + } + } + return Optional.empty(); + }); + } + + private CompletableFuture> findExecutableProjects() { + List>> futures = projectFinder.all().stream() + .filter(p -> p.getProjectBuild().getGav() != null) + .filter(p -> SpringProjectUtil.isBootProject(p)) + .map(this::mapToExecProject) + .collect(Collectors.toList()); + List executableProjects = Collections.synchronizedList(new ArrayList<>()); + futures.forEach(f -> f.thenAccept(opt -> opt.ifPresent(executableProjects::add))); + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])).thenApply(v -> executableProjects); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java index a550dae7e..8da636e52 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/jdt/ls/JdtLsProjectCache.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2023 Pivotal, Inc. + * Copyright (c) 2018, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -385,7 +385,7 @@ public class JdtLsProjectCache implements InitializableJavaProjectsService, Serv } private static IProjectBuild from(ProjectBuild projectBuild) { - return projectBuild == null ? null : IProjectBuild.create(projectBuild.getType(), projectBuild.getBuildFile() == null ? null : URI.create(projectBuild.getBuildFile())); + return projectBuild == null ? null : IProjectBuild.create(projectBuild.type(), projectBuild.buildFile() == null ? null : URI.create(projectBuild.buildFile()), projectBuild.gav()); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjectsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjectsTest.java new file mode 100644 index 000000000..0a2b6a31b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/commands/WorkspaceBootExecutableProjectsTest.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) 2024 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.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +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.boot.java.commands.WorkspaceBootExecutableProjects.ExecutableProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class WorkspaceBootExecutableProjectsTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex symbolIndex; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + } + + @SuppressWarnings("unchecked") + @Test + void singleProject() throws Exception { + MavenJavaProject project = ProjectsHarness.INSTANCE.mavenProject("test-spring-indexing"); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(project.getProjectBuild().getBuildFile().toASCIIString())).get(); + + CompletableFuture initProject = symbolIndex.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + + List res = (List) harness.getServer().getWorkspaceService().executeCommand(new ExecuteCommandParams(WorkspaceBootExecutableProjects.CMD, Collections.emptyList())).get(); + assertNotNull(res); + assertEquals(1, res.size()); + ExecutableProject execProject = res.get(0); + assertEquals("test-spring-indexing", execProject.name()); + assertEquals("org.test.MainClass", execProject.mainClass()); + assertEquals(project.getLocationUri().toASCIIString(), execProject.uri()); + assertEquals("com.example:test-spring-indexing:0.0.1-SNAPSHOT", execProject.gav()); + assertEquals(99, execProject.classpath().size()); + + assertTrue(execProject.classpath().stream().map(path -> Path.of(path)).anyMatch(p -> p.endsWith("target/classes"))); + assertFalse(execProject.classpath().stream().map(path -> Path.of(path)).anyMatch(p -> p.endsWith("target/test-classes"))); + } + + @SuppressWarnings("unchecked") + @Test + void multipleProjects() throws Exception { + MavenJavaProject project1 = ProjectsHarness.INSTANCE.mavenProject("test-spring-indexing"); + MavenJavaProject project2 = ProjectsHarness.INSTANCE.mavenProject("test-spring-data-symbols"); + ProjectsHarness.INSTANCE.mavenProject("test-annotation-indexing-non-boot-project"); + ProjectsHarness.INSTANCE.mavenProject("test-xml-validations"); + + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(project1.getProjectBuild().getBuildFile().toASCIIString())).get(); + projectFinder.find(new TextDocumentIdentifier(project2.getProjectBuild().getBuildFile().toASCIIString())).get(); + + CompletableFuture initProject = symbolIndex.waitOperation(); + initProject.get(10, TimeUnit.SECONDS); + + List res = (List) harness.getServer().getWorkspaceService().executeCommand(new ExecuteCommandParams(WorkspaceBootExecutableProjects.CMD, Collections.emptyList())).get(); + assertNotNull(res); + assertEquals(2, res.size()); + + ExecutableProject execProject2 = res.get(0); + assertEquals("test-spring-data-symbols", execProject2.name()); + assertEquals("org.test.Application", execProject2.mainClass()); + assertEquals(project2.getLocationUri().toASCIIString(), execProject2.uri()); + assertEquals("com.example:test-spring-data-symbols:0.0.1-SNAPSHOT", execProject2.gav()); + assertEquals(44, execProject2.classpath().size()); + assertTrue(execProject2.classpath().stream().map(path -> Path.of(path)).anyMatch(p -> p.endsWith("target/classes"))); + assertFalse(execProject2.classpath().stream().map(path -> Path.of(path)).anyMatch(p -> p.endsWith("target/test-classes"))); + + ExecutableProject execProject1 = res.get(1); + assertEquals("test-spring-indexing", execProject1.name()); + assertEquals("org.test.MainClass", execProject1.mainClass()); + assertEquals(project1.getLocationUri().toASCIIString(), execProject1.uri()); + assertEquals("com.example:test-spring-indexing:0.0.1-SNAPSHOT", execProject1.gav()); + assertEquals(99, execProject1.classpath().size()); + assertTrue(execProject1.classpath().stream().map(path -> Path.of(path)).anyMatch(p -> p.endsWith("target/classes"))); + assertFalse(execProject1.classpath().stream().map(path -> Path.of(path)).anyMatch(p -> p.endsWith("target/test-classes"))); + } + +} diff --git a/vscode-extensions/vscode-spring-boot/lib/Main.ts b/vscode-extensions/vscode-spring-boot/lib/Main.ts index 089673929..e25d523fc 100644 --- a/vscode-extensions/vscode-spring-boot/lib/Main.ts +++ b/vscode-extensions/vscode-spring-boot/lib/Main.ts @@ -1,8 +1,13 @@ 'use strict'; import * as OS from "os"; -import * as VSCode from 'vscode'; -import { workspace } from 'vscode'; +import { + commands, + window, + workspace, + ExtensionContext, + Uri + } from 'vscode'; import * as commons from '@pivotal-tools/commons-vscode'; import * as liveHoverUi from './live-hover-connect-ui'; @@ -24,7 +29,7 @@ const FACTORIES_LANGUAGE_ID = "spring-factories"; const STOP_ASKING = "Stop Asking"; /** Called when extension is activated */ -export function activate(context: VSCode.ExtensionContext): Thenable { +export function activate(context: ExtensionContext): Thenable { // registerPipelineGenerator(context); let options : commons.ActivatorOptions = { @@ -36,14 +41,14 @@ export function activate(context: VSCode.ExtensionContext): Thenable { + checkjvm: (context: ExtensionContext, jvm: commons.JVM) => { let version = jvm.getMajorVersion(); if (version < 17) { throw Error(`Spring Tools Language Server requires Java 17 or higher to be launched. Current Java version is ${version}`); } if (!jvm.isJdk()) { - VSCode.window.showWarningMessage( + window.showWarningMessage( 'JAVA_HOME or PATH environment variable seems to point to a JRE. A JDK is required, hence Boot Hints are unavailable.', STOP_ASKING).then(selection => { if (selection === STOP_ASKING) { @@ -58,7 +63,7 @@ export function activate(context: VSCode.ExtensionContext): Thenable VSCode.Uri.parse(uri) + protocol2Code: uri => Uri.parse(uri) }, // See PT-158992999 as to why a scheme is added to the document selector // documentSelector: [ PROPERTIES_LANGUAGE_ID, YAML_LANGUAGE_ID, JAVA_LANGUAGE_ID ], @@ -132,34 +137,34 @@ export function activate(context: VSCode.ExtensionContext): Thenable { - VSCode.commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => { + commands.registerCommand('vscode-spring-boot.ls.start', () => client.start().then(() => { // Boot LS is fully started registerClasspathService(client); registerJavaDataService(client); // Force classpath listener to be enabled. Boot LS can only be launched iff classpath is available and there Spring-Boot on the classpath somewhere. - VSCode.commands.executeCommand('sts.vscode-spring-boot.enableClasspathListening', true); + commands.executeCommand('sts.vscode-spring-boot.enableClasspathListening', true); + + // Register launch config contributior to java debug launch to be able to connect to JMX + context.subscriptions.push(startDebugSupport()); })); - VSCode.commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop()); + commands.registerCommand('vscode-spring-boot.ls.stop', () => client.stop()); liveHoverUi.activate(client, options, context); rewrite.activate(client, options, context); setLogLevelUi.activate(client, options, context); - VSCode.commands.registerCommand('vscode-spring-boot.spring.modulith.metadata.refresh', async () => { - const modulithProjects = await VSCode.commands.executeCommand('sts/modulith/projects'); + commands.registerCommand('vscode-spring-boot.spring.modulith.metadata.refresh', async () => { + const modulithProjects = await commands.executeCommand('sts/modulith/projects'); const projectNames = Object.keys(modulithProjects); if (projectNames.length === 0) { - VSCode.window.showErrorMessage('No Spring Modulith projects found'); + window.showErrorMessage('No Spring Modulith projects found'); } else { - const projectName = projectNames.length === 1 ? projectNames[0] : await VSCode.window.showQuickPick( + const projectName = projectNames.length === 1 ? projectNames[0] : await window.showQuickPick( projectNames, { placeHolder: "Select the target project." }, ); - VSCode.commands.executeCommand('sts/modulith/metadata/refresh', modulithProjects[projectName]); + commands.executeCommand('sts/modulith/metadata/refresh', modulithProjects[projectName]); } }); diff --git a/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts b/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts index 192acf338..3a9cf42e6 100644 --- a/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts +++ b/vscode-extensions/vscode-spring-boot/lib/debug-config-provider.ts @@ -1,9 +1,21 @@ -import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, WorkspaceFolder } from "vscode"; +import { debug, + window, + commands, + workspace, + CancellationToken, + DebugConfiguration, + DebugConfigurationProvider, + WorkspaceFolder, + DebugConfigurationProviderTriggerKind, + DebugSession, + DebugSessionCustomEvent, + Disposable +} from "vscode"; import * as path from "path"; -import * as VSCode from "vscode"; -import { Disposable } from "vscode"; import psList from 'ps-list'; -import { ListenablePreferenceSetting } from "@pivotal-tools/commons-vscode/lib/launch-util"; +import * as fs from "fs"; +import { tmpdir } from "os"; +import { randomUUID } from "crypto"; const JMX_VM_ARG = '-Dspring.jmx.enabled=' const ACTUATOR_JMX_EXPOSURE_ARG = '-Dmanagement.endpoints.jmx.exposure.include=' @@ -11,15 +23,57 @@ const ADMIN_VM_ARG = '-Dspring.application.admin.enabled=' const BOOT_PROJECT_ARG = '-Dspring.boot.project.name='; const RMI_HOSTNAME = '-Djava.rmi.server.hostname=localhost'; +const ENV_TESTJAR_ARTIFACT_PREFIX = "TESTJARS_ARTIFACT_"; + const TEST_RUNNER_MAIN_CLASSES = [ 'org.eclipse.jdt.internal.junit.runner.RemoteTestRunner', 'com.microsoft.java.test.runner.Launcher' ]; +interface ExecutableBootProject { + name: string; + uri: string; + mainClass: string; + classpath: string[]; + gav: string; +} + +interface ProcessEvent { + type: string; + processId: number; + shellProcessId: number +} + class SpringBootDebugConfigProvider implements DebugConfigurationProvider { - resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { - if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { + async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise { + // TestJar launch support + if (TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isTestJarsOnClasspath(debugConfiguration)) { + const projects = await commands.executeCommand("sts/spring-boot/executableBootProjects") as ExecutableBootProject[]; + let env = debugConfiguration.env; + if (!env) { + env = {}; + debugConfiguration.env = env; + } + const projectsWithErrors: ExecutableBootProject[] = []; + // Create all project classparth data files and add env vars for workspace projects + await Promise.all(projects.map(async p => { + const envName = this.createEnvVarName(p); + if (!env[envName]) { + try { + env[envName] = await this.createFile(p); + } catch (error) { + projectsWithErrors.push(p); + } + } + })); + if (projectsWithErrors.length > 0) { + const projectStr = projectsWithErrors.map(p => `'${p.name}'`); + window.showWarningMessage(`TestJar Support: Could not provide data for workspace projects: ${projectStr}`); + } + } + // Running app live hovers support + if (isAutoConnectOn() && !TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { if (debugConfiguration.vmArgs) { if (debugConfiguration.vmArgs.indexOf(JMX_VM_ARG) < 0) { debugConfiguration.vmArgs += ` ${JMX_VM_ARG}true`; @@ -43,58 +97,46 @@ class SpringBootDebugConfigProvider implements DebugConfigurationProvider { return debugConfiguration; } -} + private createEnvVarName(project: ExecutableBootProject) { + return `${ENV_TESTJAR_ARTIFACT_PREFIX}${project.gav.replace(/:/g, "_")}`; + } -interface ProcessEvent { - type: string; - processId: number; - shellProcessId: number -} + private async createFile(project: ExecutableBootProject) { + const filePath = path.join(tmpdir(), `${project.gav.replace(/:/g, "_")}-${randomUUID()}`); + await fs.writeFile(filePath, `# the main class to invoke\nmain=${project.mainClass}\n# the classpath to use delimited by the OS specific delimiters\nclasspath=${project.classpath.join(path.delimiter)}`, function(err) { + if(err) { + throw Error(); + } + }); + return filePath; + } -function hookListenerToBooleanPreference(setting: string, listenerCreator: () => Disposable): Disposable { - const listenableSetting = new ListenablePreferenceSetting(setting); - let listener: Disposable | undefined = listenableSetting.value ? listenerCreator() : undefined; - listenableSetting.onDidChangeValue(() => { - if (listenableSetting.value) { - if (!listener) { - listener = listenerCreator(); - } - } else { - if (listener) { - listener.dispose(); - listener = undefined; - } - } - }); - - return { - dispose: () => { - if (listener) { - listener.dispose(); - } - listenableSetting.dispose(); - } - }; } export function startDebugSupport(): Disposable { - return hookListenerToBooleanPreference( - 'boot-java.live-information.automatic-connection.on', - () => Disposable.from( - VSCode.debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), - VSCode.debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), VSCode.DebugConfigurationProviderTriggerKind.Initial) - ) + return Disposable.from( + debug.onDidReceiveDebugSessionCustomEvent(handleCustomDebugEvent), + debug.onDidTerminateDebugSession(cleanupDebugSession), + debug.registerDebugConfigurationProvider('java', new SpringBootDebugConfigProvider(), DebugConfigurationProviderTriggerKind.Initial), + new Disposable(() => cleanupDebugSession(debug.activeDebugSession)) // If VSCode is shutdown then clean active debug session if it satidfies conditions ); } -async function handleCustomDebugEvent(e: VSCode.DebugSessionCustomEvent): Promise { - if (e.session?.type === 'java' && e?.body?.type === 'processid') { +async function cleanupDebugSession(session: DebugSession) { + // Handle termination of a Boot app with TestJars on the classpath + if (session.type === 'java' && TEST_RUNNER_MAIN_CLASSES.includes(session.configuration.mainClass) && isTestJarsOnClasspath(session.configuration) && session.configuration.env) { + await Promise.all(Object.keys(session.configuration.env).filter(k => k.startsWith(ENV_TESTJAR_ARTIFACT_PREFIX)).map(k => fs.rm(session.configuration.env[k], () => {}))); + } +} + +async function handleCustomDebugEvent(e: DebugSessionCustomEvent): Promise { + if (isAutoConnectOn() && e.session?.type === 'java' && e?.body?.type === 'processid') { const debugConfiguration: DebugConfiguration = e.session.configuration; if (canConnect(debugConfiguration)) { setTimeout(async () => { const pid = await getAppPid(e.body as ProcessEvent); const processKey = pid.toString(); - VSCode.commands.executeCommand('sts/livedata/connect', { processKey }); + commands.executeCommand('sts/livedata/connect', { processKey }); }, 500); } } @@ -130,6 +172,26 @@ function isActuatorJarFile(f: string): boolean { return false; } +function isTestJarsOnClasspath(debugConfiguration: DebugConfiguration): boolean { + if (Array.isArray(debugConfiguration.classPaths)) { + return !!debugConfiguration.classPaths.find(isTestJarFile); + } + return false; + +} + +function isTestJarFile(f: string): boolean { + const fileName = path.basename(f || ""); + if (/^spring-boot-testjars-\d+\.\d+\.\d+(.*)?.jar$/.test(fileName)) { + return true; + } + return false; +} + +function isAutoConnectOn(): boolean { + return workspace.getConfiguration().get("boot-java.live-information.automatic-connection.on", true); +} + function canConnect(debugConfiguration: DebugConfiguration): boolean { if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) { return debugConfiguration.vmArgs