17
17
package org .springframework .core .io .support ;
18
18
19
19
import java .io .File ;
20
- import java .io .FileNotFoundException ;
21
20
import java .io .IOException ;
22
21
import java .io .UncheckedIOException ;
23
22
import java .lang .module .ModuleFinder ;
27
26
import java .lang .reflect .Method ;
28
27
import java .net .JarURLConnection ;
29
28
import java .net .MalformedURLException ;
29
+ import java .net .URI ;
30
30
import java .net .URISyntaxException ;
31
31
import java .net .URL ;
32
32
import java .net .URLClassLoader ;
33
33
import java .net .URLConnection ;
34
- import java .util .Arrays ;
34
+ import java .nio .file .FileSystem ;
35
+ import java .nio .file .FileSystems ;
36
+ import java .nio .file .Files ;
37
+ import java .nio .file .Path ;
35
38
import java .util .Collections ;
36
- import java .util .Comparator ;
37
39
import java .util .Enumeration ;
38
40
import java .util .LinkedHashSet ;
41
+ import java .util .Map ;
39
42
import java .util .Objects ;
40
43
import java .util .Set ;
41
44
import java .util .function .Predicate ;
96
99
* classpath:com/mycompany/**/applicationContext.xml</pre>
97
100
* the resolver follows a more complex but defined procedure to try to resolve
98
101
* the wildcard. It produces a {@code Resource} for the path up to the last
99
- * non-wildcard segment and obtains a {@code URL} from it. If this URL is
100
- * not a "{@code jar:}" URL or container-specific variant (e.g.
101
- * "{@code zip:}" in WebLogic, "{@code wsjar}" in WebSphere", etc.),
102
- * then a {@code java.io.File} is obtained from it, and used to resolve the
103
- * wildcard by walking the filesystem. In the case of a jar URL, the resolver
104
- * either gets a {@code java.net.JarURLConnection} from it, or manually parses
105
- * the jar URL, and then traverses the contents of the jar file, to resolve the
106
- * wildcards.
102
+ * non-wildcard segment and obtains a {@code URL} from it. If this URL is not a
103
+ * "{@code jar:}" URL or container-specific variant (e.g. "{@code zip:}" in WebLogic,
104
+ * "{@code wsjar}" in WebSphere", etc.), then the root directory of the filesystem
105
+ * associated with the URL is obtained and used to resolve the wildcards by walking
106
+ * the filesystem. In the case of a jar URL, the resolver either gets a
107
+ * {@code java.net.JarURLConnection} from it, or manually parses the jar URL, and
108
+ * then traverses the contents of the jar file, to resolve the wildcards.
107
109
*
108
110
* <p><b>Implications on portability:</b>
109
111
*
133
135
*
134
136
* <p>There is special support for retrieving multiple class path resources with
135
137
* the same name, via the "{@code classpath*:}" prefix. For example,
136
- * "{@code classpath*:META-INF/beans.xml}" will find all "beans.xml"
138
+ * "{@code classpath*:META-INF/beans.xml}" will find all "META-INF/ beans.xml"
137
139
* files in the class path, be it in "classes" directories or in JAR files.
138
140
* This is particularly useful for autodetecting config files of the same name
139
141
* at the same location within each jar file. Internally, this happens via a
145
147
* {@code ClassLoader.getResources()} call is used on the last non-wildcard
146
148
* path segment to get all the matching resources in the class loader hierarchy,
147
149
* and then off each resource the same PathMatcher resolution strategy described
148
- * above is used for the wildcard subpath .
150
+ * above is used for the wildcard sub pattern .
149
151
*
150
152
* <p><b>Other notes:</b>
151
153
*
193
195
* @author Phillip Webb
194
196
* @author Sam Brannen
195
197
* @author Sebastien Deleuze
198
+ * @author Dave Syer
196
199
* @since 1.0.2
197
200
* @see #CLASSPATH_ALL_URL_PREFIX
198
201
* @see org.springframework.util.AntPathMatcher
@@ -521,8 +524,8 @@ private boolean hasDuplicate(String filePath, Set<Resource> result) {
521
524
522
525
/**
523
526
* Find all resources that match the given location pattern via the
524
- * Ant-style PathMatcher. Supports resources in jar files and zip files
525
- * and in the file system .
527
+ * Ant-style PathMatcher. Supports resources in OSGi bundles, JBoss VFS,
528
+ * jar files, zip files, and file systems .
526
529
* @param locationPattern the location pattern to match
527
530
* @return the result as Resource array
528
531
* @throws IOException in case of I/O errors
@@ -563,15 +566,13 @@ else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
563
566
564
567
/**
565
568
* Determine the root directory for the given location.
566
- * <p>Used for determining the starting point for file matching,
567
- * resolving the root directory location to a {@code java.io.File}
568
- * and passing it into {@code retrieveMatchingFiles}, with the
569
- * remainder of the location as pattern.
570
- * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
571
- * for example.
569
+ * <p>Used for determining the starting point for file matching, resolving the
570
+ * root directory location to be passed into {@link #getResources(String)},
571
+ * with the remainder of the location to be used as the sub pattern.
572
+ * <p>Will return "/WEB-INF/" for the location "/WEB-INF/*.xml", for example.
572
573
* @param location the location to check
573
574
* @return the part of the location that denotes the root directory
574
- * @see #retrieveMatchingFiles
575
+ * @see #findPathMatchingResources(String)
575
576
*/
576
577
protected String determineRootDir (String location ) {
577
578
int prefixEnd = location .indexOf (':' ) + 1 ;
@@ -724,151 +725,99 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException {
724
725
}
725
726
726
727
/**
727
- * Find all resources in the file system that match the given location pattern
728
- * via the Ant-style PathMatcher.
729
- * @param rootDirResource the root directory as Resource
728
+ * Find all resources in the file system of the supplied root directory that
729
+ * match the given location sub pattern via the Ant-style PathMatcher.
730
+ * @param rootDirResource the root directory as a Resource
730
731
* @param subPattern the sub pattern to match (below the root directory)
731
732
* @return a mutable Set of matching Resource instances
732
733
* @throws IOException in case of I/O errors
733
- * @see #retrieveMatchingFiles
734
734
* @see org.springframework.util.PathMatcher
735
735
*/
736
736
protected Set <Resource > doFindPathMatchingFileResources (Resource rootDirResource , String subPattern )
737
737
throws IOException {
738
738
739
- File rootDir ;
739
+ URI rootDirUri ;
740
+ String rootDir ;
740
741
try {
741
- rootDir = rootDirResource .getFile ().getAbsoluteFile ();
742
- }
743
- catch (FileNotFoundException ex ) {
744
- if (logger .isDebugEnabled ()) {
745
- logger .debug ("Cannot search for matching files underneath " + rootDirResource +
746
- " in the file system: " + ex .getMessage ());
742
+ rootDirUri = rootDirResource .getURI ();
743
+ rootDir = rootDirUri .getPath ();
744
+ // If the URI is for a "resource" in the GraalVM native image file system, we have to
745
+ // ensure that the root directory does not end in a slash while simultaneously ensuring
746
+ // that the root directory is not an empty string (since fileSystem.getPath("").resolve(str)
747
+ // throws an ArrayIndexOutOfBoundsException in a native image).
748
+ if ("resource" .equals (rootDirUri .getScheme ()) && (rootDir .length () > 1 ) && rootDir .endsWith ("/" )) {
749
+ rootDir = rootDir .substring (0 , rootDir .length () - 1 );
747
750
}
748
- return Collections .emptySet ();
749
751
}
750
752
catch (Exception ex ) {
751
753
if (logger .isInfoEnabled ()) {
752
- logger .info ("Failed to resolve " + rootDirResource + " in the file system: " + ex );
754
+ logger .info ("Failed to resolve %s in the file system: %s" . formatted ( rootDirResource , ex ) );
753
755
}
754
756
return Collections .emptySet ();
755
757
}
756
- return doFindMatchingFileSystemResources (rootDir , subPattern );
757
- }
758
758
759
- /**
760
- * Find all resources in the file system that match the given location pattern
761
- * via the Ant-style PathMatcher.
762
- * @param rootDir the root directory in the file system
763
- * @param subPattern the sub pattern to match (below the root directory)
764
- * @return a mutable Set of matching Resource instances
765
- * @throws IOException in case of I/O errors
766
- * @see #retrieveMatchingFiles
767
- * @see org.springframework.util.PathMatcher
768
- */
769
- protected Set <Resource > doFindMatchingFileSystemResources (File rootDir , String subPattern ) throws IOException {
770
- if (logger .isTraceEnabled ()) {
771
- logger .trace ("Looking for matching resources in directory tree [" + rootDir .getPath () + "]" );
772
- }
773
- Set <File > matchingFiles = retrieveMatchingFiles (rootDir , subPattern );
774
- Set <Resource > result = new LinkedHashSet <>(matchingFiles .size ());
775
- for (File file : matchingFiles ) {
776
- result .add (new FileSystemResource (file ));
759
+ FileSystem fileSystem = getFileSystem (rootDirUri );
760
+ if (fileSystem == null ) {
761
+ return Collections .emptySet ();
777
762
}
778
- return result ;
779
- }
780
763
781
- /**
782
- * Retrieve files that match the given path pattern,
783
- * checking the given directory and its subdirectories.
784
- * @param rootDir the directory to start from
785
- * @param pattern the pattern to match against,
786
- * relative to the root directory
787
- * @return a mutable Set of matching Resource instances
788
- * @throws IOException if directory contents could not be retrieved
789
- */
790
- protected Set <File > retrieveMatchingFiles (File rootDir , String pattern ) throws IOException {
791
- if (!rootDir .exists ()) {
792
- // Silently skip non-existing directories.
793
- if (logger .isDebugEnabled ()) {
794
- logger .debug ("Skipping [" + rootDir .getAbsolutePath () + "] because it does not exist" );
764
+ try {
765
+ Path rootPath = fileSystem .getPath (rootDir );
766
+ String resourcePattern = rootPath .resolve (subPattern ).toString ();
767
+ Predicate <Path > resourcePatternMatches = path -> getPathMatcher ().match (resourcePattern , path .toString ());
768
+ if (logger .isTraceEnabled ()) {
769
+ logger .trace ("Searching directory [%s] for files matching pattern [%s]"
770
+ .formatted (rootPath .toAbsolutePath (), subPattern ));
795
771
}
796
- return Collections .emptySet ();
797
- }
798
- if (!rootDir .isDirectory ()) {
799
- // Complain louder if it exists but is no directory.
800
- if (logger .isInfoEnabled ()) {
801
- logger .info ("Skipping [" + rootDir .getAbsolutePath () + "] because it does not denote a directory" );
772
+ Set <Resource > result = new LinkedHashSet <>();
773
+ try (Stream <Path > files = Files .walk (rootPath )) {
774
+ files .filter (resourcePatternMatches ).sorted ().forEach (file -> {
775
+ try {
776
+ result .add (convertToResource (file .toUri ()));
777
+ }
778
+ catch (Exception ex ) {
779
+ if (logger .isDebugEnabled ()) {
780
+ logger .debug ("Failed to convert file %s to an org.springframework.core.io.Resource: %s"
781
+ .formatted (file , ex ));
782
+ }
783
+ }
784
+ });
802
785
}
803
- return Collections .emptySet ();
804
- }
805
- if (!rootDir .canRead ()) {
806
- if (logger .isInfoEnabled ()) {
807
- logger .info ("Skipping search for matching files underneath directory [" + rootDir .getAbsolutePath () +
808
- "] because the application is not allowed to read the directory" );
786
+ catch (Exception ex ) {
787
+ if (logger .isDebugEnabled ()) {
788
+ logger .debug ("Faild to complete search in directory [%s] for files matching pattern [%s]: %s"
789
+ .formatted (rootPath .toAbsolutePath (), subPattern , ex ));
790
+ }
809
791
}
810
- return Collections . emptySet () ;
792
+ return result ;
811
793
}
812
- String fullPattern = StringUtils .replace (rootDir .getAbsolutePath (), File .separator , "/" );
813
- if (!pattern .startsWith ("/" )) {
814
- fullPattern += "/" ;
794
+ finally {
795
+ try {
796
+ fileSystem .close ();
797
+ }
798
+ catch (UnsupportedOperationException ex ) {
799
+ // ignore
800
+ }
815
801
}
816
- fullPattern = fullPattern + StringUtils .replace (pattern , File .separator , "/" );
817
- Set <File > result = new LinkedHashSet <>(8 );
818
- doRetrieveMatchingFiles (fullPattern , rootDir , result );
819
- return result ;
820
802
}
821
803
822
- /**
823
- * Recursively retrieve files that match the given pattern,
824
- * adding them to the given result list.
825
- * @param fullPattern the pattern to match against,
826
- * with prepended root directory path
827
- * @param dir the current directory
828
- * @param result the Set of matching File instances to add to
829
- * @throws IOException if directory contents could not be retrieved
830
- */
831
- protected void doRetrieveMatchingFiles (String fullPattern , File dir , Set <File > result ) throws IOException {
832
- if (logger .isTraceEnabled ()) {
833
- logger .trace ("Searching directory [" + dir .getAbsolutePath () +
834
- "] for files matching pattern [" + fullPattern + "]" );
835
- }
836
- for (File content : listDirectory (dir )) {
837
- String currPath = StringUtils .replace (content .getAbsolutePath (), File .separator , "/" );
838
- if (content .isDirectory () && getPathMatcher ().matchStart (fullPattern , currPath + "/" )) {
839
- if (!content .canRead ()) {
840
- if (logger .isDebugEnabled ()) {
841
- logger .debug ("Skipping subdirectory [" + dir .getAbsolutePath () +
842
- "] because the application is not allowed to read the directory" );
843
- }
844
- }
845
- else {
846
- doRetrieveMatchingFiles (fullPattern , content , result );
847
- }
804
+ @ Nullable
805
+ private FileSystem getFileSystem (URI uri ) {
806
+ try {
807
+ URI root = uri .resolve ("/" );
808
+ try {
809
+ return FileSystems .getFileSystem (root );
848
810
}
849
- if ( getPathMatcher (). match ( fullPattern , currPath ) ) {
850
- result . add ( content );
811
+ catch ( Exception ex ) {
812
+ return FileSystems . newFileSystem ( root , Map . of (), ClassUtils . getDefaultClassLoader () );
851
813
}
852
814
}
853
- }
854
-
855
- /**
856
- * Determine a sorted list of files in the given directory.
857
- * @param dir the directory to introspect
858
- * @return the sorted list of files (by default in alphabetical order)
859
- * @since 5.1
860
- * @see File#listFiles()
861
- */
862
- protected File [] listDirectory (File dir ) {
863
- File [] files = dir .listFiles ();
864
- if (files == null ) {
815
+ catch (Exception ex ) {
865
816
if (logger .isInfoEnabled ()) {
866
- logger .info ("Could not retrieve contents of directory [" + dir . getAbsolutePath () + "]" );
817
+ logger .info ("Failed to resolve java.nio.file.FileSystem for %s: %s" . formatted ( uri , ex ) );
867
818
}
868
- return new File [ 0 ] ;
819
+ return null ;
869
820
}
870
- Arrays .sort (files , Comparator .comparing (File ::getName ));
871
- return files ;
872
821
}
873
822
874
823
/**
@@ -935,14 +884,12 @@ protected Set<Resource> findAllModulePathResources(String locationPattern) throw
935
884
}
936
885
937
886
@ Nullable
938
- private static Resource findResource (ModuleReader moduleReader , String name ) {
887
+ private Resource findResource (ModuleReader moduleReader , String name ) {
939
888
try {
940
889
return moduleReader .find (name )
941
890
// If it's a "file:" URI, use FileSystemResource to avoid duplicates
942
891
// for the same path discovered via class-path scanning.
943
- .map (uri -> ResourceUtils .URL_PROTOCOL_FILE .equals (uri .getScheme ()) ?
944
- new FileSystemResource (uri .getPath ()) :
945
- UrlResource .from (uri ))
892
+ .map (this ::convertToResource )
946
893
.orElse (null );
947
894
}
948
895
catch (Exception ex ) {
@@ -953,6 +900,12 @@ private static Resource findResource(ModuleReader moduleReader, String name) {
953
900
}
954
901
}
955
902
903
+ private Resource convertToResource (URI uri ) {
904
+ return ResourceUtils .URL_PROTOCOL_FILE .equals (uri .getScheme ()) ?
905
+ new FileSystemResource (uri .getPath ()) :
906
+ UrlResource .from (uri );
907
+ }
908
+
956
909
private static String stripLeadingSlash (String path ) {
957
910
return (path .startsWith ("/" ) ? path .substring (1 ) : path );
958
911
}
0 commit comments