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