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 .Objects ;
4546import java .util .Set ;
47+ import java .util .StringTokenizer ;
4648import java .util .function .Predicate ;
49+ import java .util .jar .Attributes ;
50+ import java .util .jar .Attributes .Name ;
4751import java .util .jar .JarEntry ;
4852import java .util .jar .JarFile ;
53+ import java .util .jar .Manifest ;
4954import java .util .stream .Collectors ;
5055import java .util .stream .Stream ;
5156import java .util .zip .ZipException ;
@@ -227,6 +232,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
227232 private static final Predicate <ResolvedModule > isNotSystemModule =
228233 resolvedModule -> !systemModuleNames .contains (resolvedModule .name ());
229234
235+ @ Nullable
236+ private static Set <ClassPathManifestEntry > classPathManifestEntriesCache ;
237+
230238 @ Nullable
231239 private static Method equinoxResolveMethod ;
232240
@@ -505,25 +513,30 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<
505513 * @since 4.3
506514 */
507515 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 <>();
508532 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 )) {
511535 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 ));
527540 }
528541 }
529542 catch (MalformedURLException ex ) {
@@ -533,34 +546,39 @@ protected void addClassPathManifestEntries(Set<Resource> result) {
533546 }
534547 }
535548 }
549+ return Collections .unmodifiableSet (manifestEntries );
536550 }
537551 catch (Exception ex ) {
538552 if (logger .isDebugEnabled ()) {
539553 logger .debug ("Failed to evaluate 'java.class.path' manifest entries: " + ex );
540554 }
555+ return Collections .emptySet ();
541556 }
542557 }
543558
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 );
560576 }
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 ();
564582 }
565583 }
566584
@@ -1062,4 +1080,51 @@ public String toString() {
10621080 }
10631081 }
10641082
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+ }
10651130}
0 commit comments