IDE support for TestJars: maven projects in the workspace only

This commit is contained in:
aboyko
2024-02-26 10:33:17 -05:00
parent 13c2c04997
commit 52dd4d2ff5
24 changed files with 828 additions and 176 deletions

View File

@@ -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<CommandInfo> listCommands() {
List<CompletableFuture<List<CommandInfo>>> futures = getLanguageServers().computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(
List<CompletableFuture<List<CommandInfo>>> 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<Void> 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<String> supportedCommands = commandCap.getCommands();
return supportedCommands!=null && supportedCommands.contains(CMD_LIST_PROCESSES);
}
return false;
}).withPreferredServer(LanguageServersRegistry.getInstance()
.getDefinition("org.eclipse.languageserver.languages.springboot"));
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="output" path="target/classes"/>

View File

@@ -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

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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<String> supportedCommands = commandCap.getCommands();
return supportedCommands!=null && supportedCommands.contains(command);
}
return false;
}).withPreferredServer(LanguageServersRegistry.getInstance()
.getDefinition("org.eclipse.languageserver.languages.springboot"));
}
public static <T> CompletableFuture<Optional<T>> executeCommand(TypeToken<T> 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)));
}
}

View File

@@ -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<String> 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<String, String> oldTestJarArtifacts = configuration.getAttribute(TESTJAR_ARTIFACTS, Collections.emptyMap());
Map<String, String> env = new HashMap<>(configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, Collections.emptyMap()));
for (Map.Entry<String, String> 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<String, String> testJarArtifactsEnvMap) {
try {
Map<String, String> originalEnv = new HashMap<>(configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, Collections.emptyMap()));
Map<String, String> oldTestJarArtifacts = configuration.getAttribute(TESTJAR_ARTIFACTS, Collections.emptyMap());
for (Map.Entry<String, String> 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<Map<String, String>> 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<String, String> 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());
}
}
}

View File

@@ -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) {

View File

@@ -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();
}
};
}
}

View File

@@ -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);
}
};
}

View File

@@ -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<Object> handle(ExecuteCommandParams params);
public interface ExecuteCommandHandler<T> {
CompletableFuture<T> handle(ExecuteCommandParams params);
}

View File

@@ -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) {
}

View File

@@ -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 + "]";
}
}

View File

@@ -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<Gav> 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<Gav> 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());
}
}

View File

@@ -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) {

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src/"/>
<classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry exported="true" kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry exported="true" kind="lib" path="lib/commons-lsp-extensions.jar"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -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,

View File

@@ -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<String> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> 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<Optional<ExecutableProject>> mapToExecProject(IJavaProject project) {
BeansParams params = new BeansParams();
params.setProjectName(project.getElementName());
return symbolIndex.beans(params).thenApply(beans -> {
List<Bean> 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<String> 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<List<ExecutableProject>> findExecutableProjects() {
List<CompletableFuture<Optional<ExecutableProject>>> futures = projectFinder.all().stream()
.filter(p -> p.getProjectBuild().getGav() != null)
.filter(p -> SpringProjectUtil.isBootProject(p))
.map(this::mapToExecProject)
.collect(Collectors.toList());
List<ExecutableProject> 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);
}
}

View File

@@ -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());
}
}

View File

@@ -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<Void> initProject = symbolIndex.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
List<WorkspaceBootExecutableProjects.ExecutableProject> res = (List<WorkspaceBootExecutableProjects.ExecutableProject>) 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<Void> initProject = symbolIndex.waitOperation();
initProject.get(10, TimeUnit.SECONDS);
List<WorkspaceBootExecutableProjects.ExecutableProject> res = (List<WorkspaceBootExecutableProjects.ExecutableProject>) 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")));
}
}

View File

@@ -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<ExtensionAPI> {
export function activate(context: ExtensionContext): Thenable<ExtensionAPI> {
// registerPipelineGenerator(context);
let options : commons.ActivatorOptions = {
@@ -36,14 +41,14 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
vmArgs: [
'-XX:+HeapDumpOnOutOfMemoryError'
],
checkjvm: (context: VSCode.ExtensionContext, jvm: commons.JVM) => {
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<ExtensionAP
mainClass: 'org.springframework.ide.vscode.boot.app.BootLanguageServerBootApp',
configFileName: 'application.properties'
},
workspaceOptions: VSCode.workspace.getConfiguration("spring-boot.ls"),
workspaceOptions: workspace.getConfiguration("spring-boot.ls"),
clientOptions: {
markdown: {
isTrusted: true
@@ -88,7 +93,7 @@ export function activate(context: VSCode.ExtensionContext): Thenable<ExtensionAP
}
return uri.toString();
},
protocol2Code: uri => 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<ExtensionAP
highlightCodeLensSettingKey: 'boot-java.highlight-codelens.on'
};
// Register launch config contributior to java debug launch to be able to connect to JMX
context.subscriptions.push(startDebugSupport());
return commons.activate(options, context).then(client => {
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]);
}
});

View File

@@ -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<DebugConfiguration> {
if (!TEST_RUNNER_MAIN_CLASSES.includes(debugConfiguration.mainClass) && isActuatorOnClasspath(debugConfiguration)) {
async resolveDebugConfigurationWithSubstitutedVariables(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise<DebugConfiguration> {
// 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<boolean>(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<void> {
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<void> {
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