/* * Written by Dawid Kurzyniec and released to the public domain, as explained * at http://creativecommons.org/licenses/publicdomain */ package edu.emory.mathcs.util.classloader; import java.io.File; import java.io.IOException; import java.net.*; import java.security.*; import java.util.Enumeration; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.Manifest; /** * Equivalent of java.net.URLClassloader but without bugs related to ill-formed * URLs and with customizable JAR caching policy. The standard URLClassLoader * accepts URLs containing spaces and other characters which are forbidden in * the URI syntax, according to the RFC 2396. * As a workaround to this problem, Java escapes and un-escapes URLs in various * arbitrary places; however, this is inconsistent and leads to numerous * problems with URLs referring to local files with spaces in the path. SUN * acknowledges the problem, but refuses to modify the behavior for * compatibility reasons; see Java Bug Parade 4273532, 4466485. * <p> * Additionally, the JAR caching policy used by * URLClassLoader is system-wide and inflexible: once downloaded JAR files are * never re-downloaded, even if one creates a fresh instance of the class * loader that happens to have the same URL in its search path. In fact, that * policy is a security vulnerability: it is possible to crash any URL class * loader, thus affecting potentially separate part of the system, by creating * URL connection to one of the URLs of that class loader search path and * closing the associated JAR file. * See Java Bug Parade 4405789, 4388666, 4639900. * <p> * This class avoids these problems by 1) using URIs instead of URLs for the * search path (thus enforcing strict syntax conformance and defining precise * escaping semantics), and 2) using custom URLStreamHandler which ensures * per-classloader JAR caching policy. * * @author Dawid Kurzyniec * @version 1.0 * * @see java.io.File#toURL * @see java.io.File#toURI */ @SuppressWarnings("unchecked") public class URIClassLoader extends URLClassLoader { final URIResourceFinder finder; final AccessControlContext acc; /** * Creates URIClassLoader with the specified search path. * @param uris the search path */ public URIClassLoader(URI[] uris) { this(uris, (URLStreamHandler)null); } /** * Creates URIClassLoader with the specified search path. * @param uris the search path * @param jarHandler stream handler for JAR files; implements caching policy */ public URIClassLoader(URI[] uris, URLStreamHandler jarHandler) { super(new URL[0]); this.finder = new URIResourceFinder(uris, jarHandler); this.acc = AccessController.getContext(); } /** * Creates URIClassLoader with the specified search path and parent class * loader. * @param uris the search path * @param parent the parent class loader. */ public URIClassLoader(URI[] uris, ClassLoader parent) { this(uris, parent, null); } /** * Creates URIClassLoader with the specified search path and parent class * loader. * @param uris the search path * @param parent the parent class loader. * @param jarHandler stream handler for JAR files; implements caching policy */ public URIClassLoader(URI[] uris, ClassLoader parent, URLStreamHandler jarHandler) { super(new URL[0], parent); this.finder = new URIResourceFinder(uris, jarHandler); this.acc = AccessController.getContext(); } /** * Add specified URI at the end of the search path. * @param uri the URI to add */ protected void addURI(URI uri) { finder.addURI(uri); } /** * Add specified URL at the end of the search path. * @param url the URL to add * @deprecated use addURI */ protected void addURL(URL url) { finder.addURI(URI.create(url.toExternalForm())); } public URL[] getURLs() { return (URL[])finder.getUrls().clone(); } // public URL[] getAllResolvedURLs() { // return finder.getResolvedSearchPath(); // } /** * Finds and loads the class with the specified name. * * @param name the name of the class * @return the resulting class * @exception ClassNotFoundException if the class could not be found */ protected Class findClass(final String name) throws ClassNotFoundException { try { return (Class) AccessController.doPrivileged(new PrivilegedExceptionAction() { public Object run() throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); ResourceHandle h = finder.getResource(path); if (h != null) { try { return defineClass(name, h); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { throw new ClassNotFoundException(name); } } }, acc); } catch (PrivilegedActionException pae) { throw (ClassNotFoundException)pae.getException(); } } protected Class defineClass(String name, ResourceHandle h) throws IOException { int i = name.lastIndexOf('.'); URL url = h.getCodeSourceURL(); if (i != -1) { // check package String pkgname = name.substring(0, i); // check if package already loaded Package pkg = getPackage(pkgname); Manifest man = h.getManifest(); if (pkg != null) { // package found, so check package sealing boolean ok; if (pkg.isSealed()) { // verify that code source URLs are the same ok = pkg.isSealed(url); } else { // make sure we are not attempting to seal the package // at this code source URL ok = (man == null) || !isSealed(pkgname, man); } if (!ok) { throw new SecurityException("sealing violation: " + name); } } else { // package not yet defined if (man != null) { definePackage(pkgname, man, url); } else { definePackage(pkgname, null, null, null, null, null, null, null); } } } // now read the class bytes and define the class byte[] b = h.getBytes(); java.security.cert.Certificate[] certs = h.getCertificates(); CodeSource cs = new CodeSource(url, certs); return defineClass(name, b, 0, b.length, cs); } /** * returns true if the specified package name is sealed according to the * given manifest. */ private boolean isSealed(String name, Manifest man) { String path = name.replace('.', '/').concat("/"); Attributes attr = man.getAttributes(path); String sealed = null; if (attr != null) { sealed = attr.getValue(Name.SEALED); } if (sealed == null) { if ((attr = man.getMainAttributes()) != null) { sealed = attr.getValue(Name.SEALED); } } return "true".equalsIgnoreCase(sealed); } /** * Finds the resource with the specified name. * * @param name the name of the resource * @return a <code>URL</code> for the resource, or <code>null</code> * if the resource could not be found. */ public URL findResource(final String name) { return (URL) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return finder.findResource(name); } }, acc); } /** * Returns an Enumeration of URLs representing all of the resources * having the specified name. * * @param name the resource name * @exception java.io.IOException if an I/O exception occurs * @return an <code>Enumeration</code> of <code>URL</code>s */ public Enumeration findResources(final String name) throws IOException { return (Enumeration) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return finder.findResources(name); } }, acc); } /** * Returns the absolute path name of a native library. The VM * invokes this method to locate the native libraries that belong * to classes loaded with this class loader. If this method returns * <code>null</code>, the VM searches the library along the path * specified as the <code>java.library.path</code> property. * This method invoke {@link #getLibraryHandle} method to find handle * of this library. If the handle is found and its URL protocol is "file", * the system-dependent absolute library file path is returned. * Otherwise this method returns null. <p> * * Subclasses can override this method to provide specific approaches * in library searching. * * @param libname the library name * @return the absolute path of the native library * @see System#loadLibrary(String) * @see System#mapLibraryName(String) */ protected String findLibrary(String libname) { ResourceHandle md = getLibraryHandle(libname); if (md == null) return null; URL url = md.getURL(); if (!"file".equals(url.getProtocol())) return null; return new File(URI.create(url.toString())).getPath(); } // // /** // * Gets the ResourceHandle object for the specified loaded class. // * // * @param clazz the Class // * @return the ResourceHandle of the Class // */ // protected ResourceHandle getClassHandle(Class clazz) // { // return null; // } /** * Finds the ResourceHandle object for the class with the specified name. * Unlike <code>findClass()</code>, this method does not load the class. * * @param name the name of the class * @return the ResourceHandle of the class */ protected ResourceHandle getClassHandle(final String name) { String path = name.replace('.', '/').concat(".class"); return getResourceHandle(path); } /** * Finds the ResourceHandle object for the resource with the specified name. * * @param name the name of the resource * @return the ResourceHandle of the resource */ protected ResourceHandle getResourceHandle(final String name) { return (ResourceHandle) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return finder.getResource(name); } }, acc); } /** * Finds the ResourceHandle object for the native library with the specified * name. * The library name must be '/'-separated path. The last part of this * path is substituted by its system-dependent mapping (using * {@link System#mapLibraryName(String)} method). Next, the * <code>ResourceFinder</code> is used to look for the library as it * was ordinary resource. <p> * * Subclasses can override this method to provide specific approaches * in library searching. * * @param name the name of the library * @return the ResourceHandle of the library */ protected ResourceHandle getLibraryHandle(final String name) { int idx = name.lastIndexOf('/'); String path; String simplename; if (idx == -1) { path = ""; simplename = name; } else if (idx == name.length()-1) { // name.endsWith('/') throw new IllegalArgumentException(name); } else { path = name.substring(0, idx+1); // including '/' simplename = name.substring(idx+1); } return getResourceHandle(path + System.mapLibraryName(simplename)); } /** * Returns an Enumeration of ResourceHandle objects representing all of the * resources having the specified name. * * @param name the name of the resource * @return the ResourceHandle of the resource */ protected Enumeration getResourceHandles(final String name) { return (Enumeration) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return finder.getResources(name); } }, acc); } protected URLStreamHandler getJarHandler() { return finder.loader.jarHandler; } private static class URIResourceFinder implements ResourceFinder { URL[] urls; final ResourceLoader loader; public URIResourceFinder(URI[] uris, URLStreamHandler jarHandler) { try { this.loader = (jarHandler != null) ? new ResourceLoader(jarHandler) : new ResourceLoader(); URL[] urls = new URL[uris.length]; for (int i = 0; i < uris.length; i++) { urls[i] = uris[i].toURL(); } this.urls = urls; } catch (MalformedURLException e) { throw new IllegalArgumentException(e.getMessage()); } } public synchronized void addURI(URI uri) { try { URL url = uri.toURL(); int len = this.urls.length; URL[] urls = new URL[len + 1]; System.arraycopy(this.urls, 0, urls, 0, len); urls[len] = url; this.urls = urls; } catch (MalformedURLException e) { throw new IllegalArgumentException(e.getMessage()); } } private synchronized final URL[] getUrls() { return urls; } public ResourceHandle getResource(String name) { return loader.getResource(getUrls(), name); } public Enumeration getResources(String name) { return loader.getResources(getUrls(), name); } public URL findResource(String name) { return loader.findResource(getUrls(), name); } public Enumeration findResources(String name) { return loader.findResources(getUrls(), name); } // public URL[] getResolvedSearchPath() { // return loader.getResolvedSearchPath(getUrls()); // } } }