From f751b38795c7ceb3f2688838387ec87452c16446 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Sun, 25 Sep 2022 16:51:54 +0200 Subject: [PATCH 1/7] 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; } /** From 385292dc41c62e96a083c41c9c686825db251d63 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 25 Sep 2022 17:41:58 +0200 Subject: [PATCH 2/7] Fix Checkstyle violations See gh-29163 --- .../support/PathMatchingResourcePatternResolver.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 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 1522d3b7bc..177d01ce57 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 @@ -745,7 +745,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol FileSystem fileSystem; try { fileSystem = FileSystems.getFileSystem(rootDirResource.getURI().resolve("/")); - } catch (Exception e) { + } + catch (Exception ex) { fileSystem = FileSystems.newFileSystem(rootDirResource.getURI().resolve("/"), Map.of(), ClassUtils.getDefaultClassLoader()); } @@ -769,17 +770,20 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol if (getPathMatcher().match(patternPath.toString(), file.toString())) { try { result.add(new UrlResource(file.toUri())); - } catch (MalformedURLException e) { + } + catch (MalformedURLException ex) { // ignore } } }); - } catch (NoSuchFileException e) { + } + catch (NoSuchFileException ex) { // ignore } try { fileSystem.close(); - } catch (UnsupportedOperationException e) { + } + catch (UnsupportedOperationException ex) { // ignore } return result; From 508be4e0facf32233287fdfa624f65654c6505e4 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 25 Sep 2022 18:05:59 +0200 Subject: [PATCH 3/7] Update Javadoc to reflect changes See gh-29163 --- .../PathMatchingResourcePatternResolver.java | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 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 177d01ce57..65d17a38e3 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 @@ -100,14 +100,13 @@ import org.springframework.util.StringUtils; * classpath:com/mycompany/**/applicationContext.xml * the resolver follows a more complex but defined procedure to try to resolve * the wildcard. It produces a {@code Resource} for the path up to the last - * non-wildcard segment and obtains a {@code URL} from it. If this URL is - * not a "{@code jar:}" URL or container-specific variant (e.g. - * "{@code zip:}" in WebLogic, "{@code wsjar}" in WebSphere", etc.), - * then a {@code java.io.File} is obtained from it, and used to resolve the - * wildcard by walking the filesystem. In the case of a jar URL, the resolver - * either gets a {@code java.net.JarURLConnection} from it, or manually parses - * the jar URL, and then traverses the contents of the jar file, to resolve the - * wildcards. + * non-wildcard segment and obtains a {@code URL} from it. If this URL is not a + * "{@code jar:}" URL or container-specific variant (e.g. "{@code zip:}" in WebLogic, + * "{@code wsjar}" in WebSphere", etc.), then the root directory of the filesystem + * associated with the URL is obtained and used to resolve the wildcards by walking + * the filesystem. In the case of a jar URL, the resolver either gets a + * {@code java.net.JarURLConnection} from it, or manually parses the jar URL, and + * then traverses the contents of the jar file, to resolve the wildcards. * *

Implications on portability: * @@ -137,7 +136,7 @@ import org.springframework.util.StringUtils; * *

There is special support for retrieving multiple class path resources with * the same name, via the "{@code classpath*:}" prefix. For example, - * "{@code classpath*:META-INF/beans.xml}" will find all "beans.xml" + * "{@code classpath*:META-INF/beans.xml}" will find all "META-INF/beans.xml" * files in the class path, be it in "classes" directories or in JAR files. * This is particularly useful for autodetecting config files of the same name * at the same location within each jar file. Internally, this happens via a @@ -149,7 +148,7 @@ import org.springframework.util.StringUtils; * {@code ClassLoader.getResources()} call is used on the last non-wildcard * path segment to get all the matching resources in the class loader hierarchy, * and then off each resource the same PathMatcher resolution strategy described - * above is used for the wildcard subpath. + * above is used for the wildcard sub pattern. * *

Other notes: * @@ -526,8 +525,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol /** * Find all resources that match the given location pattern via the - * Ant-style PathMatcher. Supports resources in jar files and zip files - * and in the file system. + * Ant-style PathMatcher. Supports resources in OSGi bundles, JBoss VFS, + * jar files, zip files, and file systems. * @param locationPattern the location pattern to match * @return the result as Resource array * @throws IOException in case of I/O errors @@ -568,15 +567,13 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol /** * Determine the root directory for the given location. - *

Used for determining the starting point for file matching, - * resolving the root directory location to a {@code java.io.File} - * and passing it into {@code retrieveMatchingFiles}, with the - * remainder of the location as pattern. - *

Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", - * for example. + *

Used for determining the starting point for file matching, resolving the + * root directory location to be passed into {@link #getResources(String)}, + * with the remainder of the location to be used as the sub pattern. + *

Will return "/WEB-INF/" for the location "/WEB-INF/*.xml", for example. * @param location the location to check * @return the part of the location that denotes the root directory - * @see #retrieveMatchingFiles + * @see #findPathMatchingResources(String) */ protected String determineRootDir(String location) { int prefixEnd = location.indexOf(':') + 1; @@ -729,13 +726,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } /** - * Find all resources in the file system that match the given location pattern - * via the Ant-style PathMatcher. + * Find all resources in the file system of the supplied root directory that + * match the given location sub pattern via the Ant-style PathMatcher. * @param rootDirResource the root directory as Resource * @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 doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) From 0e22c3d89d1c227f6dc8f5ae5cdc3b84a25bf818 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 25 Sep 2022 18:23:13 +0200 Subject: [PATCH 4/7] Convert `file:` URL to FileSystemResource for backward compatibility See gh-29163 --- .../PathMatchingResourcePatternResolver.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 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 65d17a38e3..31f1ef9eee 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 @@ -26,6 +26,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.net.JarURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; @@ -765,9 +766,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol files.forEach(file -> { if (getPathMatcher().match(patternPath.toString(), file.toString())) { try { - result.add(new UrlResource(file.toUri())); + result.add(convertToResource(file.toUri())); } - catch (MalformedURLException ex) { + catch (Exception ex) { // ignore } } @@ -849,14 +850,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } @Nullable - private static Resource findResource(ModuleReader moduleReader, String name) { + private Resource findResource(ModuleReader moduleReader, String name) { try { return moduleReader.find(name) // If it's a "file:" URI, use FileSystemResource to avoid duplicates // for the same path discovered via class-path scanning. - .map(uri -> ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()) ? - new FileSystemResource(uri.getPath()) : - UrlResource.from(uri)) + .map(this::convertToResource) .orElse(null); } catch (Exception ex) { @@ -867,6 +866,12 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol } } + private Resource convertToResource(URI uri) { + return ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()) ? + new FileSystemResource(uri.getPath()) : + UrlResource.from(uri); + } + private static String stripLeadingSlash(String path) { return (path.startsWith("/") ? path.substring(1) : path); } From 7a15805f6cd5220cbbcf513273da295cd1b6b763 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 25 Sep 2022 19:19:47 +0200 Subject: [PATCH 5/7] Polish contribution See gh-29163 --- .../PathMatchingResourcePatternResolver.java | 61 ++++++++++--------- ...hMatchingResourcePatternResolverTests.java | 1 + 2 files changed, 32 insertions(+), 30 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 31f1ef9eee..ba8bfc17e6 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 @@ -729,7 +729,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol /** * Find all resources in the file system of the supplied root directory that * match the given location sub pattern via the Ant-style PathMatcher. - * @param rootDirResource the root directory as Resource + * @param rootDirResource the root directory as a Resource * @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 @@ -738,50 +738,47 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - Set result = new HashSet<>(); + + URI rootDirUri = rootDirResource.getURI(); + String rootDir = rootDirUri.getRawPath(); + if (!"file".equals(rootDirUri.getScheme()) && rootDir.startsWith("/")) { + rootDir = stripLeadingSlash(rootDir); + rootDir = stripTrailingSlash(rootDir); + if (rootDir.isEmpty()) { + rootDir = "/"; + } + } FileSystem fileSystem; try { - fileSystem = FileSystems.getFileSystem(rootDirResource.getURI().resolve("/")); + fileSystem = FileSystems.getFileSystem(rootDirUri.resolve("/")); } catch (Exception ex) { - fileSystem = FileSystems.newFileSystem(rootDirResource.getURI().resolve("/"), Map.of(), + fileSystem = FileSystems.newFileSystem(rootDirUri.resolve("/"), Map.of(), ClassUtils.getDefaultClassLoader()); } - String rootPath = rootDirResource.getURI().getRawPath(); - if (!("file").equals(rootDirResource.getURI().getScheme()) && rootPath.startsWith("/")) { - rootPath = rootPath.substring(1); - if (rootPath.length()==0) { - return result; - } - if (rootPath.endsWith("/")) { - rootPath = rootPath.substring(0, rootPath.length()-1); - } - if (rootPath.length()==0) { - return result; - } - } - 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(convertToResource(file.toUri())); - } - catch (Exception ex) { - // ignore - } + + Path rootPath = fileSystem.getPath(rootDir); + String resourcePattern = rootPath.resolve(subPattern).toString(); + Predicate resourcePatternMatches = path -> getPathMatcher().match(resourcePattern, path.toString()); + Set result = new HashSet<>(); + try (Stream files = Files.walk(rootPath)) { + files.filter(resourcePatternMatches).sorted().forEach(file -> { + try { + result.add(convertToResource(file.toUri())); + } + catch (Exception ex) { + // TODO Introduce logging } }); } catch (NoSuchFileException ex) { - // ignore + // TODO Introduce logging } try { fileSystem.close(); } catch (UnsupportedOperationException ex) { - // ignore + // TODO Introduce logging } return result; } @@ -876,6 +873,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol return (path.startsWith("/") ? path.substring(1) : path); } + private static String stripTrailingSlash(String path) { + return (path.endsWith("/") ? path.substring(0, path.length() - 1) : path); + } + /** * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index 0410d11e92..25febc2d72 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -137,6 +137,7 @@ class PathMatchingResourcePatternResolverTests { Resource[] resources = resolver.getResources(pattern); List actualNames = Arrays.stream(resources) .map(Resource::getFilename) + // Need to decode within GraalVM native image to get %23 converted to #. .map(filename -> URLDecoder.decode(filename, UTF_8)) .sorted() .toList(); From 7cb54de985b648d637cb46c3d7180fa184aa4379 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 26 Sep 2022 16:52:34 +0200 Subject: [PATCH 6/7] Apply root dir cleanup only for GraalVM native image resource scheme See gh-29163 --- .../PathMatchingResourcePatternResolver.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 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 ba8bfc17e6..603311f447 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 @@ -738,16 +738,16 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - URI rootDirUri = rootDirResource.getURI(); - String rootDir = rootDirUri.getRawPath(); - if (!"file".equals(rootDirUri.getScheme()) && rootDir.startsWith("/")) { - rootDir = stripLeadingSlash(rootDir); - rootDir = stripTrailingSlash(rootDir); - if (rootDir.isEmpty()) { - rootDir = "/"; - } + String rootDir = rootDirUri.getPath(); + // If the URI is for a "resource" in the GraalVM native image file system, we have to + // ensure that the root directory does not end in a slash while simultaneously ensuring + // that the root directory is not an empty string (since fileSystem.getPath("").resolve(str) + // throws an ArrayIndexOutOfBoundsException in a native image). + if ("resource".equals(rootDirUri.getScheme()) && (rootDir.length() > 1) && rootDir.endsWith("/")) { + rootDir = rootDir.substring(0, rootDir.length() - 1); } + FileSystem fileSystem; try { fileSystem = FileSystems.getFileSystem(rootDirUri.resolve("/")); @@ -873,10 +873,6 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol return (path.startsWith("/") ? path.substring(1) : path); } - private static String stripTrailingSlash(String path) { - return (path.endsWith("/") ? path.substring(0, path.length() - 1) : path); - } - /** * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime. From ccff52645459be77919d2703e35e322d4b6abc0b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 26 Sep 2022 19:49:40 +0200 Subject: [PATCH 7/7] Reintroduce logging and exception handling for file system resource scanning See gh-29163 --- .../PathMatchingResourcePatternResolver.java | 109 ++++++++++++------ 1 file changed, 73 insertions(+), 36 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 603311f447..c474db8ed4 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 @@ -34,11 +34,9 @@ import java.net.URLConnection; 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.Enumeration; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; @@ -738,49 +736,88 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - URI rootDirUri = rootDirResource.getURI(); - String rootDir = rootDirUri.getPath(); - // If the URI is for a "resource" in the GraalVM native image file system, we have to - // ensure that the root directory does not end in a slash while simultaneously ensuring - // that the root directory is not an empty string (since fileSystem.getPath("").resolve(str) - // throws an ArrayIndexOutOfBoundsException in a native image). - if ("resource".equals(rootDirUri.getScheme()) && (rootDir.length() > 1) && rootDir.endsWith("/")) { - rootDir = rootDir.substring(0, rootDir.length() - 1); - } - - FileSystem fileSystem; + URI rootDirUri; + String rootDir; try { - fileSystem = FileSystems.getFileSystem(rootDirUri.resolve("/")); + rootDirUri = rootDirResource.getURI(); + rootDir = rootDirUri.getPath(); + // If the URI is for a "resource" in the GraalVM native image file system, we have to + // ensure that the root directory does not end in a slash while simultaneously ensuring + // that the root directory is not an empty string (since fileSystem.getPath("").resolve(str) + // throws an ArrayIndexOutOfBoundsException in a native image). + if ("resource".equals(rootDirUri.getScheme()) && (rootDir.length() > 1) && rootDir.endsWith("/")) { + rootDir = rootDir.substring(0, rootDir.length() - 1); + } } catch (Exception ex) { - fileSystem = FileSystems.newFileSystem(rootDirUri.resolve("/"), Map.of(), - ClassUtils.getDefaultClassLoader()); + if (logger.isInfoEnabled()) { + logger.info("Failed to resolve %s in the file system: %s".formatted(rootDirResource, ex)); + } + return Collections.emptySet(); } - Path rootPath = fileSystem.getPath(rootDir); - String resourcePattern = rootPath.resolve(subPattern).toString(); - Predicate resourcePatternMatches = path -> getPathMatcher().match(resourcePattern, path.toString()); - Set result = new HashSet<>(); - try (Stream files = Files.walk(rootPath)) { - files.filter(resourcePatternMatches).sorted().forEach(file -> { - try { - result.add(convertToResource(file.toUri())); - } - catch (Exception ex) { - // TODO Introduce logging - } - }); - } - catch (NoSuchFileException ex) { - // TODO Introduce logging + FileSystem fileSystem = getFileSystem(rootDirUri); + if (fileSystem == null) { + return Collections.emptySet(); } + try { - fileSystem.close(); + Path rootPath = fileSystem.getPath(rootDir); + String resourcePattern = rootPath.resolve(subPattern).toString(); + Predicate resourcePatternMatches = path -> getPathMatcher().match(resourcePattern, path.toString()); + if (logger.isTraceEnabled()) { + logger.trace("Searching directory [%s] for files matching pattern [%s]" + .formatted(rootPath.toAbsolutePath(), subPattern)); + } + Set result = new LinkedHashSet<>(); + try (Stream files = Files.walk(rootPath)) { + files.filter(resourcePatternMatches).sorted().forEach(file -> { + try { + result.add(convertToResource(file.toUri())); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to convert file %s to an org.springframework.core.io.Resource: %s" + .formatted(file, ex)); + } + } + }); + } + catch (Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Faild to complete search in directory [%s] for files matching pattern [%s]: %s" + .formatted(rootPath.toAbsolutePath(), subPattern, ex)); + } + } + return result; } - catch (UnsupportedOperationException ex) { - // TODO Introduce logging + finally { + try { + fileSystem.close(); + } + catch (UnsupportedOperationException ex) { + // ignore + } + } + } + + @Nullable + private FileSystem getFileSystem(URI uri) { + try { + URI root = uri.resolve("/"); + try { + return FileSystems.getFileSystem(root); + } + catch (Exception ex) { + return FileSystems.newFileSystem(root, Map.of(), ClassUtils.getDefaultClassLoader()); + } + } + catch (Exception ex) { + if (logger.isInfoEnabled()) { + logger.info("Failed to resolve java.nio.file.FileSystem for %s: %s".formatted(uri, ex)); + } + return null; } - return result; } /**