struts-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From lukaszlen...@apache.org
Subject [33/57] [partial] struts git commit: Merges xwork packages into struts
Date Wed, 17 Jun 2015 21:09:33 GMT
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/finder/ResourceFinder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/finder/ResourceFinder.java b/core/src/main/java/com/opensymphony/xwork2/util/finder/ResourceFinder.java
new file mode 100644
index 0000000..c7f9fd1
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/finder/ResourceFinder.java
@@ -0,0 +1,1124 @@
+/*
+ * Copyright 2002-2003,2009 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.finder;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.*;
+import java.util.*;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * @author David Blevins
+ * @version $Rev$ $Date$
+ */
+public class ResourceFinder {
+    private static final Logger LOG = LogManager.getLogger(ResourceFinder.class);
+
+    private final URL[] urls;
+    private final String path;
+    private final ClassLoaderInterface classLoaderInterface;
+    private final List<String> resourcesNotLoaded = new ArrayList<>();
+
+    public ResourceFinder(URL... urls) {
+        this(null, new ClassLoaderInterfaceDelegate(Thread.currentThread().getContextClassLoader()), urls);
+    }
+
+    public ResourceFinder(String path) {
+        this(path, new ClassLoaderInterfaceDelegate(Thread.currentThread().getContextClassLoader()), null);
+    }
+
+    public ResourceFinder(String path, URL... urls) {
+        this(path, new ClassLoaderInterfaceDelegate(Thread.currentThread().getContextClassLoader()), urls);
+    }
+
+    public ResourceFinder(String path, ClassLoaderInterface classLoaderInterface) {
+        this(path, classLoaderInterface, null);
+    }
+
+    public ResourceFinder(String path, ClassLoaderInterface classLoaderInterface, URL... urls) {
+        path = StringUtils.trimToEmpty(path);
+        if (!StringUtils.endsWith(path, "/")) {
+            path += "/";
+        }
+        this.path = path;
+
+        this.classLoaderInterface = classLoaderInterface == null ? new ClassLoaderInterfaceDelegate(Thread.currentThread().getContextClassLoader()) : classLoaderInterface ;
+
+        for (int i = 0; urls != null && i < urls.length; i++) {
+            URL url = urls[i];
+            if (url == null || isDirectory(url) || "jar".equals(url.getProtocol())) {
+                continue;
+            }
+            try {
+                urls[i] = new URL("jar", "", -1, url.toString() + "!/");
+            } catch (MalformedURLException e) {
+            }
+        }
+        this.urls = (urls == null || urls.length == 0)? null : urls;
+    }
+
+    private static boolean isDirectory(URL url) {
+        String file = url.getFile();
+        return (file.length() > 0 && file.charAt(file.length() - 1) == '/');
+    }
+
+    /**
+     * Returns a list of resources that could not be loaded in the last invoked findAvailable* or
+     * mapAvailable* methods.
+     * <p/>
+     * The list will only contain entries of resources that match the requirements
+     * of the last invoked findAvailable* or mapAvailable* methods, but were unable to be
+     * loaded and included in their results.
+     * <p/>
+     * The list returned is unmodifiable and the results of this method will change
+     * after each invocation of a findAvailable* or mapAvailable* methods.
+     * <p/>
+     * This method is not thread safe.
+     */
+    public List<String> getResourcesNotLoaded() {
+        return Collections.unmodifiableList(resourcesNotLoaded);
+    }
+
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+    //
+    //   Find
+    //
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+    public URL find(String uri) throws IOException {
+        String fullUri = path + uri;
+
+        return getResource(fullUri);
+    }
+
+    public List<URL> findAll(String uri) throws IOException {
+        String fullUri = path + uri;
+
+        Enumeration<URL> resources = getResources(fullUri);
+        List<URL> list = new ArrayList<>();
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            list.add(url);
+        }
+        return list;
+    }
+
+
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+    //
+    //   Find String
+    //
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+    /**
+     * Reads the contents of the URL as a {@link String}'s and returns it.
+     *
+     * @param uri
+     * @return a stringified content of a resource
+     * @throws IOException if a resource pointed out by the uri param could not be find
+     * @see ClassLoader#getResource(String)
+     */
+    public String findString(String uri) throws IOException {
+        String fullUri = path + uri;
+
+        URL resource = getResource(fullUri);
+        if (resource == null) {
+            throw new IOException("Could not find a resource in : " + fullUri);
+        }
+
+        return readContents(resource);
+    }
+
+    /**
+     * Reads the contents of the found URLs as a list of {@link String}'s and returns them.
+     *
+     * @param uri
+     * @return a list of the content of each resource URL found
+     * @throws IOException if any of the found URLs are unable to be read.
+     */
+    public List<String> findAllStrings(String uri) throws IOException {
+        String fulluri = path + uri;
+
+        List<String> strings = new ArrayList<>();
+
+        Enumeration<URL> resources = getResources(fulluri);
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            String string = readContents(url);
+            strings.add(string);
+        }
+        return strings;
+    }
+
+    /**
+     * Reads the contents of the found URLs as a Strings and returns them.
+     * Individual URLs that cannot be read are skipped and added to the
+     * list of 'resourcesNotLoaded'
+     *
+     * @param uri
+     * @return a list of the content of each resource URL found
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public List<String> findAvailableStrings(String uri) throws IOException {
+        resourcesNotLoaded.clear();
+        String fulluri = path + uri;
+
+        List<String> strings = new ArrayList<>();
+
+        Enumeration<URL> resources = getResources(fulluri);
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            try {
+                String string = readContents(url);
+                strings.add(string);
+            } catch (IOException notAvailable) {
+                resourcesNotLoaded.add(url.toExternalForm());
+            }
+        }
+        return strings;
+    }
+
+    /**
+     * Reads the contents of all non-directory URLs immediately under the specified
+     * location and returns them in a map keyed by the file name.
+     * <p/>
+     * Any URLs that cannot be read will cause an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/serializables/one
+     * META-INF/serializables/two
+     * META-INF/serializables/three
+     * META-INF/serializables/four/foo.txt
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Map map = finder.mapAvailableStrings("serializables");
+     * map.contains("one");  // true
+     * map.contains("two");  // true
+     * map.contains("three");  // true
+     * map.contains("four");  // false
+     *
+     * @param uri
+     * @return a list of the content of each resource URL found
+     * @throws IOException if any of the urls cannot be read
+     */
+    public Map<String, String> mapAllStrings(String uri) throws IOException {
+        Map<String, String> strings = new HashMap<>();
+        Map<String, URL> resourcesMap = getResourcesMap(uri);
+        for (Map.Entry<String, URL> entry : resourcesMap.entrySet()) {
+            String name = entry.getKey();
+            URL url = entry.getValue();
+            String value = readContents(url);
+            strings.put(name, value);
+        }
+        return strings;
+    }
+
+    /**
+     * Reads the contents of all non-directory URLs immediately under the specified
+     * location and returns them in a map keyed by the file name.
+     * <p/>
+     * Individual URLs that cannot be read are skipped and added to the
+     * list of 'resourcesNotLoaded'
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/serializables/one
+     * META-INF/serializables/two      # not readable
+     * META-INF/serializables/three
+     * META-INF/serializables/four/foo.txt
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Map map = finder.mapAvailableStrings("serializables");
+     * map.contains("one");  // true
+     * map.contains("two");  // false
+     * map.contains("three");  // true
+     * map.contains("four");  // false
+     *
+     * @param uri
+     * @return a list of the content of each resource URL found
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public Map<String, String> mapAvailableStrings(String uri) throws IOException {
+        resourcesNotLoaded.clear();
+        Map<String, String> strings = new HashMap<>();
+        Map<String, URL> resourcesMap = getResourcesMap(uri);
+        for (Map.Entry<String, URL> entry  : resourcesMap.entrySet()) {
+            String name = entry.getKey();
+            URL url = entry.getValue();
+            try {
+                String value = readContents(url);
+                strings.put(name, value);
+            } catch (IOException notAvailable) {
+                resourcesNotLoaded.add(url.toExternalForm());
+            }
+        }
+        return strings;
+    }
+
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+    //
+    //   Find Class
+    //
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+    /**
+     * Executes {@link #findString(String)} assuming the contents URL found is the name of
+     * a class that should be loaded and returned.
+     *
+     * @param uri
+     * @return
+     * @throws IOException
+     * @throws ClassNotFoundException
+     */
+    public Class findClass(String uri) throws IOException, ClassNotFoundException {
+        String className = findString(uri);
+        return (Class) classLoaderInterface.loadClass(className);
+    }
+
+    /**
+     * Executes findAllStrings assuming the strings are
+     * the names of a classes that should be loaded and returned.
+     * <p/>
+     * Any URL or class that cannot be loaded will cause an exception to be thrown.
+     *
+     * @param uri
+     * @return
+     * @throws IOException
+     * @throws ClassNotFoundException
+     */
+    public List<Class> findAllClasses(String uri) throws IOException, ClassNotFoundException {
+        List<Class> classes = new ArrayList<>();
+        List<String> strings = findAllStrings(uri);
+        for (String className : strings) {
+            Class clazz = classLoaderInterface.loadClass(className);
+            classes.add(clazz);
+        }
+        return classes;
+    }
+
+    /**
+     * Executes findAvailableStrings assuming the strings are
+     * the names of a classes that should be loaded and returned.
+     * <p/>
+     * Any class that cannot be loaded will be skipped and placed in the
+     * 'resourcesNotLoaded' collection.
+     *
+     * @param uri
+     * @return
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public List<Class> findAvailableClasses(String uri) throws IOException {
+        resourcesNotLoaded.clear();
+        List<Class> classes = new ArrayList<>();
+        List<String> strings = findAvailableStrings(uri);
+        for (String className : strings) {
+            try {
+                Class clazz = classLoaderInterface.loadClass(className);
+                classes.add(clazz);
+            } catch (Exception notAvailable) {
+                resourcesNotLoaded.add(className);
+            }
+        }
+        return classes;
+    }
+
+    /**
+     * Executes mapAllStrings assuming the value of each entry in the
+     * map is the name of a class that should be loaded.
+     * <p/>
+     * Any class that cannot be loaded will be cause an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/xmlparsers/xerces
+     * META-INF/xmlparsers/crimson
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Map map = finder.mapAvailableStrings("xmlparsers");
+     * map.contains("xerces");  // true
+     * map.contains("crimson");  // true
+     * Class xercesClass = map.get("xerces");
+     * Class crimsonClass = map.get("crimson");
+     *
+     * @param uri
+     * @return
+     * @throws IOException
+     * @throws ClassNotFoundException
+     */
+    public Map<String, Class> mapAllClasses(String uri) throws IOException, ClassNotFoundException {
+        Map<String, Class> classes = new HashMap<>();
+        Map<String, String> map = mapAllStrings(uri);
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            String string = entry.getKey();
+            String className = entry.getValue();
+            Class clazz = classLoaderInterface.loadClass(className);
+            classes.put(string, clazz);
+        }
+        return classes;
+    }
+
+    /**
+     * Executes mapAvailableStrings assuming the value of each entry in the
+     * map is the name of a class that should be loaded.
+     * <p/>
+     * Any class that cannot be loaded will be skipped and placed in the
+     * 'resourcesNotLoaded' collection.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/xmlparsers/xerces
+     * META-INF/xmlparsers/crimson
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Map map = finder.mapAvailableStrings("xmlparsers");
+     * map.contains("xerces");  // true
+     * map.contains("crimson");  // true
+     * Class xercesClass = map.get("xerces");
+     * Class crimsonClass = map.get("crimson");
+     *
+     * @param uri
+     * @return
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public Map<String, Class> mapAvailableClasses(String uri) throws IOException {
+        resourcesNotLoaded.clear();
+        Map<String, Class> classes = new HashMap<>();
+        Map<String, String> map = mapAvailableStrings(uri);
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            String string = entry.getKey();
+            String className = entry.getValue();
+            try {
+                Class clazz = classLoaderInterface.loadClass(className);
+                classes.put(string, clazz);
+            } catch (Exception notAvailable) {
+                resourcesNotLoaded.add(className);
+            }
+        }
+        return classes;
+    }
+
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+    //
+    //   Find Implementation
+    //
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+    /**
+     * Assumes the class specified points to a file in the classpath that contains
+     * the name of a class that implements or is a subclass of the specfied class.
+     * <p/>
+     * Any class that cannot be loaded will be cause an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
+     * META-INF/java.io.OutputStream
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Class clazz = finder.findImplementation(java.io.InputStream.class);
+     * clazz.getName();  // returns "org.acme.AcmeInputStream"
+     *
+     * @param interfase a superclass or interface
+     * @return
+     * @throws IOException            if the URL cannot be read
+     * @throws ClassNotFoundException if the class found is not loadable
+     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
+     */
+    public Class findImplementation(Class interfase) throws IOException, ClassNotFoundException {
+        String className = findString(interfase.getName());
+        Class impl = classLoaderInterface.loadClass(className);
+        if (!interfase.isAssignableFrom(impl)) {
+            throw new ClassCastException("Class not of type: " + interfase.getName());
+        }
+        return impl;
+    }
+
+    /**
+     * Assumes the class specified points to a file in the classpath that contains
+     * the name of a class that implements or is a subclass of the specfied class.
+     * <p/>
+     * Any class that cannot be loaded or assigned to the specified interface will be cause
+     * an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
+     * META-INF/java.io.InputStream    # contains the classname org.widget.NeatoInputStream
+     * META-INF/java.io.InputStream    # contains the classname com.foo.BarInputStream
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * List classes = finder.findAllImplementations(java.io.InputStream.class);
+     * classes.contains("org.acme.AcmeInputStream");  // true
+     * classes.contains("org.widget.NeatoInputStream");  // true
+     * classes.contains("com.foo.BarInputStream");  // true
+     *
+     * @param interfase a superclass or interface
+     * @return
+     * @throws IOException            if the URL cannot be read
+     * @throws ClassNotFoundException if the class found is not loadable
+     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
+     */
+    public List<Class> findAllImplementations(Class interfase) throws IOException, ClassNotFoundException {
+        List<Class> implementations = new ArrayList<>();
+        List<String> strings = findAllStrings(interfase.getName());
+        for (String className : strings) {
+            Class impl = classLoaderInterface.loadClass(className);
+            if (!interfase.isAssignableFrom(impl)) {
+                throw new ClassCastException("Class not of type: " + interfase.getName());
+            }
+            implementations.add(impl);
+        }
+        return implementations;
+    }
+
+    /**
+     * Assumes the class specified points to a file in the classpath that contains
+     * the name of a class that implements or is a subclass of the specfied class.
+     * <p/>
+     * Any class that cannot be loaded or are not assignable to the specified class will be
+     * skipped and placed in the 'resourcesNotLoaded' collection.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/java.io.InputStream    # contains the classname org.acme.AcmeInputStream
+     * META-INF/java.io.InputStream    # contains the classname org.widget.NeatoInputStream
+     * META-INF/java.io.InputStream    # contains the classname com.foo.BarInputStream
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * List classes = finder.findAllImplementations(java.io.InputStream.class);
+     * classes.contains("org.acme.AcmeInputStream");  // true
+     * classes.contains("org.widget.NeatoInputStream");  // true
+     * classes.contains("com.foo.BarInputStream");  // true
+     *
+     * @param interfase a superclass or interface
+     * @return
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public List<Class> findAvailableImplementations(Class interfase) throws IOException {
+        resourcesNotLoaded.clear();
+        List<Class> implementations = new ArrayList<>();
+        List<String> strings = findAvailableStrings(interfase.getName());
+        for (String className : strings) {
+            try {
+                Class impl = classLoaderInterface.loadClass(className);
+                if (interfase.isAssignableFrom(impl)) {
+                    implementations.add(impl);
+                } else {
+                    resourcesNotLoaded.add(className);
+                }
+            } catch (Exception notAvailable) {
+                resourcesNotLoaded.add(className);
+            }
+        }
+        return implementations;
+    }
+
+    /**
+     * Assumes the class specified points to a directory in the classpath that holds files
+     * containing the name of a class that implements or is a subclass of the specfied class.
+     * <p/>
+     * Any class that cannot be loaded or assigned to the specified interface will be cause
+     * an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/java.net.URLStreamHandler/jar
+     * META-INF/java.net.URLStreamHandler/file
+     * META-INF/java.net.URLStreamHandler/http
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Map map = finder.mapAllImplementations(java.net.URLStreamHandler.class);
+     * Class jarUrlHandler = map.get("jar");
+     * Class fileUrlHandler = map.get("file");
+     * Class httpUrlHandler = map.get("http");
+     *
+     * @param interfase a superclass or interface
+     * @return
+     * @throws IOException            if the URL cannot be read
+     * @throws ClassNotFoundException if the class found is not loadable
+     * @throws ClassCastException     if the class found is not assignable to the specified superclass or interface
+     */
+    public Map<String, Class> mapAllImplementations(Class interfase) throws IOException, ClassNotFoundException {
+        Map<String, Class> implementations = new HashMap<>();
+        Map<String, String> map = mapAllStrings(interfase.getName());
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            String string = entry.getKey();
+            String className = entry.getValue();
+            Class impl = classLoaderInterface.loadClass(className);
+            if (!interfase.isAssignableFrom(impl)) {
+                throw new ClassCastException("Class not of type: " + interfase.getName());
+            }
+            implementations.put(string, impl);
+        }
+        return implementations;
+    }
+
+    /**
+     * Assumes the class specified points to a directory in the classpath that holds files
+     * containing the name of a class that implements or is a subclass of the specfied class.
+     * <p/>
+     * Any class that cannot be loaded or are not assignable to the specified class will be
+     * skipped and placed in the 'resourcesNotLoaded' collection.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/java.net.URLStreamHandler/jar
+     * META-INF/java.net.URLStreamHandler/file
+     * META-INF/java.net.URLStreamHandler/http
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Map map = finder.mapAllImplementations(java.net.URLStreamHandler.class);
+     * Class jarUrlHandler = map.get("jar");
+     * Class fileUrlHandler = map.get("file");
+     * Class httpUrlHandler = map.get("http");
+     *
+     * @param interfase a superclass or interface
+     * @return
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public Map<String, Class> mapAvailableImplementations(Class interfase) throws IOException {
+        resourcesNotLoaded.clear();
+        Map<String, Class> implementations = new HashMap<>();
+        Map<String, String> map = mapAvailableStrings(interfase.getName());
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            String string = entry.getKey();
+            String className = entry.getValue();
+            try {
+                Class impl = classLoaderInterface.loadClass(className);
+                if (interfase.isAssignableFrom(impl)) {
+                    implementations.put(string, impl);
+                } else {
+                    resourcesNotLoaded.add(className);
+                }
+            } catch (Exception notAvailable) {
+                resourcesNotLoaded.add(className);
+            }
+        }
+        return implementations;
+    }
+
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+    //
+    //   Find Properties
+    //
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+    /**
+     * Finds the corresponding resource and reads it in as a properties file
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/widget.properties
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * Properties widgetProps = finder.findProperties("widget.properties");
+     *
+     * @param uri
+     * @return
+     * @throws IOException if the URL cannot be read or is not in properties file format
+     */
+    public Properties findProperties(String uri) throws IOException {
+        String fulluri = path + uri;
+
+        URL resource = getResource(fulluri);
+        if (resource == null) {
+            throw new IOException("Could not find command in : " + fulluri);
+        }
+
+        return loadProperties(resource);
+    }
+
+    /**
+     * Finds the corresponding resources and reads them in as a properties files
+     * <p/>
+     * Any URL that cannot be read in as a properties file will cause an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/app.properties
+     * META-INF/app.properties
+     * META-INF/app.properties
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * List<Properties> appProps = finder.findAllProperties("app.properties");
+     *
+     * @param uri
+     * @return
+     * @throws IOException if the URL cannot be read or is not in properties file format
+     */
+    public List<Properties> findAllProperties(String uri) throws IOException {
+        String fulluri = path + uri;
+
+        List<Properties> properties = new ArrayList<>();
+
+        Enumeration<URL> resources = getResources(fulluri);
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            Properties props = loadProperties(url);
+            properties.add(props);
+        }
+        return properties;
+    }
+
+    /**
+     * Finds the corresponding resources and reads them in as a properties files
+     * <p/>
+     * Any URL that cannot be read in as a properties file will be added to the
+     * 'resourcesNotLoaded' collection.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/app.properties
+     * META-INF/app.properties
+     * META-INF/app.properties
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * List<Properties> appProps = finder.findAvailableProperties("app.properties");
+     *
+     * @param uri
+     * @return
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public List<Properties> findAvailableProperties(String uri) throws IOException {
+        resourcesNotLoaded.clear();
+        String fulluri = path + uri;
+
+        List<Properties> properties = new ArrayList<>();
+
+        Enumeration<URL> resources = getResources(fulluri);
+        while (resources.hasMoreElements()) {
+            URL url = resources.nextElement();
+            try {
+                Properties props = loadProperties(url);
+                properties.add(props);
+            } catch (Exception notAvailable) {
+                resourcesNotLoaded.add(url.toExternalForm());
+            }
+        }
+        return properties;
+    }
+
+    /**
+     * Finds the corresponding resources and reads them in as a properties files
+     * <p/>
+     * Any URL that cannot be read in as a properties file will cause an exception to be thrown.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/jdbcDrivers/oracle.properties
+     * META-INF/jdbcDrivers/mysql.props
+     * META-INF/jdbcDrivers/derby
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * List<Properties> driversList = finder.findAvailableProperties("jdbcDrivers");
+     * Properties oracleProps = driversList.get("oracle.properties");
+     * Properties mysqlProps = driversList.get("mysql.props");
+     * Properties derbyProps = driversList.get("derby");
+     *
+     * @param uri
+     * @return
+     * @throws IOException if the URL cannot be read or is not in properties file format
+     */
+    public Map<String, Properties> mapAllProperties(String uri) throws IOException {
+        Map<String, Properties> propertiesMap = new HashMap<>();
+        Map<String, URL> map = getResourcesMap(uri);
+        for (Map.Entry<String, URL> entry : map.entrySet()) {
+            String string = entry.getKey();
+            URL url = entry.getValue();
+            Properties properties = loadProperties(url);
+            propertiesMap.put(string, properties);
+        }
+        return propertiesMap;
+    }
+
+    /**
+     * Finds the corresponding resources and reads them in as a properties files
+     * <p/>
+     * Any URL that cannot be read in as a properties file will be added to the
+     * 'resourcesNotLoaded' collection.
+     * <p/>
+     * Example classpath:
+     * <p/>
+     * META-INF/jdbcDrivers/oracle.properties
+     * META-INF/jdbcDrivers/mysql.props
+     * META-INF/jdbcDrivers/derby
+     * <p/>
+     * ResourceFinder finder = new ResourceFinder("META-INF/");
+     * List<Properties> driversList = finder.findAvailableProperties("jdbcDrivers");
+     * Properties oracleProps = driversList.get("oracle.properties");
+     * Properties mysqlProps = driversList.get("mysql.props");
+     * Properties derbyProps = driversList.get("derby");
+     *
+     * @param uri
+     * @return
+     * @throws IOException if classLoader.getResources throws an exception
+     */
+    public Map<String, Properties> mapAvailableProperties(String uri) throws IOException {
+        resourcesNotLoaded.clear();
+        Map<String, Properties> propertiesMap = new HashMap<>();
+        Map<String, URL> map = getResourcesMap(uri);
+        for (Map.Entry<String, URL> entry : map.entrySet()) {
+            String string = entry.getKey();
+            URL url = entry.getValue();
+            try {
+                Properties properties = loadProperties(url);
+                propertiesMap.put(string, properties);
+            } catch (Exception notAvailable) {
+                resourcesNotLoaded.add(url.toExternalForm());
+            }
+        }
+        return propertiesMap;
+    }
+
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+    //
+    //   Map Resources
+    //
+    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+    public Map<String, URL> getResourcesMap(String uri) throws IOException {
+        String basePath = path + uri;
+
+        Map<String, URL> resources = new HashMap<>();
+        if (!basePath.endsWith("/")) {
+            basePath += "/";
+        }
+        Enumeration<URL> urls = getResources(basePath);
+
+        while (urls.hasMoreElements()) {
+            URL location = urls.nextElement();
+
+            try {
+                if ("jar".equals(location.getProtocol())) {
+                    readJarEntries(location, basePath, resources);
+                } else if ("file".equals(location.getProtocol())) {
+                    readDirectoryEntries(location, resources);
+                }
+            } catch (Exception e) {
+                LOG.debug("Got exception loading resources for {}", uri, e);
+            }
+        }
+
+        return resources;
+    }
+
+    /**
+     * Gets a list of subpckages from jars or dirs
+     */
+    public Set<String> findPackages(String uri) throws IOException {
+        String basePath = path + uri;
+
+        Set<String> resources = new HashSet<>();
+        if (!basePath.endsWith("/")) {
+            basePath += "/";
+        }
+        Enumeration<URL> urls = getResources(basePath);
+
+        while (urls.hasMoreElements()) {
+            URL location = urls.nextElement();
+
+            try {
+                if ("jar".equals(location.getProtocol())) {
+                    readJarDirectoryEntries(location, basePath, resources);
+                } else if ("file".equals(location.getProtocol())) {
+                    readSubDirectories(new File(location.toURI()), uri, resources);
+                }
+            } catch (Exception e) {
+                LOG.debug("Got exception search for subpackages for {}", uri, e);
+            }
+        }
+
+        return convertPathsToPackages(resources);
+    }
+
+    /**
+     * Gets a list of subpckages from jars or dirs
+     */
+    public Map<URL, Set<String>> findPackagesMap(String uri) throws IOException {
+        String basePath = path + uri;
+
+        if (!basePath.endsWith("/")) {
+            basePath += "/";
+        }
+        Enumeration<URL> urls = getResources(basePath);
+        Map<URL, Set<String>> result = new HashMap<>();
+
+        while (urls.hasMoreElements()) {
+            URL location = urls.nextElement();
+
+            try {
+                if ("jar".equals(location.getProtocol())) {
+                    Set<String> resources = new HashSet<>();
+                    readJarDirectoryEntries(location, basePath, resources);
+                    result.put(location, convertPathsToPackages(resources));
+                } else if ("file".equals(location.getProtocol())) {
+                    Set<String> resources = new HashSet<>();
+                    readSubDirectories(new File(location.toURI()), uri, resources);
+                    result.put(location, convertPathsToPackages(resources));
+                }
+            } catch (Exception e) {
+                LOG.debug("Got exception finding subpackages for {}", uri, e);
+            }
+        }
+
+        return result;
+    }
+
+    private Set<String> convertPathsToPackages(Set<String> resources) {
+        Set<String> packageNames = new HashSet<>(resources.size());
+        for(String resource : resources) {
+            packageNames.add(StringUtils.removeEnd(StringUtils.replace(resource, "/", "."), "."));
+        }
+
+        return packageNames;
+    }
+
+    private static void readDirectoryEntries(URL location, Map<String, URL> resources) throws MalformedURLException {
+        File dir = new File(URLDecoder.decode(location.getPath()));
+        if (dir.isDirectory()) {
+            File[] files = dir.listFiles();
+            for (File file : files) {
+                if (!file.isDirectory()) {
+                    String name = file.getName();
+                    URL url = file.toURL();
+                    resources.put(name, url);
+                }
+            }
+        }
+    }
+
+    /**
+     * Reads subdirectories of a file. The output is a list of subdirectories, relative to the basepath
+     */
+     private static void readSubDirectories(File dir, String basePath, Set<String> resources) throws MalformedURLException {
+        if (dir.isDirectory()) {
+            File[] files = dir.listFiles();
+            for (File file : files) {
+                if (file.isDirectory()) {
+                    String name = file.getName();
+                    String subName = StringUtils.removeEnd(basePath, "/") + "/" + name;
+                    resources.add(subName);
+                    readSubDirectories(file, subName, resources);
+                }
+            }
+        }
+    }
+
+    private static void readJarEntries(URL location, String basePath, Map<String, URL> resources) throws IOException {
+        JarURLConnection conn = (JarURLConnection) location.openConnection();
+        JarFile jarfile;
+        jarfile = conn.getJarFile();
+
+        Enumeration<JarEntry> entries = jarfile.entries();
+        while (entries != null && entries.hasMoreElements()) {
+            JarEntry entry = entries.nextElement();
+            String name = entry.getName();
+
+            if (entry.isDirectory() || !name.startsWith(basePath) || name.length() == basePath.length()) {
+                continue;
+            }
+
+            name = name.substring(basePath.length());
+
+            if (name.contains("/")) {
+                continue;
+            }
+
+            URL resource = new URL(location, name);
+            resources.put(name, resource);
+        }
+    }
+
+    //read directories in the jar that start with the basePath
+    private static void readJarDirectoryEntries(URL location, String basePath, Set<String> resources) throws IOException {
+        JarURLConnection conn = (JarURLConnection) location.openConnection();
+        JarFile jarfile;
+        jarfile = conn.getJarFile();
+
+        Enumeration<JarEntry> entries = jarfile.entries();
+        while (entries != null && entries.hasMoreElements()) {
+            JarEntry entry = entries.nextElement();
+            String name = entry.getName();
+
+            if (entry.isDirectory() && StringUtils.startsWith(name, basePath)) {
+                resources.add(name);
+            }
+        }
+    }
+
+    private Properties loadProperties(URL resource) throws IOException {
+        try (InputStream reader = new BufferedInputStream(resource.openStream())) {
+            Properties properties = new Properties();
+            properties.load(reader);
+
+            return properties;
+        }
+    }
+
+    private String readContents(URL resource) throws IOException {
+        StringBuilder sb = new StringBuilder();
+
+        try (InputStream reader = new BufferedInputStream(resource.openStream())) {
+            int b = reader.read();
+            while (b != -1) {
+                sb.append((char) b);
+                b = reader.read();
+            }
+
+            return sb.toString().trim();
+        }
+    }
+
+    private URL getResource(String fullUri) {
+        if (urls == null){
+            return classLoaderInterface.getResource(fullUri);
+        }
+        return findResource(fullUri, urls);
+    }
+
+    private Enumeration<URL> getResources(String fulluri) throws IOException {
+        if (urls == null) {
+            return classLoaderInterface.getResources(fulluri);
+        }
+        Vector<URL> resources = new Vector();
+        for (URL url : urls) {
+            URL resource = findResource(fulluri, url);
+            if (resource != null){
+                resources.add(resource);
+            }
+        }
+        return resources.elements();
+    }
+
+    private URL findResource(String resourceName, URL... search) {
+        for (int i = 0; i < search.length; i++) {
+            URL currentUrl = search[i];
+            if (currentUrl == null) {
+                continue;
+            }
+            JarFile jarFile;
+            try {
+                String protocol = currentUrl.getProtocol();
+                if ("jar".equals(protocol)) {
+                    /*
+                    * If the connection for currentUrl or resURL is
+                    * used, getJarFile() will throw an exception if the
+                    * entry doesn't exist.
+                    */
+                    URL jarURL = ((JarURLConnection) currentUrl.openConnection()).getJarFileURL();
+                    try {
+                        JarURLConnection juc = (JarURLConnection) new URL("jar", "", jarURL.toExternalForm() + "!/").openConnection();
+                        jarFile = juc.getJarFile();
+                    } catch (IOException e) {
+                        // Don't look for this jar file again
+                        search[i] = null;
+                        throw e;
+                    }
+
+                    String entryName;
+                    if (currentUrl.getFile().endsWith("!/")) {
+                        entryName = resourceName;
+                    } else {
+                        String file = currentUrl.getFile();
+                        int sepIdx = file.lastIndexOf("!/");
+                        if (sepIdx == -1) {
+                            // Invalid URL, don't look here again
+                            search[i] = null;
+                            continue;
+                        }
+                        sepIdx += 2;
+                        StringBuilder sb = new StringBuilder(file.length() - sepIdx + resourceName.length());
+                        sb.append(file.substring(sepIdx));
+                        sb.append(resourceName);
+                        entryName = sb.toString();
+                    }
+                    if ("META-INF/".equals(entryName) && jarFile.getEntry("META-INF/MANIFEST.MF") != null){
+                        return targetURL(currentUrl, "META-INF/MANIFEST.MF");
+                    }
+                    if (jarFile.getEntry(entryName) != null) {
+                        return targetURL(currentUrl, resourceName);
+                    }
+                } else if ("file".equals(protocol)) {
+                    String baseFile = currentUrl.getFile();
+                    String host = currentUrl.getHost();
+                    int hostLength = 0;
+                    if (host != null) {
+                        hostLength = host.length();
+                    }
+                    StringBuilder buf = new StringBuilder(2 + hostLength + baseFile.length() + resourceName.length());
+
+                    if (hostLength > 0) {
+                        buf.append("//").append(host);
+                    }
+                    // baseFile always ends with '/'
+                    buf.append(baseFile);
+                    String fixedResName = resourceName;
+                    // Do not create a UNC path, i.e. \\host
+                    while (fixedResName.startsWith("/") || fixedResName.startsWith("\\")) {
+                        fixedResName = fixedResName.substring(1);
+                    }
+                    buf.append(fixedResName);
+                    String filename = buf.toString();
+                    File file = new File(filename);
+                    File file2 = new File(URLDecoder.decode(filename));
+
+                    if (file.exists() || file2.exists()) {
+                        return targetURL(currentUrl, fixedResName);
+                    }
+                } else {
+                    URL resourceURL = targetURL(currentUrl, resourceName);
+                    URLConnection urlConnection = resourceURL.openConnection();
+
+                    try {
+                        urlConnection.getInputStream().close();
+                    } catch (SecurityException e) {
+                        return null;
+                    }
+                    // HTTP can return a stream on a non-existent file
+                    // So check for the return code;
+                    if (!"http".equals(resourceURL.getProtocol())) {
+                        return resourceURL;
+                    }
+
+                    int code = ((HttpURLConnection) urlConnection).getResponseCode();
+                    if (code >= 200 && code < 300) {
+                        return resourceURL;
+                    }
+                }
+            } catch (IOException | SecurityException e) {
+                // Keep iterating through the URL list
+            }
+        }
+        return null;
+    }
+
+    private URL targetURL(URL base, String name) throws MalformedURLException {
+        StringBuilder sb = new StringBuilder(base.getFile().length() + name.length());
+        sb.append(base.getFile());
+        sb.append(name);
+        String file = sb.toString();
+        return new URL(base.getProtocol(), base.getHost(), base.getPort(), file, null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/finder/Test.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/finder/Test.java b/core/src/main/java/com/opensymphony/xwork2/util/finder/Test.java
new file mode 100644
index 0000000..b78d6f4
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/finder/Test.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2002-2003,2009 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.finder;
+
+/**
+ * This is the testing interface that is used to accept or reject resources.
+ */
+public interface Test<T> {
+    /**
+     * The test method.
+     *
+     * @param   t The resource object to test.
+     * @return  True if the resource should be accepted, false otherwise.
+     */
+    public boolean test(T t);
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/finder/UrlSet.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/finder/UrlSet.java b/core/src/main/java/com/opensymphony/xwork2/util/finder/UrlSet.java
new file mode 100644
index 0000000..34e0937
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/finder/UrlSet.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2002-2003,2009 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.finder;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.*;
+
+/**
+ * Use with ClassFinder to filter the Urls to be scanned, example:
+ * <pre>
+ * UrlSet urlSet = new UrlSet(classLoader);
+ * urlSet = urlSet.exclude(ClassLoader.getSystemClassLoader().getParent());
+ * urlSet = urlSet.excludeJavaExtDirs();
+ * urlSet = urlSet.excludeJavaEndorsedDirs();
+ * urlSet = urlSet.excludeJavaHome();
+ * urlSet = urlSet.excludePaths(System.getProperty("sun.boot.class.path", ""));
+ * urlSet = urlSet.exclude(".*?/JavaVM.framework/.*");
+ * urlSet = urlSet.exclude(".*?/activemq-(core|ra)-[\\d.]+.jar(!/)?");
+ * </pre>
+ * @author David Blevins
+ * @version $Rev$ $Date$
+ */
+public class UrlSet {
+
+    private static final Logger LOG = LogManager.getLogger(UrlSet.class);
+
+    private final Map<String,URL> urls;
+    private Set<String> protocols;
+
+    private UrlSet() {
+        this.urls = new HashMap<>();
+    }
+
+    public UrlSet(ClassLoaderInterface classLoader) throws IOException {
+        this();
+        load(getUrls(classLoader));
+    }
+
+    public UrlSet(ClassLoaderInterface classLoader, Set<String> protocols) throws IOException {
+        this();
+        this.protocols = protocols;
+        load(getUrls(classLoader, protocols));
+    }
+
+    public UrlSet(URL... urls){
+        this(Arrays.asList(urls));
+    }
+    /**
+     * Ignores all URLs that are not "jar" or "file"
+     * @param urls
+     */
+    public UrlSet(Collection<URL> urls){
+        this();
+        load(urls);
+    }
+
+    private UrlSet(Map<String, URL> urls) {
+        this.urls = urls;
+    }
+
+    private void load(Collection<URL> urls){
+        for (URL location : urls) {
+            try {
+                this.urls.put(location.toExternalForm(), location);
+            } catch (Exception e) {
+                LOG.warn("Cannot translate url to external form!", e);
+            }
+        }
+    }
+
+    public UrlSet include(UrlSet urlSet){
+        Map<String, URL> urls = new HashMap<>(this.urls);
+        urls.putAll(urlSet.urls);
+        return new UrlSet(urls);
+    }
+
+    public UrlSet exclude(UrlSet urlSet) {
+        Map<String, URL> urls = new HashMap<>(this.urls);
+        Map<String, URL> parentUrls = urlSet.urls;
+        for (String url : parentUrls.keySet()) {
+            urls.remove(url);
+        }
+        return new UrlSet(urls);
+    }
+
+    public UrlSet exclude(ClassLoaderInterface parent) throws IOException {
+        return exclude(new UrlSet(parent, this.protocols));
+    }
+
+    public UrlSet exclude(File file) throws MalformedURLException {
+        return exclude(relative(file));
+    }
+
+    public UrlSet exclude(String pattern) throws MalformedURLException {
+        return exclude(matching(pattern));
+    }
+
+    /**
+     * Calls excludePaths(System.getProperty("java.ext.dirs"))
+     * @return
+     * @throws MalformedURLException
+     */
+    public UrlSet excludeJavaExtDirs() throws MalformedURLException {
+        return excludePaths(System.getProperty("java.ext.dirs", ""));
+    }
+
+    /**
+     * Calls excludePaths(System.getProperty("java.endorsed.dirs"))
+     *
+     * @return
+     * @throws MalformedURLException
+     */
+    public UrlSet excludeJavaEndorsedDirs() throws MalformedURLException {
+        return excludePaths(System.getProperty("java.endorsed.dirs", ""));
+    }
+
+    public UrlSet excludeJavaHome() throws MalformedURLException {
+        String path = System.getProperty("java.home");
+        if (path != null) {
+            File java = new File(path);
+            if (path.matches("/System/Library/Frameworks/JavaVM.framework/Versions/[^/]+/Home")){
+                java = java.getParentFile();
+            }
+            return exclude(java);
+        } else {
+            return this;
+        }
+    }
+
+    public UrlSet excludePaths(String pathString) throws MalformedURLException {
+        String[] paths = pathString.split(File.pathSeparator);
+        UrlSet urlSet = this;
+        for (String path : paths) {
+            if (StringUtils.isNotEmpty(path)) {
+                File file = new File(path);
+                urlSet = urlSet.exclude(file);
+            }
+        }
+        return urlSet;
+    }
+
+    public UrlSet matching(String pattern) {
+        Map<String, URL> urls = new HashMap<>();
+        for (Map.Entry<String, URL> entry : this.urls.entrySet()) {
+            String url = entry.getKey();
+            if (url.matches(pattern)){
+                urls.put(url, entry.getValue());
+            }
+        }
+        return new UrlSet(urls);
+    }
+
+    /**
+     * Try to find a classes directory inside a war file add its normalized url to this set
+     */
+    public UrlSet includeClassesUrl(ClassLoaderInterface classLoaderInterface, FileProtocolNormalizer normalizer) throws IOException {
+        Enumeration<URL> rootUrlEnumeration = classLoaderInterface.getResources("");
+        while (rootUrlEnumeration.hasMoreElements()) {
+            URL url = rootUrlEnumeration.nextElement();
+            String externalForm = StringUtils.removeEnd(url.toExternalForm(), "/");
+            if (externalForm.endsWith(".war/WEB-INF/classes")) {
+                //if it is inside a war file, get the url to the file
+                externalForm = StringUtils.substringBefore(externalForm, "/WEB-INF/classes");
+                URL warUrl = new URL(externalForm);
+                URL normalizedUrl = normalizer.normalizeToFileProtocol(warUrl);
+                URL finalUrl = ObjectUtils.defaultIfNull(normalizedUrl, warUrl);
+
+                Map<String, URL> newUrls = new HashMap<>(this.urls);
+                if ("jar".equals(finalUrl.getProtocol()) || "file".equals(finalUrl.getProtocol())) {
+                    newUrls.put(finalUrl.toExternalForm(), finalUrl);
+                }
+                return new UrlSet(newUrls);
+            }
+        }
+
+        return this;
+    }
+
+    public UrlSet relative(File file) throws MalformedURLException {
+        String urlPath = file.toURI().toURL().toExternalForm();
+        Map<String, URL> urls = new HashMap<>();
+        for (Map.Entry<String, URL> entry : this.urls.entrySet()) {
+            String url = entry.getKey();
+            if (url.startsWith(urlPath) || url.startsWith("jar:"+urlPath)){
+                urls.put(url, entry.getValue());
+            }
+        }
+        return new UrlSet(urls);
+    }
+
+    public List<URL> getUrls() {
+        return new ArrayList<>(urls.values());
+    }
+
+    private List<URL> getUrls(ClassLoaderInterface classLoader) throws IOException {
+        List<URL> list = new ArrayList<>();
+
+        //find jars
+        ArrayList<URL> urls = Collections.list(classLoader.getResources("META-INF"));
+
+        for (URL url : urls) {
+            if ("jar".equalsIgnoreCase(url.getProtocol())) {
+                String externalForm = url.toExternalForm();
+                //build a URL pointing to the jar, instead of the META-INF dir
+                url = new URL(StringUtils.substringBefore(externalForm, "META-INF"));
+                list.add(url);
+            } else {
+                LOG.debug("Ignoring URL [{}] because it is not a jar", url.toExternalForm());
+            }
+        }
+
+        //usually the "classes" dir
+        list.addAll(Collections.list(classLoader.getResources("")));
+        return list;
+    }
+
+    private List<URL> getUrls(ClassLoaderInterface classLoader, Set<String> protocols) throws IOException {
+
+        if (protocols == null) {
+            return getUrls(classLoader);
+        }
+
+        List<URL> list = new ArrayList<>();
+
+        //find jars
+        ArrayList<URL> urls = Collections.list(classLoader.getResources("META-INF"));
+
+        for (URL url : urls) {
+            if (protocols.contains(url.getProtocol())) {
+                String externalForm = url.toExternalForm();
+                //build a URL pointing to the jar, instead of the META-INF dir
+                url = new URL(StringUtils.substringBefore(externalForm, "META-INF"));
+                list.add(url);
+            } else {
+                LOG.debug("Ignoring URL [{}] because it is not a valid protocol", url.toExternalForm());
+            }
+        }
+        return list;
+    }
+
+    public static interface FileProtocolNormalizer {
+        URL normalizeToFileProtocol(URL url);
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManager.java b/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManager.java
new file mode 100644
index 0000000..86fda9b
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManager.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2002-2003,2009 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.fs;
+
+import com.opensymphony.xwork2.FileManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Default implementation of {@link FileManager}
+ */
+public class DefaultFileManager implements FileManager {
+
+    private static Logger LOG = LogManager.getLogger(DefaultFileManager.class);
+
+    private static final Pattern JAR_PATTERN = Pattern.compile("^(jar:|wsjar:|zip:|vfsfile:|code-source:)?(file:)?(.*?)(\\!/|\\.jar/)(.*)");
+    private static final int JAR_FILE_PATH = 3;
+
+    protected static Map<String, Revision> files = Collections.synchronizedMap(new HashMap<String, Revision>());
+
+    protected boolean reloadingConfigs = false;
+
+    public DefaultFileManager() {
+    }
+
+    public void setReloadingConfigs(boolean reloadingConfigs) {
+        this.reloadingConfigs = reloadingConfigs;
+    }
+
+    public boolean fileNeedsReloading(URL fileUrl) {
+        return fileUrl != null && fileNeedsReloading(fileUrl.toString());
+    }
+
+    public boolean fileNeedsReloading(String fileName) {
+        Revision revision = files.get(fileName);
+        if (revision == null) {
+            // no revision yet and we keep the revision history, so
+            // return whether the file needs to be loaded for the first time
+            return reloadingConfigs;
+        }
+        return revision.needsReloading();
+    }
+
+    public InputStream loadFile(URL fileUrl) {
+        if (fileUrl == null) {
+            return null;
+        }
+        InputStream is = openFile(fileUrl);
+        monitorFile(fileUrl);
+        return is;
+    }
+
+    private InputStream openFile(URL fileUrl) {
+        try {
+            InputStream is = fileUrl.openStream();
+            if (is == null) {
+                throw new IllegalArgumentException("No file '" + fileUrl + "' found as a resource");
+            }
+            return is;
+        } catch (IOException e) {
+            throw new IllegalArgumentException("No file '" + fileUrl + "' found as a resource");
+        }
+    }
+
+    public void monitorFile(URL fileUrl) {
+        String fileName = fileUrl.toString();
+        Revision revision;
+        LOG.debug("Creating revision for URL: {}", fileName);
+        if (isJarURL(fileUrl)) {
+            revision = JarEntryRevision.build(fileUrl, this);
+        } else {
+            revision = FileRevision.build(fileUrl);
+        }
+        if (revision == null) {
+            files.put(fileName, Revision.build(fileUrl));
+        } else {
+            files.put(fileName, revision);
+        }
+    }
+
+    /**
+     * Check if given URL is matching Jar pattern for different servers
+     *
+     * @param fileUrl
+     * @return
+     */
+    protected boolean isJarURL(URL fileUrl) {
+        Matcher jarMatcher = JAR_PATTERN.matcher(fileUrl.getPath());
+        return jarMatcher.matches();
+    }
+
+    public URL normalizeToFileProtocol(URL url) {
+        String fileName = url.toExternalForm();
+        Matcher jarMatcher = JAR_PATTERN.matcher(fileName);
+        try {
+            if (jarMatcher.matches()) {
+                String path = jarMatcher.group(JAR_FILE_PATH);
+                return new URL("file", "", path);
+            } else if ("file".equals(url.getProtocol())) {
+                return url; // it's already a file
+            } else {
+                LOG.warn("Could not normalize URL [{}] to file protocol!", url);
+                return null;
+            }
+        } catch (MalformedURLException e) {
+            LOG.warn("Error normalizing URL [{}] to file protocol!", url, e);
+            return null;
+        }
+    }
+
+    public boolean support() {
+        return false; // allow other implementation to be used first
+    }
+
+    public boolean internal() {
+        return true;
+    }
+
+    public Collection<? extends URL> getAllPhysicalUrls(URL url) throws IOException {
+        return Arrays.asList(url);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManagerFactory.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManagerFactory.java b/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManagerFactory.java
new file mode 100644
index 0000000..c19385f
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/fs/DefaultFileManagerFactory.java
@@ -0,0 +1,80 @@
+package com.opensymphony.xwork2.util.fs;
+
+import com.opensymphony.xwork2.FileManager;
+import com.opensymphony.xwork2.FileManagerFactory;
+import com.opensymphony.xwork2.XWorkConstants;
+import com.opensymphony.xwork2.inject.Container;
+import com.opensymphony.xwork2.inject.Inject;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Default implementation
+ */
+public class DefaultFileManagerFactory implements FileManagerFactory {
+
+    private static final Logger LOG = LogManager.getLogger(DefaultFileManagerFactory.class);
+
+    private boolean reloadingConfigs;
+    private FileManager systemFileManager;
+    private Container container;
+
+    @Inject(value = "system")
+    public void setFileManager(FileManager fileManager) {
+        this.systemFileManager = fileManager;
+    }
+
+    @Inject
+    public void setContainer(Container container) {
+        this.container = container;
+    }
+
+    @Inject(value = XWorkConstants.RELOAD_XML_CONFIGURATION, required = false)
+    public void setReloadingConfigs(String reloadingConfigs) {
+        this.reloadingConfigs = Boolean.parseBoolean(reloadingConfigs);
+    }
+
+    public FileManager getFileManager() {
+        FileManager fileManager = lookupFileManager();
+        if (fileManager != null) {
+            LOG.debug("Using FileManager implementation [{}]", fileManager.getClass().getSimpleName());
+            fileManager.setReloadingConfigs(reloadingConfigs);
+            return fileManager;
+        }
+        LOG.debug("Using default implementation of FileManager provided under name [system]: {}", systemFileManager.getClass().getSimpleName());
+        systemFileManager.setReloadingConfigs(reloadingConfigs);
+        return systemFileManager;
+    }
+
+    private FileManager lookupFileManager() {
+        Set<String> names = container.getInstanceNames(FileManager.class);
+        LOG.debug("Found following implementations of FileManager interface: {}", names);
+        Set<FileManager> internals = new HashSet<>();
+        Set<FileManager> users = new HashSet<>();
+        for (String fmName : names) {
+            FileManager fm = container.getInstance(FileManager.class, fmName);
+            if (fm.internal()) {
+                internals.add(fm);
+            } else {
+                users.add(fm);
+            }
+        }
+        for (FileManager fm : users) {
+            if (fm.support()) {
+                LOG.debug("Using FileManager implementation [{}]", fm.getClass().getSimpleName());
+                return fm;
+            }
+        }
+        LOG.debug("No user defined FileManager, looking up for internal implementations!");
+        for (FileManager fm : internals) {
+            if (fm.support()) {
+                return fm;
+            }
+        }
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/fs/FileRevision.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/fs/FileRevision.java b/core/src/main/java/com/opensymphony/xwork2/util/fs/FileRevision.java
new file mode 100644
index 0000000..f59d1d4
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/fs/FileRevision.java
@@ -0,0 +1,52 @@
+package com.opensymphony.xwork2.util.fs;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * Represents file resource revision, used for file://* resources
+ */
+public class FileRevision extends Revision {
+
+    private File file;
+    private long lastModified;
+
+    public static Revision build(URL fileUrl) {
+        File file;
+        try {
+            if (fileUrl != null) {
+                file = new File(fileUrl.toURI());
+            } else {
+                return null;
+            }
+        } catch (URISyntaxException e) {
+            file = new File(fileUrl.getPath());
+        }  catch (Throwable t) {
+            return null;
+        }
+        if (file.exists() && file.canRead()) {
+            long lastModified = file.lastModified();
+            return new FileRevision(file, lastModified);
+        }
+        return null;
+    }
+
+    private FileRevision(File file, long lastUpdated) {
+        if (file == null) {
+            throw new IllegalArgumentException("File cannot be null");
+        }
+
+        this.file = file;
+        this.lastModified = lastUpdated;
+    }
+
+    public File getFile() {
+        return file;
+    }
+
+    public boolean needsReloading() {
+        return this.lastModified < this.file.lastModified();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/fs/JarEntryRevision.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/fs/JarEntryRevision.java b/core/src/main/java/com/opensymphony/xwork2/util/fs/JarEntryRevision.java
new file mode 100644
index 0000000..4b962af
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/fs/JarEntryRevision.java
@@ -0,0 +1,82 @@
+package com.opensymphony.xwork2.util.fs;
+
+import com.opensymphony.xwork2.FileManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import org.apache.commons.io.FileUtils;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+/**
+ * Represents jar resource revision, used for jar://* resource
+ */
+public class JarEntryRevision extends Revision {
+
+    private static Logger LOG = LogManager.getLogger(JarEntryRevision.class);
+
+    private static final String JAR_FILE_NAME_SEPARATOR = "!/";
+    private static final String JAR_FILE_EXTENSION_END = ".jar/";
+
+    private String jarFileName;
+    private String fileNameInJar;
+    private long lastModified;
+
+    public static Revision build(URL fileUrl, FileManager fileManager) {
+        // File within a Jar
+        // Find separator index of jar filename and filename within jar
+        String jarFileName = "";
+        try {
+            String fileName = fileUrl.toString();
+            int separatorIndex = fileName.indexOf(JAR_FILE_NAME_SEPARATOR);
+            if (separatorIndex == -1) {
+                separatorIndex = fileName.lastIndexOf(JAR_FILE_EXTENSION_END);
+            }
+            if (separatorIndex == -1) {
+                LOG.warn("Could not find end of jar file!");
+                return null;
+            }
+
+            // Split file name
+            jarFileName = fileName.substring(0, separatorIndex);
+            int index = separatorIndex + JAR_FILE_NAME_SEPARATOR.length();
+            String fileNameInJar = fileName.substring(index).replaceAll("%20", " ");
+
+            URL url = fileManager.normalizeToFileProtocol(fileUrl);
+            if (url != null) {
+                JarFile jarFile = new JarFile(FileUtils.toFile(url));
+                ZipEntry entry = jarFile.getEntry(fileNameInJar);
+                return new JarEntryRevision(jarFileName, fileNameInJar, entry.getTime());
+            } else {
+                return null;
+            }
+        } catch (Throwable e) {
+            LOG.warn("Could not create JarEntryRevision for [{}]!", jarFileName, e);
+            return null;
+        }
+    }
+
+    private JarEntryRevision(String jarFileName, String fileNameInJar, long lastModified) {
+        if ((jarFileName == null) || (fileNameInJar == null)) {
+            throw new IllegalArgumentException("JarFileName and FileNameInJar cannot be null");
+        }
+        this.jarFileName = jarFileName;
+        this.fileNameInJar = fileNameInJar;
+        this.lastModified = lastModified;
+    }
+
+    public boolean needsReloading() {
+        ZipEntry entry;
+        try {
+            JarFile jarFile = new JarFile(this.jarFileName);
+            entry = jarFile.getEntry(this.fileNameInJar);
+        } catch (IOException e) {
+            entry = null;
+        }
+
+        return entry != null && (lastModified < entry.getTime());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/fs/Revision.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/fs/Revision.java b/core/src/main/java/com/opensymphony/xwork2/util/fs/Revision.java
new file mode 100644
index 0000000..8da65fc
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/fs/Revision.java
@@ -0,0 +1,21 @@
+package com.opensymphony.xwork2.util.fs;
+
+import java.net.URL;
+
+/**
+ * Class represents common revision resource, should be used as default class when no other option exists
+ */
+public class Revision {
+
+    protected Revision() {
+    }
+
+    public boolean needsReloading() {
+        return false;
+    }
+
+    public static Revision build(URL fileUrl) {
+        return new Revision();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/location/Locatable.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/location/Locatable.java b/core/src/main/java/com/opensymphony/xwork2/util/location/Locatable.java
new file mode 100644
index 0000000..fc6f69c
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/location/Locatable.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.location;
+
+/**
+ * A interface that should be implemented by objects knowning their location (i.e. where they
+ * have been created from).
+ */
+public interface Locatable {
+    /**
+     * Get the location of this object
+     * 
+     * @return the location
+     */
+    public Location getLocation();
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/location/LocatableProperties.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/location/LocatableProperties.java b/core/src/main/java/com/opensymphony/xwork2/util/location/LocatableProperties.java
new file mode 100644
index 0000000..8df44f3
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/location/LocatableProperties.java
@@ -0,0 +1,81 @@
+package com.opensymphony.xwork2.util.location;
+
+import com.opensymphony.xwork2.util.PropertiesReader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Properties implementation that remembers the location of each property.  When
+ * loaded, a custom properties file parser is used to remember both the line number
+ * and preceeding comments for each property entry.
+ */
+public class LocatableProperties extends Properties implements Locatable {
+
+    Location location;
+    Map<String, Location> propLocations;
+
+    public LocatableProperties() {
+        this(Location.UNKNOWN);
+    }
+
+    public LocatableProperties(Location loc) {
+        super();
+        this.location = loc;
+        this.propLocations = new HashMap<>();
+    }
+
+    @Override
+    public void load(InputStream in) throws IOException {
+        Reader reader = new InputStreamReader(in);
+        PropertiesReader pr = new PropertiesReader(reader);
+        while (pr.nextProperty()) {
+            String name = pr.getPropertyName();
+            String val = pr.getPropertyValue();
+            int line = pr.getLineNumber();
+            String desc = convertCommentsToString(pr.getCommentLines());
+
+            Location loc = new LocationImpl(desc, location.getURI(), line, 0);
+            setProperty(name, val, loc);
+        }
+    }
+
+    String convertCommentsToString(List<String> lines) {
+        StringBuilder sb = new StringBuilder();
+        if (lines != null && !lines.isEmpty()) {
+            for (String line : lines) {
+                sb.append(line).append('\n');
+            }
+        }
+        return sb.toString();
+    }
+
+    public Object setProperty(String key, String value, Object locationObj) {
+        Object obj = super.setProperty(key, value);
+        if (location != null) {
+            Location loc = LocationUtils.getLocation(locationObj);
+            propLocations.put(key, loc);
+        }
+        return obj;
+    }
+
+    public Location getPropertyLocation(String key) {
+        Location loc = propLocations.get(key);
+        if (loc != null) {
+            return loc;
+        } else {
+            return Location.UNKNOWN;
+        }
+    }
+
+    public Location getLocation() {
+        return location;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/location/Located.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/location/Located.java b/core/src/main/java/com/opensymphony/xwork2/util/location/Located.java
new file mode 100644
index 0000000..7c2c795
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/location/Located.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.location;
+
+/**
+ * Base class for location aware objects
+ */
+public abstract class Located implements Locatable {
+    
+    protected Location location;
+    
+    /**
+     * Get the location of this object
+     * 
+     * @return the location
+     */
+    public Location getLocation() {
+        return location;
+    }
+    
+    /**
+     * Set the location of this object
+     * 
+     * @param loc the location
+     */
+    public void setLocation(Location loc) {
+        this.location = loc;
+    }
+}

http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/util/location/Location.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/util/location/Location.java b/core/src/main/java/com/opensymphony/xwork2/util/location/Location.java
new file mode 100644
index 0000000..7791d4f
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/util/location/Location.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2005 The Apache Software Foundation.
+ * 
+ * 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 com.opensymphony.xwork2.util.location;
+
+import java.util.List;
+
+
+/**
+ * A location in a resource. The location is composed of the URI of the resource, and 
+ * the line and column numbers within that resource (when available), along with a description.
+ * <p>
+ * Locations are mostly provided by {@link Locatable}s objects.
+ */
+public interface Location {
+    
+    /**
+     * Constant for unknown locations.
+     */
+    public static final Location UNKNOWN = LocationImpl.UNKNOWN;
+    
+    /**
+     * Get the description of this location
+     * 
+     * @return the description (can be <code>null</code>)
+     */
+    String getDescription();
+    
+    /**
+     * Get the URI of this location
+     * 
+     * @return the URI (<code>null</code> if unknown).
+     */
+    String getURI();
+
+    /**
+     * Get the line number of this location
+     * 
+     * @return the line number (<code>-1</code> if unknown)
+     */
+    int getLineNumber();
+    
+    /**
+     * Get the column number of this location
+     * 
+     * @return the column number (<code>-1</code> if unknown)
+     */
+    int getColumnNumber();
+    
+    /**
+     * Gets a source code snippet with the default padding
+     *
+     * @param padding The amount of lines before and after the error to include
+     * @return A list of source lines
+     */
+    List<String> getSnippet(int padding);
+}


Mime
View raw message