/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.archive.AbstractArchiveFile; import com.mucommander.commons.file.protocol.local.LocalFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; /** * This class provides methods to load resources located within the reach of a <code>ClassLoader</code>. Those resources * can reside either in a JAR file or in a local directory that are in the classpath -- all methods of this class are * agnostic to either type of location. * * <p>The <code>getResourceAsURL</code> and <code>getResourceAsStream</code> methods are akin to those of * <code>java.lang.Class</code> and <code>java.lang.ClassLoader</code>, except that they are not sensitive to the * presence of a leading forward-slash separator in the resource path, and that they allow the search to be limited * to a particular classpath location.</p> * * <p>But the real fun lies in the <code>getResourceAsFile</code> methods, which allow to manipulate resources as * regular files -- again, whether they be in a regular directory or in a JAR file. Likewise, * the {@link #getRootPackageAsFile(Class)} allows to dynamically explore and manipulate the resource files contained * in a particular classpath's location.</p> * * @author Maxence Bernard */ public class ResourceLoader { private static final Logger LOGGER = LoggerFactory.getLogger(ResourceLoader.class); /** the default ClassLoader that is used by methods without a ClassLoader argument */ private static ClassLoader defaultClassLoader = ResourceLoader.class.getClassLoader(); /** * Returns the default <code>ClassLoader</code> that is used by methods without a <code>ClassLoader</code> argument. * This default <code>ClassLoader</code> is the one that loaded this class, and <b>not</b> the system <code>ClassLoader</code>. * * @return the default <code>ClassLoader</code> that is used by methods without a <code>ClassLoader</code> argument */ public static ClassLoader getDefaultClassLoader() { // We do not use the system class loader because it does not work with JNLP/Webstart applications. // A quote from a FAQ at java.sun.com: // // Java Web Start uses a user-level classloader to load all the application resources specified in the JNLP file. // This classloader implements the security model and the downloading model defined by the JNLP specification. // This is no different than how the AppletViewer or the Java Plug-In works. // This has the, unfortunate, side-effect that Class.forName will not find any resources that are defined in the // JNLP file. The same is true for looking up resources and classes using the system class loader // (ClassLoader.getSystemClassLoader). // return defaultClassLoader; } /** * This method is similar to {@link #getResourceAsURL(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located */ public static URL getPackageResourceAsURL(Package ppackage, String name) { return getPackageResourceAsURL(ppackage, name, getDefaultClassLoader(), null); } /** * This method is similar to {@link #getResourceAsURL(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * <code>null</code> to look for the resource in the whole class path. * @param name name of the resource in the package. This is a filename only, not a path. * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located * @see #getRootPackageAsFile(Class) */ public static URL getPackageResourceAsURL(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) { return ResourceLoader.getResourceAsURL(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile); } /** * Shorthand for {@link #getResourceAsURL(String, ClassLoader, AbstractFile)} called with the * {@link #getDefaultClassLoader() default class loader} and a <code>null</code> root package file. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located */ public static URL getResourceAsURL(String path) { return getResourceAsURL(path, getDefaultClassLoader(), null); } /** * Finds the resource with the given path and returns a URL pointing to its location, or <code>null</code> * if the resource couldn't be located. The given <code>ClassLoader</code> is used for locating the resource. * * <p>The given resource path must be forward slash (<code>/</code>) separated. It may or may not start with a * leading forward slash character, this doesn't affect the way it is interpreted.</p> * * <p>The <code>rootPackageFile</code> argument can be used to limit the scope of the search to a specific * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched. * This avoids potential ambiguities that can arise if the specified resource path exists in several locations. * If this parameter is <code>null</code>, the resource is looked up in the whole class path. In that case and if * several resources with the specified path exist, the choice of the resource to return is arbitrary.</p> * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * <code>null</code> to look for the resource in the whole class path. * @return a URL pointing to the resource, or <code>null</code> if the resource couldn't be located */ public static URL getResourceAsURL(String path, ClassLoader classLoader, AbstractFile rootPackageFile) { path = removeLeadingSlash(path); if(rootPackageFile==null) return classLoader.getResource(path); String separator = rootPackageFile.getSeparator(); String nativePath; if(separator.equals("/")) nativePath = path; else nativePath = path.replace("/", separator); try { // Iterate through all resources that match the given path, and return the one located inside the // given root package file. Enumeration<URL> resourceEnum = classLoader.getResources(path); String rootPackagePath = rootPackageFile.getAbsolutePath(); String resourcePath = rootPackageFile.getAbsolutePath(true)+nativePath; URL resourceURL; while(resourceEnum.hasMoreElements()) { resourceURL = resourceEnum.nextElement(); if("jar".equals(resourceURL.getProtocol())) { if(getJarFilePath(resourceURL).equals(rootPackagePath)) return resourceURL; } else { if(normalizeUrlPath(getDecodedURLPath(resourceURL)).equals(resourcePath)) return resourceURL; } } } catch(IOException e) { LOGGER.info("Failed to lookup resource {}", path, e); return null; } return null; } /** * This method is similar to {@link #getResourceAsStream(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located */ public static InputStream getPackageResourceAsStream(Package ppackage, String name) { return getPackageResourceAsStream(ppackage, name, getDefaultClassLoader(), null); } /** * This method is similar to {@link #getResourceAsStream(String, ClassLoader, AbstractFile)} except that it looks * for a resource with a given name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * <code>null</code> to look for the resource in the whole class path. * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located */ public static InputStream getPackageResourceAsStream(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) { return ResourceLoader.getResourceAsStream(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile); } /** * Shorthand for {@link #getResourceAsStream(String, ClassLoader, AbstractFile)} called with the * {@link #getDefaultClassLoader() default class loader} and a <code>null</code> root package file. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located */ public static InputStream getResourceAsStream(String path) { return getResourceAsStream(path, getDefaultClassLoader(), null); } /** * Finds the resource with the given path and returns an <code>InputStream</code> to read it, or <code>null</code> * if the resource couldn't be located. The given <code>ClassLoader</code> is used for locating the resource. * * <p>The given resource path must be forward slash (<code>/</code>) separated. It may or may not start with a * leading forward slash character, this doesn't affect the way it is interpreted.</p> * * <p>The <code>rootPackageFile</code> argument can be used to limit the scope of the search to a specific * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched. * This avoids potential ambiguities that can arise if the specified resource path exists in several locations. * If this parameter is <code>null</code>, the resource is looked up in the whole class path. In that case and if * several resources with the specified path exist, the choice of the resource to return is arbitrary.</p> * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * <code>null</code> to look for the resource in the whole class path. * @return an InputStream that allows to read the resource, or <code>null</code> if the resource couldn't be located */ public static InputStream getResourceAsStream(String path, ClassLoader classLoader, AbstractFile rootPackageFile) { try { URL resourceURL = getResourceAsURL(path, classLoader, rootPackageFile); return resourceURL==null?null:resourceURL.openStream(); } catch(IOException e) { return null; } } /** * This method is similar to {@link #getResourceAsFile(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located */ public static AbstractFile getPackageResourceAsFile(Package ppackage, String name) { return getPackageResourceAsFile(ppackage, name, getDefaultClassLoader(), null); } /** * This method is similar to {@link #getResourceAsFile(String, ClassLoader, AbstractFile)} except that it looks for * a resource with a given name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @param classLoader the ClassLoader used for locating the resource. May not be <code>null</code>. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * <code>null</code> to look for the resource in the whole class path. * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located */ public static AbstractFile getPackageResourceAsFile(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) { return ResourceLoader.getResourceAsFile(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile); } /** * Shorthand for {@link #getResourceAsFile(String, ClassLoader, AbstractFile)} called with the * {@link #getDefaultClassLoader() default class loader} and a <code>null</code> root package file. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located */ public static AbstractFile getResourceAsFile(String path) { return getResourceAsFile(removeLeadingSlash(path), getDefaultClassLoader(), null); } /** * Finds the resource with the given path and returns an {@link AbstractFile} that gives full access to it, * or <code>null</code> if the resource couldn't be located. The given <code>ClassLoader</code> is used for locating * the resource. * * <p>The given resource path must be forward slash (<code>/</code>) separated. It may or may not start with a * leading forward slash character, this doesn't affect the way it is interpreted.</p> * * <p>It is worth noting that this method may be slower than {@link #getResourceAsStream(String)} if * the resource is located inside a JAR file, because the Zip file headers will have to be parsed the first time * the archive is accessed. Therefore, the latter approach should be favored if the file is simply used for * reading the resource.</p> * * <p>The <code>rootPackageFile</code> argument can be used to limit the scope of the search to a specific * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched. * This avoids potential ambiguities that can arise if the specified resource path exists in several locations. * If this parameter is <code>null</code>, the resource is looked up in the whole class path. In that case and if * several resources with the specified path exist, the choice of the resource to return is arbitrary.</p> * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @param classLoader the ClassLoader is used for locating the resource * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * <code>null</code> to look for the resource in the whole class path. * @return an AbstractFile that represents the resource, or <code>null</code> if the resource couldn't be located */ public static AbstractFile getResourceAsFile(String path, ClassLoader classLoader, AbstractFile rootPackageFile) { if(classLoader==null) classLoader = getDefaultClassLoader(); path = removeLeadingSlash(path); URL aClassURL = getResourceAsURL(path, classLoader, rootPackageFile); if(aClassURL==null) return null; // no resource under that path if("jar".equals(aClassURL.getProtocol())) { try { return ((AbstractArchiveFile)FileFactory.getFile(getJarFilePath(aClassURL))).getArchiveEntryFile(path); } catch(Exception e) { // Shouldn't normally happen, unless the JAR file is corrupt or cannot be parsed by the file API return null; } } return FileFactory.getFile(getLocalFilePath(aClassURL)); } /** * Returns an {@link AbstractFile} to the root package of the given <code>Class</code>. For example, if the * specified <code>Class</code> is <code>java.lang.Object</code>'s, the returned file will be the Java runtime * JAR file, which on most platforms is <code>$JAVA_HOME/lib/jre/rt.jar</code>.<br> * The returned file can be used to list or manipulate all resource files contained in a particular classpath's * location, including the .class files. * * @param aClass the class for which to locate the root package. * @return an AbstractFile to the root package of the given <code>Class</code> */ public static AbstractFile getRootPackageAsFile(Class<?> aClass) { ClassLoader classLoader = aClass.getClassLoader(); if(classLoader==null) classLoader = getDefaultClassLoader(); String aClassRelPath = getRelativeClassPath(aClass); URL aClassURL = getResourceAsURL(aClassRelPath, classLoader, null); if(aClassURL==null) return null; // no resource under that path if("jar".equals(aClassURL.getProtocol())) return FileFactory.getFile(getJarFilePath(aClassURL)); String aClassPath = getLocalFilePath(aClassURL); return FileFactory.getFile(aClassPath.substring(0, aClassPath.length()-aClassRelPath.length())); } /** * Returns a path to the given package. The returned path is relative, forward slash-separated and does not end * with a trailing separator. For example, if the package <code>com.mucommander.commons.file</code> is passed, the returned * path will be <code>com/mucommander/commons/file</code>. * * @param ppackage the package for which to return a path * @return a path to the given package */ public static String getRelativePackagePath(Package ppackage) { return ppackage.getName().replace('.', '/'); } /** * Returns a path to the given class. The returned path is relative, forward slash-separated. For example, if the * class <code>com.mucommander.commons.file.AbstractFile</code> is passed, the returned path will be * <code>com/mucommander/commons/file/AbstractFile.class</code>. * * @param cclass the class for which to return a path * @return a path to the given package */ public static String getRelativeClassPath(Class<?> cclass) { return cclass.getName().replace('.', '/')+".class"; } ///////////////////// // Private methods // ///////////////////// /** * Extracts and returns the path to the JAR file from a URL that points to a resource inside a JAR file. * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}. * * @param url a URL that points to a resource inside a JAR file * @return returns the path to the JAR file */ private static String getJarFilePath(URL url) { // URL-decode the path String path = getDecodedURLPath(url); // Here are a couple examples of such paths: // file:/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Classes/classes.jar!/java/lang/Object.class // http://www.mucommander.com/webstart/nightly/mucommander.jar!/com/mucommander/RuntimeConstants.class int pos = path.indexOf(".jar!"); if(pos==-1) return path; // Strip out the part after ".jar" and normalize the path return normalizeUrlPath(path.substring(0, pos+4)); } /** * Extracts and returns the path to a local file represented by the given URL. * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}. * * @param url a URL that points to a resource inside a JAR file * @return returns the path to the JAR file */ private static String getLocalFilePath(URL url) { // Here's an example of such a path under Windows: // /C:/cygwin/home/Administrator/mucommander/tmp/compile/classes/ // URL-decode the path and normalize it return normalizeUrlPath(getDecodedURLPath(url)); } /** * Removes any leading slash from the given path and returns it. Does nothing if the path does not have a * leading path. * * @param path the path to normalize * @return the path without a leading slash */ private static String removeLeadingSlash(String path) { return PathUtils.removeLeadingSeparator(path, "/"); } /** * Normalizes the specified path issued from a <code>java.net.URL</code> and returns it. * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}. * * @param path the URL path to normalize * @return the normalized path */ private static String normalizeUrlPath(String path) { // Don't touch http/https URLs if(path.startsWith("http:") || path.startsWith("https:")) return path; // Remove the leading "file:" (if any) if(path.startsWith("file:")) path = path.substring(5, path.length()); // Under platforms that use root drives (Windows and OS/2), strip out the leading '/' if(LocalFile.hasRootDrives() && path.startsWith("/")) path = removeLeadingSlash(path); // Use the local file separator String separator = LocalFile.SEPARATOR; if(!"/".equals(separator)) path = path.replace("/", separator); return path; } /** * Returns the URL-decoded path of the given <code>java.net.URL</code>. The encoding used for URL-decoding is * <code>UTF-8</code>. * * @param url the URL for which to decode the path * @return the URL-decoded path of the given URL */ private static String getDecodedURLPath(URL url) { try { // Decode the URL's path which may contain URL-encoded characters such as %20 for spaces, or non-ASCII // characters. // Note: the Java API's javadoc doesn't specify which encoding has been used to encoded URL paths. // The only indication is in URLDecoder#decode(String, String) javadoc which says: // "The World Wide Web Consortium Recommendation states that UTF-8 should be used. Not doing so may // introduce incompatibilites." // Also Note that URLDecoder#decode(String) uses System.getProperty("file.encoding") as the default encoding, // using this value has been tested without luck under Mac OS X where the value equals "MacRoman" but // URL are actually encoded in UTF-8. The bottom line is that we blindly use UTF-8 to decode resource URLs. return URLDecoder.decode(url.getPath(), "UTF-8"); } catch(UnsupportedEncodingException e) { // This should never happen, UTF-8 is necessarily supported by the Java runtime return null; } } }