You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tamaya.apache.org by an...@apache.org on 2014/12/01 10:43:47 UTC

[4/8] incubator-tamaya git commit: TAMAYA-14: added resource support. TAMAYA-15: Moved PropertyProviders to API, added SPI. TAMAYA-8: Added/improved Javadoc.

http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/a55d1c97/core/src/main/java/org/apache/tamaya/core/internal/resources/io/PathMatchingResourcePatternResolver.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resources/io/PathMatchingResourcePatternResolver.java b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/PathMatchingResourcePatternResolver.java
new file mode 100644
index 0000000..caab938
--- /dev/null
+++ b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/PathMatchingResourcePatternResolver.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright 2002-2008 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tamaya.core.internal.resources.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import java.lang.reflect.InvocationHandler;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URLClassLoader;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * A {@code ResourcePatternResolver} implementation that is able to resolve a
+ * specified resource location path into one or more matching Resources.
+ * The source path may be a simple path which has a one-to-one mapping to a
+ * target {@code org.springframework.core.io.Resource}, or alternatively
+ * may contain the special "{@code classpath*:}" prefix and/or
+ * internal Ant-style regular expressions (matched using Spring's
+ * {@code org.springframework.util.AntPathMatcher} utility).
+ * Both of the latter are effectively wildcards.
+ *
+ * <p><b>No Wildcards:</b>
+ *
+ * <p>In the simple case, if the specified location path does not start with the
+ * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern,
+ * this resolver will simply return a single resource via a
+ * {@code getResource()} call on the underlying {@code ResourceLoader}.
+ * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs
+ * such as "{@code classpath:/context.xml}", and simple unprefixed paths
+ * such as "{@code /WEB-INF/context.xml}". The latter will resolve in a
+ * fashion specific to the underlying {@code ResourceLoader} (e.g.
+ * {@code ServletContextResource} for a {@code WebApplicationContext}).
+ *
+ * <p><b>Ant-style Patterns:</b>
+ *
+ * <p>When the path location contains an Ant-style pattern, e.g.:
+ * <pre class="code">
+ * /WEB-INF/*-context.xml
+ * com/mycompany/**&#47;applicationContext.xml
+ * file:C:/some/path/*-context.xml
+ * classpath:com/mycompany/**&#47;applicationContext.xml</pre>
+ * 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.
+ *
+ * <p><b>Implications on portability:</b>
+ *
+ * <p>If the specified path is already a file URL (either explicitly, or
+ * implicitly because the base {@code ResourceLoader} is a filesystem one,
+ * then wildcarding is guaranteed to work in a completely portable fashion.
+ *
+ * <p>If the specified path is a classpath location, then the resolver must
+ * obtain the last non-wildcard path segment URL via a
+ * {@code Classloader.getResource()} call. Since this is just a
+ * node of the path (not the file at the end) it is actually undefined
+ * (in the ClassLoader Javadocs) exactly what sort of a URL is returned in
+ * this case. In practice, it is usually a {@code java.io.File} representing
+ * the directory, where the classpath resource resolves to a filesystem
+ * location, or a jar URL of some sort, where the classpath resource resolves
+ * to a jar location. Still, there is a portability concern on this operation.
+ *
+ * <p>If a jar URL is obtained for the last non-wildcard segment, the resolver
+ * must be able to get a {@code java.net.JarURLConnection} from it, or
+ * manually parse the jar URL, to be able to walk the contents of the jar,
+ * and resolve the wildcard. This will work in most environments, but will
+ * fail in others, and it is strongly recommended that the wildcard
+ * resolution of resources coming from jars be thoroughly tested in your
+ * specific environment before you rely on it.
+ *
+ * <p><b>{@code classpath*:} Prefix:</b>
+ *
+ * <p>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"
+ * 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
+ * {@code ClassLoader.getResources()} call, and is completely portable.
+ *
+ * <p>The "classpath*:" prefix can also be combined with a PathMatcher pattern in
+ * the rest of the location path, for example "classpath*:META-INF/*-beans.xml".
+ * In this case, the resolution strategy is fairly simple: a
+ * {@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.
+ *
+ * <p><b>Other notes:</b>
+ *
+ * <p><b>WARNING:</b> Note that "{@code classpath*:}" when combined with
+ * Ant-style patterns will only work reliably with at least one root directory
+ * before the pattern starts, unless the actual target files reside in the file
+ * system. This means that a pattern like "{@code classpath*:*.xml}" will
+ * <i>not</i> retrieve files from the root of jar files but rather only from the
+ * root of expanded directories. This originates from a limitation in the JDK's
+ * {@code ClassLoader.getResources()} method which only returns file system
+ * locations for a passed-in empty String (indicating potential roots to search).
+ *
+ * <p><b>WARNING:</b> Ant-style patterns with "classpath:" resources are not
+ * guaranteed to find matching resources if the root package to search is available
+ * in multiple class path locations. This is because a resource such as
+ * <pre class="code">
+ *     com/mycompany/package1/service-context.xml
+ * </pre>
+ * may be in only one location, but when a path such as
+ * <pre class="code">
+ *     classpath:com/mycompany/**&#47;service-context.xml
+ * </pre>
+ * is used to try to resolve it, the resolver will work off the (first) URL
+ * returned by {@code getResource("com/mycompany");}. If this base package
+ * node exists in multiple classloader locations, the actual end resource may
+ * not be underneath. Therefore, preferably, use "{@code classpath*:}" with the same
+ * Ant-style pattern in such a case, which will search <i>all</i> class path
+ * locations that contain the root package.
+ *
+ * @author Juergen Hoeller
+ * @author Colin Sampaleanu
+ * @author Marius Bogoevici
+ * @author Costin Leau
+ * @since 1.0.2
+ * @see ClassLoader#getResources(String)
+ */
+public final class PathMatchingResourcePatternResolver{
+
+    private static final Logger logger = Logger.getLogger(PathMatchingResourcePatternResolver.class.getName());
+    private static final java.lang.String CLASSPATH_ALL_URL_PREFIX = "classpath:";
+
+    private static Method equinoxResolveMethod;
+
+    static {
+        try {
+            // Detect Equinox OSGi (e.g. on WebSphere 6.1)
+            Class<?> fileLocatorClass = ClassUtils.forName("org.eclipse.core.runtime.FileLocator",
+                    PathMatchingResourcePatternResolver.class.getClassLoader());
+            equinoxResolveMethod = fileLocatorClass.getMethod("resolve", URL.class);
+            logger.finest("Found Equinox FileLocator for OSGi bundle URL resolution");
+        }
+        catch (Throwable ex) {
+            equinoxResolveMethod = null;
+        }
+    }
+
+
+    private final DefaultResourceLoader resourceLoader;
+
+    private AntPathMatcher pathMatcher = new AntPathMatcher();
+
+    private static Map<ClassLoader, PathMatchingResourcePatternResolver> resolvers = new ConcurrentHashMap<>();
+
+    public static PathMatchingResourcePatternResolver of(ClassLoader loader){
+        return resolvers.computeIfAbsent(loader, (cl) -> new PathMatchingResourcePatternResolver(cl));
+    }
+
+    /**
+     * Create a new PathMatchingResourcePatternResolver.
+     * <p>ClassLoader access will happen via the thread context class loader.
+     */
+    public PathMatchingResourcePatternResolver() {
+        this.resourceLoader = new DefaultResourceLoader();
+    }
+
+    /**
+     * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
+     * @param classLoader the ClassLoader to load classpath resources with,
+     * or {@code null} for using the thread context class loader
+     * at the time of actual resource access
+     * @see DefaultResourceLoader
+     */
+    public PathMatchingResourcePatternResolver(ClassLoader classLoader) {
+        this.resourceLoader = new DefaultResourceLoader(classLoader);
+    }
+
+    public ClassLoader getClassLoader() {
+        return resourceLoader.getClassLoader();
+    }
+
+    /**
+     * Return the PathMatcher that this resource pattern resolver uses.
+     */
+    public AntPathMatcher getPathMatcher() {
+        return this.pathMatcher;
+    }
+
+    public Resource getResource(String location) {
+        return resourceLoader.getResource(location);
+    }
+
+    public Resource[] getResources(String locationPattern) throws IOException {
+        Objects.requireNonNull(locationPattern, "Location pattern must not be null");
+        if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
+            // a class path resource (multiple resources for same name possible)
+            if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
+                // a class path resource pattern
+                return findPathMatchingResources(locationPattern);
+            }
+            else {
+                // all class path resources with the given name
+                return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
+            }
+        }
+        else {
+            // Only look for a pattern after a prefix here
+            // (to not get fooled by a pattern symbol in a strange prefix).
+            int prefixEnd = locationPattern.indexOf(":") + 1;
+            if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
+                // a file pattern
+                return findPathMatchingResources(locationPattern);
+            }
+            else {
+                // a single resource with the given name
+                return new Resource[] {this.resourceLoader.getResource(locationPattern)};
+            }
+        }
+    }
+
+    /**
+     * Find all class location resources with the given location via the ClassLoader.
+     * Delegates to {@link #doFindAllClassPathResources(String)}.
+     * @param location the absolute path within the classpath
+     * @return the result as Resource array
+     * @throws IOException in case of I/O errors
+     * @see java.lang.ClassLoader#getResources
+     * @see #convertClassLoaderURL
+     */
+    protected Resource[] findAllClassPathResources(String location) throws IOException {
+        String path = location;
+        if (path.startsWith("/")) {
+            path = path.substring(1);
+        }
+        Set<Resource> result = doFindAllClassPathResources(path);
+        return result.toArray(new Resource[result.size()]);
+    }
+
+    /**
+     * Find all class location resources with the given path via the ClassLoader.
+     * Called by {@link #findAllClassPathResources(String)}.
+     * @param path the absolute path within the classpath (never a leading slash)
+     * @return a mutable Set of matching Resource instances
+     */
+    protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
+        Set<Resource> result = new LinkedHashSet<Resource>(16);
+        ClassLoader cl = getClassLoader();
+        Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
+        while (resourceUrls.hasMoreElements()) {
+            URL url = resourceUrls.nextElement();
+            result.add(convertClassLoaderURL(url));
+        }
+        if ("".equals(path)) {
+            // The above result is likely to be incomplete, i.e. only containing file system references.
+            // We need to have pointers to each of the jar files on the classpath as well...
+            addAllClassLoaderJarRoots(cl, result);
+        }
+        return result;
+    }
+
+    /**
+     * Convert the given URL as returned from the ClassLoader into a {@link Resource}.
+     * <p>The default implementation simply creates a {@link UrlResource} instance.
+     * @param url a URL as returned from the ClassLoader
+     * @return the corresponding Resource object
+     * @see java.lang.ClassLoader#getResources
+     * @see Resource
+     */
+    protected Resource convertClassLoaderURL(URL url) {
+        return new UrlResource(url);
+    }
+
+    /**
+     * Search all {@link URLClassLoader} URLs for jar file references and add them to the
+     * given set of resources in the form of pointers to the root of the jar file content.
+     * @param classLoader the ClassLoader to search (including its ancestors)
+     * @param result the set of resources to add jar roots to
+     */
+    protected void addAllClassLoaderJarRoots(ClassLoader classLoader, Set<Resource> result) {
+        if (classLoader instanceof URLClassLoader) {
+            try {
+                for (URL url : ((URLClassLoader) classLoader).getURLs()) {
+                    if (ResourceUtils.isJarFileURL(url)) {
+                        try {
+                            UrlResource jarResource = new UrlResource(
+                                    ResourceUtils.JAR_URL_PREFIX + url.toString() + ResourceUtils.JAR_URL_SEPARATOR);
+                            if (jarResource.exists()) {
+                                result.add(jarResource);
+                            }
+                        }
+                        catch (MalformedURLException ex) {
+                            logger.finest(() -> "Cannot search for matching files underneath [" + url +
+                                    "] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
+                        }
+                    }
+                }
+            }
+            catch (Exception ex) {
+                logger.finest(() -> "Cannot introspect jar files since ClassLoader [" + classLoader +
+                        "] does not support 'getURLs()': " + ex);
+            }
+        }
+        if (classLoader != null) {
+            try {
+                addAllClassLoaderJarRoots(classLoader.getParent(), result);
+            }
+            catch (Exception ex) {
+                logger.finest(() -> "Cannot introspect jar files in parent ClassLoader since [" + classLoader +
+                        "] does not support 'getParent()': " + ex);
+            }
+        }
+    }
+
+    /**
+     * 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.
+     * @param locationPattern the location pattern to match
+     * @return the result as Resource array
+     * @throws IOException in case of I/O errors
+     * @see #doFindPathMatchingJarResources
+     * @see #doFindPathMatchingFileResources
+     */
+    protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
+        String rootDirPath = determineRootDir(locationPattern);
+        String subPattern = locationPattern.substring(rootDirPath.length());
+        Resource[] rootDirResources = getResources(rootDirPath);
+        Set<Resource> result = new LinkedHashSet<Resource>(16);
+        for (Resource rootDirResource : rootDirResources) {
+            rootDirResource = resolveRootDirResource(rootDirResource);
+            if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
+                result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));
+            }
+            else if (isJarResource(rootDirResource)) {
+                result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern));
+            }
+            else {
+                result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
+            }
+        }
+        logger.finest(() -> "Resolved location pattern [" + locationPattern + "] to resources " + result);
+        return result.toArray(new Resource[result.size()]);
+    }
+
+    /**
+     * Determine the root directory for the given location.
+     * <p>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.
+     * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
+     * for example.
+     * @param location the location to check
+     * @return the part of the location that denotes the root directory
+     * @see #retrieveMatchingFiles
+     */
+    protected String determineRootDir(String location) {
+        int prefixEnd = location.indexOf(":") + 1;
+        int rootDirEnd = location.length();
+        while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
+            rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
+        }
+        if (rootDirEnd == 0) {
+            rootDirEnd = prefixEnd;
+        }
+        return location.substring(0, rootDirEnd);
+    }
+
+    /**
+     * Resolve the specified resource for path matching.
+     * <p>The default implementation detects an Equinox OSGi "bundleresource:"
+     * / "bundleentry:" URL and resolves it into a standard jar file URL that
+     * can be traversed using Spring's standard jar file traversal algorithm.
+     * @param original the resource to resolve
+     * @return the resolved resource (may be identical to the passed-in resource)
+     * @throws IOException in case of resolution failure
+     */
+    protected Resource resolveRootDirResource(Resource original) throws IOException {
+        if (equinoxResolveMethod != null) {
+            URL url = original.getURL();
+            if (url.getProtocol().startsWith("bundle")) {
+                try {
+                    return new UrlResource((URL) equinoxResolveMethod.invoke(url));
+                } catch (Exception e) {
+                    ReflectionUtils.handleReflectionException(e);
+                }
+            }
+        }
+        return original;
+    }
+
+    /**
+     * Return whether the given resource handle indicates a jar resource
+     * that the {@code doFindPathMatchingJarResources} method can handle.
+     * <p>The default implementation checks against the URL protocols
+     * "jar", "zip" and "wsjar" (the latter are used by BEA WebLogic Server
+     * and IBM WebSphere, respectively, but can be treated like jar files).
+     * @param resource the resource handle to check
+     * (usually the root directory to start path matching from)
+     * @see #doFindPathMatchingJarResources
+     * @see ResourceUtils#isJarURL
+     */
+    protected boolean isJarResource(Resource resource) throws IOException {
+        return ResourceUtils.isJarURL(resource.getURL());
+    }
+
+    /**
+     * Find all resources in jar files that match the given location 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 java.net.JarURLConnection
+     */
+    protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, String subPattern)
+            throws IOException {
+
+        URLConnection con = rootDirResource.getURL().openConnection();
+        JarFile jarFile;
+        String jarFileUrl;
+        String rootEntryPath;
+        boolean newJarFile = false;
+
+        if (con instanceof JarURLConnection) {
+            // Should usually be the case for traditional JAR files.
+            JarURLConnection jarCon = (JarURLConnection) con;
+            ResourceUtils.useCachesIfNecessary(jarCon);
+            jarFile = jarCon.getJarFile();
+            jarFileUrl = jarCon.getJarFileURL().toExternalForm();
+            JarEntry jarEntry = jarCon.getJarEntry();
+            rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
+        }
+        else {
+            // No JarURLConnection -> need to resort to URL file parsing.
+            // We'll assume URLs of the format "jar:path!/entry", with the protocol
+            // being arbitrary as long as following the entry format.
+            // We'll also handle paths with and without leading "file:" prefix.
+            String urlFile = rootDirResource.getURL().getFile();
+            int separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
+            if (separatorIndex != -1) {
+                jarFileUrl = urlFile.substring(0, separatorIndex);
+                rootEntryPath = urlFile.substring(separatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
+                jarFile = getJarFile(jarFileUrl);
+            }
+            else {
+                jarFile = new JarFile(urlFile);
+                jarFileUrl = urlFile;
+                rootEntryPath = "";
+            }
+            newJarFile = true;
+        }
+
+        try {
+            logger.finest("Looking for matching resources in jar file [" + jarFileUrl + "]");
+            if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {
+                // Root entry path must end with slash to allow for proper matching.
+                // The Sun JRE does not return a slash here, but BEA JRockit does.
+                rootEntryPath = rootEntryPath + "/";
+            }
+            Set<Resource> result = new LinkedHashSet<Resource>(8);
+            for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
+                JarEntry entry = entries.nextElement();
+                String entryPath = entry.getName();
+                if (entryPath.startsWith(rootEntryPath)) {
+                    String relativePath = entryPath.substring(rootEntryPath.length());
+                    if (getPathMatcher().match(subPattern, relativePath)) {
+                        result.add(rootDirResource.createRelative(relativePath));
+                    }
+                }
+            }
+            return result;
+        }
+        finally {
+            // Close jar file, but only if freshly obtained -
+            // not from JarURLConnection, which might cache the file reference.
+            if (newJarFile) {
+                jarFile.close();
+            }
+        }
+    }
+
+    /**
+     * Resolve the given jar file URL into a JarFile object.
+     */
+    protected JarFile getJarFile(String jarFileUrl) throws IOException {
+        if (jarFileUrl.startsWith(ResourceUtils.FILE_URL_PREFIX)) {
+            try {
+                return new JarFile(ResourceUtils.toURI(jarFileUrl).getSchemeSpecificPart());
+            }
+            catch (URISyntaxException ex) {
+                // Fallback for URLs that are not valid URIs (should hardly ever happen).
+                return new JarFile(jarFileUrl.substring(ResourceUtils.FILE_URL_PREFIX.length()));
+            }
+        }
+        else {
+            return new JarFile(jarFileUrl);
+        }
+    }
+
+    /**
+     * Find all resources in the file system that match the given location 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
+     */
+    protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
+            throws IOException {
+
+        File rootDir;
+        try {
+            rootDir = rootDirResource.getFile().getAbsoluteFile();
+        }
+        catch (IOException ex) {
+            logger.log(Level.WARNING, ex, () -> "Cannot search for matching files underneath " + rootDirResource +
+                        " because it does not correspond to a directory in the file system");
+            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
+     */
+    protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
+        logger.finest(() -> "Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
+        Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
+        Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size());
+        for (File file : matchingFiles) {
+            result.add(new FileSystemResource(file));
+        }
+        return result;
+    }
+
+    /**
+     * Retrieve files that match the given path pattern,
+     * checking the given directory and its subdirectories.
+     * @param rootDir the directory to start from
+     * @param pattern the pattern to match against,
+     * relative to the root directory
+     * @return a mutable Set of matching Resource instances
+     * @throws IOException if directory contents could not be retrieved
+     */
+    protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
+        if (!rootDir.exists()) {
+            // Silently skip non-existing directories.
+            logger.finest(() -> "Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
+            return Collections.emptySet();
+        }
+        if (!rootDir.isDirectory()) {
+            // Complain louder if it exists but is no directory.
+            logger.log(Level.WARNING, () -> "Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
+            return Collections.emptySet();
+        }
+        if (!rootDir.canRead()) {
+            logger.log(Level.WARNING, () -> "Cannot 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<File> result = new LinkedHashSet<File>(8);
+        doRetrieveMatchingFiles(fullPattern, rootDir, result);
+        return result;
+    }
+
+    /**
+     * Recursively retrieve files that match the given pattern,
+     * adding them to the given result list.
+     * @param fullPattern the pattern to match against,
+     * with prepended root directory path
+     * @param dir the current directory
+     * @param result the Set of matching File instances to add to
+     * @throws IOException if directory contents could not be retrieved
+     */
+    protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
+        logger.finest(() -> "Searching directory [" + dir.getAbsolutePath() +
+                "] for files matching pattern [" + fullPattern + "]");
+        File[] dirContents = dir.listFiles();
+        if (dirContents == null) {
+            logger.log(Level.WARNING, () -> "Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
+            return;
+        }
+        for (File content : dirContents) {
+            String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
+            if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
+                if (!content.canRead()) {
+                   logger.finest(() -> "Skipping subdirectory [" + dir.getAbsolutePath() +
+                                "] because the application is not allowed to read the directory");
+                }
+                else {
+                    doRetrieveMatchingFiles(fullPattern, content, result);
+                }
+            }
+            if (getPathMatcher().match(fullPattern, currPath)) {
+                result.add(content);
+            }
+        }
+    }
+
+
+    /**
+     * Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime.
+     */
+    private static class VfsResourceMatchingDelegate {
+
+        public static Set<Resource> findMatchingResources(
+                Resource rootResource, String locationPattern, AntPathMatcher pathMatcher) throws IOException {
+            Object root = VfsUtils.getRoot(rootResource.getURL());
+            PatternVirtualFileVisitor visitor =
+                    new PatternVirtualFileVisitor(VfsUtils.getPath(root), locationPattern, pathMatcher);
+            VfsUtils.visit(root, visitor);
+            return visitor.getResources();
+        }
+    }
+
+
+    /**
+     * VFS visitor for path matching purposes.
+     */
+    @SuppressWarnings("unused")
+    private static class PatternVirtualFileVisitor implements InvocationHandler {
+
+        private final String subPattern;
+
+        private final AntPathMatcher pathMatcher;
+
+        private final String rootPath;
+
+        private final Set<Resource> resources = new LinkedHashSet<Resource>();
+
+        public PatternVirtualFileVisitor(String rootPath, String subPattern, AntPathMatcher pathMatcher) {
+            this.subPattern = subPattern;
+            this.pathMatcher = pathMatcher;
+            this.rootPath = (rootPath.length() == 0 || rootPath.endsWith("/") ? rootPath : rootPath + "/");
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+            String methodName = method.getName();
+            if (Object.class.equals(method.getDeclaringClass())) {
+                if (methodName.equals("equals")) {
+                    // Only consider equal when proxies are identical.
+                    return (proxy == args[0]);
+                }
+                else if (methodName.equals("hashCode")) {
+                    return System.identityHashCode(proxy);
+                }
+            }
+            else if ("getAttributes".equals(methodName)) {
+                return getAttributes();
+            }
+            else if ("visit".equals(methodName)) {
+                visit(args[0]);
+                return null;
+            }
+            else if ("toString".equals(methodName)) {
+                return toString();
+            }
+
+            throw new IllegalStateException("Unexpected method invocation: " + method);
+        }
+
+        public void visit(Object vfsResource) {
+            if (this.pathMatcher.match(this.subPattern,
+                    VfsUtils.getPath(vfsResource).substring(this.rootPath.length()))) {
+                this.resources.add(new VfsResource(vfsResource));
+            }
+        }
+
+        public Object getAttributes() {
+            return VfsUtils.getVisitorAttribute();
+        }
+
+        public Set<Resource> getResources() {
+            return this.resources;
+        }
+
+        public int size() {
+            return this.resources.size();
+        }
+
+        @Override
+        public String toString() {
+            return "sub-pattern: " + this.subPattern + ", resources: " + this.resources;
+        }
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/a55d1c97/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ReflectionUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ReflectionUtils.java b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ReflectionUtils.java
new file mode 100644
index 0000000..2bffcce
--- /dev/null
+++ b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ReflectionUtils.java
@@ -0,0 +1,191 @@
+/*
+* Copyright 2002-2014 the original author or authors.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package org.apache.tamaya.core.internal.resources.io;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+* Simple utility class for working with the reflection API and handling
+* reflection exceptions.
+*
+* <p>Only intended for internal use.
+*
+* @author Juergen Hoeller
+* @author Rob Harrop
+* @author Rod Johnson
+* @author Costin Leau
+* @author Sam Brannen
+* @author Chris Beams
+* @since 1.2.2
+*/
+public abstract class ReflectionUtils {
+	/**
+	 * Cache for {@link Class#getDeclaredMethods()}, allowing for fast resolution.
+	 */
+	private static final Map<Class<?>, Method[]> declaredMethodsCache =
+			new ConcurrentHashMap<>(256);
+
+
+	/**
+	 * Attempt to find a {@link Field field} on the supplied {@link Class} with the
+	 * supplied {@code name}. Searches all superclasses up to {@link Object}.
+	 * @param clazz the class to introspect
+	 * @param name the name of the field
+	 * @return the corresponding Field object, or {@code null} if not found
+	 */
+	public static Field findField(Class<?> clazz, String name) {
+		return findField(clazz, name, null);
+	}
+
+	/**
+	 * Attempt to find a {@link Field field} on the supplied {@link Class} with the
+	 * supplied {@code name} and/or {@link Class type}. Searches all superclasses
+	 * up to {@link Object}.
+	 * @param clazz the class to introspect
+	 * @param name the name of the field (may be {@code null} if type is specified)
+	 * @param type the type of the field (may be {@code null} if name is specified)
+	 * @return the corresponding Field object, or {@code null} if not found
+	 */
+	public static Field findField(Class<?> clazz, String name, Class<?> type) {
+		Objects.requireNonNull(clazz, "Class must not be null");
+		if(name == null && type == null) throw new IllegalArgumentException("Either name or type of the field must be specified");
+		Class<?> searchType = clazz;
+		while (!Object.class.equals(searchType) && searchType != null) {
+			Field[] fields = searchType.getDeclaredFields();
+			for (Field field : fields) {
+				if ((name == null || name.equals(field.getName())) && (type == null || type.equals(field.getType()))) {
+					return field;
+				}
+			}
+			searchType = searchType.getSuperclass();
+		}
+		return null;
+	}
+
+	/**
+	 * Attempt to find a {@link Method} on the supplied class with the supplied name
+	 * and no parameters. Searches all superclasses up to {@code Object}.
+	 * <p>Returns {@code null} if no {@link Method} can be found.
+	 * @param clazz the class to introspect
+	 * @param name the name of the method
+	 * @return the Method object, or {@code null} if none found
+	 */
+	public static Method findMethod(Class<?> clazz, String name) {
+		return findMethod(clazz, name, new Class<?>[0]);
+	}
+
+	/**
+	 * Attempt to find a {@link Method} on the supplied class with the supplied name
+	 * and parameter types. Searches all superclasses up to {@code Object}.
+	 * <p>Returns {@code null} if no {@link Method} can be found.
+	 * @param clazz the class to introspect
+	 * @param name the name of the method
+	 * @param paramTypes the parameter types of the method
+	 * (may be {@code null} to indicate any signature)
+	 * @return the Method object, or {@code null} if none found
+	 */
+	public static Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
+		Objects.requireNonNull(clazz, "Class must not be null");
+		Objects.requireNonNull(name, "Method name must not be null");
+		Class<?> searchType = clazz;
+		while (searchType != null) {
+			Method[] methods = (searchType.isInterface() ? searchType.getMethods() : getDeclaredMethods(searchType));
+			for (Method method : methods) {
+				if (name.equals(method.getName()) &&
+						(paramTypes == null || Arrays.equals(paramTypes, method.getParameterTypes()))) {
+					return method;
+				}
+			}
+			searchType = searchType.getSuperclass();
+		}
+		return null;
+	}
+
+	/**
+	 * Handle the given reflection exception. Should only be called if no
+	 * checked exception is expected to be thrown by the target method.
+	 * <p>Throws the underlying RuntimeException or Error in case of an
+	 * InvocationTargetException with such a root cause. Throws an
+	 * IllegalStateException with an appropriate message else.
+	 * @param ex the reflection exception to handle
+	 */
+	public static void handleReflectionException(Exception ex) {
+		if (ex instanceof NoSuchMethodException) {
+			throw new IllegalStateException("Method not found: " + ex.getMessage());
+		}
+		if (ex instanceof IllegalAccessException) {
+			throw new IllegalStateException("Could not access method: " + ex.getMessage());
+		}
+		if (ex instanceof InvocationTargetException) {
+			handleInvocationTargetException((InvocationTargetException) ex);
+		}
+		if (ex instanceof RuntimeException) {
+			throw (RuntimeException) ex;
+		}
+		throw new UndeclaredThrowableException(ex);
+	}
+
+	/**
+	 * Handle the given invocation target exception. Should only be called if no
+	 * checked exception is expected to be thrown by the target method.
+	 * <p>Throws the underlying RuntimeException or Error in case of such a root
+	 * cause. Throws an IllegalStateException else.
+	 * @param ex the invocation target exception to handle
+	 */
+	public static void handleInvocationTargetException(InvocationTargetException ex) {
+		rethrowRuntimeException(ex.getTargetException());
+	}
+
+	/**
+	 * Rethrow the given {@link Throwable exception}, which is presumably the
+	 * <em>target exception</em> of an {@link InvocationTargetException}. Should
+	 * only be called if no checked exception is expected to be thrown by the
+	 * target method.
+	 * <p>Rethrows the underlying exception cast to an {@link RuntimeException} or
+	 * {@link Error} if appropriate; otherwise, throws an
+	 * {@link IllegalStateException}.
+	 * @param ex the exception to rethrow
+	 * @throws RuntimeException the rethrown exception
+	 */
+	public static void rethrowRuntimeException(Throwable ex) {
+		if (ex instanceof RuntimeException) {
+			throw (RuntimeException) ex;
+		}
+		if (ex instanceof Error) {
+			throw (Error) ex;
+		}
+		throw new UndeclaredThrowableException(ex);
+	}
+
+	/**
+	 * This method retrieves {@link Class#getDeclaredMethods()} from a local cache
+	 * in order to avoid the JVM's SecurityManager check and defensive array copying.
+	 */
+	private static Method[] getDeclaredMethods(Class<?> clazz) {
+		Method[] result = declaredMethodsCache.get(clazz);
+		if (result == null) {
+			result = clazz.getDeclaredMethods();
+			declaredMethodsCache.put(clazz, result);
+		}
+		return result;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/a55d1c97/core/src/main/java/org/apache/tamaya/core/internal/resources/io/Resource.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resources/io/Resource.java b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/Resource.java
new file mode 100644
index 0000000..696412e
--- /dev/null
+++ b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/Resource.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tamaya.core.internal.resources.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.function.Supplier;
+
+/**
+ * Interface for a resource descriptor that abstracts from the actual
+ * type of underlying resource, such as a file or class path resource.
+ *
+ * <p>An InputStream can be opened for every resource if it exists in
+ * physical form, but a URL or File handle can just be returned for
+ * certain resources. The actual behavior is implementation-specific.
+ *
+ * @author Juergen Hoeller
+ * @since 28.12.2003
+ * @see #getInputStream()
+ * @see #getURL()
+ * @see #getURI()
+ * @see #getFile()
+ * @see FileSystemResource
+ * @see ClassPathResource
+ * @see UrlResource
+ * @see ByteArrayResource
+ * @see InputStreamResource
+ * @see PathResource
+ */
+public interface Resource extends InputStreamSource {
+
+	/**
+	 * Return whether this resource actually exists in physical form.
+	 * <p>This method performs a definitive existence check, whereas the
+	 * existence of a {@code Resource} handle only guarantees a
+	 * valid descriptor handle.
+	 */
+	boolean exists();
+
+	/**
+	 * Return whether the contents of this resource can be read,
+	 * e.g. via {@link #getInputStream()} or {@link #getFile()}.
+	 * <p>Will be {@code true} for typical resource descriptors;
+	 * note that actual content reading may still fail when attempted.
+	 * However, a value of {@code false} is a definitive indication
+	 * that the resource content cannot be read.
+	 * @see #getInputStream
+	 */
+	boolean isReadable();
+
+	/**
+	 * Return whether this resource represents a handle with an open
+	 * stream. If true, the InputStream cannot be read multiple times,
+	 * and must be read and closed to avoid resource leaks.
+	 * <p>Will be {@code false} for typical resource descriptors.
+	 */
+	boolean isOpen();
+
+	/**
+	 * Return a URL handle for this resource.
+	 * @throws IOException if the resource cannot be resolved as URL,
+	 * i.e. if the resource is not available as descriptor
+	 */
+	URL getURL() throws IOException;
+
+	/**
+	 * Return a URI handle for this resource.
+	 * @throws IOException if the resource cannot be resolved as URI,
+	 * i.e. if the resource is not available as descriptor
+	 */
+	URI getURI() throws IOException;
+
+	/**
+	 * Return a File handle for this resource.
+	 * @throws IOException if the resource cannot be resolved as absolute
+	 * file path, i.e. if the resource is not available in a file system
+	 */
+	File getFile() throws IOException;
+
+	/**
+	 * Determine the content length for this resource.
+	 * @throws IOException if the resource cannot be resolved
+	 * (in the file system or as some other known physical resource type)
+	 */
+	long contentLength() throws IOException;
+
+	/**
+	 * Determine the last-modified timestamp for this resource.
+	 * @throws IOException if the resource cannot be resolved
+	 * (in the file system or as some other known physical resource type)
+	 */
+	long lastModified() throws IOException;
+
+	/**
+	 * Create a resource relative to this resource.
+	 * @param relativePath the relative path (relative to this resource)
+	 * @return the resource handle for the relative resource
+	 * @throws IOException if the relative resource cannot be determined
+	 */
+	Resource createRelative(String relativePath) throws IOException;
+
+	/**
+	 * Determine a filename for this resource, i.e. typically the last
+	 * part of the path: for example, "myfile.txt".
+	 * <p>Returns {@code null} if this type of resource does not
+	 * have a filename.
+	 */
+	String getFilename();
+
+	/**
+	 * Return a description for this resource,
+	 * to be used for error output when working with the resource.
+	 * <p>Implementations are also encouraged to return this value
+	 * from their {@code toString} method.
+	 * @see Object#toString()
+	 */
+	String getDescription();
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-tamaya/blob/a55d1c97/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ResourceUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ResourceUtils.java b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ResourceUtils.java
new file mode 100644
index 0000000..6835ffa
--- /dev/null
+++ b/core/src/main/java/org/apache/tamaya/core/internal/resources/io/ResourceUtils.java
@@ -0,0 +1,350 @@
+/*
+* Copyright 2002-2014 the original author or authors.
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package org.apache.tamaya.core.internal.resources.io;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Objects;
+
+/**
+* Utility methods for resolving resource locations to files in the
+* file system. Mainly for internal use within the framework.
+*
+* <p>Consider using Spring's Resource abstraction in the core package
+* for handling all kinds of file resources in a uniform manner.
+* {@code org.springframework.core.io.ResourceLoader}'s {@code getResource()}
+* method can resolve any location to a {@code org.springframework.core.io.Resource}
+* object, which in turn allows one to obtain a {@code java.io.File} in the
+* file system through its {@code getFile()} method.
+*
+* <p>The main reason for these utility methods for resource location handling
+* is to support {@code Log4jConfigurer}, which must be able to resolve
+* resource locations <i>before the logging system has been initialized</i>.
+* Spring's {@code Resource} abstraction in the core package, on the other hand,
+* already expects the logging system to be available.
+*
+* @author Juergen Hoeller
+* @since 1.1.5
+*/
+public abstract class ResourceUtils {
+
+	/** URL prefix for loading from the file system: "file:" */
+	public static final String FILE_URL_PREFIX = "file:";
+
+	/** URL prefix for loading from the file system: "jar:" */
+	public static final String JAR_URL_PREFIX = "jar:";
+
+	/** URL protocol for a file in the file system: "file" */
+	public static final String URL_PROTOCOL_FILE = "file";
+
+	/** URL protocol for an entry from a jar file: "jar" */
+	public static final String URL_PROTOCOL_JAR = "jar";
+
+	/** URL protocol for an entry from a zip file: "zip" */
+	public static final String URL_PROTOCOL_ZIP = "zip";
+
+	/** URL protocol for an entry from a WebSphere jar file: "wsjar" */
+	public static final String URL_PROTOCOL_WSJAR = "wsjar";
+
+	/** URL protocol for an entry from a JBoss jar file: "vfszip" */
+	public static final String URL_PROTOCOL_VFSZIP = "vfszip";
+
+	/** URL protocol for a JBoss file system resource: "vfsfile" */
+	public static final String URL_PROTOCOL_VFSFILE = "vfsfile";
+
+	/** URL protocol for a general JBoss VFS resource: "vfs" */
+	public static final String URL_PROTOCOL_VFS = "vfs";
+
+	/** File extension for a regular jar file: ".jar" */
+	public static final String JAR_FILE_EXTENSION = ".jar";
+
+	/** Separator between JAR URL and file path within the JAR: "!/" */
+	public static final String JAR_URL_SEPARATOR = "!/";
+
+
+	/**
+	 * Return whether the given resource location is a URL:
+	 * either a special "classpath" pseudo URL or a standard URL.
+	 * @param resourceLocation the location String to check
+	 * @return whether the location qualifies as a URL
+	 * @see DefaultResourceLoader#CLASSPATH_URL_PREFIX
+	 * @see java.net.URL
+	 */
+	public static boolean isUrl(String resourceLocation) {
+		if (resourceLocation == null) {
+			return false;
+		}
+		if (resourceLocation.startsWith(DefaultResourceLoader.CLASSPATH_URL_PREFIX)) {
+			return true;
+		}
+		try {
+			new URL(resourceLocation);
+			return true;
+		}
+		catch (MalformedURLException ex) {
+			return false;
+		}
+	}
+
+	/**
+	 * Resolve the given resource location to a {@code java.net.URL}.
+	 * <p>Does not check whether the URL actually exists; simply returns
+	 * the URL that the given location would correspond to.
+	 * @param resourceLocation the resource location to resolve: either a
+	 * "classpath:" pseudo URL, a "file:" URL, or a plain file path
+	 * @return a corresponding URL object
+	 * @throws FileNotFoundException if the resource cannot be resolved to a URL
+	 */
+	public static URL getURL(String resourceLocation) throws FileNotFoundException {
+		Objects.requireNonNull(resourceLocation, "Resource location must not be null");
+		if (resourceLocation.startsWith(DefaultResourceLoader.CLASSPATH_URL_PREFIX)) {
+			String path = resourceLocation.substring(DefaultResourceLoader.CLASSPATH_URL_PREFIX.length());
+			ClassLoader cl = ClassUtils.getDefaultClassLoader();
+			URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path));
+			if (url == null) {
+				String description = "class path resource [" + path + "]";
+				throw new FileNotFoundException(description +
+						" cannot be resolved to URL because it does not exist");
+			}
+			return url;
+		}
+		try {
+			// try URL
+			return new URL(resourceLocation);
+		}
+		catch (MalformedURLException ex) {
+			// no URL -> treat as file path
+			try {
+				return new File(resourceLocation).toURI().toURL();
+			}
+			catch (MalformedURLException ex2) {
+				throw new FileNotFoundException("Resource location [" + resourceLocation +
+						"] is neither a URL not a well-formed file path");
+			}
+		}
+	}
+
+	/**
+	 * Resolve the given resource location to a {@code java.io.File},
+	 * i.e. to a file in the file system.
+	 * <p>Does not check whether the file actually exists; simply returns
+	 * the File that the given location would correspond to.
+	 * @param resourceLocation the resource location to resolve: either a
+	 * "classpath:" pseudo URL, a "file:" URL, or a plain file path
+	 * @return a corresponding File object
+	 * @throws FileNotFoundException if the resource cannot be resolved to
+	 * a file in the file system
+	 */
+	public static File getFile(String resourceLocation) throws FileNotFoundException {
+		Objects.requireNonNull(resourceLocation, "Resource location must not be null");
+		if (resourceLocation.startsWith(DefaultResourceLoader.CLASSPATH_URL_PREFIX)) {
+			String path = resourceLocation.substring(DefaultResourceLoader.CLASSPATH_URL_PREFIX.length());
+			String description = "class path resource [" + path + "]";
+			ClassLoader cl = ClassUtils.getDefaultClassLoader();
+			URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path));
+			if (url == null) {
+				throw new FileNotFoundException(description +
+						" cannot be resolved to absolute file path because it does not exist");
+			}
+			return getFile(url, description);
+		}
+		try {
+			// try URL
+			return getFile(new URL(resourceLocation));
+		}
+		catch (MalformedURLException ex) {
+			// no URL -> treat as file path
+			return new File(resourceLocation);
+		}
+	}
+
+	/**
+	 * Resolve the given resource URL to a {@code java.io.File},
+	 * i.e. to a file in the file system.
+	 * @param resourceUrl the resource URL to resolve
+	 * @return a corresponding File object
+	 * @throws FileNotFoundException if the URL cannot be resolved to
+	 * a file in the file system
+	 */
+	public static File getFile(URL resourceUrl) throws FileNotFoundException {
+		return getFile(resourceUrl, "URL");
+	}
+
+	/**
+	 * Resolve the given resource URL to a {@code java.io.File},
+	 * i.e. to a file in the file system.
+	 * @param resourceUrl the resource URL to resolve
+	 * @param description a description of the original resource that
+	 * the URL was created for (for example, a class path location)
+	 * @return a corresponding File object
+	 * @throws FileNotFoundException if the URL cannot be resolved to
+	 * a file in the file system
+	 */
+	public static File getFile(URL resourceUrl, String description) throws FileNotFoundException {
+		Objects.requireNonNull(resourceUrl, "Resource URL must not be null");
+		if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) {
+			throw new FileNotFoundException(
+					description + " cannot be resolved to absolute file path " +
+					"because it does not reside in the file system: " + resourceUrl);
+		}
+		try {
+			return new File(toURI(resourceUrl).getSchemeSpecificPart());
+		}
+		catch (URISyntaxException ex) {
+			// Fallback for URLs that are not valid URIs (should hardly ever happen).
+			return new File(resourceUrl.getFile());
+		}
+	}
+
+	/**
+	 * Resolve the given resource URI to a {@code java.io.File},
+	 * i.e. to a file in the file system.
+	 * @param resourceUri the resource URI to resolve
+	 * @return a corresponding File object
+	 * @throws FileNotFoundException if the URL cannot be resolved to
+	 * a file in the file system
+	 */
+	public static File getFile(URI resourceUri) throws FileNotFoundException {
+		return getFile(resourceUri, "URI");
+	}
+
+	/**
+	 * Resolve the given resource URI to a {@code java.io.File},
+	 * i.e. to a file in the file system.
+	 * @param resourceUri the resource URI to resolve
+	 * @param description a description of the original resource that
+	 * the URI was created for (for example, a class path location)
+	 * @return a corresponding File object
+	 * @throws FileNotFoundException if the URL cannot be resolved to
+	 * a file in the file system
+	 */
+	public static File getFile(URI resourceUri, String description) throws FileNotFoundException {
+		Objects.requireNonNull(resourceUri, "Resource URI must not be null");
+		if (!URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) {
+			throw new FileNotFoundException(
+					description + " cannot be resolved to absolute file path " +
+					"because it does not reside in the file system: " + resourceUri);
+		}
+		return new File(resourceUri.getSchemeSpecificPart());
+	}
+
+	/**
+	 * Determine whether the given URL points to a resource in the file system,
+	 * that is, has protocol "file", "vfsfile" or "vfs".
+	 * @param url the URL to check
+	 * @return whether the URL has been identified as a file system URL
+	 */
+	public static boolean isFileURL(URL url) {
+		String protocol = url.getProtocol();
+		return (URL_PROTOCOL_FILE.equals(protocol) || URL_PROTOCOL_VFSFILE.equals(protocol) ||
+				URL_PROTOCOL_VFS.equals(protocol));
+	}
+
+	/**
+	 * Determine whether the given URL points to a resource in a jar file,
+	 * that is, has protocol "jar", "zip", "vfszip" or "wsjar".
+	 * @param url the URL to check
+	 * @return whether the URL has been identified as a JAR URL
+	 */
+	public static boolean isJarURL(URL url) {
+		String protocol = url.getProtocol();
+		return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_ZIP.equals(protocol) ||
+				URL_PROTOCOL_VFSZIP.equals(protocol) || URL_PROTOCOL_WSJAR.equals(protocol));
+	}
+
+	/**
+	 * Determine whether the given URL points to a jar file itself,
+	 * that is, has protocol "file" and ends with the ".jar" extension.
+	 * @param url the URL to check
+	 * @return whether the URL has been identified as a JAR file URL
+	 * @since 4.1
+	 */
+	public static boolean isJarFileURL(URL url) {
+		return (URL_PROTOCOL_FILE.equals(url.getProtocol()) &&
+				url.getPath().toLowerCase().endsWith(JAR_FILE_EXTENSION));
+	}
+
+	/**
+	 * Extract the URL for the actual jar file from the given URL
+	 * (which may point to a resource in a jar file or to a jar file itself).
+	 * @param jarUrl the original URL
+	 * @return the URL for the actual jar file
+	 * @throws MalformedURLException if no valid jar file URL could be extracted
+	 */
+	public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException {
+		String urlFile = jarUrl.getFile();
+		int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR);
+		if (separatorIndex != -1) {
+			String jarFile = urlFile.substring(0, separatorIndex);
+			try {
+				return new URL(jarFile);
+			}
+			catch (MalformedURLException ex) {
+				// Probably no protocol in original jar URL, like "jar:C:/mypath/myjar.jar".
+				// This usually indicates that the jar file resides in the file system.
+				if (!jarFile.startsWith("/")) {
+					jarFile = "/" + jarFile;
+				}
+				return new URL(FILE_URL_PREFIX + jarFile);
+			}
+		}
+		else {
+			return jarUrl;
+		}
+	}
+
+	/**
+	 * Create a URI instance for the given URL,
+	 * replacing spaces with "%20" URI encoding first.
+	 * <p>Furthermore, this method works on JDK 1.4 as well,
+	 * in contrast to the {@code URL.toURI()} method.
+	 * @param url the URL to convert into a URI instance
+	 * @return the URI instance
+	 * @throws URISyntaxException if the URL wasn't a valid URI
+	 * @see java.net.URL#toURI()
+	 */
+	public static URI toURI(URL url) throws URISyntaxException {
+		return toURI(url.toString());
+	}
+
+	/**
+	 * Create a URI instance for the given location String,
+	 * replacing spaces with "%20" URI encoding first.
+	 * @param location the location String to convert into a URI instance
+	 * @return the URI instance
+	 * @throws URISyntaxException if the location wasn't a valid URI
+	 */
+	public static URI toURI(String location) throws URISyntaxException {
+		return new URI(location.replaceAll(" ", "%20"));
+	}
+
+	/**
+	 * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the
+	 * given connection, preferring {@code false} but leaving the
+	 * flag at {@code true} for JNLP based resources.
+	 * @param con the URLConnection to set the flag on
+	 */
+	public static void useCachesIfNecessary(URLConnection con) {
+		con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP"));
+	}
+
+}