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 .Objects ;
45
46
import java .util .Set ;
47
+ import java .util .StringTokenizer ;
46
48
import java .util .function .Predicate ;
49
+ import java .util .jar .Attributes ;
50
+ import java .util .jar .Attributes .Name ;
47
51
import java .util .jar .JarEntry ;
48
52
import java .util .jar .JarFile ;
53
+ import java .util .jar .Manifest ;
49
54
import java .util .stream .Collectors ;
50
55
import java .util .stream .Stream ;
51
56
import java .util .zip .ZipException ;
@@ -227,6 +232,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
227
232
private static final Predicate <ResolvedModule > isNotSystemModule =
228
233
resolvedModule -> !systemModuleNames .contains (resolvedModule .name ());
229
234
235
+ @ Nullable
236
+ private static Set <ClassPathManifestEntry > classPathManifestEntriesCache ;
237
+
230
238
@ Nullable
231
239
private static Method equinoxResolveMethod ;
232
240
@@ -505,25 +513,30 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<
505
513
* @since 4.3
506
514
*/
507
515
protected void addClassPathManifestEntries (Set <Resource > result ) {
516
+ Set <ClassPathManifestEntry > entries = classPathManifestEntriesCache ;
517
+ if (entries == null ) {
518
+ entries = getClassPathManifestEntries ();
519
+ classPathManifestEntriesCache = entries ;
520
+ }
521
+ for (ClassPathManifestEntry entry : entries ) {
522
+ if (!result .contains (entry .resource ()) &&
523
+ (entry .alternative () != null && !result .contains (entry .alternative ()))) {
524
+ result .add (entry .resource ());
525
+ }
526
+ }
527
+ }
528
+
529
+ private Set <ClassPathManifestEntry > getClassPathManifestEntries () {
530
+ Set <ClassPathManifestEntry > manifestEntries = new HashSet <>();
531
+ Set <File > seen = new HashSet <>();
508
532
try {
509
- String javaClassPathProperty = System .getProperty ("java.class.path" );
510
- for (String path : StringUtils .delimitedListToStringArray (javaClassPathProperty , File .pathSeparator )) {
533
+ String paths = System .getProperty ("java.class.path" );
534
+ for (String path : StringUtils .delimitedListToStringArray (paths , File .pathSeparator )) {
511
535
try {
512
- String filePath = new File (path ).getAbsolutePath ();
513
- int prefixIndex = filePath .indexOf (':' );
514
- if (prefixIndex == 1 ) {
515
- // Possibly a drive prefix on Windows (for example, "c:"), so we prepend a slash
516
- // and convert the drive letter to uppercase for consistent duplicate detection.
517
- filePath = "/" + StringUtils .capitalize (filePath );
518
- }
519
- // Since '#' can appear in directories/filenames, java.net.URL should not treat it as a fragment
520
- filePath = StringUtils .replace (filePath , "#" , "%23" );
521
- // Build URL that points to the root of the jar file
522
- UrlResource jarResource = new UrlResource (ResourceUtils .JAR_URL_PREFIX +
523
- ResourceUtils .FILE_URL_PREFIX + filePath + ResourceUtils .JAR_URL_SEPARATOR );
524
- // Potentially overlapping with URLClassLoader.getURLs() result in addAllClassLoaderJarRoots().
525
- if (!result .contains (jarResource ) && !hasDuplicate (filePath , result ) && jarResource .exists ()) {
526
- result .add (jarResource );
536
+ File jar = new File (path ).getAbsoluteFile ();
537
+ if (jar .isFile () && seen .add (jar )) {
538
+ manifestEntries .add (ClassPathManifestEntry .of (jar ));
539
+ manifestEntries .addAll (getClassPathManifestEntriesFromJar (jar ));
527
540
}
528
541
}
529
542
catch (MalformedURLException ex ) {
@@ -533,34 +546,39 @@ protected void addClassPathManifestEntries(Set<Resource> result) {
533
546
}
534
547
}
535
548
}
549
+ return Collections .unmodifiableSet (manifestEntries );
536
550
}
537
551
catch (Exception ex ) {
538
552
if (logger .isDebugEnabled ()) {
539
553
logger .debug ("Failed to evaluate 'java.class.path' manifest entries: " + ex );
540
554
}
555
+ return Collections .emptySet ();
541
556
}
542
557
}
543
558
544
- /**
545
- * Check whether the given file path has a duplicate but differently structured entry
546
- * in the existing result, i.e. with or without a leading slash.
547
- * @param filePath the file path (with or without a leading slash)
548
- * @param result the current result
549
- * @return {@code true} if there is a duplicate (i.e. to ignore the given file path),
550
- * {@code false} to proceed with adding a corresponding resource to the current result
551
- */
552
- private boolean hasDuplicate (String filePath , Set <Resource > result ) {
553
- if (result .isEmpty ()) {
554
- return false ;
555
- }
556
- String duplicatePath = (filePath .startsWith ("/" ) ? filePath .substring (1 ) : "/" + filePath );
557
- try {
558
- return result .contains (new UrlResource (ResourceUtils .JAR_URL_PREFIX + ResourceUtils .FILE_URL_PREFIX +
559
- duplicatePath + ResourceUtils .JAR_URL_SEPARATOR ));
559
+ private Set <ClassPathManifestEntry > getClassPathManifestEntriesFromJar (File jar ) throws IOException {
560
+ File parent = jar .getAbsoluteFile ().getParentFile ();
561
+ try (JarFile jarFile = new JarFile (jar )) {
562
+ Manifest manifest = jarFile .getManifest ();
563
+ Attributes attributes = (manifest != null ) ? manifest .getMainAttributes () : null ;
564
+ String classPath = (attributes != null ) ? attributes .getValue (Name .CLASS_PATH ) : null ;
565
+ Set <ClassPathManifestEntry > manifestEntries = new HashSet <>();
566
+ if (StringUtils .hasLength (classPath )) {
567
+ StringTokenizer tokenizer = new StringTokenizer (classPath );
568
+ while (tokenizer .hasMoreTokens ()) {
569
+ File candidate = new File (parent , tokenizer .nextToken ());
570
+ if (candidate .isFile () && candidate .getCanonicalPath ().contains (parent .getCanonicalPath ())) {
571
+ manifestEntries .add (ClassPathManifestEntry .of (candidate ));
572
+ }
573
+ }
574
+ }
575
+ return Collections .unmodifiableSet (manifestEntries );
560
576
}
561
- catch (MalformedURLException ex ) {
562
- // Ignore: just for testing against duplicate.
563
- return false ;
577
+ catch (Exception ex ) {
578
+ if (logger .isDebugEnabled ()) {
579
+ logger .debug ("Failed to load manifest entries from jar file '" + jar + "': " + ex );
580
+ }
581
+ return Collections .emptySet ();
564
582
}
565
583
}
566
584
@@ -1062,4 +1080,51 @@ public String toString() {
1062
1080
}
1063
1081
}
1064
1082
1083
+
1084
+ /**
1085
+ * A single {@code Class-Path} manifest entry.
1086
+ */
1087
+ private record ClassPathManifestEntry (Resource resource , @ Nullable Resource alternative ) {
1088
+
1089
+ private static final String JARFILE_URL_PREFIX = ResourceUtils .JAR_URL_PREFIX + ResourceUtils .FILE_URL_PREFIX ;
1090
+
1091
+ static ClassPathManifestEntry of (File file ) throws MalformedURLException {
1092
+ String path = fixPath (file .getAbsolutePath ());
1093
+ Resource resource = asJarFileResource (path );
1094
+ Resource alternative = createAlternative (path );
1095
+ return new ClassPathManifestEntry (resource , alternative );
1096
+ }
1097
+
1098
+ private static String fixPath (String path ) {
1099
+ int prefixIndex = path .indexOf (':' );
1100
+ if (prefixIndex == 1 ) {
1101
+ // Possibly a drive prefix on Windows (for example, "c:"), so we prepend a slash
1102
+ // and convert the drive letter to uppercase for consistent duplicate detection.
1103
+ path = "/" + StringUtils .capitalize (path );
1104
+ }
1105
+ // Since '#' can appear in directories/filenames, java.net.URL should not treat it as a fragment
1106
+ return StringUtils .replace (path , "#" , "%23" );
1107
+ }
1108
+
1109
+ /**
1110
+ * Return a alternative form of the resource, i.e. with or without a leading slash.
1111
+ * @param path the file path (with or without a leading slash)
1112
+ * @return the alternative form or {@code null}
1113
+ */
1114
+ @ Nullable
1115
+ private static Resource createAlternative (String path ) {
1116
+ try {
1117
+ String alternativePath = path .startsWith ("/" ) ? path .substring (1 ) : "/" + path ;
1118
+ return asJarFileResource (alternativePath );
1119
+ }
1120
+ catch (MalformedURLException ex ) {
1121
+ return null ;
1122
+ }
1123
+ }
1124
+
1125
+ private static Resource asJarFileResource (String path )
1126
+ throws MalformedURLException {
1127
+ return new UrlResource (JARFILE_URL_PREFIX + path + ResourceUtils .JAR_URL_SEPARATOR );
1128
+ }
1129
+ }
1065
1130
}
0 commit comments