001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.core.io.support;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.lang.reflect.InvocationHandler;
023import java.lang.reflect.Method;
024import java.net.JarURLConnection;
025import java.net.MalformedURLException;
026import java.net.URISyntaxException;
027import java.net.URL;
028import java.net.URLClassLoader;
029import java.net.URLConnection;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.Comparator;
033import java.util.Enumeration;
034import java.util.LinkedHashSet;
035import java.util.Set;
036import java.util.jar.JarEntry;
037import java.util.jar.JarFile;
038import java.util.zip.ZipException;
039
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042
043import org.springframework.core.io.DefaultResourceLoader;
044import org.springframework.core.io.FileSystemResource;
045import org.springframework.core.io.Resource;
046import org.springframework.core.io.ResourceLoader;
047import org.springframework.core.io.UrlResource;
048import org.springframework.core.io.VfsResource;
049import org.springframework.lang.Nullable;
050import org.springframework.util.AntPathMatcher;
051import org.springframework.util.Assert;
052import org.springframework.util.ClassUtils;
053import org.springframework.util.PathMatcher;
054import org.springframework.util.ReflectionUtils;
055import org.springframework.util.ResourceUtils;
056import org.springframework.util.StringUtils;
057
058/**
059 * A {@link ResourcePatternResolver} implementation that is able to resolve a
060 * specified resource location path into one or more matching Resources.
061 * The source path may be a simple path which has a one-to-one mapping to a
062 * target {@link org.springframework.core.io.Resource}, or alternatively
063 * may contain the special "{@code classpath*:}" prefix and/or
064 * internal Ant-style regular expressions (matched using Spring's
065 * {@link org.springframework.util.AntPathMatcher} utility).
066 * Both of the latter are effectively wildcards.
067 *
068 * <p><b>No Wildcards:</b>
069 *
070 * <p>In the simple case, if the specified location path does not start with the
071 * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern,
072 * this resolver will simply return a single resource via a
073 * {@code getResource()} call on the underlying {@code ResourceLoader}.
074 * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs
075 * such as "{@code classpath:/context.xml}", and simple unprefixed paths
076 * such as "{@code /WEB-INF/context.xml}". The latter will resolve in a
077 * fashion specific to the underlying {@code ResourceLoader} (e.g.
078 * {@code ServletContextResource} for a {@code WebApplicationContext}).
079 *
080 * <p><b>Ant-style Patterns:</b>
081 *
082 * <p>When the path location contains an Ant-style pattern, e.g.:
083 * <pre class="code">
084 * /WEB-INF/*-context.xml
085 * com/mycompany/**&#47;applicationContext.xml
086 * file:C:/some/path/*-context.xml
087 * classpath:com/mycompany/**&#47;applicationContext.xml</pre>
088 * the resolver follows a more complex but defined procedure to try to resolve
089 * the wildcard. It produces a {@code Resource} for the path up to the last
090 * non-wildcard segment and obtains a {@code URL} from it. If this URL is
091 * not a "{@code jar:}" URL or container-specific variant (e.g.
092 * "{@code zip:}" in WebLogic, "{@code wsjar}" in WebSphere", etc.),
093 * then a {@code java.io.File} is obtained from it, and used to resolve the
094 * wildcard by walking the filesystem. In the case of a jar URL, the resolver
095 * either gets a {@code java.net.JarURLConnection} from it, or manually parses
096 * the jar URL, and then traverses the contents of the jar file, to resolve the
097 * wildcards.
098 *
099 * <p><b>Implications on portability:</b>
100 *
101 * <p>If the specified path is already a file URL (either explicitly, or
102 * implicitly because the base {@code ResourceLoader} is a filesystem one,
103 * then wildcarding is guaranteed to work in a completely portable fashion.
104 *
105 * <p>If the specified path is a classpath location, then the resolver must
106 * obtain the last non-wildcard path segment URL via a
107 * {@code Classloader.getResource()} call. Since this is just a
108 * node of the path (not the file at the end) it is actually undefined
109 * (in the ClassLoader Javadocs) exactly what sort of a URL is returned in
110 * this case. In practice, it is usually a {@code java.io.File} representing
111 * the directory, where the classpath resource resolves to a filesystem
112 * location, or a jar URL of some sort, where the classpath resource resolves
113 * to a jar location. Still, there is a portability concern on this operation.
114 *
115 * <p>If a jar URL is obtained for the last non-wildcard segment, the resolver
116 * must be able to get a {@code java.net.JarURLConnection} from it, or
117 * manually parse the jar URL, to be able to walk the contents of the jar,
118 * and resolve the wildcard. This will work in most environments, but will
119 * fail in others, and it is strongly recommended that the wildcard
120 * resolution of resources coming from jars be thoroughly tested in your
121 * specific environment before you rely on it.
122 *
123 * <p><b>{@code classpath*:} Prefix:</b>
124 *
125 * <p>There is special support for retrieving multiple class path resources with
126 * the same name, via the "{@code classpath*:}" prefix. For example,
127 * "{@code classpath*:META-INF/beans.xml}" will find all "beans.xml"
128 * files in the class path, be it in "classes" directories or in JAR files.
129 * This is particularly useful for autodetecting config files of the same name
130 * at the same location within each jar file. Internally, this happens via a
131 * {@code ClassLoader.getResources()} call, and is completely portable.
132 *
133 * <p>The "classpath*:" prefix can also be combined with a PathMatcher pattern in
134 * the rest of the location path, for example "classpath*:META-INF/*-beans.xml".
135 * In this case, the resolution strategy is fairly simple: a
136 * {@code ClassLoader.getResources()} call is used on the last non-wildcard
137 * path segment to get all the matching resources in the class loader hierarchy,
138 * and then off each resource the same PathMatcher resolution strategy described
139 * above is used for the wildcard subpath.
140 *
141 * <p><b>Other notes:</b>
142 *
143 * <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with
144 * Ant-style patterns will only work reliably with at least one root directory
145 * before the pattern starts, unless the actual target files reside in the file
146 * system. This means that a pattern like "{@code classpath*:*.xml}" will
147 * <i>not</i> retrieve files from the root of jar files but rather only from the
148 * root of expanded directories. This originates from a limitation in the JDK's
149 * {@code ClassLoader.getResources()} method which only returns file system
150 * locations for a passed-in empty String (indicating potential roots to search).
151 * This {@code ResourcePatternResolver} implementation is trying to mitigate the
152 * jar root lookup limitation through {@link URLClassLoader} introspection and
153 * "java.class.path" manifest evaluation; however, without portability guarantees.
154 *
155 * <p><b>WARNING:</b> Ant-style patterns with "classpath:" resources are not
156 * guaranteed to find matching resources if the root package to search is available
157 * in multiple class path locations. This is because a resource such as
158 * <pre class="code">
159 *     com/mycompany/package1/service-context.xml
160 * </pre>
161 * may be in only one location, but when a path such as
162 * <pre class="code">
163 *     classpath:com/mycompany/**&#47;service-context.xml
164 * </pre>
165 * is used to try to resolve it, the resolver will work off the (first) URL
166 * returned by {@code getResource("com/mycompany");}. If this base package node
167 * exists in multiple classloader locations, the actual end resource may not be
168 * underneath. Therefore, preferably, use "{@code classpath*:}" with the same
169 * Ant-style pattern in such a case, which will search <i>all</i> class path
170 * locations that contain the root package.
171 *
172 * @author Juergen Hoeller
173 * @author Colin Sampaleanu
174 * @author Marius Bogoevici
175 * @author Costin Leau
176 * @author Phillip Webb
177 * @since 1.0.2
178 * @see #CLASSPATH_ALL_URL_PREFIX
179 * @see org.springframework.util.AntPathMatcher
180 * @see org.springframework.core.io.ResourceLoader#getResource(String)
181 * @see ClassLoader#getResources(String)
182 */
183public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
184
185        private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class);
186
187        @Nullable
188        private static Method equinoxResolveMethod;
189
190        static {
191                try {
192                        // Detect Equinox OSGi (e.g. on WebSphere 6.1)
193                        Class<?> fileLocatorClass = ClassUtils.forName("org.eclipse.core.runtime.FileLocator",
194                                        PathMatchingResourcePatternResolver.class.getClassLoader());
195                        equinoxResolveMethod = fileLocatorClass.getMethod("resolve", URL.class);
196                        logger.trace("Found Equinox FileLocator for OSGi bundle URL resolution");
197                }
198                catch (Throwable ex) {
199                        equinoxResolveMethod = null;
200                }
201        }
202
203
204        private final ResourceLoader resourceLoader;
205
206        private PathMatcher pathMatcher = new AntPathMatcher();
207
208
209        /**
210         * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
211         * <p>ClassLoader access will happen via the thread context class loader.
212         * @see org.springframework.core.io.DefaultResourceLoader
213         */
214        public PathMatchingResourcePatternResolver() {
215                this.resourceLoader = new DefaultResourceLoader();
216        }
217
218        /**
219         * Create a new PathMatchingResourcePatternResolver.
220         * <p>ClassLoader access will happen via the thread context class loader.
221         * @param resourceLoader the ResourceLoader to load root directories and
222         * actual resources with
223         */
224        public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
225                Assert.notNull(resourceLoader, "ResourceLoader must not be null");
226                this.resourceLoader = resourceLoader;
227        }
228
229        /**
230         * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
231         * @param classLoader the ClassLoader to load classpath resources with,
232         * or {@code null} for using the thread context class loader
233         * at the time of actual resource access
234         * @see org.springframework.core.io.DefaultResourceLoader
235         */
236        public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
237                this.resourceLoader = new DefaultResourceLoader(classLoader);
238        }
239
240
241        /**
242         * Return the ResourceLoader that this pattern resolver works with.
243         */
244        public ResourceLoader getResourceLoader() {
245                return this.resourceLoader;
246        }
247
248        @Override
249        @Nullable
250        public ClassLoader getClassLoader() {
251                return getResourceLoader().getClassLoader();
252        }
253
254        /**
255         * Set the PathMatcher implementation to use for this
256         * resource pattern resolver. Default is AntPathMatcher.
257         * @see org.springframework.util.AntPathMatcher
258         */
259        public void setPathMatcher(PathMatcher pathMatcher) {
260                Assert.notNull(pathMatcher, "PathMatcher must not be null");
261                this.pathMatcher = pathMatcher;
262        }
263
264        /**
265         * Return the PathMatcher that this resource pattern resolver uses.
266         */
267        public PathMatcher getPathMatcher() {
268                return this.pathMatcher;
269        }
270
271
272        @Override
273        public Resource getResource(String location) {
274                return getResourceLoader().getResource(location);
275        }
276
277        @Override
278        public Resource[] getResources(String locationPattern) throws IOException {
279                Assert.notNull(locationPattern, "Location pattern must not be null");
280                if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
281                        // a class path resource (multiple resources for same name possible)
282                        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
283                                // a class path resource pattern
284                                return findPathMatchingResources(locationPattern);
285                        }
286                        else {
287                                // all class path resources with the given name
288                                return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
289                        }
290                }
291                else {
292                        // Generally only look for a pattern after a prefix here,
293                        // and on Tomcat only after the "*/" separator for its "war:" protocol.
294                        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
295                                        locationPattern.indexOf(':') + 1);
296                        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
297                                // a file pattern
298                                return findPathMatchingResources(locationPattern);
299                        }
300                        else {
301                                // a single resource with the given name
302                                return new Resource[] {getResourceLoader().getResource(locationPattern)};
303                        }
304                }
305        }
306
307        /**
308         * Find all class location resources with the given location via the ClassLoader.
309         * Delegates to {@link #doFindAllClassPathResources(String)}.
310         * @param location the absolute path within the classpath
311         * @return the result as Resource array
312         * @throws IOException in case of I/O errors
313         * @see java.lang.ClassLoader#getResources
314         * @see #convertClassLoaderURL
315         */
316        protected Resource[] findAllClassPathResources(String location) throws IOException {
317                String path = location;
318                if (path.startsWith("/")) {
319                        path = path.substring(1);
320                }
321                Set<Resource> result = doFindAllClassPathResources(path);
322                if (logger.isTraceEnabled()) {
323                        logger.trace("Resolved classpath location [" + location + "] to resources " + result);
324                }
325                return result.toArray(new Resource[0]);
326        }
327
328        /**
329         * Find all class location resources with the given path via the ClassLoader.
330         * Called by {@link #findAllClassPathResources(String)}.
331         * @param path the absolute path within the classpath (never a leading slash)
332         * @return a mutable Set of matching Resource instances
333         * @since 4.1.1
334         */
335        protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
336                Set<Resource> result = new LinkedHashSet<>(16);
337                ClassLoader cl = getClassLoader();
338                Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
339                while (resourceUrls.hasMoreElements()) {
340                        URL url = resourceUrls.nextElement();
341                        result.add(convertClassLoaderURL(url));
342                }
343                if (!StringUtils.hasLength(path)) {
344                        // The above result is likely to be incomplete, i.e. only containing file system references.
345                        // We need to have pointers to each of the jar files on the classpath as well...
346                        addAllClassLoaderJarRoots(cl, result);
347                }
348                return result;
349        }
350
351        /**
352         * Convert the given URL as returned from the ClassLoader into a {@link Resource}.
353         * <p>The default implementation simply creates a {@link UrlResource} instance.
354         * @param url a URL as returned from the ClassLoader
355         * @return the corresponding Resource object
356         * @see java.lang.ClassLoader#getResources
357         * @see org.springframework.core.io.Resource
358         */
359        protected Resource convertClassLoaderURL(URL url) {
360                return new UrlResource(url);
361        }
362
363        /**
364         * Search all {@link URLClassLoader} URLs for jar file references and add them to the
365         * given set of resources in the form of pointers to the root of the jar file content.
366         * @param classLoader the ClassLoader to search (including its ancestors)
367         * @param result the set of resources to add jar roots to
368         * @since 4.1.1
369         */
370        protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<Resource> result) {
371                if (classLoader instanceof URLClassLoader) {
372                        try {
373                                for (URL url : ((URLClassLoader) classLoader).getURLs()) {
374                                        try {
375                                                UrlResource jarResource = (ResourceUtils.URL_PROTOCOL_JAR.equals(url.getProtocol()) ?
376                                                                new UrlResource(url) :
377                                                                new UrlResource(ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR));
378                                                if (jarResource.exists()) {
379                                                        result.add(jarResource);
380                                                }
381                                        }
382                                        catch (MalformedURLException ex) {
383                                                if (logger.isDebugEnabled()) {
384                                                        logger.debug("Cannot search for matching files underneath [" + url +
385                                                                        "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
386                                                }
387                                        }
388                                }
389                        }
390                        catch (Exception ex) {
391                                if (logger.isDebugEnabled()) {
392                                        logger.debug("Cannot introspect jar files since ClassLoader [" + classLoader +
393                                                        "] does not support 'getURLs()': " + ex);
394                                }
395                        }
396                }
397
398                if (classLoader == ClassLoader.getSystemClassLoader()) {
399                        // "java.class.path" manifest evaluation...
400                        addClassPathManifestEntries(result);
401                }
402
403                if (classLoader != null) {
404                        try {
405                                // Hierarchy traversal...
406                                addAllClassLoaderJarRoots(classLoader.getParent(), result);
407                        }
408                        catch (Exception ex) {
409                                if (logger.isDebugEnabled()) {
410                                        logger.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader +
411                                                        "] does not support 'getParent()': " + ex);
412                                }
413                        }
414                }
415        }
416
417        /**
418         * Determine jar file references from the "java.class.path." manifest property and add them
419         * to the given set of resources in the form of pointers to the root of the jar file content.
420         * @param result the set of resources to add jar roots to
421         * @since 4.3
422         */
423        protected void addClassPathManifestEntries(Set<Resource> result) {
424                try {
425                        String javaClassPathProperty = System.getProperty("java.class.path");
426                        for (String path : StringUtils.delimitedListToStringArray(
427                                        javaClassPathProperty, System.getProperty("path.separator"))) {
428                                try {
429                                        String filePath = new File(path).getAbsolutePath();
430                                        int prefixIndex = filePath.indexOf(':');
431                                        if (prefixIndex == 1) {
432                                                // Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection
433                                                filePath = StringUtils.capitalize(filePath);
434                                        }
435                                        // # can appear in directories/filenames, java.net.URL should not treat it as a fragment
436                                        filePath = StringUtils.replace(filePath, "#", "%23");
437                                        // Build URL that points to the root of the jar file
438                                        UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX +
439                                                        ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR);
440                                        // Potentially overlapping with URLClassLoader.getURLs() result above!
441                                        if (!result.contains(jarResource) && !hasDuplicate(filePath, result) && jarResource.exists()) {
442                                                result.add(jarResource);
443                                        }
444                                }
445                                catch (MalformedURLException ex) {
446                                        if (logger.isDebugEnabled()) {
447                                                logger.debug("Cannot search for matching files underneath [" + path +
448                                                                "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
449                                        }
450                                }
451                        }
452                }
453                catch (Exception ex) {
454                        if (logger.isDebugEnabled()) {
455                                logger.debug("Failed to evaluate 'java.class.path' manifest entries: " + ex);
456                        }
457                }
458        }
459
460        /**
461         * Check whether the given file path has a duplicate but differently structured entry
462         * in the existing result, i.e. with or without a leading slash.
463         * @param filePath the file path (with or without a leading slash)
464         * @param result the current result
465         * @return {@code true} if there is a duplicate (i.e. to ignore the given file path),
466         * {@code false} to proceed with adding a corresponding resource to the current result
467         */
468        private boolean hasDuplicate(String filePath, Set<Resource> result) {
469                if (result.isEmpty()) {
470                        return false;
471                }
472                String duplicatePath = (filePath.startsWith("/") ? filePath.substring(1) : "/" + filePath);
473                try {
474                        return result.contains(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX +
475                                        duplicatePath + ResourceUtils.JAR_URL_SEPARATOR));
476                }
477                catch (MalformedURLException ex) {
478                        // Ignore: just for testing against duplicate.
479                        return false;
480                }
481        }
482
483        /**
484         * Find all resources that match the given location pattern via the
485         * Ant-style PathMatcher. Supports resources in jar files and zip files
486         * and in the file system.
487         * @param locationPattern the location pattern to match
488         * @return the result as Resource array
489         * @throws IOException in case of I/O errors
490         * @see #doFindPathMatchingJarResources
491         * @see #doFindPathMatchingFileResources
492         * @see org.springframework.util.PathMatcher
493         */
494        protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
495                String rootDirPath = determineRootDir(locationPattern);
496                String subPattern = locationPattern.substring(rootDirPath.length());
497                Resource[] rootDirResources = getResources(rootDirPath);
498                Set<Resource> result = new LinkedHashSet<>(16);
499                for (Resource rootDirResource : rootDirResources) {
500                        rootDirResource = resolveRootDirResource(rootDirResource);
501                        URL rootDirUrl = rootDirResource.getURL();
502                        if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
503                                URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
504                                if (resolvedUrl != null) {
505                                        rootDirUrl = resolvedUrl;
506                                }
507                                rootDirResource = new UrlResource(rootDirUrl);
508                        }
509                        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
510                                result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
511                        }
512                        else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
513                                result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
514                        }
515                        else {
516                                result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
517                        }
518                }
519                if (logger.isTraceEnabled()) {
520                        logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
521                }
522                return result.toArray(new Resource[0]);
523        }
524
525        /**
526         * Determine the root directory for the given location.
527         * <p>Used for determining the starting point for file matching,
528         * resolving the root directory location to a {@code java.io.File}
529         * and passing it into {@code retrieveMatchingFiles}, with the
530         * remainder of the location as pattern.
531         * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
532         * for example.
533         * @param location the location to check
534         * @return the part of the location that denotes the root directory
535         * @see #retrieveMatchingFiles
536         */
537        protected String determineRootDir(String location) {
538                int prefixEnd = location.indexOf(':') + 1;
539                int rootDirEnd = location.length();
540                while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
541                        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
542                }
543                if (rootDirEnd == 0) {
544                        rootDirEnd = prefixEnd;
545                }
546                return location.substring(0, rootDirEnd);
547        }
548
549        /**
550         * Resolve the specified resource for path matching.
551         * <p>By default, Equinox OSGi "bundleresource:" / "bundleentry:" URL will be
552         * resolved into a standard jar file URL that be traversed using Spring's
553         * standard jar file traversal algorithm. For any preceding custom resolution,
554         * override this method and replace the resource handle accordingly.
555         * @param original the resource to resolve
556         * @return the resolved resource (may be identical to the passed-in resource)
557         * @throws IOException in case of resolution failure
558         */
559        protected Resource resolveRootDirResource(Resource original) throws IOException {
560                return original;
561        }
562
563        /**
564         * Return whether the given resource handle indicates a jar resource
565         * that the {@code doFindPathMatchingJarResources} method can handle.
566         * <p>By default, the URL protocols "jar", "zip", "vfszip and "wsjar"
567         * will be treated as jar resources. This template method allows for
568         * detecting further kinds of jar-like resources, e.g. through
569         * {@code instanceof} checks on the resource handle type.
570         * @param resource the resource handle to check
571         * (usually the root directory to start path matching from)
572         * @see #doFindPathMatchingJarResources
573         * @see org.springframework.util.ResourceUtils#isJarURL
574         */
575        protected boolean isJarResource(Resource resource) throws IOException {
576                return false;
577        }
578
579        /**
580         * Find all resources in jar files that match the given location pattern
581         * via the Ant-style PathMatcher.
582         * @param rootDirResource the root directory as Resource
583         * @param rootDirURL the pre-resolved root directory URL
584         * @param subPattern the sub pattern to match (below the root directory)
585         * @return a mutable Set of matching Resource instances
586         * @throws IOException in case of I/O errors
587         * @since 4.3
588         * @see java.net.JarURLConnection
589         * @see org.springframework.util.PathMatcher
590         */
591        protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern)
592                        throws IOException {
593
594                URLConnection con = rootDirURL.openConnection();
595                JarFile jarFile;
596                String jarFileUrl;
597                String rootEntryPath;
598                boolean closeJarFile;
599
600                if (con instanceof JarURLConnection) {
601                        // Should usually be the case for traditional JAR files.
602                        JarURLConnection jarCon = (JarURLConnection) con;
603                        ResourceUtils.useCachesIfNecessary(jarCon);
604                        jarFile = jarCon.getJarFile();
605                        jarFileUrl = jarCon.getJarFileURL().toExternalForm();
606                        JarEntry jarEntry = jarCon.getJarEntry();
607                        rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
608                        closeJarFile = !jarCon.getUseCaches();
609                }
610                else {
611                        // No JarURLConnection -> need to resort to URL file parsing.
612                        // We'll assume URLs of the format "jar:path!/entry", with the protocol
613                        // being arbitrary as long as following the entry format.
614                        // We'll also handle paths with and without leading "file:" prefix.
615                        String urlFile = rootDirURL.getFile();
616                        try {
617                                int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
618                                if (separatorIndex == -1) {
619                                        separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
620                                }
621                                if (separatorIndex != -1) {
622                                        jarFileUrl = urlFile.substring(0, separatorIndex);
623                                        rootEntryPath = urlFile.substring(separatorIndex + 2);  // both separators are 2 chars
624                                        jarFile = getJarFile(jarFileUrl);
625                                }
626                                else {
627                                        jarFile = new JarFile(urlFile);
628                                        jarFileUrl = urlFile;
629                                        rootEntryPath = "";
630                                }
631                                closeJarFile = true;
632                        }
633                        catch (ZipException ex) {
634                                if (logger.isDebugEnabled()) {
635                                        logger.debug("Skipping invalid jar classpath entry [" + urlFile + "]");
636                                }
637                                return Collections.emptySet();
638                        }
639                }
640
641                try {
642                        if (logger.isTraceEnabled()) {
643                                logger.trace("Looking for matching resources in jar file [" + jarFileUrl + "]");
644                        }
645                        if (StringUtils.hasLength(rootEntryPath) && !rootEntryPath.endsWith("/")) {
646                                // Root entry path must end with slash to allow for proper matching.
647                                // The Sun JRE does not return a slash here, but BEA JRockit does.
648                                rootEntryPath = rootEntryPath + "/";
649                        }
650                        Set<Resource> result = new LinkedHashSet<>(8);
651                        for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
652                                JarEntry entry = entries.nextElement();
653                                String entryPath = entry.getName();
654                                if (entryPath.startsWith(rootEntryPath)) {
655                                        String relativePath = entryPath.substring(rootEntryPath.length());
656                                        if (getPathMatcher().match(subPattern, relativePath)) {
657                                                result.add(rootDirResource.createRelative(relativePath));
658                                        }
659                                }
660                        }
661                        return result;
662                }
663                finally {
664                        if (closeJarFile) {
665                                jarFile.close();
666                        }
667                }
668        }
669
670        /**
671         * Resolve the given jar file URL into a JarFile object.
672         */
673        protected JarFile getJarFile(String jarFileUrl) throws IOException {
674                if (jarFileUrl.startsWith(ResourceUtils.FILE_URL_PREFIX)) {
675                        try {
676                                return new JarFile(ResourceUtils.toURI(jarFileUrl).getSchemeSpecificPart());
677                        }
678                        catch (URISyntaxException ex) {
679                                // Fallback for URLs that are not valid URIs (should hardly ever happen).
680                                return new JarFile(jarFileUrl.substring(ResourceUtils.FILE_URL_PREFIX.length()));
681                        }
682                }
683                else {
684                        return new JarFile(jarFileUrl);
685                }
686        }
687
688        /**
689         * Find all resources in the file system that match the given location pattern
690         * via the Ant-style PathMatcher.
691         * @param rootDirResource the root directory as Resource
692         * @param subPattern the sub pattern to match (below the root directory)
693         * @return a mutable Set of matching Resource instances
694         * @throws IOException in case of I/O errors
695         * @see #retrieveMatchingFiles
696         * @see org.springframework.util.PathMatcher
697         */
698        protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
699                        throws IOException {
700
701                File rootDir;
702                try {
703                        rootDir = rootDirResource.getFile().getAbsoluteFile();
704                }
705                catch (FileNotFoundException ex) {
706                        if (logger.isDebugEnabled()) {
707                                logger.debug("Cannot search for matching files underneath " + rootDirResource +
708                                                " in the file system: " + ex.getMessage());
709                        }
710                        return Collections.emptySet();
711                }
712                catch (Exception ex) {
713                        if (logger.isInfoEnabled()) {
714                                logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
715                        }
716                        return Collections.emptySet();
717                }
718                return doFindMatchingFileSystemResources(rootDir, subPattern);
719        }
720
721        /**
722         * Find all resources in the file system that match the given location pattern
723         * via the Ant-style PathMatcher.
724         * @param rootDir the root directory in the file system
725         * @param subPattern the sub pattern to match (below the root directory)
726         * @return a mutable Set of matching Resource instances
727         * @throws IOException in case of I/O errors
728         * @see #retrieveMatchingFiles
729         * @see org.springframework.util.PathMatcher
730         */
731        protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
732                if (logger.isTraceEnabled()) {
733                        logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
734                }
735                Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
736                Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
737                for (File file : matchingFiles) {
738                        result.add(new FileSystemResource(file));
739                }
740                return result;
741        }
742
743        /**
744         * Retrieve files that match the given path pattern,
745         * checking the given directory and its subdirectories.
746         * @param rootDir the directory to start from
747         * @param pattern the pattern to match against,
748         * relative to the root directory
749         * @return a mutable Set of matching Resource instances
750         * @throws IOException if directory contents could not be retrieved
751         */
752        protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
753                if (!rootDir.exists()) {
754                        // Silently skip non-existing directories.
755                        if (logger.isDebugEnabled()) {
756                                logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
757                        }
758                        return Collections.emptySet();
759                }
760                if (!rootDir.isDirectory()) {
761                        // Complain louder if it exists but is no directory.
762                        if (logger.isInfoEnabled()) {
763                                logger.info("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
764                        }
765                        return Collections.emptySet();
766                }
767                if (!rootDir.canRead()) {
768                        if (logger.isInfoEnabled()) {
769                                logger.info("Skipping search for matching files underneath directory [" + rootDir.getAbsolutePath() +
770                                                "] because the application is not allowed to read the directory");
771                        }
772                        return Collections.emptySet();
773                }
774                String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
775                if (!pattern.startsWith("/")) {
776                        fullPattern += "/";
777                }
778                fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
779                Set<File> result = new LinkedHashSet<>(8);
780                doRetrieveMatchingFiles(fullPattern, rootDir, result);
781                return result;
782        }
783
784        /**
785         * Recursively retrieve files that match the given pattern,
786         * adding them to the given result list.
787         * @param fullPattern the pattern to match against,
788         * with prepended root directory path
789         * @param dir the current directory
790         * @param result the Set of matching File instances to add to
791         * @throws IOException if directory contents could not be retrieved
792         */
793        protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
794                if (logger.isTraceEnabled()) {
795                        logger.trace("Searching directory [" + dir.getAbsolutePath() +
796                                        "] for files matching pattern [" + fullPattern + "]");
797                }
798                for (File content : listDirectory(dir)) {
799                        String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
800                        if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
801                                if (!content.canRead()) {
802                                        if (logger.isDebugEnabled()) {
803                                                logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
804                                                                "] because the application is not allowed to read the directory");
805                                        }
806                                }
807                                else {
808                                        doRetrieveMatchingFiles(fullPattern, content, result);
809                                }
810                        }
811                        if (getPathMatcher().match(fullPattern, currPath)) {
812                                result.add(content);
813                        }
814                }
815        }
816
817        /**
818         * Determine a sorted list of files in the given directory.
819         * @param dir the directory to introspect
820         * @return the sorted list of files (by default in alphabetical order)
821         * @since 5.1
822         * @see File#listFiles()
823         */
824        protected File[] listDirectory(File dir) {
825                File[] files = dir.listFiles();
826                if (files == null) {
827                        if (logger.isInfoEnabled()) {
828                                logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
829                        }
830                        return new File[0];
831                }
832                Arrays.sort(files, Comparator.comparing(File::getName));
833                return files;
834        }
835
836
837        /**
838         * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime.
839         */
840        private static class VfsResourceMatchingDelegate {
841
842                public static Set<Resource> findMatchingResources(
843                                URL rootDirURL, String locationPattern, PathMatcher pathMatcher) throws IOException {
844
845                        Object root = VfsPatternUtils.findRoot(rootDirURL);
846                        PatternVirtualFileVisitor visitor =
847                                        new PatternVirtualFileVisitor(VfsPatternUtils.getPath(root), locationPattern, pathMatcher);
848                        VfsPatternUtils.visit(root, visitor);
849                        return visitor.getResources();
850                }
851        }
852
853
854        /**
855         * VFS visitor for path matching purposes.
856         */
857        @SuppressWarnings("unused")
858        private static class PatternVirtualFileVisitor implements InvocationHandler {
859
860                private final String subPattern;
861
862                private final PathMatcher pathMatcher;
863
864                private final String rootPath;
865
866                private final Set<Resource> resources = new LinkedHashSet<>();
867
868                public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
869                        this.subPattern = subPattern;
870                        this.pathMatcher = pathMatcher;
871                        this.rootPath = (rootPath.isEmpty() || rootPath.endsWith("/") ? rootPath : rootPath + "/");
872                }
873
874                @Override
875                @Nullable
876                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
877                        String methodName = method.getName();
878                        if (Object.class == method.getDeclaringClass()) {
879                                if (methodName.equals("equals")) {
880                                        // Only consider equal when proxies are identical.
881                                        return (proxy == args[0]);
882                                }
883                                else if (methodName.equals("hashCode")) {
884                                        return System.identityHashCode(proxy);
885                                }
886                        }
887                        else if ("getAttributes".equals(methodName)) {
888                                return getAttributes();
889                        }
890                        else if ("visit".equals(methodName)) {
891                                visit(args[0]);
892                                return null;
893                        }
894                        else if ("toString".equals(methodName)) {
895                                return toString();
896                        }
897
898                        throw new IllegalStateException("Unexpected method invocation: " + method);
899                }
900
901                public void visit(Object vfsResource) {
902                        if (this.pathMatcher.match(this.subPattern,
903                                        VfsPatternUtils.getPath(vfsResource).substring(this.rootPath.length()))) {
904                                this.resources.add(new VfsResource(vfsResource));
905                        }
906                }
907
908                @Nullable
909                public Object getAttributes() {
910                        return VfsPatternUtils.getVisitorAttributes();
911                }
912
913                public Set<Resource> getResources() {
914                        return this.resources;
915                }
916
917                public int size() {
918                        return this.resources.size();
919                }
920
921                @Override
922                public String toString() {
923                        return "sub-pattern: " + this.subPattern + ", resources: " + this.resources;
924                }
925        }
926
927}