ResourceReader.java

package emissary.util.io;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.annotation.Nullable;

/**
 * This class reads a resource with utilities to read those with common names
 */
public class ResourceReader {

    private static final Logger logger = LoggerFactory.getLogger(ResourceReader.class);

    public static final String CONFIG_SUFFIX = ".cfg";
    public static final String XML_SUFFIX = ".xml";
    public static final String DATA_SUFFIX = ".dat";
    public static final String JS_SUFFIX = ".js";
    public static final String PROP_SUFFIX = ".properties";
    public static final String CLASS_SUFFIX = ".class";

    /**
     * Create a resource reader for use
     */
    public ResourceReader() {}

    /**
     * Return the config stream for the class config file Caller must close the stream
     * 
     * @param o the object whose class name matches the resource
     */
    public InputStream getConfigDataAsStream(Object o) {
        return getConfigDataAsStream(o.getClass());
    }

    /**
     * Return the config stream for the class config file Caller must close the stream
     * 
     * @param c the class name matching the desired resource
     */
    public InputStream getConfigDataAsStream(Class<?> c) {
        String name = getConfigDataName(c);
        return getResourceAsStream(name);
    }

    /**
     * Get the config name
     */
    public String getConfigDataName(Class<?> c) {
        return getResourceName(c) + CONFIG_SUFFIX;
    }

    /**
     * Return the stream for the class xml resource file Caller must close the stream
     * 
     * @param o the object whose class name matches the resource
     */
    public InputStream getXmlStream(Object o) {
        return getXmlStream(o.getClass());
    }

    /**
     * Return the xml stream for the class resource Caller must close the stream
     * 
     * @param c the class name matching the desired resource
     */
    public InputStream getXmlStream(Class<?> c) {
        String name = getXmlName(c);
        return getResourceAsStream(name);
    }

    /**
     * Get the xml name
     */
    public String getXmlName(Class<?> c) {
        return getResourceName(c) + XML_SUFFIX;
    }

    /**
     * Get the xml name
     */
    public String getXmlName(Package pkg, String name) {
        return getResourceName(pkg, name) + XML_SUFFIX;
    }

    /**
     * Get the resource name
     */
    public String getResourceName(Class<?> c) {
        return c.getName().replace('.', '/');
    }

    /**
     * Get the resource name
     */
    public String getResourceName(Package pkg, String name) {
        return (pkg.getName().replace('.', '/') + "/" + name);
    }

    /**
     * Get the url of the specified resource
     * 
     * @param name name of the resource
     * @return url to the resource
     */
    public URL getResource(@Nullable String name) {
        if (name != null && name.length() > 1 && name.charAt(1) == ':') {
            name = name.substring(2);
        }
        return Thread.currentThread().getContextClassLoader().getResource(name);
    }

    /**
     * Get the specified resource
     * 
     * @param name name of resource
     * @return stream, caller must close
     */
    public InputStream getResourceAsStream(@Nullable String name) {
        if (name != null && name.length() > 1 && name.charAt(1) == ':') {
            name = name.substring(2);
        }
        return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
    }

    /**
     * Indicate the URL that the specified class was loaded from
     * 
     * @param c the class
     * @return a URL indicating the jar file or file location or null if none
     */
    @Nullable
    public URL which(@Nullable Class<?> c) {
        if (c == null) {
            return null;
        }
        String cx = getResourceName(c) + CLASS_SUFFIX;
        ClassLoader loader = c.getClassLoader();
        if (loader != null) {
            return loader.getResource(cx);
        }
        return ClassLoader.getSystemResource(cx);
    }

    /**
     * Find all the config resources present for the specified class
     * 
     * @param c the class
     * @return sorted list of resources found or an empty list if none
     */
    public List<String> findConfigResourcesFor(Class<?> c) {
        return findResourcesFor(c, CONFIG_SUFFIX);
    }

    /**
     * Find all the data resources present for the specified class
     * 
     * @param c the class
     * @return sorted list of resources found or an empty list if none
     */
    public List<String> findDataResourcesFor(Class<?> c) {
        return findResourcesFor(c, DATA_SUFFIX);
    }

    /**
     * Find all the xml resources present for the specified class
     * 
     * @param c the class
     * @return sorted list of resources found or an empty list if none
     */
    public List<String> findXmlResourcesFor(Class<?> c) {
        return findResourcesFor(c, XML_SUFFIX);
    }

    /**
     * Find all the properties resources present for the specified class
     * 
     * @param c the class
     * @return sorted list of resources found or an empty list if none
     */
    public List<String> findPropertyResourcesFor(Class<?> c) {
        return findResourcesFor(c, PROP_SUFFIX);
    }

    /**
     * Find all the resources present for the specified class that use the indicated suffix.
     * 
     * @param c the class
     * @param suffix the resource suffix to hunt for, use "" for all
     * @return sorted list of resources found or an empty list if none
     */
    public List<String> findResourcesFor(Class<?> c, String suffix) {
        List<String> results = new ArrayList<>();
        URL url = which(c);
        if (url == null) {
            return results;
        }

        if (url.getProtocol().equals("jar")) {
            results.addAll(getJarResourcesFor(c, url, suffix));
        } else if (url.getProtocol().equals("file")) {
            results.addAll(getFileResourcesFor(c, url, suffix));
        }

        Collections.sort(results);
        return results;
    }

    /**
     * Find resources for the specified class from the Jar URL This finds resources at multiple levels at ones. For example
     * if you pass in emissary.util.Version.class with the ".cfg" suffix, you could get back resources that are located at
     * emissary/util/Version.cfg and emissary/util/Version/foo.cfg in the list.
     * 
     * @param c the class
     * @param url the jar url
     * @param suffix the ending suffix of desired resources
     * @return list of resources found
     */
    public List<String> getJarResourcesFor(Class<?> c, URL url, String suffix) {
        List<String> results = new ArrayList<>();
        try {
            JarURLConnection jc = (JarURLConnection) url.openConnection();
            JarFile jf = jc.getJarFile();
            String cmatch = getResourceName(c);
            for (Enumeration<JarEntry> entries = jf.entries(); entries.hasMoreElements();) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (name.startsWith(cmatch) && name.endsWith(suffix)) {
                    results.add(name);
                }
            }
            logger.debug("Found {} jar resources for {}", results.size(), cmatch);
        } catch (IOException ex) {
            logger.warn("Cannot get jar url connection to {}", url, ex);
        }
        return results;
    }


    /**
     * Find resources for the specified class from the file URL This finds resources at multiple levels at ones. For example
     * if you pass in emissary.util.Version.class with the ".cfg" suffix, you could get back resources that are located at
     * emissary/util/Version.cfg and emissary/util/Version/foo.cfg in the list.
     * 
     * @param c the class
     * @param url the file url
     * @param suffix the ending suffix of desired resources
     * @return list of resources found
     */
    public List<String> getFileResourcesFor(Class<?> c, URL url, String suffix) {
        List<String> results = new ArrayList<>();
        String cmatch = getResourceName(c);

        // The url may or may not have the class portion on it
        String path = url.getPath();
        if (path.contains(CLASS_SUFFIX)) {
            // Take off the ".class"
            path = path.substring(0, path.length() - CLASS_SUFFIX.length());
        } else {
            // Add on the package and class names
            path += "/" + cmatch;
        }

        // Look for a base resource at same level as class (in package dir)
        File base = new File(path.substring(0, path.lastIndexOf('/')));
        String pkgNamePart = cmatch.substring(0, cmatch.lastIndexOf('/'));
        String classNamePart = cmatch.substring(cmatch.lastIndexOf('/') + 1);
        if (base.exists() && base.isDirectory()) {
            String[] list = base.list();
            if (list != null) {
                Arrays.stream(list)
                        .filter(s -> s.startsWith(classNamePart) && s.endsWith(suffix))
                        .forEach(s -> results.add(pkgNamePart + "/" + s));
            }
        }

        // Look for more resources in a directory with the class name
        File dir = new File(path);
        if (dir.isDirectory()) {
            String[] list = dir.list();
            if (list != null) {
                Arrays.stream(list)
                        .filter(s -> s.endsWith(suffix))
                        .forEach(s -> results.add(cmatch + '/' + s));
            }
        }

        logger.debug("Found {} file resources for {}", results.size(), cmatch);
        return results;
    }

}