Use java.nio and FileSystems to resolve files in PathMatchingResourcePatternResolver
The JDK has decent support for walking a file system (via the FileSystem API in java.nio.file), but prior to this commit Spring still used hand-rolled pre-Java 7 code. Also, in principle, FileSystem can be an abstraction used to express any hierarchy of resources through their URIs. This is a good fit with the Spring Resource abstraction, and for instance would mean that GraalVM native images could potentially do classpath scanning through a custom URI and FileSystem already provided by GraalVM. In light of the above, this commit overhauls the implementation of PathMatchingResourcePatternResolver to use a java.nio.file.FileSystem to find matching resources within a file system. As a side effect, several obsolete `protected` methods have been removed from PathMatchingResourcePatternResolver. See gh-29163
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
package org.springframework.core.io.support;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.module.ModuleFinder;
|
||||
@@ -31,11 +30,16 @@ import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Arrays;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
@@ -193,6 +197,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @author Sebastien Deleuze
|
||||
* @author Dave Syer
|
||||
* @since 1.0.2
|
||||
* @see #CLASSPATH_ALL_URL_PREFIX
|
||||
* @see org.springframework.util.AntPathMatcher
|
||||
@@ -736,139 +741,48 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
|
||||
throws IOException {
|
||||
|
||||
File rootDir;
|
||||
Set<Resource> result = new HashSet<>();
|
||||
FileSystem fileSystem;
|
||||
try {
|
||||
rootDir = rootDirResource.getFile().getAbsoluteFile();
|
||||
fileSystem = FileSystems.getFileSystem(rootDirResource.getURI().resolve("/"));
|
||||
} catch (Exception e) {
|
||||
fileSystem = FileSystems.newFileSystem(rootDirResource.getURI().resolve("/"), Map.of(),
|
||||
ClassUtils.getDefaultClassLoader());
|
||||
}
|
||||
catch (FileNotFoundException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Cannot search for matching files underneath " + rootDirResource +
|
||||
" in the file system: " + ex.getMessage());
|
||||
String rootPath = rootDirResource.getURI().getRawPath();
|
||||
if (!("file").equals(rootDirResource.getURI().getScheme()) && rootPath.startsWith("/")) {
|
||||
rootPath = rootPath.substring(1);
|
||||
if (rootPath.length()==0) {
|
||||
return result;
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
|
||||
if (rootPath.endsWith("/")) {
|
||||
rootPath = rootPath.substring(0, rootPath.length()-1);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return doFindMatchingFileSystemResources(rootDir, subPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all resources in the file system that match the given location pattern
|
||||
* via the Ant-style PathMatcher.
|
||||
* @param rootDir the root directory in the file system
|
||||
* @param subPattern the sub pattern to match (below the root directory)
|
||||
* @return a mutable Set of matching Resource instances
|
||||
* @throws IOException in case of I/O errors
|
||||
* @see #retrieveMatchingFiles
|
||||
* @see org.springframework.util.PathMatcher
|
||||
*/
|
||||
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
|
||||
}
|
||||
Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
|
||||
Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
|
||||
for (File file : matchingFiles) {
|
||||
result.add(new FileSystemResource(file));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve files that match the given path pattern,
|
||||
* checking the given directory and its subdirectories.
|
||||
* @param rootDir the directory to start from
|
||||
* @param pattern the pattern to match against,
|
||||
* relative to the root directory
|
||||
* @return a mutable Set of matching Resource instances
|
||||
* @throws IOException if directory contents could not be retrieved
|
||||
*/
|
||||
protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
|
||||
if (!rootDir.exists()) {
|
||||
// Silently skip non-existing directories.
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
|
||||
if (rootPath.length()==0) {
|
||||
return result;
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
if (!rootDir.isDirectory()) {
|
||||
// Complain louder if it exists but is no directory.
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
if (!rootDir.canRead()) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Skipping search for matching files underneath directory [" + rootDir.getAbsolutePath() +
|
||||
"] because the application is not allowed to read the directory");
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
|
||||
if (!pattern.startsWith("/")) {
|
||||
fullPattern += "/";
|
||||
}
|
||||
fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
|
||||
Set<File> result = new LinkedHashSet<>(8);
|
||||
doRetrieveMatchingFiles(fullPattern, rootDir, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively retrieve files that match the given pattern,
|
||||
* adding them to the given result list.
|
||||
* @param fullPattern the pattern to match against,
|
||||
* with prepended root directory path
|
||||
* @param dir the current directory
|
||||
* @param result the Set of matching File instances to add to
|
||||
* @throws IOException if directory contents could not be retrieved
|
||||
*/
|
||||
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Searching directory [" + dir.getAbsolutePath() +
|
||||
"] for files matching pattern [" + fullPattern + "]");
|
||||
}
|
||||
for (File content : listDirectory(dir)) {
|
||||
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
|
||||
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
|
||||
if (!content.canRead()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
|
||||
"] because the application is not allowed to read the directory");
|
||||
Path path = fileSystem.getPath(rootPath);
|
||||
Path patternPath = path.resolve(subPattern);
|
||||
try (Stream<Path> files = Files.walk(path)) {
|
||||
files.forEach(file -> {
|
||||
if (getPathMatcher().match(patternPath.toString(), file.toString())) {
|
||||
try {
|
||||
result.add(new UrlResource(file.toUri()));
|
||||
} catch (MalformedURLException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
else {
|
||||
doRetrieveMatchingFiles(fullPattern, content, result);
|
||||
}
|
||||
}
|
||||
if (getPathMatcher().match(fullPattern, currPath)) {
|
||||
result.add(content);
|
||||
}
|
||||
});
|
||||
} catch (NoSuchFileException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine a sorted list of files in the given directory.
|
||||
* @param dir the directory to introspect
|
||||
* @return the sorted list of files (by default in alphabetical order)
|
||||
* @since 5.1
|
||||
* @see File#listFiles()
|
||||
*/
|
||||
protected File[] listDirectory(File dir) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
|
||||
}
|
||||
return new File[0];
|
||||
try {
|
||||
fileSystem.close();
|
||||
} catch (UnsupportedOperationException e) {
|
||||
// ignore
|
||||
}
|
||||
Arrays.sort(files, Comparator.comparing(File::getName));
|
||||
return files;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user