1
1
/*
2
- * Copyright 2002-2023 the original author or authors.
2
+ * Copyright 2002-2024 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
40
40
import java .util .Enumeration ;
41
41
import java .util .LinkedHashSet ;
42
42
import java .util .Map ;
43
+ import java .util .NavigableSet ;
43
44
import java .util .Objects ;
44
45
import java .util .Set ;
46
+ import java .util .TreeSet ;
47
+ import java .util .concurrent .ConcurrentHashMap ;
45
48
import java .util .function .Predicate ;
46
49
import java .util .jar .JarEntry ;
47
50
import java .util .jar .JarFile ;
@@ -247,6 +250,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
247
250
248
251
private PathMatcher pathMatcher = new AntPathMatcher ();
249
252
253
+ private final Map <String , Resource []> rootDirCache = new ConcurrentHashMap <>();
254
+
255
+ private final Map <String , NavigableSet <String >> jarEntryCache = new ConcurrentHashMap <>();
256
+
250
257
251
258
/**
252
259
* Create a {@code PathMatchingResourcePatternResolver} with a
@@ -355,6 +362,16 @@ public Resource[] getResources(String locationPattern) throws IOException {
355
362
}
356
363
}
357
364
365
+ /**
366
+ * Clear the local resource cache, removing all cached classpath/jar structures.
367
+ * @since 6.2
368
+ */
369
+ public void clearCache () {
370
+ this .rootDirCache .clear ();
371
+ this .jarEntryCache .clear ();
372
+ }
373
+
374
+
358
375
/**
359
376
* Find all class location resources with the given location via the ClassLoader.
360
377
* <p>Delegates to {@link #doFindAllClassPathResources(String)}.
@@ -567,9 +584,73 @@ private boolean hasDuplicate(String filePath, Set<Resource> result) {
567
584
protected Resource [] findPathMatchingResources (String locationPattern ) throws IOException {
568
585
String rootDirPath = determineRootDir (locationPattern );
569
586
String subPattern = locationPattern .substring (rootDirPath .length ());
570
- Resource [] rootDirResources = getResources (rootDirPath );
571
- Set <Resource > result = new LinkedHashSet <>(16 );
587
+
588
+ // Look for pre-cached root dir resources, either a direct match
589
+ // or for a parent directory in the same classpath locations.
590
+ Resource [] rootDirResources = this .rootDirCache .get (rootDirPath );
591
+ String actualRootPath = null ;
592
+ if (rootDirResources == null ) {
593
+ // No direct match -> search for parent directory match.
594
+ String commonPrefix = null ;
595
+ String existingPath = null ;
596
+ boolean commonUnique = true ;
597
+ for (String path : this .rootDirCache .keySet ()) {
598
+ String currentPrefix = null ;
599
+ for (int i = 0 ; i < path .length (); i ++) {
600
+ if (i == rootDirPath .length () || path .charAt (i ) != rootDirPath .charAt (i )) {
601
+ currentPrefix = path .substring (0 , path .lastIndexOf ('/' , i - 1 ) + 1 );
602
+ break ;
603
+ }
604
+ }
605
+ if (currentPrefix != null ) {
606
+ // A prefix match found, potentially to be turned into a common parent cache entry.
607
+ if (commonPrefix == null || !commonUnique || currentPrefix .length () > commonPrefix .length ()) {
608
+ commonPrefix = currentPrefix ;
609
+ existingPath = path ;
610
+ }
611
+ else if (currentPrefix .equals (commonPrefix )) {
612
+ commonUnique = false ;
613
+ }
614
+ }
615
+ else if (actualRootPath == null || path .length () > actualRootPath .length ()) {
616
+ // A direct match found for a parent directory -> use it.
617
+ rootDirResources = this .rootDirCache .get (path );
618
+ actualRootPath = path ;
619
+ }
620
+ }
621
+ if (rootDirResources == null & StringUtils .hasLength (commonPrefix )) {
622
+ // Try common parent directory as long as it points to the same classpath locations.
623
+ rootDirResources = getResources (commonPrefix );
624
+ Resource [] existingResources = this .rootDirCache .get (existingPath );
625
+ if (existingResources != null && rootDirResources .length == existingResources .length ) {
626
+ // Replace existing subdirectory cache entry with common parent directory.
627
+ this .rootDirCache .remove (existingPath );
628
+ this .rootDirCache .put (commonPrefix , rootDirResources );
629
+ actualRootPath = commonPrefix ;
630
+ }
631
+ else if (commonPrefix .equals (rootDirPath )) {
632
+ // The identified common directory is equal to the currently requested path ->
633
+ // worth caching specifically, even if it cannot replace the existing sub-entry.
634
+ this .rootDirCache .put (rootDirPath , rootDirResources );
635
+ }
636
+ else {
637
+ // Mismatch: parent directory points to more classpath locations.
638
+ rootDirResources = null ;
639
+ }
640
+ }
641
+ if (rootDirResources == null ) {
642
+ // Lookup for specific directory, creating a cache entry for it.
643
+ rootDirResources = getResources (rootDirPath );
644
+ this .rootDirCache .put (rootDirPath , rootDirResources );
645
+ }
646
+ }
647
+
648
+ Set <Resource > result = new LinkedHashSet <>(64 );
572
649
for (Resource rootDirResource : rootDirResources ) {
650
+ if (actualRootPath != null && actualRootPath .length () < rootDirPath .length ()) {
651
+ // Create sub-resource for requested sub-location from cached common root directory.
652
+ rootDirResource = rootDirResource .createRelative (rootDirPath .substring (actualRootPath .length ()));
653
+ }
573
654
rootDirResource = resolveRootDirResource (rootDirResource );
574
655
URL rootDirUrl = rootDirResource .getURL ();
575
656
if (equinoxResolveMethod != null && rootDirUrl .getProtocol ().startsWith ("bundle" )) {
@@ -672,10 +753,37 @@ protected boolean isJarResource(Resource resource) throws IOException {
672
753
protected Set <Resource > doFindPathMatchingJarResources (Resource rootDirResource , URL rootDirUrl , String subPattern )
673
754
throws IOException {
674
755
756
+ String jarFileUrl = null ;
757
+ String rootEntryPath = null ;
758
+
759
+ String urlFile = rootDirUrl .getFile ();
760
+ int separatorIndex = urlFile .indexOf (ResourceUtils .WAR_URL_SEPARATOR );
761
+ if (separatorIndex == -1 ) {
762
+ separatorIndex = urlFile .indexOf (ResourceUtils .JAR_URL_SEPARATOR );
763
+ }
764
+ if (separatorIndex != -1 ) {
765
+ jarFileUrl = urlFile .substring (0 , separatorIndex );
766
+ rootEntryPath = urlFile .substring (separatorIndex + 2 ); // both separators are 2 chars
767
+ NavigableSet <String > entryCache = this .jarEntryCache .get (jarFileUrl );
768
+ if (entryCache != null ) {
769
+ Set <Resource > result = new LinkedHashSet <>(64 );
770
+ // Search sorted entries from first entry with rootEntryPath prefix
771
+ for (String entryPath : entryCache .tailSet (rootEntryPath , false )) {
772
+ if (!entryPath .startsWith (rootEntryPath )) {
773
+ // We are beyond the potential matches in the current TreeSet.
774
+ break ;
775
+ }
776
+ String relativePath = entryPath .substring (rootEntryPath .length ());
777
+ if (getPathMatcher ().match (subPattern , relativePath )) {
778
+ result .add (rootDirResource .createRelative (relativePath ));
779
+ }
780
+ }
781
+ return result ;
782
+ }
783
+ }
784
+
675
785
URLConnection con = rootDirUrl .openConnection ();
676
786
JarFile jarFile ;
677
- String jarFileUrl ;
678
- String rootEntryPath ;
679
787
boolean closeJarFile ;
680
788
681
789
if (con instanceof JarURLConnection jarCon ) {
@@ -691,15 +799,8 @@ protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource,
691
799
// We'll assume URLs of the format "jar:path!/entry", with the protocol
692
800
// being arbitrary as long as following the entry format.
693
801
// We'll also handle paths with and without leading "file:" prefix.
694
- String urlFile = rootDirUrl .getFile ();
695
802
try {
696
- int separatorIndex = urlFile .indexOf (ResourceUtils .WAR_URL_SEPARATOR );
697
- if (separatorIndex == -1 ) {
698
- separatorIndex = urlFile .indexOf (ResourceUtils .JAR_URL_SEPARATOR );
699
- }
700
- if (separatorIndex != -1 ) {
701
- jarFileUrl = urlFile .substring (0 , separatorIndex );
702
- rootEntryPath = urlFile .substring (separatorIndex + 2 ); // both separators are 2 chars
803
+ if (jarFileUrl != null ) {
703
804
jarFile = getJarFile (jarFileUrl );
704
805
}
705
806
else {
@@ -726,17 +827,21 @@ protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource,
726
827
// The Sun JRE does not return a slash here, but BEA JRockit does.
727
828
rootEntryPath = rootEntryPath + "/" ;
728
829
}
729
- Set <Resource > result = new LinkedHashSet <>(8 );
730
- for (Enumeration <JarEntry > entries = jarFile .entries (); entries .hasMoreElements ();) {
830
+ Set <Resource > result = new LinkedHashSet <>(64 );
831
+ NavigableSet <String > entryCache = new TreeSet <>();
832
+ for (Enumeration <JarEntry > entries = jarFile .entries (); entries .hasMoreElements (); ) {
731
833
JarEntry entry = entries .nextElement ();
732
834
String entryPath = entry .getName ();
835
+ entryCache .add (entryPath );
733
836
if (entryPath .startsWith (rootEntryPath )) {
734
837
String relativePath = entryPath .substring (rootEntryPath .length ());
735
838
if (getPathMatcher ().match (subPattern , relativePath )) {
736
839
result .add (rootDirResource .createRelative (relativePath ));
737
840
}
738
841
}
739
842
}
843
+ // Cache jar entries in TreeSet for efficient searching on re-encounter.
844
+ this .jarEntryCache .put (jarFileUrl , entryCache );
740
845
return result ;
741
846
}
742
847
finally {
@@ -777,7 +882,7 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException {
777
882
protected Set <Resource > doFindPathMatchingFileResources (Resource rootDirResource , String subPattern )
778
883
throws IOException {
779
884
780
- Set <Resource > result = new LinkedHashSet <>();
885
+ Set <Resource > result = new LinkedHashSet <>(64 );
781
886
URI rootDirUri ;
782
887
try {
783
888
rootDirUri = rootDirResource .getURI ();
@@ -886,7 +991,7 @@ protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource
886
991
* @see PathMatcher#match(String, String)
887
992
*/
888
993
protected Set <Resource > findAllModulePathResources (String locationPattern ) throws IOException {
889
- Set <Resource > result = new LinkedHashSet <>(16 );
994
+ Set <Resource > result = new LinkedHashSet <>(64 );
890
995
891
996
// Skip scanning the module path when running in a native image.
892
997
if (NativeDetector .inNativeImage ()) {
@@ -987,7 +1092,7 @@ private static class PatternVirtualFileVisitor implements InvocationHandler {
987
1092
988
1093
private final String rootPath ;
989
1094
990
- private final Set <Resource > resources = new LinkedHashSet <>();
1095
+ private final Set <Resource > resources = new LinkedHashSet <>(64 );
991
1096
992
1097
public PatternVirtualFileVisitor (String rootPath , String subPattern , PathMatcher pathMatcher ) {
993
1098
this .subPattern = subPattern ;
@@ -1000,15 +1105,17 @@ public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher
1000
1105
public Object invoke (Object proxy , Method method , Object [] args ) throws Throwable {
1001
1106
String methodName = method .getName ();
1002
1107
if (Object .class == method .getDeclaringClass ()) {
1003
- switch (methodName ) {
1004
- case "equals" :
1108
+ switch (methodName ) {
1109
+ case "equals" -> {
1005
1110
// Only consider equal when proxies are identical.
1006
1111
return (proxy == args [0 ]);
1007
- case "hashCode" :
1112
+ }
1113
+ case "hashCode" -> {
1008
1114
return System .identityHashCode (proxy );
1115
+ }
1009
1116
}
1010
1117
}
1011
- return switch (methodName ) {
1118
+ return switch (methodName ) {
1012
1119
case "getAttributes" -> getAttributes ();
1013
1120
case "visit" -> {
1014
1121
visit (args [0 ]);
0 commit comments