
package uk.co.wingpath.util;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.net.*;

/**
* This class extends {@link URLClassLoader} to enable loading from JAR files
* and native libraries contained as resources within a JAR file.
* <p>The resources are copied into a temporary directory in order to make
* them available to the loader.
* <p>The class may also be used to copy other resources (e.g. HTML or image
* files) to the temporary directory.
* <p>See {@link Installer} for an example of the use of JarLoader.
*/
public class JarLoader
    extends URLClassLoader
{
    private File tempDir;
    private final Map<String,String> libMap;

    private JarLoader (ClassLoader parent, URL [] urls, File tempDir,
        Map<String,String> libMap)
    {
        super (urls, parent);
        this.tempDir = tempDir;
        this.libMap = libMap;
    }

/*
    // Override loadClass to provide debug printing.
    @Override
    protected Class<?> loadClass(String name, boolean resolve)
          throws ClassNotFoundException
    {
System.out.println ("loadClass " + name + " " + resolve);
        Class<?> cl = super.loadClass (name, resolve);
System.out.println (" -> " + cl);
        return cl;
    }

    // Override loadClass to provide debug printing.
    @Override
    public Class<?> loadClass(String name)
          throws ClassNotFoundException
    {
System.out.println ("loadClass " + name);
        Class<?> cl = super.loadClass (name);
System.out.println (" -> " + cl);
        return cl;
    }

    // Override findClass to provide debug printing.
    @Override
    protected Class<?> findClass(String name)
          throws ClassNotFoundException
    {
System.out.println ("findClass " + name);
        Class<?> cl = super.findClass (name);
System.out.println (" -> " + cl);
        return cl;
    }
*/

    // Override findLibrary so that we can find the libraries we have copied
    // into the temporary directory.
    @Override
    protected String findLibrary (String libname)
    {
// System.out.println ("findLibrary " + libname);
        String pathName = libMap.get (libname);
        if (pathName != null)
        {
// System.out.println ("  libMap.get " + pathName);
            return pathName;
        }
// System.out.println ("System.mapLibraryName " + System.mapLibraryName (libname));
        pathName = libMap.get (System.mapLibraryName (libname));
        if (pathName != null)
        {
// System.out.println ("  libMap.get " + pathName);
            return pathName;
        }

        pathName = super.findLibrary (libname);
        if (pathName != null)
        {
// System.out.println ("  super.findLibrary " + pathName);
            return pathName;
        }
        return null;
    }

    /**
    * Returns the temporary directory containing the JAR/library files.
    * <p>This may be used to access any other resources that have been copied
    * from the JAR file.
    * @return the temporary directory.
    */
    public File getTempDir ()
    {
        return tempDir;
    }

    /**
    * Gets the name of the JAR file that the program was loaded from.
    * @return the JAR file name.
    */
    private static String getJarFilename (ClassLoader sysLoader)
    {
        // Get resource name of this class.
        String className = JarLoader.class.getName ().replace ('.', '/') +
            ".class";
// System.out.println ("className " + className);

        URL url = sysLoader.getResource (className);
        if (url == null)
            return null;
        try
        {
            String filename = URLDecoder.decode (
                url.getPath ().replace ("+", "%2B"), "UTF-8");
// System.out.println ("getJarFilename " + filename);
            if (filename.startsWith ("file:"))
                filename = filename.substring (5);
// System.out.println ("getJarFilename " + filename);
            int excl = filename.indexOf ("!");
            if (excl >= 0)
                filename = filename.substring (0, excl);
// System.out.println ("getJarFilename " + filename);
            return filename;
        }
        catch (Exception e)
        {
            return null;
        }
    }

    /**
    * Gets a list of the resources in the JAR file that the program was
    * loaded from.
    * @param sysLoader the system class loader.
    * @return list of resources.
    */
    private static List<String> getResources (ClassLoader sysLoader)
    {
        ArrayList<String> resources = new ArrayList<String> ();

        try
        {
            // Get name of JAR file that this class was loaded from.
            String filename = getJarFilename (sysLoader);

            // Get names of all entries in the JAR file.
            ZipFile zipFile = new ZipFile (filename);
            Enumeration<? extends ZipEntry> entries = zipFile.entries ();

            while (entries.hasMoreElements ())
            {
                ZipEntry entry = entries.nextElement ();
                String name = entry.getName ();
// System.out.println ("name " + name);
                if (!name.endsWith ("/"))
                    resources.add (name);
            }

            zipFile.close ();
        }
        catch (IOException e)
        {
// System.out.println ("getResources IOException " + e.getMessage ());
        }

        return resources;
    }

    /**
    * Constructs a {@code JarLoader}.
    * A temporary directory is created, and each specified library or JAR file
    * (or other resource) is copied from the containing JAR file to the
    * temporary directory.
    * <p>If a directory is specified, it is recursively copied to the
    * temporary directory.
    * <p>Files with the suffix '.jar' are added to the classpath, and files
    * with the suffix '.dll' or '.so' are made available as native libraries
    * to the class-loader.
    * <p>Any directory hierarchy is preserved beneath the temporary directory.
    * @param paths the pathnames of the resources to be extracted.
    * @return the loader.
    * @throws RuntimeException if anything goes wrong.
    */
    public static JarLoader create (Collection<String> paths)
    {
        // Create a temporary directory to hold the copied JAR files and
        // native libraries.
        File tempDir = FileUtils.createTempDir ();

        // Get the system class loader.
        // The loader that we are constructing will replace the system class
        // loader, which (hopefully) was only searching the containing JAR file.
        // Our loader will also search the containing JAR file, but will
        // additionally search any JAR files copied from the containing JAR
        // file. It will also know how to find any copied native libraries.
        ClassLoader sysLoader = ClassLoader.getSystemClassLoader ();
// System.out.println ("sysLoader " + sysLoader);
// System.out.println ("sysLoader parent " + sysLoader.getParent ());

        // Get list of resources in the JAR file.
        List<String> resources = getResources (sysLoader);

        // Copy resources from the JAR file to the temporary directory,
        // building a list of JAR URLs and a map of native library names
        // to their filenames as we do so.
        ArrayList<URL> jars = new ArrayList<URL> ();
        Map<String,String> libMap = new TreeMap<String,String> ();
        try
        {
            jars.add (new File (getJarFilename (sysLoader)).toURI ().toURL ());
        }
        catch (Exception e)
        {
        }

        for (String jarPath : paths)
        {
// System.out.println ("jarPath " + jarPath);
            for (String resource : resources)
            {
// System.out.println ("resource " + resource);
                if (resource.equals (jarPath) ||
                    (resource.startsWith (jarPath) &&
                        resource.charAt (jarPath.length ()) == '/'))
                {
                    String filePath = resource.replace ('/',
                        File.separatorChar);
// System.out.println ("filePath " + filePath);
                    File file = new File (tempDir, filePath);
                    try
                    {
                        FileUtils.copyResource (resource, file);
                    }
                    catch (IOException e)
                    {
// System.out.println ("copyResource IOException " + e.getMessage ());
                        throw new RuntimeException ("Can't copy " + resource +
                            " to temporary directory", e);
                    }
                    if (resource.endsWith (".jar"))
                    {
                        try
                        {
                            jars.add (file.toURI ().toURL ());
                        }
                        catch (MalformedURLException e)
                        {
                        }
                    }
                    else if (resource.endsWith (".so") ||
                        resource.endsWith (".dll"))
                    {
                        String libName =
                            resource.substring (resource.lastIndexOf ('/') + 1);
// System.out.println ("libName " + libName);
                        libMap.put (libName, file.getAbsolutePath ());
                    }
                }
            }
        }

        // Construct an array of the URLs to be used by the new class loader.
        URL [] newUrls = new URL [jars.size ()];

        for (int i = 0 ; i < jars.size () ; i++)
            newUrls [i] = jars.get (i);

// System.out.println ("URLs:");
// for (URL url : newUrls)
// System.out.println ("   " + url);

        // Finally create a JarLoader that uses all this stuff.
        return new JarLoader (sysLoader.getParent (), newUrls, tempDir, libMap);
    }
}

