Skip to content

Commit

Permalink
Use java.nio and FileSystems to resolve files in PathMatchingResource…
Browse files Browse the repository at this point in the history
…PatternResolver

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 spring-projectsgh-29163
  • Loading branch information
dsyer authored and sbrannen committed Sep 28, 2022
1 parent c5fc0a5 commit f751b38
Showing 1 changed file with 40 additions and 126 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,11 +30,16 @@
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;
Expand Down Expand Up @@ -193,6 +197,7 @@
* @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
Expand Down Expand Up @@ -736,139 +741,48 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException {
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());
}
return Collections.emptySet();
}
catch (Exception ex) {
if (logger.isInfoEnabled()) {
logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
}
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");
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();
}
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");
if (rootPath.endsWith("/")) {
rootPath = rootPath.substring(0, rootPath.length()-1);
}
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");
if (rootPath.length()==0) {
return result;
}
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;
}

/**
Expand Down

0 comments on commit f751b38

Please sign in to comment.