Skip to content

Commit 83f7494

Browse files
committed
Use SPIClassloader
1 parent c9454a2 commit 83f7494

File tree

5 files changed

+251
-106
lines changed

5 files changed

+251
-106
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*
13+
*/
14+
package org.eclipse.pde.internal.junit.runtime;
15+
16+
import org.eclipse.core.runtime.Platform;
17+
import org.osgi.framework.Bundle;
18+
import org.osgi.framework.FrameworkUtil;
19+
20+
/**
21+
* Default implementation used with Java 1.8
22+
* TODO provide MR variant using stack walker, currently blocked by JDT bug ...
23+
*/
24+
public class Caller {
25+
private static final Bundle BUNDLE = FrameworkUtil.getBundle(Caller.class);
26+
private static final Bundle loaderBundle;
27+
28+
static {
29+
Bundle junit5RuntimeBundle = Platform.getBundle("org.eclipse.jdt.junit5.runtime"); //$NON-NLS-1$
30+
if (junit5RuntimeBundle == null) {
31+
loaderBundle = BUNDLE;
32+
} else {
33+
loaderBundle = junit5RuntimeBundle;
34+
}
35+
}
36+
37+
static Bundle getBundle() {
38+
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
39+
for (StackTraceElement element : stackTraceElements) {
40+
try {
41+
Class<?> clz = loaderBundle.loadClass(element.getClassName());
42+
Bundle bundle = FrameworkUtil.getBundle(clz);
43+
if (bundle == BUNDLE) {
44+
continue;
45+
}
46+
if (bundle != null) {
47+
return bundle;
48+
}
49+
} catch (ClassNotFoundException e) {
50+
continue;
51+
}
52+
}
53+
return null;
54+
}
55+
56+
}

ui/org.eclipse.pde.junit.runtime/src/org/eclipse/pde/internal/junit/runtime/MultiBundleClassLoader.java

Lines changed: 0 additions & 100 deletions
This file was deleted.

ui/org.eclipse.pde.junit.runtime/src/org/eclipse/pde/internal/junit/runtime/RemotePluginTestRunner.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
*******************************************************************************/
1515
package org.eclipse.pde.internal.junit.runtime;
1616

17-
import static java.util.stream.Collectors.toCollection;
18-
1917
import java.io.IOException;
2018
import java.net.URL;
2119
import java.util.ArrayList;
@@ -24,6 +22,7 @@
2422
import java.util.Enumeration;
2523
import java.util.List;
2624
import java.util.function.Predicate;
25+
import java.util.stream.Collectors;
2726

2827
import org.eclipse.core.runtime.Platform;
2928
import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
@@ -143,12 +142,12 @@ private static ClassLoader createJUnit5PluginClassLoader(String testPluginName)
143142
if (junit5RuntimeBundle != null) {
144143
platformEngineBundles.add(junit5RuntimeBundle);
145144
}
146-
return new MultiBundleClassLoader(platformEngineBundles, junit5RuntimeBundle);
145+
return new SPIBundleClassLoader(platformEngineBundles);
147146
}
148147

149148
private static List<Bundle> findTestEngineBundles() {
150149
BundleContext bundleContext = FrameworkUtil.getBundle(RemotePluginTestRunner.class).getBundleContext();
151-
return Arrays.stream(bundleContext.getBundles()).filter(RemotePluginTestRunner::providesTestEngine).collect(toCollection(ArrayList::new));
150+
return Arrays.stream(bundleContext.getBundles()).filter(RemotePluginTestRunner::providesTestEngine).collect(Collectors.toCollection(ArrayList::new));
152151
}
153152

154153
/**
@@ -192,8 +191,7 @@ public void init(String[] args) {
192191
// during initialization - see bug 520811
193192
ClassLoader currentTCCL = Thread.currentThread().getContextClassLoader();
194193
try {
195-
Bundle junit5RuntimeBundle = Platform.getBundle("org.eclipse.jdt.junit5.runtime"); //$NON-NLS-1$
196-
Thread.currentThread().setContextClassLoader(new MultiBundleClassLoader(findTestEngineBundles(), junit5RuntimeBundle));
194+
Thread.currentThread().setContextClassLoader(new SPIBundleClassLoader(findTestEngineBundles()));
197195
defaultInit(args);
198196
} finally {
199197
Thread.currentThread().setContextClassLoader(currentTCCL);
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*
13+
*/
14+
package org.eclipse.pde.internal.junit.runtime;
15+
16+
import java.io.IOException;
17+
import java.net.URL;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.Enumeration;
22+
import java.util.Iterator;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.concurrent.ConcurrentHashMap;
26+
27+
import org.eclipse.core.runtime.FileLocator;
28+
import org.osgi.framework.Bundle;
29+
30+
/**
31+
* The classloader wraps the OSGi provided one but gives access for the JUnit
32+
* runer to any SPI declared services.
33+
*/
34+
class SPIBundleClassLoader extends ClassLoader {
35+
36+
private static final String META_INF_SERVICES = "META-INF/services/"; //$NON-NLS-1$
37+
private List<Bundle> bundles;
38+
private Map<String, List<SPIMapping>> mappings = new ConcurrentHashMap<>();
39+
40+
SPIBundleClassLoader(List<Bundle> bundles) {
41+
super(null);
42+
this.bundles = bundles;
43+
}
44+
45+
@Override
46+
protected Class<?> findClass(String name) throws ClassNotFoundException {
47+
Iterator<SPIMapping> spi = mappings.values().stream().flatMap(Collection::stream).filter(mapping -> mapping.hasService(name)).iterator();
48+
if (spi.hasNext()) {
49+
Bundle caller = Caller.getBundle();
50+
while (spi.hasNext()) {
51+
SPIMapping mapping = spi.next();
52+
if (mapping.isCompatible(caller)) {
53+
return mapping.loadImplementation(name);
54+
}
55+
}
56+
throw new ClassNotFoundException(name);
57+
}
58+
for (Bundle bundle : bundles) {
59+
try {
60+
Class<?> c = bundle.loadClass(name);
61+
if (c != null) {
62+
return c;
63+
}
64+
} catch (ClassNotFoundException e) {
65+
}
66+
}
67+
throw new ClassNotFoundException(name);
68+
}
69+
70+
@Override
71+
protected URL findResource(String name) {
72+
try {
73+
Enumeration<URL> resources = findResources(name);
74+
if (resources.hasMoreElements()) {
75+
return resources.nextElement();
76+
}
77+
} catch (IOException e) {
78+
}
79+
return null;
80+
}
81+
82+
@Override
83+
protected Enumeration<URL> findResources(String name) throws IOException {
84+
List<URL> result = new ArrayList<>();
85+
if (name.startsWith(META_INF_SERVICES)) {
86+
String serviceName = name.substring(META_INF_SERVICES.length());
87+
List<SPIMapping> spis = mappings.computeIfAbsent(name, spi -> {
88+
List<SPIMapping> list = new ArrayList<>();
89+
for (Bundle other : bundles) {
90+
URL entry = other.getEntry(name);
91+
if (entry != null) {
92+
try {
93+
list.add(new SPIMapping(other.loadClass(serviceName), other, entry));
94+
} catch (ClassNotFoundException e) {
95+
// should not happen
96+
}
97+
}
98+
}
99+
return list;
100+
});
101+
Bundle caller = Caller.getBundle();
102+
for (SPIMapping mapping : spis) {
103+
if (mapping.isCompatible(caller)) {
104+
result.add(mapping.getUrl());
105+
}
106+
}
107+
return Collections.enumeration(result);
108+
}
109+
for (Bundle bundle : bundles) {
110+
Enumeration<URL> resources = bundle.getResources(name);
111+
while (resources != null && resources.hasMoreElements()) {
112+
result.add(FileLocator.resolve(resources.nextElement()));
113+
}
114+
}
115+
return Collections.enumeration(result);
116+
}
117+
118+
@Override
119+
public String toString() {
120+
return "SPIBundleClassLoader for bundles " + bundles; //$NON-NLS-1$
121+
}
122+
123+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Christoph Läubrich - initial API and implementation
12+
*
13+
*/
14+
package org.eclipse.pde.internal.junit.runtime;
15+
16+
import java.io.BufferedReader;
17+
import java.io.IOException;
18+
import java.io.InputStreamReader;
19+
import java.net.URL;
20+
import java.util.HashSet;
21+
import java.util.Set;
22+
import java.util.stream.Collectors;
23+
24+
import org.osgi.framework.Bundle;
25+
26+
final class SPIMapping {
27+
28+
private final Class<?> serviceClass;
29+
private final Bundle bundle;
30+
private final Set<String> classes;
31+
private final URL url;
32+
33+
SPIMapping(Class<?> serviceClass, Bundle bundle, URL url) {
34+
this.serviceClass = serviceClass;
35+
this.bundle = bundle;
36+
this.url = url;
37+
this.classes = readClasses(url);
38+
}
39+
40+
private Set<String> readClasses(URL entry) {
41+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(entry.openStream()))) {
42+
return reader.lines().collect(Collectors.toSet());
43+
} catch (IOException e) {
44+
return new HashSet<>();
45+
}
46+
}
47+
48+
boolean isCompatible(Bundle other) {
49+
try {
50+
return other.loadClass(serviceClass.getName()) == serviceClass;
51+
} catch (ClassNotFoundException e) {
52+
return false;
53+
}
54+
}
55+
56+
URL getUrl() {
57+
return url;
58+
}
59+
60+
boolean hasService(String implementation) {
61+
return classes != null && classes.contains(implementation);
62+
}
63+
64+
Class<?> loadImplementation(String name) throws ClassNotFoundException {
65+
return bundle.loadClass(name);
66+
}
67+
68+
}

0 commit comments

Comments
 (0)