1616
1717package org .springframework .boot .testsupport .runner .classpath ;
1818
19+ import java .io .File ;
20+ import java .lang .management .ManagementFactory ;
21+ import java .net .URISyntaxException ;
1922import java .net .URL ;
2023import 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 ;
2154
2255/**
2356 * Custom {@link URLClassLoader} that modifies the class path.
2457 *
2558 * @author Andy Wilkinson
2659 * @author Christoph Dreis
27- * @see ModifiedClassPathClassLoaderFactory
2860 */
2961final class ModifiedClassPathClassLoader extends URLClassLoader {
3062
63+ private static final Pattern INTELLIJ_CLASSPATH_JAR_PATTERN = Pattern .compile (".*classpath(\\ d+)?\\ .jar" );
64+
3165 private final ClassLoader junitLoader ;
3266
3367 ModifiedClassPathClassLoader (URL [] urls , ClassLoader parent , ClassLoader junitLoader ) {
@@ -43,4 +77,178 @@ public Class<?> loadClass(String name) throws ClassNotFoundException {
4377 return super .loadClass (name );
4478 }
4579
80+ static ModifiedClassPathClassLoader get (Class <?> testClass ) {
81+ ClassLoader classLoader = testClass .getClassLoader ();
82+ return new ModifiedClassPathClassLoader (processUrls (extractUrls (classLoader ), testClass ),
83+ classLoader .getParent (), classLoader );
84+ }
85+
86+ private static URL [] extractUrls (ClassLoader classLoader ) {
87+ List <URL > extractedUrls = new ArrayList <>();
88+ doExtractUrls (classLoader ).forEach ((URL url ) -> {
89+ if (isManifestOnlyJar (url )) {
90+ extractedUrls .addAll (extractUrlsFromManifestClassPath (url ));
91+ }
92+ else {
93+ extractedUrls .add (url );
94+ }
95+ });
96+ return extractedUrls .toArray (new URL [0 ]);
97+ }
98+
99+ private static Stream <URL > doExtractUrls (ClassLoader classLoader ) {
100+ if (classLoader instanceof URLClassLoader ) {
101+ return Stream .of (((URLClassLoader ) classLoader ).getURLs ());
102+ }
103+ return Stream .of (ManagementFactory .getRuntimeMXBean ().getClassPath ().split (File .pathSeparator ))
104+ .map (ModifiedClassPathClassLoader ::toURL );
105+ }
106+
107+ private static URL toURL (String entry ) {
108+ try {
109+ return new File (entry ).toURI ().toURL ();
110+ }
111+ catch (Exception ex ) {
112+ throw new IllegalArgumentException (ex );
113+ }
114+ }
115+
116+ private static boolean isManifestOnlyJar (URL url ) {
117+ return isSurefireBooterJar (url ) || isShortenedIntelliJJar (url );
118+ }
119+
120+ private static boolean isSurefireBooterJar (URL url ) {
121+ return url .getPath ().contains ("surefirebooter" );
122+ }
123+
124+ private static boolean isShortenedIntelliJJar (URL url ) {
125+ String urlPath = url .getPath ();
126+ boolean isCandidate = INTELLIJ_CLASSPATH_JAR_PATTERN .matcher (urlPath ).matches ();
127+ if (isCandidate ) {
128+ try {
129+ Attributes attributes = getManifestMainAttributesFromUrl (url );
130+ String createdBy = attributes .getValue ("Created-By" );
131+ return createdBy != null && createdBy .contains ("IntelliJ" );
132+ }
133+ catch (Exception ex ) {
134+ }
135+ }
136+ return false ;
137+ }
138+
139+ private static List <URL > extractUrlsFromManifestClassPath (URL booterJar ) {
140+ List <URL > urls = new ArrayList <>();
141+ try {
142+ for (String entry : getClassPath (booterJar )) {
143+ urls .add (new URL (entry ));
144+ }
145+ }
146+ catch (Exception ex ) {
147+ throw new RuntimeException (ex );
148+ }
149+ return urls ;
150+ }
151+
152+ private static String [] getClassPath (URL booterJar ) throws Exception {
153+ Attributes attributes = getManifestMainAttributesFromUrl (booterJar );
154+ return StringUtils .delimitedListToStringArray (attributes .getValue (Attributes .Name .CLASS_PATH ), " " );
155+ }
156+
157+ private static Attributes getManifestMainAttributesFromUrl (URL url ) throws Exception {
158+ try (JarFile jarFile = new JarFile (new File (url .toURI ()))) {
159+ return jarFile .getManifest ().getMainAttributes ();
160+ }
161+ }
162+
163+ private static URL [] processUrls (URL [] urls , Class <?> testClass ) {
164+ MergedAnnotations annotations = MergedAnnotations .from (testClass , MergedAnnotations .SearchStrategy .EXHAUSTIVE );
165+ ClassPathEntryFilter filter = new ClassPathEntryFilter (annotations .get (ClassPathExclusions .class ));
166+ List <URL > processedUrls = new ArrayList <>();
167+ List <URL > additionalUrls = getAdditionalUrls (annotations .get (ClassPathOverrides .class ));
168+ processedUrls .addAll (additionalUrls );
169+ for (URL url : urls ) {
170+ if (!filter .isExcluded (url )) {
171+ processedUrls .add (url );
172+ }
173+ }
174+ return processedUrls .toArray (new URL [0 ]);
175+ }
176+
177+ private static List <URL > getAdditionalUrls (MergedAnnotation <ClassPathOverrides > annotation ) {
178+ if (!annotation .isPresent ()) {
179+ return Collections .emptyList ();
180+ }
181+ return resolveCoordinates (annotation .getStringArray (MergedAnnotation .VALUE ));
182+ }
183+
184+ private static List <URL > resolveCoordinates (String [] coordinates ) {
185+ DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils .newServiceLocator ();
186+ serviceLocator .addService (RepositoryConnectorFactory .class , BasicRepositoryConnectorFactory .class );
187+ serviceLocator .addService (TransporterFactory .class , HttpTransporterFactory .class );
188+ RepositorySystem repositorySystem = serviceLocator .getService (RepositorySystem .class );
189+ DefaultRepositorySystemSession session = MavenRepositorySystemUtils .newSession ();
190+ LocalRepository localRepository = new LocalRepository (System .getProperty ("user.home" ) + "/.m2/repository" );
191+ session .setLocalRepositoryManager (repositorySystem .newLocalRepositoryManager (session , localRepository ));
192+ CollectRequest collectRequest = new CollectRequest (null , Arrays .asList (
193+ new RemoteRepository .Builder ("central" , "default" , "https://repo.maven.apache.org/maven2" ).build ()));
194+
195+ collectRequest .setDependencies (createDependencies (coordinates ));
196+ DependencyRequest dependencyRequest = new DependencyRequest (collectRequest , null );
197+ try {
198+ DependencyResult result = repositorySystem .resolveDependencies (session , dependencyRequest );
199+ List <URL > resolvedArtifacts = new ArrayList <>();
200+ for (ArtifactResult artifact : result .getArtifactResults ()) {
201+ resolvedArtifacts .add (artifact .getArtifact ().getFile ().toURI ().toURL ());
202+ }
203+ return resolvedArtifacts ;
204+ }
205+ catch (Exception ignored ) {
206+ return Collections .emptyList ();
207+
208+ }
209+ }
210+
211+ private static List <Dependency > createDependencies (String [] allCoordinates ) {
212+ List <Dependency > dependencies = new ArrayList <>();
213+ for (String coordinate : allCoordinates ) {
214+ dependencies .add (new Dependency (new DefaultArtifact (coordinate ), null ));
215+ }
216+ return dependencies ;
217+ }
218+
219+ /**
220+ * Filter for class path entries.
221+ */
222+ private static final class ClassPathEntryFilter {
223+
224+ private final List <String > exclusions ;
225+
226+ private final AntPathMatcher matcher = new AntPathMatcher ();
227+
228+ private ClassPathEntryFilter (MergedAnnotation <ClassPathExclusions > annotation ) {
229+ this .exclusions = new ArrayList <>();
230+ this .exclusions .add ("log4j-*.jar" );
231+ if (annotation .isPresent ()) {
232+ this .exclusions .addAll (Arrays .asList (annotation .getStringArray (MergedAnnotation .VALUE )));
233+ }
234+ }
235+
236+ private boolean isExcluded (URL url ) {
237+ if ("file" .equals (url .getProtocol ())) {
238+ try {
239+ String name = new File (url .toURI ()).getName ();
240+ for (String exclusion : this .exclusions ) {
241+ if (this .matcher .match (exclusion , name )) {
242+ return true ;
243+ }
244+ }
245+ }
246+ catch (URISyntaxException ex ) {
247+ }
248+ }
249+ return false ;
250+ }
251+
252+ }
253+
46254}
0 commit comments