1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.core.io.support;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.lang.reflect.InvocationHandler;
22  import java.lang.reflect.Method;
23  import java.net.JarURLConnection;
24  import java.net.MalformedURLException;
25  import java.net.URISyntaxException;
26  import java.net.URL;
27  import java.net.URLClassLoader;
28  import java.net.URLConnection;
29  import java.util.Collections;
30  import java.util.Enumeration;
31  import java.util.LinkedHashSet;
32  import java.util.Set;
33  import java.util.jar.JarEntry;
34  import java.util.jar.JarFile;
35  
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  
39  import org.springframework.core.io.DefaultResourceLoader;
40  import org.springframework.core.io.FileSystemResource;
41  import org.springframework.core.io.Resource;
42  import org.springframework.core.io.ResourceLoader;
43  import org.springframework.core.io.UrlResource;
44  import org.springframework.core.io.VfsResource;
45  import org.springframework.util.AntPathMatcher;
46  import org.springframework.util.Assert;
47  import org.springframework.util.ClassUtils;
48  import org.springframework.util.PathMatcher;
49  import org.springframework.util.ReflectionUtils;
50  import org.springframework.util.ResourceUtils;
51  import org.springframework.util.StringUtils;
52  
53  /**
54   * A {@link ResourcePatternResolver} implementation that is able to resolve a
55   * specified resource location path into one or more matching Resources.
56   * The source path may be a simple path which has a one-to-one mapping to a
57   * target {@link org.springframework.core.io.Resource}, or alternatively
58   * may contain the special "{@code classpath*:}" prefix and/or
59   * internal Ant-style regular expressions (matched using Spring's
60   * {@link org.springframework.util.AntPathMatcher} utility).
61   * Both of the latter are effectively wildcards.
62   *
63   * <p><b>No Wildcards:</b>
64   *
65   * <p>In the simple case, if the specified location path does not start with the
66   * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern,
67   * this resolver will simply return a single resource via a
68   * {@code getResource()} call on the underlying {@code ResourceLoader}.
69   * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs
70   * such as "{@code classpath:/context.xml}", and simple unprefixed paths
71   * such as "{@code /WEB-INF/context.xml}". The latter will resolve in a
72   * fashion specific to the underlying {@code ResourceLoader} (e.g.
73   * {@code ServletContextResource} for a {@code WebApplicationContext}).
74   *
75   * <p><b>Ant-style Patterns:</b>
76   *
77   * <p>When the path location contains an Ant-style pattern, e.g.:
78   * <pre class="code">
79   * /WEB-INF/*-context.xml
80   * com/mycompany/**&#47;applicationContext.xml
81   * file:C:/some/path/*-context.xml
82   * classpath:com/mycompany/**&#47;applicationContext.xml</pre>
83   * the resolver follows a more complex but defined procedure to try to resolve
84   * the wildcard. It produces a {@code Resource} for the path up to the last
85   * non-wildcard segment and obtains a {@code URL} from it. If this URL is
86   * not a "{@code jar:}" URL or container-specific variant (e.g.
87   * "{@code zip:}" in WebLogic, "{@code wsjar}" in WebSphere", etc.),
88   * then a {@code java.io.File} is obtained from it, and used to resolve the
89   * wildcard by walking the filesystem. In the case of a jar URL, the resolver
90   * either gets a {@code java.net.JarURLConnection} from it, or manually parses
91   * the jar URL, and then traverses the contents of the jar file, to resolve the
92   * wildcards.
93   *
94   * <p><b>Implications on portability:</b>
95   *
96   * <p>If the specified path is already a file URL (either explicitly, or
97   * implicitly because the base {@code ResourceLoader} is a filesystem one,
98   * then wildcarding is guaranteed to work in a completely portable fashion.
99   *
100  * <p>If the specified path is a classpath location, then the resolver must
101  * obtain the last non-wildcard path segment URL via a
102  * {@code Classloader.getResource()} call. Since this is just a
103  * node of the path (not the file at the end) it is actually undefined
104  * (in the ClassLoader Javadocs) exactly what sort of a URL is returned in
105  * this case. In practice, it is usually a {@code java.io.File} representing
106  * the directory, where the classpath resource resolves to a filesystem
107  * location, or a jar URL of some sort, where the classpath resource resolves
108  * to a jar location. Still, there is a portability concern on this operation.
109  *
110  * <p>If a jar URL is obtained for the last non-wildcard segment, the resolver
111  * must be able to get a {@code java.net.JarURLConnection} from it, or
112  * manually parse the jar URL, to be able to walk the contents of the jar,
113  * and resolve the wildcard. This will work in most environments, but will
114  * fail in others, and it is strongly recommended that the wildcard
115  * resolution of resources coming from jars be thoroughly tested in your
116  * specific environment before you rely on it.
117  *
118  * <p><b>{@code classpath*:} Prefix:</b>
119  *
120  * <p>There is special support for retrieving multiple class path resources with
121  * the same name, via the "{@code classpath*:}" prefix. For example,
122  * "{@code classpath*:META-INF/beans.xml}" will find all "beans.xml"
123  * files in the class path, be it in "classes" directories or in JAR files.
124  * This is particularly useful for autodetecting config files of the same name
125  * at the same location within each jar file. Internally, this happens via a
126  * {@code ClassLoader.getResources()} call, and is completely portable.
127  *
128  * <p>The "classpath*:" prefix can also be combined with a PathMatcher pattern in
129  * the rest of the location path, for example "classpath*:META-INF/*-beans.xml".
130  * In this case, the resolution strategy is fairly simple: a
131  * {@code ClassLoader.getResources()} call is used on the last non-wildcard
132  * path segment to get all the matching resources in the class loader hierarchy,
133  * and then off each resource the same PathMatcher resolution strategy described
134  * above is used for the wildcard subpath.
135  *
136  * <p><b>Other notes:</b>
137  *
138  * <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with
139  * Ant-style patterns will only work reliably with at least one root directory
140  * before the pattern starts, unless the actual target files reside in the file
141  * system. This means that a pattern like "{@code classpath*:*.xml}" will
142  * <i>not</i> retrieve files from the root of jar files but rather only from the
143  * root of expanded directories. This originates from a limitation in the JDK's
144  * {@code ClassLoader.getResources()} method which only returns file system
145  * locations for a passed-in empty String (indicating potential roots to search).
146  *
147  * <p><b>WARNING:</b> Ant-style patterns with "classpath:" resources are not
148  * guaranteed to find matching resources if the root package to search is available
149  * in multiple class path locations. This is because a resource such as
150  * <pre class="code">
151  *     com/mycompany/package1/service-context.xml
152  * </pre>
153  * may be in only one location, but when a path such as
154  * <pre class="code">
155  *     classpath:com/mycompany/**&#47;service-context.xml
156  * </pre>
157  * is used to try to resolve it, the resolver will work off the (first) URL
158  * returned by {@code getResource("com/mycompany");}. If this base package
159  * node exists in multiple classloader locations, the actual end resource may
160  * not be underneath. Therefore, preferably, use "{@code classpath*:}" with the same
161  * Ant-style pattern in such a case, which will search <i>all</i> class path
162  * locations that contain the root package.
163  *
164  * @author Juergen Hoeller
165  * @author Colin Sampaleanu
166  * @author Marius Bogoevici
167  * @author Costin Leau
168  * @since 1.0.2
169  * @see #CLASSPATH_ALL_URL_PREFIX
170  * @see org.springframework.util.AntPathMatcher
171  * @see org.springframework.core.io.ResourceLoader#getResource(String)
172  * @see ClassLoader#getResources(String)
173  */
174 public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
175 
176 	private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class);
177 
178 	private static Method equinoxResolveMethod;
179 
180 	static {
181 		try {
182 			// Detect Equinox OSGi (e.g. on WebSphere 6.1)
183 			Class<?> fileLocatorClass = ClassUtils.forName("org.eclipse.core.runtime.FileLocator",
184 					PathMatchingResourcePatternResolver.class.getClassLoader());
185 			equinoxResolveMethod = fileLocatorClass.getMethod("resolve", URL.class);
186 			logger.debug("Found Equinox FileLocator for OSGi bundle URL resolution");
187 		}
188 		catch (Throwable ex) {
189 			equinoxResolveMethod = null;
190 		}
191 	}
192 
193 
194 	private final ResourceLoader resourceLoader;
195 
196 	private PathMatcher pathMatcher = new AntPathMatcher();
197 
198 
199 	/**
200 	 * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
201 	 * <p>ClassLoader access will happen via the thread context class loader.
202 	 * @see org.springframework.core.io.DefaultResourceLoader
203 	 */
204 	public PathMatchingResourcePatternResolver() {
205 		this.resourceLoader = new DefaultResourceLoader();
206 	}
207 
208 	/**
209 	 * Create a new PathMatchingResourcePatternResolver.
210 	 * <p>ClassLoader access will happen via the thread context class loader.
211 	 * @param resourceLoader the ResourceLoader to load root directories and
212 	 * actual resources with
213 	 */
214 	public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
215 		Assert.notNull(resourceLoader, "ResourceLoader must not be null");
216 		this.resourceLoader = resourceLoader;
217 	}
218 
219 	/**
220 	 * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
221 	 * @param classLoader the ClassLoader to load classpath resources with,
222 	 * or {@code null} for using the thread context class loader
223 	 * at the time of actual resource access
224 	 * @see org.springframework.core.io.DefaultResourceLoader
225 	 */
226 	public PathMatchingResourcePatternResolver(ClassLoader classLoader) {
227 		this.resourceLoader = new DefaultResourceLoader(classLoader);
228 	}
229 
230 
231 	/**
232 	 * Return the ResourceLoader that this pattern resolver works with.
233 	 */
234 	public ResourceLoader getResourceLoader() {
235 		return this.resourceLoader;
236 	}
237 
238 	@Override
239 	public ClassLoader getClassLoader() {
240 		return getResourceLoader().getClassLoader();
241 	}
242 
243 	/**
244 	 * Set the PathMatcher implementation to use for this
245 	 * resource pattern resolver. Default is AntPathMatcher.
246 	 * @see org.springframework.util.AntPathMatcher
247 	 */
248 	public void setPathMatcher(PathMatcher pathMatcher) {
249 		Assert.notNull(pathMatcher, "PathMatcher must not be null");
250 		this.pathMatcher = pathMatcher;
251 	}
252 
253 	/**
254 	 * Return the PathMatcher that this resource pattern resolver uses.
255 	 */
256 	public PathMatcher getPathMatcher() {
257 		return this.pathMatcher;
258 	}
259 
260 
261 	@Override
262 	public Resource getResource(String location) {
263 		return getResourceLoader().getResource(location);
264 	}
265 
266 	@Override
267 	public Resource[] getResources(String locationPattern) throws IOException {
268 		Assert.notNull(locationPattern, "Location pattern must not be null");
269 		if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
270 			// a class path resource (multiple resources for same name possible)
271 			if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
272 				// a class path resource pattern
273 				return findPathMatchingResources(locationPattern);
274 			}
275 			else {
276 				// all class path resources with the given name
277 				return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
278 			}
279 		}
280 		else {
281 			// Only look for a pattern after a prefix here
282 			// (to not get fooled by a pattern symbol in a strange prefix).
283 			int prefixEnd = locationPattern.indexOf(":") + 1;
284 			if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
285 				// a file pattern
286 				return findPathMatchingResources(locationPattern);
287 			}
288 			else {
289 				// a single resource with the given name
290 				return new Resource[] {getResourceLoader().getResource(locationPattern)};
291 			}
292 		}
293 	}
294 
295 	/**
296 	 * Find all class location resources with the given location via the ClassLoader.
297 	 * Delegates to {@link #doFindAllClassPathResources(String)}.
298 	 * @param location the absolute path within the classpath
299 	 * @return the result as Resource array
300 	 * @throws IOException in case of I/O errors
301 	 * @see java.lang.ClassLoader#getResources
302 	 * @see #convertClassLoaderURL
303 	 */
304 	protected Resource[] findAllClassPathResources(String location) throws IOException {
305 		String path = location;
306 		if (path.startsWith("/")) {
307 			path = path.substring(1);
308 		}
309 		Set<Resource> result = doFindAllClassPathResources(path);
310 		return result.toArray(new Resource[result.size()]);
311 	}
312 
313 	/**
314 	 * Find all class location resources with the given path via the ClassLoader.
315 	 * Called by {@link #findAllClassPathResources(String)}.
316 	 * @param path the absolute path within the classpath (never a leading slash)
317 	 * @return a mutable Set of matching Resource instances
318 	 */
319 	protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
320 		Set<Resource> result = new LinkedHashSet<Resource>(16);
321 		ClassLoader cl = getClassLoader();
322 		Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
323 		while (resourceUrls.hasMoreElements()) {
324 			URL url = resourceUrls.nextElement();
325 			result.add(convertClassLoaderURL(url));
326 		}
327 		if ("".equals(path)) {
328 			// The above result is likely to be incomplete, i.e. only containing file system references.
329 			// We need to have pointers to each of the jar files on the classpath as well...
330 			addAllClassLoaderJarRoots(cl, result);
331 		}
332 		return result;
333 	}
334 
335 	/**
336 	 * Convert the given URL as returned from the ClassLoader into a {@link Resource}.
337 	 * <p>The default implementation simply creates a {@link UrlResource} instance.
338 	 * @param url a URL as returned from the ClassLoader
339 	 * @return the corresponding Resource object
340 	 * @see java.lang.ClassLoader#getResources
341 	 * @see org.springframework.core.io.Resource
342 	 */
343 	protected Resource convertClassLoaderURL(URL url) {
344 		return new UrlResource(url);
345 	}
346 
347 	/**
348 	 * Search all {@link URLClassLoader} URLs for jar file references and add them to the
349 	 * given set of resources in the form of pointers to the root of the jar file content.
350 	 * @param classLoader the ClassLoader to search (including its ancestors)
351 	 * @param result the set of resources to add jar roots to
352 	 */
353 	protected void addAllClassLoaderJarRoots(ClassLoader classLoader, Set<Resource> result) {
354 		if (classLoader instanceof URLClassLoader) {
355 			try {
356 				for (URL url : ((URLClassLoader) classLoader).getURLs()) {
357 					if (ResourceUtils.isJarFileURL(url)) {
358 						try {
359 							UrlResource jarResource = new UrlResource(
360 									ResourceUtils.JAR_URL_PREFIX + url.toString() + ResourceUtils.JAR_URL_SEPARATOR);
361 							if (jarResource.exists()) {
362 								result.add(jarResource);
363 							}
364 						}
365 						catch (MalformedURLException ex) {
366 							if (logger.isDebugEnabled()) {
367 								logger.debug("Cannot search for matching files underneath [" + url +
368 										"] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
369 							}
370 						}
371 					}
372 				}
373 			}
374 			catch (Exception ex) {
375 				if (logger.isDebugEnabled()) {
376 					logger.debug("Cannot introspect jar files since ClassLoader [" + classLoader +
377 							"] does not support 'getURLs()': " + ex);
378 				}
379 			}
380 		}
381 		if (classLoader != null) {
382 			try {
383 				addAllClassLoaderJarRoots(classLoader.getParent(), result);
384 			}
385 			catch (Exception ex) {
386 				if (logger.isDebugEnabled()) {
387 					logger.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader +
388 							"] does not support 'getParent()': " + ex);
389 				}
390 			}
391 		}
392 	}
393 
394 	/**
395 	 * Find all resources that match the given location pattern via the
396 	 * Ant-style PathMatcher. Supports resources in jar files and zip files
397 	 * and in the file system.
398 	 * @param locationPattern the location pattern to match
399 	 * @return the result as Resource array
400 	 * @throws IOException in case of I/O errors
401 	 * @see #doFindPathMatchingJarResources
402 	 * @see #doFindPathMatchingFileResources
403 	 * @see org.springframework.util.PathMatcher
404 	 */
405 	protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
406 		String rootDirPath = determineRootDir(locationPattern);
407 		String subPattern = locationPattern.substring(rootDirPath.length());
408 		Resource[] rootDirResources = getResources(rootDirPath);
409 		Set<Resource> result = new LinkedHashSet<Resource>(16);
410 		for (Resource rootDirResource : rootDirResources) {
411 			rootDirResource = resolveRootDirResource(rootDirResource);
412 			if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
413 				result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));
414 			}
415 			else if (isJarResource(rootDirResource)) {
416 				result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern));
417 			}
418 			else {
419 				result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
420 			}
421 		}
422 		if (logger.isDebugEnabled()) {
423 			logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);
424 		}
425 		return result.toArray(new Resource[result.size()]);
426 	}
427 
428 	/**
429 	 * Determine the root directory for the given location.
430 	 * <p>Used for determining the starting point for file matching,
431 	 * resolving the root directory location to a {@code java.io.File}
432 	 * and passing it into {@code retrieveMatchingFiles}, with the
433 	 * remainder of the location as pattern.
434 	 * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
435 	 * for example.
436 	 * @param location the location to check
437 	 * @return the part of the location that denotes the root directory
438 	 * @see #retrieveMatchingFiles
439 	 */
440 	protected String determineRootDir(String location) {
441 		int prefixEnd = location.indexOf(":") + 1;
442 		int rootDirEnd = location.length();
443 		while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
444 			rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
445 		}
446 		if (rootDirEnd == 0) {
447 			rootDirEnd = prefixEnd;
448 		}
449 		return location.substring(0, rootDirEnd);
450 	}
451 
452 	/**
453 	 * Resolve the specified resource for path matching.
454 	 * <p>The default implementation detects an Equinox OSGi "bundleresource:"
455 	 * / "bundleentry:" URL and resolves it into a standard jar file URL that
456 	 * can be traversed using Spring's standard jar file traversal algorithm.
457 	 * @param original the resource to resolve
458 	 * @return the resolved resource (may be identical to the passed-in resource)
459 	 * @throws IOException in case of resolution failure
460 	 */
461 	protected Resource resolveRootDirResource(Resource original) throws IOException {
462 		if (equinoxResolveMethod != null) {
463 			URL url = original.getURL();
464 			if (url.getProtocol().startsWith("bundle")) {
465 				return new UrlResource((URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, url));
466 			}
467 		}
468 		return original;
469 	}
470 
471 	/**
472 	 * Return whether the given resource handle indicates a jar resource
473 	 * that the {@code doFindPathMatchingJarResources} method can handle.
474 	 * <p>The default implementation checks against the URL protocols
475 	 * "jar", "zip" and "wsjar" (the latter are used by BEA WebLogic Server
476 	 * and IBM WebSphere, respectively, but can be treated like jar files).
477 	 * @param resource the resource handle to check
478 	 * (usually the root directory to start path matching from)
479 	 * @see #doFindPathMatchingJarResources
480 	 * @see org.springframework.util.ResourceUtils#isJarURL
481 	 */
482 	protected boolean isJarResource(Resource resource) throws IOException {
483 		return ResourceUtils.isJarURL(resource.getURL());
484 	}
485 
486 	/**
487 	 * Find all resources in jar files that match the given location pattern
488 	 * via the Ant-style PathMatcher.
489 	 * @param rootDirResource the root directory as Resource
490 	 * @param subPattern the sub pattern to match (below the root directory)
491 	 * @return a mutable Set of matching Resource instances
492 	 * @throws IOException in case of I/O errors
493 	 * @see java.net.JarURLConnection
494 	 * @see org.springframework.util.PathMatcher
495 	 */
496 	protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, String subPattern)
497 			throws IOException {
498 
499 		URLConnection con = rootDirResource.getURL().openConnection();
500 		JarFile jarFile;
501 		String jarFileUrl;
502 		String rootEntryPath;
503 		boolean newJarFile = false;
504 
505 		if (con instanceof JarURLConnection) {
506 			// Should usually be the case for traditional JAR files.
507 			JarURLConnection jarCon = (JarURLConnection) con;
508 			ResourceUtils.useCachesIfNecessary(jarCon);
509 			jarFile = jarCon.getJarFile();
510 			jarFileUrl = jarCon.getJarFileURL().toExternalForm();
511 			JarEntry jarEntry = jarCon.getJarEntry();
512 			rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
513 		}
514 		else {
515 			// No JarURLConnection -> need to resort to URL file parsing.
516 			// We'll assume URLs of the format "jar:path!/entry", with the protocol
517 			// being arbitrary as long as following the entry format.
518 			// We'll also handle paths with and without leading "file:" prefix.
519 			String urlFile = rootDirResource.getURL().getFile();
520 			int separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
521 			if (separatorIndex != -1) {
522 				jarFileUrl = urlFile.substring(0, separatorIndex);
523 				rootEntryPath = urlFile.substring(separatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
524 				jarFile = getJarFile(jarFileUrl);
525 			}
526 			else {
527 				jarFile = new JarFile(urlFile);
528 				jarFileUrl = urlFile;
529 				rootEntryPath = "";
530 			}
531 			newJarFile = true;
532 		}
533 
534 		try {
535 			if (logger.isDebugEnabled()) {
536 				logger.debug("Looking for matching resources in jar file [" + jarFileUrl + "]");
537 			}
538 			if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {
539 				// Root entry path must end with slash to allow for proper matching.
540 				// The Sun JRE does not return a slash here, but BEA JRockit does.
541 				rootEntryPath = rootEntryPath + "/";
542 			}
543 			Set<Resource> result = new LinkedHashSet<Resource>(8);
544 			for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
545 				JarEntry entry = entries.nextElement();
546 				String entryPath = entry.getName();
547 				if (entryPath.startsWith(rootEntryPath)) {
548 					String relativePath = entryPath.substring(rootEntryPath.length());
549 					if (getPathMatcher().match(subPattern, relativePath)) {
550 						result.add(rootDirResource.createRelative(relativePath));
551 					}
552 				}
553 			}
554 			return result;
555 		}
556 		finally {
557 			// Close jar file, but only if freshly obtained -
558 			// not from JarURLConnection, which might cache the file reference.
559 			if (newJarFile) {
560 				jarFile.close();
561 			}
562 		}
563 	}
564 
565 	/**
566 	 * Resolve the given jar file URL into a JarFile object.
567 	 */
568 	protected JarFile getJarFile(String jarFileUrl) throws IOException {
569 		if (jarFileUrl.startsWith(ResourceUtils.FILE_URL_PREFIX)) {
570 			try {
571 				return new JarFile(ResourceUtils.toURI(jarFileUrl).getSchemeSpecificPart());
572 			}
573 			catch (URISyntaxException ex) {
574 				// Fallback for URLs that are not valid URIs (should hardly ever happen).
575 				return new JarFile(jarFileUrl.substring(ResourceUtils.FILE_URL_PREFIX.length()));
576 			}
577 		}
578 		else {
579 			return new JarFile(jarFileUrl);
580 		}
581 	}
582 
583 	/**
584 	 * Find all resources in the file system that match the given location pattern
585 	 * via the Ant-style PathMatcher.
586 	 * @param rootDirResource the root directory as Resource
587 	 * @param subPattern the sub pattern to match (below the root directory)
588 	 * @return a mutable Set of matching Resource instances
589 	 * @throws IOException in case of I/O errors
590 	 * @see #retrieveMatchingFiles
591 	 * @see org.springframework.util.PathMatcher
592 	 */
593 	protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
594 			throws IOException {
595 
596 		File rootDir;
597 		try {
598 			rootDir = rootDirResource.getFile().getAbsoluteFile();
599 		}
600 		catch (IOException ex) {
601 			if (logger.isWarnEnabled()) {
602 				logger.warn("Cannot search for matching files underneath " + rootDirResource +
603 						" because it does not correspond to a directory in the file system", ex);
604 			}
605 			return Collections.emptySet();
606 		}
607 		return doFindMatchingFileSystemResources(rootDir, subPattern);
608 	}
609 
610 	/**
611 	 * Find all resources in the file system that match the given location pattern
612 	 * via the Ant-style PathMatcher.
613 	 * @param rootDir the root directory in the file system
614 	 * @param subPattern the sub pattern to match (below the root directory)
615 	 * @return a mutable Set of matching Resource instances
616 	 * @throws IOException in case of I/O errors
617 	 * @see #retrieveMatchingFiles
618 	 * @see org.springframework.util.PathMatcher
619 	 */
620 	protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
621 		if (logger.isDebugEnabled()) {
622 			logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
623 		}
624 		Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
625 		Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size());
626 		for (File file : matchingFiles) {
627 			result.add(new FileSystemResource(file));
628 		}
629 		return result;
630 	}
631 
632 	/**
633 	 * Retrieve files that match the given path pattern,
634 	 * checking the given directory and its subdirectories.
635 	 * @param rootDir the directory to start from
636 	 * @param pattern the pattern to match against,
637 	 * relative to the root directory
638 	 * @return a mutable Set of matching Resource instances
639 	 * @throws IOException if directory contents could not be retrieved
640 	 */
641 	protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
642 		if (!rootDir.exists()) {
643 			// Silently skip non-existing directories.
644 			if (logger.isDebugEnabled()) {
645 				logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
646 			}
647 			return Collections.emptySet();
648 		}
649 		if (!rootDir.isDirectory()) {
650 			// Complain louder if it exists but is no directory.
651 			if (logger.isWarnEnabled()) {
652 				logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
653 			}
654 			return Collections.emptySet();
655 		}
656 		if (!rootDir.canRead()) {
657 			if (logger.isWarnEnabled()) {
658 				logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() +
659 						"] because the application is not allowed to read the directory");
660 			}
661 			return Collections.emptySet();
662 		}
663 		String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
664 		if (!pattern.startsWith("/")) {
665 			fullPattern += "/";
666 		}
667 		fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
668 		Set<File> result = new LinkedHashSet<File>(8);
669 		doRetrieveMatchingFiles(fullPattern, rootDir, result);
670 		return result;
671 	}
672 
673 	/**
674 	 * Recursively retrieve files that match the given pattern,
675 	 * adding them to the given result list.
676 	 * @param fullPattern the pattern to match against,
677 	 * with prepended root directory path
678 	 * @param dir the current directory
679 	 * @param result the Set of matching File instances to add to
680 	 * @throws IOException if directory contents could not be retrieved
681 	 */
682 	protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
683 		if (logger.isDebugEnabled()) {
684 			logger.debug("Searching directory [" + dir.getAbsolutePath() +
685 					"] for files matching pattern [" + fullPattern + "]");
686 		}
687 		File[] dirContents = dir.listFiles();
688 		if (dirContents == null) {
689 			if (logger.isWarnEnabled()) {
690 				logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
691 			}
692 			return;
693 		}
694 		for (File content : dirContents) {
695 			String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
696 			if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
697 				if (!content.canRead()) {
698 					if (logger.isDebugEnabled()) {
699 						logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
700 								"] because the application is not allowed to read the directory");
701 					}
702 				}
703 				else {
704 					doRetrieveMatchingFiles(fullPattern, content, result);
705 				}
706 			}
707 			if (getPathMatcher().match(fullPattern, currPath)) {
708 				result.add(content);
709 			}
710 		}
711 	}
712 
713 
714 	/**
715 	 * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime.
716 	 */
717 	private static class VfsResourceMatchingDelegate {
718 
719 		public static Set<Resource> findMatchingResources(
720 				Resource rootResource, String locationPattern, PathMatcher pathMatcher) throws IOException {
721 			Object root = VfsPatternUtils.findRoot(rootResource.getURL());
722 			PatternVirtualFileVisitor visitor =
723 					new PatternVirtualFileVisitor(VfsPatternUtils.getPath(root), locationPattern, pathMatcher);
724 			VfsPatternUtils.visit(root, visitor);
725 			return visitor.getResources();
726 		}
727 	}
728 
729 
730 	/**
731 	 * VFS visitor for path matching purposes.
732 	 */
733 	@SuppressWarnings("unused")
734 	private static class PatternVirtualFileVisitor implements InvocationHandler {
735 
736 		private final String subPattern;
737 
738 		private final PathMatcher pathMatcher;
739 
740 		private final String rootPath;
741 
742 		private final Set<Resource> resources = new LinkedHashSet<Resource>();
743 
744 		public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
745 			this.subPattern = subPattern;
746 			this.pathMatcher = pathMatcher;
747 			this.rootPath = (rootPath.length() == 0 || rootPath.endsWith("/") ? rootPath : rootPath + "/");
748 		}
749 
750 		@Override
751 		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
752 			String methodName = method.getName();
753 			if (Object.class.equals(method.getDeclaringClass())) {
754 				if (methodName.equals("equals")) {
755 					// Only consider equal when proxies are identical.
756 					return (proxy == args[0]);
757 				}
758 				else if (methodName.equals("hashCode")) {
759 					return System.identityHashCode(proxy);
760 				}
761 			}
762 			else if ("getAttributes".equals(methodName)) {
763 				return getAttributes();
764 			}
765 			else if ("visit".equals(methodName)) {
766 				visit(args[0]);
767 				return null;
768 			}
769 			else if ("toString".equals(methodName)) {
770 				return toString();
771 			}
772 
773 			throw new IllegalStateException("Unexpected method invocation: " + method);
774 		}
775 
776 		public void visit(Object vfsResource) {
777 			if (this.pathMatcher.match(this.subPattern,
778 					VfsPatternUtils.getPath(vfsResource).substring(this.rootPath.length()))) {
779 				this.resources.add(new VfsResource(vfsResource));
780 			}
781 		}
782 
783 		public Object getAttributes() {
784 			return VfsPatternUtils.getVisitorAttribute();
785 		}
786 
787 		public Set<Resource> getResources() {
788 			return this.resources;
789 		}
790 
791 		public int size() {
792 			return this.resources.size();
793 		}
794 
795 		@Override
796 		public String toString() {
797 			return "sub-pattern: " + this.subPattern + ", resources: " + this.resources;
798 		}
799 	}
800 
801 }