Skip to content

Commit 90d824f

Browse files
dreis2211philwebb
authored andcommitted
Extract ModifiedClassPathClass logic
Extract classes from `ModifiedClassPathRunner` so that they can be reused. See gh-17491
1 parent ca1808e commit 90d824f

File tree

3 files changed

+295
-222
lines changed

3 files changed

+295
-222
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testsupport.runner.classpath;
18+
19+
import java.net.URL;
20+
import java.net.URLClassLoader;
21+
22+
/**
23+
* Custom {@link URLClassLoader} that modifies the class path.
24+
*
25+
* @author Andy Wilkinson
26+
* @author Christoph Dreis
27+
* @see ModifiedClassPathClassLoaderFactory
28+
*/
29+
final class ModifiedClassPathClassLoader extends URLClassLoader {
30+
31+
private final ClassLoader junitLoader;
32+
33+
ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) {
34+
super(urls, parent);
35+
this.junitLoader = junitLoader;
36+
}
37+
38+
@Override
39+
public Class<?> loadClass(String name) throws ClassNotFoundException {
40+
if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) {
41+
return this.junitLoader.loadClass(name);
42+
}
43+
return super.loadClass(name);
44+
}
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testsupport.runner.classpath;
18+
19+
import java.io.File;
20+
import java.lang.management.ManagementFactory;
21+
import java.net.URISyntaxException;
22+
import java.net.URL;
23+
import java.net.URLClassLoader;
24+
import java.util.ArrayList;
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.jar.Attributes;
29+
import java.util.jar.JarFile;
30+
import java.util.regex.Pattern;
31+
import java.util.stream.Stream;
32+
33+
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
34+
import org.eclipse.aether.DefaultRepositorySystemSession;
35+
import org.eclipse.aether.RepositorySystem;
36+
import org.eclipse.aether.artifact.DefaultArtifact;
37+
import org.eclipse.aether.collection.CollectRequest;
38+
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
39+
import org.eclipse.aether.graph.Dependency;
40+
import org.eclipse.aether.impl.DefaultServiceLocator;
41+
import org.eclipse.aether.repository.LocalRepository;
42+
import org.eclipse.aether.repository.RemoteRepository;
43+
import org.eclipse.aether.resolution.ArtifactResult;
44+
import org.eclipse.aether.resolution.DependencyRequest;
45+
import org.eclipse.aether.resolution.DependencyResult;
46+
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
47+
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
48+
import org.eclipse.aether.transport.http.HttpTransporterFactory;
49+
50+
import org.springframework.core.annotation.MergedAnnotation;
51+
import org.springframework.core.annotation.MergedAnnotations;
52+
import org.springframework.util.AntPathMatcher;
53+
import org.springframework.util.StringUtils;
54+
55+
/**
56+
* A factory that creates a custom class loader with a modified class path that is used to
57+
* both load the test class and as the thread context class loader while the test is being
58+
* run.
59+
*
60+
* @author Andy Wilkinson
61+
* @author Christoph Dreis
62+
* @see ModifiedClassPathClassLoader
63+
*/
64+
final class ModifiedClassPathClassLoaderFactory {
65+
66+
private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern.compile(".*classpath(\\d+)?\\.jar");
67+
68+
private ModifiedClassPathClassLoaderFactory() {
69+
}
70+
71+
static URLClassLoader createTestClassLoader(Class<?> testClass) {
72+
ClassLoader classLoader = testClass.getClassLoader();
73+
return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), testClass),
74+
classLoader.getParent(), classLoader);
75+
}
76+
77+
private static URL[] extractUrls(ClassLoader classLoader) {
78+
List<URL> extractedUrls = new ArrayList<>();
79+
doExtractUrls(classLoader).forEach((URL url) -> {
80+
if (isManifestOnlyJar(url)) {
81+
extractedUrls.addAll(extractUrlsFromManifestClassPath(url));
82+
}
83+
else {
84+
extractedUrls.add(url);
85+
}
86+
});
87+
return extractedUrls.toArray(new URL[0]);
88+
}
89+
90+
private static Stream<URL> doExtractUrls(ClassLoader classLoader) {
91+
if (classLoader instanceof URLClassLoader) {
92+
return Stream.of(((URLClassLoader) classLoader).getURLs());
93+
}
94+
return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator))
95+
.map(ModifiedClassPathClassLoaderFactory::toURL);
96+
}
97+
98+
private static URL toURL(String entry) {
99+
try {
100+
return new File(entry).toURI().toURL();
101+
}
102+
catch (Exception ex) {
103+
throw new IllegalArgumentException(ex);
104+
}
105+
}
106+
107+
private static boolean isManifestOnlyJar(URL url) {
108+
return isSurefireBooterJar(url) || isShortenedIntelliJJar(url);
109+
}
110+
111+
private static boolean isSurefireBooterJar(URL url) {
112+
return url.getPath().contains("surefirebooter");
113+
}
114+
115+
private static boolean isShortenedIntelliJJar(URL url) {
116+
String urlPath = url.getPath();
117+
boolean isCandidate = INTELLIJ_CLASSPATH_JAR_PATTERN.matcher(urlPath).matches();
118+
if (isCandidate) {
119+
try {
120+
Attributes attributes = getManifestMainAttributesFromUrl(url);
121+
String createdBy = attributes.getValue("Created-By");
122+
return createdBy != null && createdBy.contains("IntelliJ");
123+
}
124+
catch (Exception ex) {
125+
}
126+
}
127+
return false;
128+
}
129+
130+
private static List<URL> extractUrlsFromManifestClassPath(URL booterJar) {
131+
List<URL> urls = new ArrayList<>();
132+
try {
133+
for (String entry : getClassPath(booterJar)) {
134+
urls.add(new URL(entry));
135+
}
136+
}
137+
catch (Exception ex) {
138+
throw new RuntimeException(ex);
139+
}
140+
return urls;
141+
}
142+
143+
private static String[] getClassPath(URL booterJar) throws Exception {
144+
Attributes attributes = getManifestMainAttributesFromUrl(booterJar);
145+
return StringUtils.delimitedListToStringArray(attributes.getValue(Attributes.Name.CLASS_PATH), " ");
146+
}
147+
148+
private static Attributes getManifestMainAttributesFromUrl(URL url) throws Exception {
149+
try (JarFile jarFile = new JarFile(new File(url.toURI()))) {
150+
return jarFile.getManifest().getMainAttributes();
151+
}
152+
}
153+
154+
private static URL[] processUrls(URL[] urls, Class<?> testClass) {
155+
MergedAnnotations annotations = MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.EXHAUSTIVE);
156+
ClassPathEntryFilter filter = new ClassPathEntryFilter(annotations.get(ClassPathExclusions.class));
157+
List<URL> processedUrls = new ArrayList<>();
158+
List<URL> additionalUrls = getAdditionalUrls(annotations.get(ClassPathOverrides.class));
159+
processedUrls.addAll(additionalUrls);
160+
for (URL url : urls) {
161+
if (!filter.isExcluded(url)) {
162+
processedUrls.add(url);
163+
}
164+
}
165+
return processedUrls.toArray(new URL[0]);
166+
}
167+
168+
private static List<URL> getAdditionalUrls(MergedAnnotation<ClassPathOverrides> annotation) {
169+
if (!annotation.isPresent()) {
170+
return Collections.emptyList();
171+
}
172+
return resolveCoordinates(annotation.getStringArray(MergedAnnotation.VALUE));
173+
}
174+
175+
private static List<URL> resolveCoordinates(String[] coordinates) {
176+
DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator();
177+
serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
178+
serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class);
179+
RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class);
180+
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
181+
LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository");
182+
session.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(session, localRepository));
183+
CollectRequest collectRequest = new CollectRequest(null, Arrays.asList(
184+
new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2").build()));
185+
186+
collectRequest.setDependencies(createDependencies(coordinates));
187+
DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null);
188+
try {
189+
DependencyResult result = repositorySystem.resolveDependencies(session, dependencyRequest);
190+
List<URL> resolvedArtifacts = new ArrayList<>();
191+
for (ArtifactResult artifact : result.getArtifactResults()) {
192+
resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL());
193+
}
194+
return resolvedArtifacts;
195+
}
196+
catch (Exception ignored) {
197+
return Collections.emptyList();
198+
199+
}
200+
}
201+
202+
private static List<Dependency> createDependencies(String[] allCoordinates) {
203+
List<Dependency> dependencies = new ArrayList<>();
204+
for (String coordinate : allCoordinates) {
205+
dependencies.add(new Dependency(new DefaultArtifact(coordinate), null));
206+
}
207+
return dependencies;
208+
}
209+
210+
/**
211+
* Filter for class path entries.
212+
*/
213+
private static final class ClassPathEntryFilter {
214+
215+
private final List<String> exclusions;
216+
217+
private final AntPathMatcher matcher = new AntPathMatcher();
218+
219+
private ClassPathEntryFilter(MergedAnnotation<ClassPathExclusions> annotation) {
220+
this.exclusions = new ArrayList<>();
221+
this.exclusions.add("log4j-*.jar");
222+
if (annotation.isPresent()) {
223+
this.exclusions.addAll(Arrays.asList(annotation.getStringArray(MergedAnnotation.VALUE)));
224+
}
225+
}
226+
227+
private boolean isExcluded(URL url) {
228+
if (!"file".equals(url.getProtocol())) {
229+
return false;
230+
}
231+
String name;
232+
try {
233+
name = new File(url.toURI()).getName();
234+
}
235+
catch (URISyntaxException ex) {
236+
return false;
237+
}
238+
for (String exclusion : this.exclusions) {
239+
if (this.matcher.match(exclusion, name)) {
240+
return true;
241+
}
242+
}
243+
return false;
244+
}
245+
246+
}
247+
248+
}

0 commit comments

Comments
 (0)