From f751b38795c7ceb3f2688838387ec87452c16446 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 25 Sep 2022 16:51:54 +0200 Subject: [PATCH] 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 --- .../PathMatchingResourcePatternResolver.java | 166 +++++------------- 1 file changed, 40 insertions(+), 126 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 490c5abc5e..1522d3b7bc 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -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 doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - File rootDir; + Set 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 doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException { - if (logger.isTraceEnabled()) { - logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]"); - } - Set matchingFiles = retrieveMatchingFiles(rootDir, subPattern); - Set 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 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 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 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 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; } /**