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,46 @@ 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
+ System .out .println ("Hello " +path );
589
+ if (path .indexOf (':' ) >= 0 && !"file" .equalsIgnoreCase (new URL (base , path ).getProtocol ())) {
590
+ // See jdk.internal.loader.URLClassPath.JarLoader.tryResolveFile(URL, String)
591
+ continue ;
592
+ }
593
+ File candidate = new File (parent , path );
594
+ if (candidate .isFile () && candidate .getCanonicalPath ().contains (parent .getCanonicalPath ())) {
595
+ manifestEntries .add (ClassPathManifestEntry .of (candidate ));
596
+ }
597
+ }
598
+ }
599
+ return Collections .unmodifiableSet (manifestEntries );
577
600
}
578
- catch (MalformedURLException ex ) {
579
- // Ignore: just for testing against duplicate.
580
- return false ;
601
+ catch (Exception ex ) {
602
+ if (logger .isDebugEnabled ()) {
603
+ logger .debug ("Failed to load manifest entries from jar file '" + jar + "': " + ex );
604
+ }
605
+ return Collections .emptySet ();
581
606
}
582
607
}
583
608
@@ -1170,4 +1195,51 @@ public String toString() {
1170
1195
}
1171
1196
}
1172
1197
1198
+
1199
+ /**
1200
+ * A single {@code Class-Path} manifest entry.
1201
+ */
1202
+ private record ClassPathManifestEntry (Resource resource , @ Nullable Resource alternative ) {
1203
+
1204
+ private static final String JARFILE_URL_PREFIX = ResourceUtils .JAR_URL_PREFIX + ResourceUtils .FILE_URL_PREFIX ;
1205
+
1206
+ static ClassPathManifestEntry of (File file ) throws MalformedURLException {
1207
+ String path = fixPath (file .getAbsolutePath ());
1208
+ Resource resource = asJarFileResource (path );
1209
+ Resource alternative = createAlternative (path );
1210
+ return new ClassPathManifestEntry (resource , alternative );
1211
+ }
1212
+
1213
+ private static String fixPath (String path ) {
1214
+ int prefixIndex = path .indexOf (':' );
1215
+ if (prefixIndex == 1 ) {
1216
+ // Possibly a drive prefix on Windows (for example, "c:"), so we prepend a slash
1217
+ // and convert the drive letter to uppercase for consistent duplicate detection.
1218
+ path = "/" + StringUtils .capitalize (path );
1219
+ }
1220
+ // Since '#' can appear in directories/filenames, java.net.URL should not treat it as a fragment
1221
+ return StringUtils .replace (path , "#" , "%23" );
1222
+ }
1223
+
1224
+ /**
1225
+ * Return a alternative form of the resource, i.e. with or without a leading slash.
1226
+ * @param path the file path (with or without a leading slash)
1227
+ * @return the alternative form or {@code null}
1228
+ */
1229
+ @ Nullable
1230
+ private static Resource createAlternative (String path ) {
1231
+ try {
1232
+ String alternativePath = path .startsWith ("/" ) ? path .substring (1 ) : "/" + path ;
1233
+ return asJarFileResource (alternativePath );
1234
+ }
1235
+ catch (MalformedURLException ex ) {
1236
+ return null ;
1237
+ }
1238
+ }
1239
+
1240
+ private static Resource asJarFileResource (String path )
1241
+ throws MalformedURLException {
1242
+ return new UrlResource (JARFILE_URL_PREFIX + path + ResourceUtils .JAR_URL_SEPARATOR );
1243
+ }
1244
+ }
1173
1245
}
0 commit comments