Skip to content

Commit 52dd4d2

Browse files
committed
IDE support for TestJars: maven projects in the workspace only
1 parent 13c2c04 commit 52dd4d2

File tree

24 files changed

+827
-175
lines changed

24 files changed

+827
-175
lines changed

eclipse-extensions/org.springframework.ide.eclipse.boot.dash/src/org/springframework/ide/eclipse/boot/dash/liveprocess/DefaultLiveProcessCommandExecutor.java

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2019, 2023 Pivotal Software, Inc.
2+
* Copyright (c) 2019, 2024 Pivotal Software, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -16,11 +16,8 @@
1616
import java.util.concurrent.CompletableFuture;
1717
import java.util.stream.Collectors;
1818

19-
import org.eclipse.lsp4e.LanguageServers;
20-
import org.eclipse.lsp4e.LanguageServers.LanguageServerProjectExecutor;
21-
import org.eclipse.lsp4e.LanguageServersRegistry;
22-
import org.eclipse.lsp4j.ExecuteCommandOptions;
2319
import org.eclipse.lsp4j.ExecuteCommandParams;
20+
import org.springframework.ide.eclipse.boot.launch.BootLsCommandUtils;
2421

2522
import com.google.common.collect.ImmutableList;
2623

@@ -35,7 +32,7 @@ public final class DefaultLiveProcessCommandExecutor implements LiveProcessComma
3532
@SuppressWarnings("unchecked")
3633
@Override
3734
public Flux<CommandInfo> listCommands() {
38-
List<CompletableFuture<List<CommandInfo>>> futures = getLanguageServers().computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(
35+
List<CompletableFuture<List<CommandInfo>>> futures = BootLsCommandUtils.getLanguageServers(CMD_LIST_PROCESSES).computeAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(
3936
CMD_LIST_PROCESSES,
4037
ImmutableList.of()
4138
)).thenApply(o -> {
@@ -54,21 +51,10 @@ public Flux<CommandInfo> listCommands() {
5451

5552
@Override
5653
public Mono<Void> executeCommand(CommandInfo cmd) {
57-
return Mono.fromFuture(getLanguageServers().collectAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(
54+
return Mono.fromFuture(BootLsCommandUtils.getLanguageServers(cmd.command).collectAll(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(
5855
cmd.command,
5956
ImmutableList.of(cmd.info)
60-
))).thenAccept(null));
57+
))).thenAccept(l -> {}));
6158
}
6259

63-
private LanguageServerProjectExecutor getLanguageServers() {
64-
return LanguageServers.forProject(null).withFilter(cap -> {
65-
ExecuteCommandOptions commandCap = cap.getExecuteCommandProvider();
66-
if (commandCap!=null) {
67-
List<String> supportedCommands = commandCap.getCommands();
68-
return supportedCommands!=null && supportedCommands.contains(CMD_LIST_PROCESSES);
69-
}
70-
return false;
71-
}).withPreferredServer(LanguageServersRegistry.getInstance()
72-
.getDefinition("org.eclipse.languageserver.languages.springboot"));
73-
}
7460
}

eclipse-extensions/org.springframework.ide.eclipse.boot.launch/.classpath

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<classpath>
3-
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
3+
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
44
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
55
<classpathentry kind="src" path="src"/>
66
<classpathentry kind="output" path="target/classes"/>
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
eclipse.preferences.version=1
22
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
3-
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
4-
org.eclipse.jdt.core.compiler.compliance=1.8
3+
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
4+
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
5+
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
6+
org.eclipse.jdt.core.compiler.compliance=17
7+
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
8+
org.eclipse.jdt.core.compiler.debug.localVariable=generate
9+
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
510
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
11+
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
612
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
13+
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
714
org.eclipse.jdt.core.compiler.release=disabled
8-
org.eclipse.jdt.core.compiler.source=1.8
15+
org.eclipse.jdt.core.compiler.source=17

eclipse-extensions/org.springframework.ide.eclipse.boot.launch/META-INF/MANIFEST.MF

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ Require-Bundle: org.eclipse.ui,
2929
org.eclipse.jface.text,
3030
org.eclipse.jdt.ui,
3131
org.eclipse.ui.genericeditor,
32-
org.eclipse.ui.ide
32+
org.eclipse.ui.ide,
33+
org.eclipse.lsp4e,
34+
org.eclipse.lsp4j,
35+
com.google.gson,
36+
org.eclipse.jdt.junit.core
3337
Bundle-RequiredExecutionEnvironment: JavaSE-17
3438
Bundle-ActivationPolicy: lazy
3539
Export-Package: org.springframework.ide.eclipse.boot.launch,

eclipse-extensions/org.springframework.ide.eclipse.boot.launch/src/org/springframework/ide/eclipse/boot/launch/BootLaunchActivator.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2015 Pivotal, Inc.
2+
* Copyright (c) 2015, 2024 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -15,6 +15,7 @@
1515
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
1616
import org.eclipse.core.runtime.preferences.InstanceScope;
1717
import org.eclipse.debug.core.DebugPlugin;
18+
import org.eclipse.debug.core.ILaunchManager;
1819
import org.eclipse.jface.preference.IPreferenceStore;
1920
import org.eclipse.ui.plugin.AbstractUIPlugin;
2021
import org.osgi.framework.Bundle;
@@ -37,14 +38,17 @@ public BootLaunchActivator() {
3738
@Override
3839
public void start(BundleContext context) throws Exception {
3940
super.start(context);
40-
workspaceListener = new BootLaunchConfDeleter(ResourcesPlugin.getWorkspace(), DebugPlugin.getDefault().getLaunchManager());
41+
ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
42+
workspaceListener = new BootLaunchConfDeleter(ResourcesPlugin.getWorkspace(), launchManager);
4143
instance = this;
4244

4345
IPreferenceStore myStore = instance.getPreferenceStore();
4446
if (!myStore.getBoolean("cglib.breakpoint.warning.disabled")) {
4547
setPreference("org.eclipse.jdt.debug.ui", "org.eclipse.jdt.debug.ui.prompt_unable_to_install_breakpoint", false);
4648
myStore.setValue("cglib.breakpoint.warning.disabled", true);
4749
}
50+
51+
launchManager.addLaunchListener(TestJarLaunchListener.getSingletonInstance());
4852
}
4953

5054
private void setPreference(String plugin, String key, boolean value) {
@@ -70,6 +74,8 @@ public void stop(BundleContext context) throws Exception {
7074
if (workspaceListener!=null) {
7175
workspaceListener.dispose();
7276
}
77+
ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
78+
launchManager.removeLaunchListener(TestJarLaunchListener.getSingletonInstance());
7379
super.stop(context);
7480
}
7581

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.eclipse.boot.launch;
12+
13+
import java.util.List;
14+
import java.util.Optional;
15+
import java.util.concurrent.CompletableFuture;
16+
17+
import org.eclipse.lsp4e.LanguageServers;
18+
import org.eclipse.lsp4e.LanguageServers.LanguageServerProjectExecutor;
19+
import org.eclipse.lsp4e.LanguageServersRegistry;
20+
import org.eclipse.lsp4j.ExecuteCommandOptions;
21+
import org.eclipse.lsp4j.ExecuteCommandParams;
22+
23+
import com.google.common.collect.ImmutableList;
24+
import com.google.gson.Gson;
25+
import com.google.gson.reflect.TypeToken;
26+
27+
@SuppressWarnings("restriction")
28+
public class BootLsCommandUtils {
29+
30+
private static Gson GSON = new Gson();
31+
32+
public static LanguageServerProjectExecutor getLanguageServers(String command) {
33+
return LanguageServers.forProject(null).withFilter(cap -> {
34+
ExecuteCommandOptions commandCap = cap.getExecuteCommandProvider();
35+
if (commandCap!=null) {
36+
List<String> supportedCommands = commandCap.getCommands();
37+
return supportedCommands!=null && supportedCommands.contains(command);
38+
}
39+
return false;
40+
}).withPreferredServer(LanguageServersRegistry.getInstance()
41+
.getDefinition("org.eclipse.languageserver.languages.springboot"));
42+
}
43+
44+
public static <T> CompletableFuture<Optional<T>> executeCommand(TypeToken<T> resType, String cmd, Object... params) {
45+
return getLanguageServers(cmd).computeFirst(ls -> ls.getWorkspaceService().executeCommand(new ExecuteCommandParams(
46+
cmd,
47+
ImmutableList.of(params)
48+
))).thenApply(o -> o.map(v -> GSON.fromJson(GSON.toJsonTree(v), resType)));
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.eclipse.boot.launch;
12+
13+
import java.io.File;
14+
import java.io.IOException;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.Set;
25+
import java.util.UUID;
26+
import java.util.concurrent.CompletableFuture;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.ExecutionException;
29+
import java.util.concurrent.TimeUnit;
30+
import java.util.concurrent.TimeoutException;
31+
import java.util.regex.Pattern;
32+
33+
import org.eclipse.core.runtime.CoreException;
34+
import org.eclipse.debug.core.ILaunch;
35+
import org.eclipse.debug.core.ILaunchConfiguration;
36+
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
37+
import org.eclipse.debug.core.ILaunchDelegate;
38+
import org.eclipse.debug.core.ILaunchManager;
39+
import org.eclipse.debug.core.ILaunchesListener2;
40+
import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
41+
import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
42+
import org.springsource.ide.eclipse.commons.livexp.util.Log;
43+
44+
import com.google.gson.reflect.TypeToken;
45+
46+
@SuppressWarnings("restriction")
47+
public class TestJarLaunchListener implements ILaunchesListener2 {
48+
49+
private static TestJarLaunchListener instance;
50+
51+
private static final Pattern TESTJAR_PATTERN = Pattern.compile("^spring-boot-testjars-\\d+\\.\\d+\\.\\d+(.*)?.jar$");
52+
53+
private static final String TESTJAR_ARTIFACTS = "spring.boot.test-jar-artifacts";
54+
55+
public record ExecutableProject(String name, String uri, String gav, String mainClass, Collection<String> classpath) {}
56+
57+
public static TestJarLaunchListener getSingletonInstance() {
58+
if (instance == null) {
59+
instance = new TestJarLaunchListener();
60+
}
61+
return instance;
62+
}
63+
64+
public void launchRemoved(ILaunch launch) {
65+
clearTestJarWorkspaceProjectFiles(launch.getLaunchConfiguration());
66+
}
67+
68+
private void clearTestJarWorkspaceProjectFiles(ILaunchConfiguration configuration) {
69+
try {
70+
if (JUnitLaunchConfigurationConstants.ID_JUNIT_APPLICATION.equals(configuration.getType().getIdentifier())) {
71+
Map<String, String> oldTestJarArtifacts = configuration.getAttribute(TESTJAR_ARTIFACTS, Collections.emptyMap());
72+
Map<String, String> env = new HashMap<>(configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, Collections.emptyMap()));
73+
for (Map.Entry<String, String> testJarArtifactEnvEntry : oldTestJarArtifacts.entrySet()) {
74+
env.remove(testJarArtifactEnvEntry.getKey());
75+
try {
76+
Files.deleteIfExists(Paths.get(testJarArtifactEnvEntry.getValue()));
77+
} catch (IOException e) {
78+
Log.log(e);
79+
}
80+
}
81+
ILaunchConfigurationWorkingCopy wc = configuration.getWorkingCopy();
82+
wc.removeAttribute(TESTJAR_ARTIFACTS);
83+
if (env.isEmpty()) {
84+
wc.removeAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES);
85+
} else {
86+
wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, env);
87+
}
88+
wc.doSave();
89+
}
90+
} catch (CoreException e) {
91+
Log.log(e);
92+
}
93+
}
94+
95+
private void setupTestJarWorkspaceProjectFiles(ILaunchConfiguration configuration, Map<String, String> testJarArtifactsEnvMap) {
96+
try {
97+
Map<String, String> originalEnv = new HashMap<>(configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, Collections.emptyMap()));
98+
Map<String, String> oldTestJarArtifacts = configuration.getAttribute(TESTJAR_ARTIFACTS, Collections.emptyMap());
99+
for (Map.Entry<String, String> envEntry : oldTestJarArtifacts.entrySet()) {
100+
try {
101+
Files.deleteIfExists(Paths.get(envEntry.getValue()));
102+
} catch (IOException e) {
103+
Log.log(e);
104+
}
105+
originalEnv.remove(envEntry.getKey());
106+
}
107+
for (String envVar : originalEnv.keySet()) {
108+
testJarArtifactsEnvMap.remove(envVar);
109+
}
110+
originalEnv.putAll(testJarArtifactsEnvMap);
111+
ILaunchConfigurationWorkingCopy wc = configuration.getWorkingCopy();
112+
wc.setAttribute(TESTJAR_ARTIFACTS, testJarArtifactsEnvMap);
113+
wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, originalEnv);
114+
wc.doSave();
115+
} catch (CoreException e) {
116+
e.printStackTrace();
117+
}
118+
}
119+
120+
public void launchAdded(ILaunch launch) {
121+
try {
122+
ILaunchConfiguration configuration = launch.getLaunchConfiguration();
123+
if (JUnitLaunchConfigurationConstants.ID_JUNIT_APPLICATION.equals(configuration.getType().getIdentifier())) {
124+
ILaunchDelegate[] delegates = configuration.getType().getDelegates(Set.of(launch.getLaunchMode()));
125+
if (delegates.length > 0) {
126+
JUnitLaunchConfigurationDelegate delegate = (JUnitLaunchConfigurationDelegate) delegates[0].getDelegate();
127+
String[] classpath = delegate.getClasspathAndModulepath(configuration)[0];
128+
if (isTestJarsOnClasspath(classpath)) {
129+
try {
130+
getTestJarArtifactsMap()
131+
.thenAccept(testJarArtifactsEnvMap -> setupTestJarWorkspaceProjectFiles(configuration, testJarArtifactsEnvMap))
132+
.get(3000000, TimeUnit.SECONDS);
133+
} catch (Exception e) {
134+
Log.log(e);
135+
}
136+
}
137+
}
138+
}
139+
} catch (CoreException e) {
140+
e.printStackTrace();
141+
}
142+
}
143+
144+
private static String createTestJarArtifactEnvKey(String gav) {
145+
return "TESTJARS_ARTIFACT_%s".formatted(gav.replace(":", "_"));
146+
}
147+
148+
private static boolean isTestJarsOnClasspath(String[] classpath) {
149+
for (String cpe : classpath) {
150+
Path p = Paths.get(cpe);
151+
if (Files.isRegularFile(p) && TESTJAR_PATTERN.matcher(p.getFileName().toString()).matches()) {
152+
return true;
153+
}
154+
}
155+
return false;
156+
}
157+
158+
private CompletableFuture<Map<String, String>> getTestJarArtifactsMap() {
159+
try {
160+
Optional<?> opt = BootLsCommandUtils.executeCommand(TypeToken.getParameterized(List.class, ExecutableProject.class), "sts/spring-boot/executableBootProjects").get(3, TimeUnit.SECONDS);
161+
if (opt.isPresent() && opt.get() instanceof List<?> projects) {
162+
Map<String, String> gavToFile = new ConcurrentHashMap<>();
163+
CompletableFuture<?>[] futures = projects.stream().map(ExecutableProject.class::cast).map(p -> CompletableFuture.runAsync(() -> {
164+
try {
165+
Path file = Files.createTempFile("%s_".formatted(p.gav().replace(":", "_")), UUID.randomUUID().toString());
166+
Files.write(file, List.of(
167+
"# the main class to invoke",
168+
"main=%s".formatted(p.mainClass()),
169+
"# the classpath to use delimited by the OS specific delimiters",
170+
"classpath=%s".formatted(String.join(File.pathSeparator, p.classpath())
171+
)));
172+
gavToFile.put(createTestJarArtifactEnvKey(p.gav()), file.toFile().toString());
173+
} catch (IOException e) {
174+
Log.log(e);
175+
}
176+
})).toArray(CompletableFuture[]::new);
177+
return CompletableFuture.allOf(futures).thenApply(v -> gavToFile);
178+
}
179+
} catch (InterruptedException | ExecutionException | TimeoutException e) {
180+
Log.log(e);
181+
}
182+
183+
return CompletableFuture.completedFuture(Collections.emptyMap());
184+
}
185+
186+
@Override
187+
public void launchesRemoved(ILaunch[] launches) {
188+
// nothing to do
189+
}
190+
191+
@Override
192+
public void launchesAdded(ILaunch[] launches) {
193+
for (ILaunch l : launches) {
194+
launchAdded(l);
195+
}
196+
}
197+
198+
@Override
199+
public void launchesChanged(ILaunch[] launches) {
200+
// nothing to do
201+
}
202+
203+
@Override
204+
public void launchesTerminated(ILaunch[] launches) {
205+
for (ILaunch l : launches) {
206+
clearTestJarWorkspaceProjectFiles(l.getLaunchConfiguration());
207+
}
208+
}
209+
210+
}

0 commit comments

Comments
 (0)