39
39
import java .nio .file .Path ;
40
40
import java .util .Collections ;
41
41
import java .util .Enumeration ;
42
+ import java .util .HashSet ;
42
43
import java .util .LinkedHashSet ;
43
44
import java .util .Map ;
44
45
import java .util .NavigableSet ;
45
46
import java .util .Objects ;
46
47
import java .util .Set ;
48
+ import java .util .StringTokenizer ;
47
49
import java .util .TreeSet ;
48
50
import java .util .concurrent .ConcurrentHashMap ;
49
51
import java .util .function .Predicate ;
52
+ import java .util .jar .Attributes ;
53
+ import java .util .jar .Attributes .Name ;
50
54
import java .util .jar .JarEntry ;
51
55
import java .util .jar .JarFile ;
56
+ import java .util .jar .Manifest ;
52
57
import java .util .stream .Collectors ;
53
58
import java .util .stream .Stream ;
54
59
import java .util .zip .ZipException ;
@@ -230,6 +235,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
230
235
private static final Predicate <ResolvedModule > isNotSystemModule =
231
236
resolvedModule -> !systemModuleNames .contains (resolvedModule .name ());
232
237
238
+ @ Nullable
239
+ private static Set <ClassPathManifestEntry > classPathManifestEntriesCache ;
240
+
233
241
@ Nullable
234
242
private static Method equinoxResolveMethod ;
235
243
@@ -522,25 +530,30 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<
522
530
* @since 4.3
523
531
*/
524
532
protected void addClassPathManifestEntries (Set <Resource > result ) {
533
+ Set <ClassPathManifestEntry > entries = classPathManifestEntriesCache ;
534
+ if (entries == null ) {
535
+ entries = getClassPathManifestEntries ();
536
+ classPathManifestEntriesCache = entries ;
537
+ }
538
+ for (ClassPathManifestEntry entry : entries ) {
539
+ if (!result .contains (entry .resource ()) &&
540
+ (entry .alternative () != null && !result .contains (entry .alternative ()))) {
541
+ result .add (entry .resource ());
542
+ }
543
+ }
544
+ }
545
+
546
+ private Set <ClassPathManifestEntry > getClassPathManifestEntries () {
547
+ Set <ClassPathManifestEntry > manifestEntries = new HashSet <>();
548
+ Set <File > seen = new HashSet <>();
525
549
try {
526
- String javaClassPathProperty = System .getProperty ("java.class.path" );
527
- for (String path : StringUtils .delimitedListToStringArray (javaClassPathProperty , File .pathSeparator )) {
550
+ String paths = System .getProperty ("java.class.path" );
551
+ for (String path : StringUtils .delimitedListToStringArray (paths , File .pathSeparator )) {
528
552
try {
529
- String filePath = new File (path ).getAbsolutePath ();
530
- int prefixIndex = filePath .indexOf (':' );
531
- if (prefixIndex == 1 ) {
532
- // Possibly a drive prefix on Windows (for example, "c:"), so we prepend a slash
533
- // and convert the drive letter to uppercase for consistent duplicate detection.
534
- filePath = "/" + StringUtils .capitalize (filePath );
535
- }
536
- // Since '#' can appear in directories/filenames, java.net.URL should not treat it as a fragment
537
- filePath = StringUtils .replace (filePath , "#" , "%23" );
538
- // Build URL that points to the root of the jar file
539
- UrlResource jarResource = new UrlResource (ResourceUtils .JAR_URL_PREFIX +
540
- ResourceUtils .FILE_URL_PREFIX + filePath + ResourceUtils .JAR_URL_SEPARATOR );
541
- // Potentially overlapping with URLClassLoader.getURLs() result in addAllClassLoaderJarRoots().
542
- if (!result .contains (jarResource ) && !hasDuplicate (filePath , result ) && jarResource .exists ()) {
543
- result .add (jarResource );
553
+ File jar = new File (path ).getAbsoluteFile ();
554
+ if (jar .isFile () && seen .add (jar )) {
555
+ manifestEntries .add (ClassPathManifestEntry .of (jar ));
556
+ manifestEntries .addAll (getClassPathManifestEntriesFromJar (jar ));
544
557
}
545
558
}
546
559
catch (MalformedURLException ex ) {
@@ -550,34 +563,45 @@ protected void addClassPathManifestEntries(Set<Resource> result) {
550
563
}
551
564
}
552
565
}
566
+ return Collections .unmodifiableSet (manifestEntries );
553
567
}
554
568
catch (Exception ex ) {
555
569
if (logger .isDebugEnabled ()) {
556
570
logger .debug ("Failed to evaluate 'java.class.path' manifest entries: " + ex );
557
571
}
572
+ return Collections .emptySet ();
558
573
}
559
574
}
560
575
561
- /**
562
- * Check whether the given file path has a duplicate but differently structured entry
563
- * in the existing result, i.e. with or without a leading slash.
564
- * @param filePath the file path (with or without a leading slash)
565
- * @param result the current result
566
- * @return {@code true} if there is a duplicate (i.e. to ignore the given file path),
567
- * {@code false} to proceed with adding a corresponding resource to the current result
568
- */
569
- private boolean hasDuplicate (String filePath , Set <Resource > result ) {
570
- if (result .isEmpty ()) {
571
- return false ;
572
- }
573
- String duplicatePath = (filePath .startsWith ("/" ) ? filePath .substring (1 ) : "/" + filePath );
574
- try {
575
- return result .contains (new UrlResource (ResourceUtils .JAR_URL_PREFIX + ResourceUtils .FILE_URL_PREFIX +
576
- duplicatePath + ResourceUtils .JAR_URL_SEPARATOR ));
576
+ private Set <ClassPathManifestEntry > getClassPathManifestEntriesFromJar (File jar ) throws IOException {
577
+ URL base = jar .toURI ().toURL ();
578
+ File parent = jar .getAbsoluteFile ().getParentFile ();
579
+ try (JarFile jarFile = new JarFile (jar )) {
580
+ Manifest manifest = jarFile .getManifest ();
581
+ Attributes attributes = (manifest != null ) ? manifest .getMainAttributes () : null ;
582
+ String classPath = (attributes != null ) ? attributes .getValue (Name .CLASS_PATH ) : null ;
583
+ Set <ClassPathManifestEntry > manifestEntries = new HashSet <>();
584
+ if (StringUtils .hasLength (classPath )) {
585
+ StringTokenizer tokenizer = new StringTokenizer (classPath );
586
+ while (tokenizer .hasMoreTokens ()) {
587
+ String path = tokenizer .nextToken ();
588
+ if (path .indexOf (':' ) >= 0 && !"file" .equalsIgnoreCase (new URL (base , path ).getProtocol ())) {
589
+ // See jdk.internal.loader.URLClassPath.JarLoader.tryResolveFile(URL, String)
590
+ continue ;
591
+ }
592
+ File candidate = new File (parent , path );
593
+ if (candidate .isFile () && candidate .getCanonicalPath ().contains (parent .getCanonicalPath ())) {
594
+ manifestEntries .add (ClassPathManifestEntry .of (candidate ));
595
+ }
596
+ }
597
+ }
598
+ return Collections .unmodifiableSet (manifestEntries );
577
599
}
578
- catch (MalformedURLException ex ) {
579
- // Ignore: just for testing against duplicate.
580
- return false ;
600
+ catch (Exception ex ) {
601
+ if (logger .isDebugEnabled ()) {
602
+ logger .debug ("Failed to load manifest entries from jar file '" + jar + "': " + ex );
603
+ }
604
+ return Collections .emptySet ();
581
605
}
582
606
}
583
607
@@ -1170,4 +1194,51 @@ public String toString() {
1170
1194
}
1171
1195
}
1172
1196
1197
+
1198
+ /**
1199
+ * A single {@code Class-Path} manifest entry.
1200
+ */
1201
+ private record ClassPathManifestEntry (Resource resource , @ Nullable Resource alternative ) {
1202
+
1203
+ private static final String JARFILE_URL_PREFIX = ResourceUtils .JAR_URL_PREFIX + ResourceUtils .FILE_URL_PREFIX ;
1204
+
1205
+ static ClassPathManifestEntry of (File file ) throws MalformedURLException {
1206
+ String path = fixPath (file .getAbsolutePath ());
1207
+ Resource resource = asJarFileResource (path );
1208
+ Resource alternative = createAlternative (path );
1209
+ return new ClassPathManifestEntry (resource , alternative );
1210
+ }
1211
+
1212
+ private static String fixPath (String path ) {
1213
+ int prefixIndex = path .indexOf (':' );
1214
+ if (prefixIndex == 1 ) {
1215
+ // Possibly a drive prefix on Windows (for example, "c:"), so we prepend a slash
1216
+ // and convert the drive letter to uppercase for consistent duplicate detection.
1217
+ path = "/" + StringUtils .capitalize (path );
1218
+ }
1219
+ // Since '#' can appear in directories/filenames, java.net.URL should not treat it as a fragment
1220
+ return StringUtils .replace (path , "#" , "%23" );
1221
+ }
1222
+
1223
+ /**
1224
+ * Return a alternative form of the resource, i.e. with or without a leading slash.
1225
+ * @param path the file path (with or without a leading slash)
1226
+ * @return the alternative form or {@code null}
1227
+ */
1228
+ @ Nullable
1229
+ private static Resource createAlternative (String path ) {
1230
+ try {
1231
+ String alternativePath = path .startsWith ("/" ) ? path .substring (1 ) : "/" + path ;
1232
+ return asJarFileResource (alternativePath );
1233
+ }
1234
+ catch (MalformedURLException ex ) {
1235
+ return null ;
1236
+ }
1237
+ }
1238
+
1239
+ private static Resource asJarFileResource (String path )
1240
+ throws MalformedURLException {
1241
+ return new UrlResource (JARFILE_URL_PREFIX + path + ResourceUtils .JAR_URL_SEPARATOR );
1242
+ }
1243
+ }
1173
1244
}
0 commit comments