/*
 * 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.net.*;
import java.util.*;
import java.security.*;
import java.io.*;
import java.util.jar.*;
import java.util.jar.Attributes.Name;
import edu.emory.mathcs.util.classloader.jar.*;

/**
 * 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
 */
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 (java.security.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 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        java.lang.System#loadLibrary(java.lang.String)
     * @see        java.lang.System#mapLibraryName(java.lang.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());
//        }
    }
}
