ResourceUtils.java

package org.andromda.core.common;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.ListIterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * Provides utilities for loading resources.
 *
 * @author Chad Brandon
 * @author Bob Fields
 * @author Michail Plushnikov
 */
public class ResourceUtils
{
    private static final Logger logger = Logger.getLogger(ResourceUtils.class);

    /**
     * All archive files start with this prefix.
     */
    private static final String ARCHIVE_PREFIX = "jar:";

    /**
     * The prefix for URL file resources.
     */
    private static final String FILE_PREFIX = "file:";

    /**
     * The prefix to use for searching the classpath for a resource.
     */
    private static final String CLASSPATH_PREFIX = "classpath:";

    /**
     * Retrieves a resource from the current classpath.
     *
     * @param resourceName the name of the resource
     * @return the resource url
     */
    public static URL getResource(final String resourceName)
    {
        ExceptionUtils.checkEmpty(
            "resourceName",
            resourceName);
        final ClassLoader loader = ClassUtils.getClassLoader();
        URL resource = loader != null ? loader.getResource(resourceName) : null;
        //if (resource==null)
        return resource;
    }

    /**
     * Loads the resource and returns the contents as a String.
     *
     * @param resource the name of the resource.
     * @return String
     */
    public static String getContents(final URL resource)
    {
        String result = null;

        InputStream in = null;
        try
        {
            if(null!=resource)
            {
                in = resource.openStream();
                result = IOUtils.toString(in);
            }
        }
        catch (final IOException ex) {
            throw new RuntimeException(ex);
        }finally {
            IOUtils.closeQuietly(in);
        }
        return result;
    }

    /**
     * Loads the resource and returns the contents as a String.
     *
     * @param resource the name of the resource.
     * @return the contents of the resource as a string.
     */
    public static String getContents(final Reader resource)
    {
        String result;
        try {
            result = IOUtils.toString(resource);
        }catch (IOException ex) {
            throw new RuntimeException(ex);
        }finally {
            IOUtils.closeQuietly(resource);
        }
        return StringUtils.trimToEmpty(result);
    }

    /**
     * If the <code>resource</code> represents a classpath archive (i.e. jar, zip, etc), this method will retrieve all
     * contents from that resource as a List of relative paths (relative to the archive base). Otherwise an empty List
     * will be returned.
     *
     * @param resource the resource from which to retrieve the contents
     * @return a list of Strings containing the names of every nested resource found in this resource.
     */
    public static List<String> getClassPathArchiveContents(final URL resource)
    {
        final List<String> contents = new ArrayList<String>();
        if (isArchive(resource))
        {
            final ZipFile archive = getArchive(resource);
            if (archive != null)
            {
                for (final Enumeration<? extends ZipEntry> entries = archive.entries(); entries.hasMoreElements();)
                {
                    final ZipEntry entry = entries.nextElement();
                    contents.add(entry.getName());
                }
                try
                {
                    archive.close();
                }
                catch (IOException ex)
                {
                    // Ignore
                }
            }
        }
        return contents;
    }

    /**
     * If this <code>resource</code> happens to be a directory, it will load the contents of that directory into a
     * List and return the list of names relative to the given <code>resource</code> (otherwise it will return an empty
     * List).
     *
     * @param resource the resource from which to retrieve the contents
     * @param levels the number of levels to step down if the resource ends up being a directory (if its an artifact,
     *               levels will be ignored).
     * @return a list of Strings containing the names of every nested resource found in this resource.
     */
    public static List<String> getDirectoryContents(
        final URL resource,
        final int levels)
    {
        return getDirectoryContents(resource, levels, true);
    }

    /**
     * The character used for substituting whitespace in paths.
     */
    private static final String PATH_WHITESPACE_CHARACTER = "%20";

    /**
     * Replaces any escape characters in the given file path with their
     * counterparts.
     *
     * @param filePath the path of the file to unescape.
     * @return the unescaped path.
     */
    public static String unescapeFilePath(String filePath)
    {
        if (StringUtils.isNotBlank(filePath))
        {
            filePath = filePath.replaceAll(PATH_WHITESPACE_CHARACTER, " ");
        }
        return filePath;
    }

    /**
     * If this <code>resource</code> happens to be a directory, it will load the contents of that directory into a
     * List and return the list of names relative to the given <code>resource</code> (otherwise it will return an empty
     * List).
     *
     * @param resource the resource from which to retrieve the contents
     * @param levels the number of levels to step down if the resource ends up being a directory (if its an artifact,
     *               levels will be ignored).
     * @param includeSubdirectories whether or not to include subdirectories in the contents.
     * @return a list of Strings containing the names of every nested resource found in this resource.
     */
    public static List<String> getDirectoryContents(
        final URL resource,
        final int levels,
        boolean includeSubdirectories)
    {
        final List<String> contents = new ArrayList<String>();
        if (resource != null)
        {
            // - create the file and make sure we remove any path white space characters
            final File fileResource = new File(unescapeFilePath(resource.getFile()));
            if (fileResource.isDirectory())
            {
                File rootDirectory = fileResource;
                for (int ctr = 0; ctr < levels; ctr++)
                {
                    rootDirectory = rootDirectory.getParentFile();
                }
                final File pluginDirectory = rootDirectory;
                loadFiles(
                    pluginDirectory,
                    contents,
                    includeSubdirectories);

                // - remove the root path from each file
                for (final ListIterator<String> iterator = contents.listIterator(); iterator.hasNext();)
                {
                    final String filePath = iterator.next();
                    iterator.set(
                        StringUtils.replace(
                            filePath.replace(
                                '\\',
                                '/'),
                            pluginDirectory.getPath().replace(
                                '\\',
                                '/') + '/',
                            ""));
                }
            }
        }
        return contents;
    }

    /**
     * Loads all files find in the <code>directory</code> and adds them to the <code>fileList</code>.
     *
     * @param directory the directory from which to load all files.
     * @param fileList  the Collection of files to which we'll add the found files.
     * @param includeSubdirectories whether or not to include sub directories when loading the files.
     */
    private static void loadFiles(
        final File directory,
        final Collection<String> fileList,
        boolean includeSubdirectories)
    {
        final Collection<File> lAllFiles = loadFiles(directory, includeSubdirectories);
        for (File file : lAllFiles)
        {
            fileList.add(file.getPath());
        }
    }

    /**
     * Loads all files find in the <code>directory</code> and returns them as Collection
     *
     * @param directory the directory from which to load all files.
     * @param includeSubdirectories whether or not to include sub directories when loading the files.
     * @return Collection with all files found in the directory
     */
    private static Collection<File> loadFiles(
        final File directory,
        boolean includeSubdirectories)
    {
        Collection<File> result = Collections.emptyList();
        if(null!=directory && directory.exists())
        {
            result = FileUtils.listFiles(
                    directory.isDirectory()? directory : directory.getParentFile(),
                    TrueFileFilter.INSTANCE,
                    includeSubdirectories ? TrueFileFilter.INSTANCE : null);
        }
        return result;
    }

    /**
     * Returns true/false on whether or not this <code>resource</code> represents an archive or not (i.e. jar, or zip,
     * etc).
     *
     * @param resource
     * @return true if its an archive, false otherwise.
     */
    public static boolean isArchive(final URL resource)
    {
        return resource != null && resource.toString().startsWith(ARCHIVE_PREFIX);
    }

    private static final String URL_DECODE_ENCODING = "UTF-8";

    /**
     * If this <code>resource</code> is an archive file, it will return the resource as an archive.
     *
     * @param resource
     * @return the archive as a ZipFile
     */
    public static ZipFile getArchive(final URL resource)
    {
        try
        {
            ZipFile archive = null;
            if (resource != null)
            {
                String resourceUrl = resource.toString();
                resourceUrl = resourceUrl.replaceFirst(
                        ARCHIVE_PREFIX,
                        "");
                final int entryPrefixIndex = resourceUrl.indexOf('!');
                if (entryPrefixIndex != -1)
                {
                    resourceUrl = resourceUrl.substring(
                            0,
                            entryPrefixIndex);
                }
                resourceUrl = URLDecoder.decode(new URL(resourceUrl).getFile(), URL_DECODE_ENCODING);
                File zipFile = new File(resourceUrl);
                if (zipFile.exists())
                {
                    archive = new ZipFile(resourceUrl);
                }
                else
                {
                    // ZipFile doesn't give enough detail about missing file
                    throw new FileNotFoundException("ResourceUtils.getArchive " + resourceUrl + " NOT FOUND.");
                }
            }
            return archive;
        }
        // Don't unnecessarily wrap RuntimeException
        catch (final RuntimeException ex)
        {
            throw ex;
        }
        // But don't require Exception declaration either.
        catch (final Throwable throwable)
        {
            throw new RuntimeException(throwable);
        }
    }

    /**
     * Loads the file resource and returns the contents as a String.
     *
     * @param resourceName the name of the resource.
     * @return String
     */
    public static String getContents(final String resourceName)
    {
        return getContents(getResource(resourceName));
    }

    /**
     * Takes a className as an argument and returns the URL for the class.
     *
     * @param className name of class
     * @return java.net.URL
     */
    public static URL getClassResource(final String className)
    {
        ExceptionUtils.checkEmpty(
            "className",
            className);
        return getResource(getClassNameAsResource(className));
    }

    /**
     * Gets the class name as a resource.
     *
     * @param className the name of the class.
     * @return the class name as a resource path.
     */
    private static String getClassNameAsResource(final String className)
    {
        return className.replace('.','/') + ".class";
    }

    /**
     * <p>
     * Retrieves a resource from an optionally given <code>directory</code> or from the package on the classpath. </p>
     * <p>
     * If the directory is specified and is a valid directory then an attempt at finding the resource by appending the
     * <code>resourceName</code> to the given <code>directory</code> will be made, otherwise an attempt to find the
     * <code>resourceName</code> directly on the classpath will be initiated. </p>
     *
     * @param resourceName the name of a resource
     * @param directory the directory location
     * @return the resource url
     */
    public static URL getResource(
        final String resourceName,
        final String directory)
    {
        ExceptionUtils.checkEmpty(
            "resourceName",
            resourceName);

        if (directory != null)
        {
            final File file = new File(directory, resourceName);
            if (file.exists())
            {
                try
                {
                    return file.toURI().toURL();
                }
                catch (final MalformedURLException exception)
                {
                    // - ignore, we just try to find the resource on the classpath
                }
            }
        }
        return getResource(resourceName);
    }

    /**
     * Makes the directory for the given location if it doesn't exist.
     *
     * @param location the location to make the directory.
     */
    public static void makeDirectories(final String location)
    {
        final File file = new File(location);
        makeDirectories(file);
    }

    /**
     * Makes the directory for the given location if it doesn't exist.
     *
     * @param location the location to make the directory.
     */
    public static void makeDirectories(final File location)
    {
        final File parent = location.getParentFile();
        if (parent != null)
        {
            parent.mkdirs();
        }
    }

    /**
     * Gets the time as a <code>long</code> when this <code>resource</code> was last modified.
     * If it can not be determined <code>0</code> is returned.
     *
     * @param resource the resource from which to retrieve
     *        the last modified time.
     * @return the last modified time or 0 if it couldn't be retrieved.
     */
    public static long getLastModifiedTime(final URL resource)
    {
        long lastModified;
        try
        {
            final File file = new File(resource.getFile());
            if (file.exists())
            {
                lastModified = file.lastModified();
            }
            else
            {
                URLConnection uriConnection = resource.openConnection();
                lastModified = uriConnection.getLastModified();

                // - we need to set the urlConnection to null and explicitly
                //   call garbage collection, otherwise the JVM won't let go
                //   of the URL resource
//                uriConnection = null;
//                System.gc();
                IOUtils.closeQuietly(uriConnection.getInputStream());
            }
        }
        catch (final Exception exception)
        {
            lastModified = 0;
        }
        return lastModified;
    }

    /**
     * <p>
     * Retrieves a resource from an optionally given <code>directory</code> or from the package on the classpath.
     * </p>
     * <p>
     * If the directory is specified and is a valid directory then an attempt at finding the resource by appending the
     * <code>resourceName</code> to the given <code>directory</code> will be made, otherwise an attempt to find the
     * <code>resourceName</code> directly on the classpath will be initiated. </p>
     *
     * @param resourceName the name of a resource
     * @param directory the directory location
     * @return the resource url
     */
    public static URL getResource(
        final String resourceName,
        final URL directory)
    {
        String directoryLocation = null;
        if (directory != null)
        {
            directoryLocation = directory.getFile();
        }
        return getResource(
            resourceName,
            directoryLocation);
    }

    /**
     * Attempts to construct the given <code>path</code>
     * to a URL instance. If the argument cannot be resolved as a resource
     * on the file system this method will attempt to locate it on the
     * classpath.
     *
     * @param path the path from which to construct the URL.
     * @return the constructed URL or null if one couldn't be constructed.
     */
    public static URL toURL(String path)
    {
        URL url = null;
        if (path != null)
        {
            path = ResourceUtils.normalizePath(path);

            try
            {
                if (path.startsWith(CLASSPATH_PREFIX))
                {
                    url = ResourceUtils.resolveClasspathResource(path);
                }
                else
                {
                    final File file = new File(path);
                    url = file.exists() ? file.toURI().toURL() : new URL(path);
                }
            }
            catch (MalformedURLException exception)
            {
                // ignore means no protocol was specified
            }
        }
        return url;
    }

    /**
     * Resolves a URL to a classpath resource, this method will treat occurrences of the exclamation mark
     * similar to what {@link URL} does with the <code>jar:file</code> protocol.
     * <p>
     * Example: <code>my/path/to/some.zip!/file.xml</code> represents a resource <code>file.xml</code>
     * that is located in a ZIP file on the classpath called <code>my/path/to/some.zip</code>
     * <p>
     * It is possible to have nested ZIP files, example:
     * <code>my/path/to/first.zip!/subdir/second.zip!/file.xml</code>.
     * <p>
     * <i>Please note that the extension of the ZIP file can be anything,
     * but in the case the extension is <code>.jar</code> the JVM will automatically unpack resources
     * one level deep and put them all on the classpath</i>
     *
     * @param path the name of the resource to resolve to a URL, potentially nested in ZIP archives
     * @return a URL pointing the resource resolved from the argument path
     *      or <code>null</code> if the argument is <code>null</code> or impossible to resolve
     */
    public static URL resolveClasspathResource(String path)
    {
        URL urlResource = null;
        if (path.startsWith(CLASSPATH_PREFIX))
        {
            path = path.substring(CLASSPATH_PREFIX.length(), path.length());

            // - the value of the following constant is -1 of no nested resources were specified,
            //   otherwise it points to the location of the first occurrence
            final int nestedPathOffset = path.indexOf("!/");

            // - take the part of the path that is not nested (may be all of it)
            final String resourcePath = nestedPathOffset == -1 ? path : path.substring(0, nestedPathOffset);
            final String nestingPath = nestedPathOffset == -1 ? "" : path.substring(nestedPathOffset);

            // - use the new path to load a URL from the classpath using the context class loader for this thread
            urlResource = Thread.currentThread().getContextClassLoader().getResource(resourcePath);

            // - at this point the URL might be null in case the resource was not found
            if (urlResource == null)
            {
                if (logger.isDebugEnabled())
                {
                    logger.debug("Resource could not be located on the classpath: " + resourcePath);
                }
            }
            else
            {
                try
                {
                    // - extract the filename from the entire resource path
                    final int fileNameOffset = resourcePath.lastIndexOf('/');
                    final String resourceFileName =
                        fileNameOffset == -1 ? resourcePath : resourcePath.substring(fileNameOffset + 1);

                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Creating temporary copy on the file system of the classpath resource");
                    }
                    final File fileSystemResource = File.createTempFile(resourceFileName, null);
                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Temporary file will be deleted on VM exit: " + fileSystemResource.getAbsolutePath());
                    }
                    fileSystemResource.deleteOnExit();
                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Copying classpath resource contents into temporary file");
                    }
                    writeUrlToFile(urlResource, fileSystemResource.toString());

                    // - count the times the actual resource to resolve has been nested
                    final int nestingCount = StringUtils.countMatches(path, "!/");
                    // - this buffer is used to construct the URL spec to that specific resource
                    final StringBuilder buffer = new StringBuilder();
                    for (int ctr = 0; ctr < nestingCount; ctr++)
                    {
                        buffer.append(ARCHIVE_PREFIX);
                    }
                    buffer.append(FILE_PREFIX).append(fileSystemResource.getAbsolutePath()).append(nestingPath);
                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Constructing URL to " +
                            (nestingCount > 0 ? "nested" : "") + " resource in temporary file");
                    }

                    urlResource = new URL(buffer.toString());
                }
                catch (final IOException exception)
                {
                    logger.warn("Unable to resolve classpath resource", exception);
                    // - impossible to properly resolve the path into a URL
                    urlResource = null;
                }
            }
        }
        return urlResource;
    }

    /**
     * Writes the URL contents to a file specified by the fileLocation argument.
     *
     * @param url the URL to read
     * @param fileLocation the location which to write.
     * @throws IOException if error writing file
     */
    public static void writeUrlToFile(final URL url, final String fileLocation)
        throws IOException
    {
        ExceptionUtils.checkNull(
            "url",
            url);
        ExceptionUtils.checkEmpty(
            "fileLocation",
            fileLocation);

        final File lOutputFile = new File(fileLocation);
        makeDirectories(lOutputFile);
        FileUtils.copyURLToFile(url, lOutputFile);
    }

    /**
     * Indicates whether or not the given <code>url</code> is a file.
     *
     * @param url the URL to check.
     * @return true/false
     */
    public static boolean isFile(final URL url)
    {
        return url != null && new File(url.getFile()).isFile();
    }

    /**
     * The forward slash character.
     */
    private static final String FORWARD_SLASH = "/";

    /**
     * Gets the contents of this directory and any of its sub directories based on the given <code>patterns</code>.
     * And returns absolute or relative paths depending on the value of <code>absolute</code>.
     *
     * @param url the URL of the directory.
     * @param absolute whether or not the returned content paths should be absolute (if
     *        false paths will be relative to URL).
     * @param patterns
     * @return a collection of paths.
     */
    public static List<String> getDirectoryContents(
        final URL url,
        boolean absolute,
        final String[] patterns)
    {
        List<String> contents = ResourceUtils.getDirectoryContents(
                url,
                0,
                true);

        // - first see if it's a directory
        if (!contents.isEmpty())
        {
            for (final ListIterator<String> iterator = contents.listIterator(); iterator.hasNext();)
            {
                String path = iterator.next();
                if (!matchesAtLeastOnePattern(
                        path,
                        patterns))
                {
                    iterator.remove();
                }
                else if (absolute)
                {
                    path = url.toString().endsWith(FORWARD_SLASH) ? path : FORWARD_SLASH + path;
                    final URL resource = ResourceUtils.toURL(url + path);
                    if (resource != null)
                    {
                        iterator.set(resource.toString());
                    }
                }
            }
        }
        else // - otherwise handle archives (i.e. jars, etc).
        {
            final String urlAsString = url.toString();
            final String delimiter = "!/";
            final String archivePath = urlAsString.replaceAll(
                    delimiter + ".*",
                    delimiter);
            contents = ResourceUtils.getClassPathArchiveContents(url);
            for (final ListIterator<String> iterator = contents.listIterator(); iterator.hasNext();)
            {
                final String relativePath = iterator.next();
                final String fullPath = archivePath + relativePath;
                if (!fullPath.startsWith(urlAsString) || fullPath.equals(urlAsString + FORWARD_SLASH))
                {
                    iterator.remove();
                }
                else if (!matchesAtLeastOnePattern(
                        relativePath,
                        patterns))
                {
                    iterator.remove();
                }
                else if (absolute)
                {
                    iterator.set(fullPath);
                }
            }
        }
        return contents;
    }

    /**
     * Indicates whether or not the given <code>path</code> matches on
     * one or more of the patterns defined within this class
     * returns true if no patterns are defined.
     *
     * @param path the path to match on.
     * @param patterns
     * @return true/false
     */
    public static boolean matchesAtLeastOnePattern(
        final String path,
        final String[] patterns)
    {
        boolean matches = (patterns == null || patterns.length == 0);
        if (!matches && patterns != null && patterns.length > 0)
        {
            final int patternNumber = patterns.length;
            for (int ctr = 0; ctr < patternNumber; ctr++)
            {
                final String pattern = patterns[ctr];
                if (PathMatcher.wildcardMatch(
                        path,
                        pattern))
                {
                    matches = true;
                    break;
                }
            }
        }
        return matches;
    }

    /**
     * Indicates whether or not the contents of the given <code>directory</code>
     * and any of its sub directories have been modified after the given <code>time</code>.
     *
     * @param directory the directory to check
     * @param time the time to check against
     * @return true/false
     */
    public static boolean modifiedAfter(
        long time,
        final File directory)
    {
        final Collection<File> files = ResourceUtils.loadFiles(directory, true);
        boolean changed = files.isEmpty();
        for (File file : files)
        {
            changed = file.lastModified() < time;
            if (changed)
            {
                break;
            }
        }
        return changed;
    }

    /**
     * The pattern used for normalizing paths paths with more than one back slash.
     */
    private static final String BACK_SLASH_NORMALIZATION_PATTERN = "\\\\+";

    /**
     * The pattern used for normalizing paths with more than one forward slash.
     */
    private static final String FORWARD_SLASH_NORMALIZATION_PATTERN = FORWARD_SLASH + '+';

    /**
     * Removes any extra path separators and converts all from back slashes
     * to forward slashes.
     *
     * @param path the path to normalize.
     * @return the normalizd path
     */
    public static String normalizePath(final String path)
    {
        return path != null
        ? path.replaceAll(
            BACK_SLASH_NORMALIZATION_PATTERN,
            FORWARD_SLASH).replaceAll(
            FORWARD_SLASH_NORMALIZATION_PATTERN,
            FORWARD_SLASH) : null;
    }

    /**
     * Takes a path and replaces the oldException with the newExtension.
     *
     * @param path the path to rename.
     * @param oldExtension the extension to rename from.
     * @param newExtension the extension to rename to.
     * @return the path with the new extension.
     */
    public static String renameExtension(
        final String path,
        final String oldExtension,
        final String newExtension)
    {
        ExceptionUtils.checkEmpty(
            "path",
            path);
        ExceptionUtils.checkNull(
            "oldExtension",
            oldExtension);
        ExceptionUtils.checkNull(
            "newExtension",
            newExtension);
        String newPath = path;
        final int oldExtensionIndex = path.lastIndexOf(oldExtension);
        if (oldExtensionIndex != -1)
        {
            newPath = path.substring(
                    0,
                    oldExtensionIndex) + newExtension;
        }
        return newPath;
    }
}