3939import java .nio .file .Path ;
4040import java .util .Collections ;
4141import java .util .Enumeration ;
42+ import java .util .HashSet ;
4243import java .util .LinkedHashSet ;
4344import java .util .Map ;
4445import java .util .NavigableSet ;
4546import java .util .Objects ;
4647import java .util .Set ;
48+ import java .util .StringTokenizer ;
4749import java .util .TreeSet ;
4850import java .util .concurrent .ConcurrentHashMap ;
4951import java .util .function .Predicate ;
52+ import java .util .jar .Attributes ;
53+ import java .util .jar .Attributes .Name ;
5054import java .util .jar .JarEntry ;
5155import java .util .jar .JarFile ;
56+ import java .util .jar .Manifest ;
5257import java .util .stream .Collectors ;
5358import java .util .stream .Stream ;
5459import java .util .zip .ZipException ;
@@ -230,6 +235,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
230235 private static final Predicate <ResolvedModule > isNotSystemModule =
231236 resolvedModule -> !systemModuleNames .contains (resolvedModule .name ());
232237
238+ @ Nullable
239+ private static Set <ClassPathManifestEntry > classPathManifestEntriesCache ;
240+
233241 @ Nullable
234242 private static Method equinoxResolveMethod ;
235243
@@ -522,25 +530,30 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<
522530 * @since 4.3
523531 */
524532 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 <>();
525549 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 )) {
528552 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 ));
544557 }
545558 }
546559 catch (MalformedURLException ex ) {
@@ -550,34 +563,46 @@ protected void addClassPathManifestEntries(Set<Resource> result) {
550563 }
551564 }
552565 }
566+ return Collections .unmodifiableSet (manifestEntries );
553567 }
554568 catch (Exception ex ) {
555569 if (logger .isDebugEnabled ()) {
556570 logger .debug ("Failed to evaluate 'java.class.path' manifest entries: " + ex );
557571 }
572+ return Collections .emptySet ();
558573 }
559574 }
560575
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 );
577600 }
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 ();
581606 }
582607 }
583608
@@ -1170,4 +1195,51 @@ public String toString() {
11701195 }
11711196 }
11721197
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+ }
11731245}
0 commit comments