/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package javassist;

import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Vector;

import javassist.bytecode.ClassFile;

/**
 * The class loader for Javassist.
 *
 * <p>This is a sample class loader using <code>ClassPool</code>.
 * Unlike a regular class loader, this class loader obtains bytecode
 * from a <code>ClassPool</code>.
 *
 * <p>Note that Javassist can be used without this class loader; programmers
 * can define their own versions of class loader.  They can run
 * a program even without any user-defined class loader if that program
 * is statically translated with Javassist.
 * This class loader is just provided as a utility class.
 *
 * <p>Suppose that an instance of <code>MyTranslator</code> implementing
 * the interface <code>Translator</code> is responsible for modifying
 * class files.
 * The startup program of an application using <code>MyTranslator</code>
 * should be something like this:
 *
 * <pre>
 * import javassist.*;
 *
 * public class Main {
 *   public static void main(String[] args) throws Throwable {
 *     MyTranslator myTrans = new MyTranslator();
 *     ClassPool cp = ClassPool.getDefault();
 *     Loader cl = new Loader(cp);
 *     cl.addTranslator(cp, myTrans);
 *     cl.run("MyApp", args);
 *   }
 * }
 * </pre>
 *
 * <p>Class <code>MyApp</code> is the main program of the application.
 *
 * <p>This program should be executed as follows:
 *
 * <pre>
 * % java Main <i>arg1</i> <i>arg2</i>...
 * </pre>
 *
 * <p>It modifies the class <code>MyApp</code> with a <code>MyTranslator</code>
 * object before the JVM loads it.
 * Then it calls <code>main()</code> in <code>MyApp</code> with arguments
 * <i>arg1</i>, <i>arg2</i>, ...
 *
 * <p>This program execution is equivalent to:
 *
 * <pre>
 * % java MyApp <i>arg1</i> <i>arg2</i>...
 * </pre>
 *
 * <p>except that classes are translated by <code>MyTranslator</code>
 * at load time.
 *
 * <p>If only a particular class must be modified when it is loaded,
 * the startup program can be simpler; <code>MyTranslator</code> is
 * unnecessary.  For example, if only a class <code>test.Rectangle</code>
 * is modified, the <code>main()</code> method above will be the following:
 *
 * <pre>
 * ClassPool cp = ClassPool.getDefault();
 * Loader cl = new Loader(cp);
 * CtClass ct = cp.get("test.Rectangle");
 * ct.setSuperclass(cp.get("test.Point"));
 * cl.run("MyApp", args);</pre>
 *
 * <p>This program changes the super class of the <code>test.Rectangle</code>
 * class.
 *
 * <p><b>Note 1:</b>
 *
 * <p>This class loader does not allow the users to intercept the loading
 * of <code>java.*</code> and <code>javax.*</code> classes (and
 * <code>sun.*</code>, <code>org.xml.*</code>, ...) unless
 * <code>Loader.doDelegation</code> is <code>false</code>.  This is because
 * the JVM prohibits a user class loader from loading a system class.
 * Also see Note 2.
 * If this behavior is not appropriate, a subclass of <code>Loader</code>
 * must be defined and <code>loadClassByDelegation()</code> must be overridden.
 *
 * <p><b>Note 2:</b>
 *
 * <p>If classes are loaded with different class loaders, they belong to
 * separate name spaces.  If class <code>C</code> is loaded by a class
 * loader <code>CL</code>, all classes that the class <code>C</code>
 * refers to are also loaded by <code>CL</code>.  However, if <code>CL</code>
 * delegates the loading of the class <code>C</code> to <code>CL'</code>,
 * then those classes that the class <code>C</code> refers to
 * are loaded by a parent class loader <code>CL'</code>
 * instead of <code>CL</code>.
 *
 * <p>If an object of class <code>C</code> is assigned
 * to a variable of class <code>C</code> belonging to a different name
 * space, then a <code>ClassCastException</code> is thrown.
 *
 * <p>Because of the fact above, this loader delegates only the loading of
 * <code>javassist.Loader</code>
 * and classes included in package <code>java.*</code> and
 * <code>javax.*</code> to the parent class
 * loader.  Other classes are directly loaded by this loader.
 *
 * <p>For example, suppose that <code>java.lang.String</code> would be loaded
 * by this loader while <code>java.io.File</code> is loaded by the parent
 * class loader.  If the constructor of <code>java.io.File</code> is called
 * with an instance of <code>java.lang.String</code>, then it may throw
 * an exception since it accepts an instance of only the
 * <code>java.lang.String</code> loaded by the parent class loader.
 *
 * @see javassist.ClassPool
 * @see javassist.Translator
 */
public class Loader extends ClassLoader {
    private Hashtable<String,ClassLoader> notDefinedHere; // must be atomic.
    private Vector<String> notDefinedPackages; // must be atomic.
    private ClassPool source;
    private Translator translator;
    private ProtectionDomain domain; 

    /**
     * Specifies the algorithm of class loading.
     *
     * <p>This class loader uses the parent class loader for
     * <code>java.*</code> and <code>javax.*</code> classes.
     * If this variable <code>doDelegation</code>
     * is <code>false</code>, this class loader does not delegate those
     * classes to the parent class loader.
     *
     * <p>The default value is <code>true</code>.
     */
    public boolean doDelegation = true;

    /**
     * Creates a new class loader.
     */
    public Loader() {
        this(null);
    }

    /**
     * Creates a new class loader.
     *
     * @param cp        the source of class files.
     */
    public Loader(ClassPool cp) {
        init(cp);
    }

    /**
     * Creates a new class loader
     * using the specified parent class loader for delegation.
     *
     * @param parent    the parent class loader.
     * @param cp        the source of class files.
     */
    public Loader(ClassLoader parent, ClassPool cp) {
        super(parent);
        init(cp);
    }

    private void init(ClassPool cp) {
        notDefinedHere = new Hashtable<String,ClassLoader>();
        notDefinedPackages = new Vector<String>();
        source = cp;
        translator = null;
        domain = null;
        delegateLoadingOf("javassist.Loader");
    }

    /**
     * Records a class so that the loading of that class is delegated
     * to the parent class loader.
     *
     * <p>If the given class name ends with <code>.</code> (dot), then
     * that name is interpreted as a package name.  All the classes
     * in that package and the sub packages are delegated.
     */
    public void delegateLoadingOf(String classname) {
        if (classname.endsWith("."))
            notDefinedPackages.addElement(classname);
        else
            notDefinedHere.put(classname, this);
    }

    /**
     * Sets the protection domain for the classes handled by this class
     * loader.  Without registering an appropriate protection domain,
     * the program loaded by this loader will not work with a security
     * manager or a signed jar file.
     */
    public void setDomain(ProtectionDomain d) {
        domain = d;
    }

    /**
     * Sets the soruce <code>ClassPool</code>.
     */
    public void setClassPool(ClassPool cp) {
        source = cp;
    }

    /**
     * Adds a translator, which is called whenever a class is loaded.
     *
     * @param cp        the <code>ClassPool</code> object for obtaining
     *                  a class file.
     * @param t         a translator.
     * @throws NotFoundException        if <code>t.start()</code> throws an exception.
     * @throws CannotCompileException   if <code>t.start()</code> throws an exception.
     */
    public void addTranslator(ClassPool cp, Translator t)
        throws NotFoundException, CannotCompileException {
        source = cp;
        translator = t;
        t.start(cp);
    }

    /**
     * Loads a class with an instance of <code>Loader</code>
     * and calls <code>main()</code> of that class.
     *
     * <p>This method calls <code>run()</code>.
     *
     * @param args              command line parameters.
     * <br>&nbsp;&nbsp;{@code args[0]} is the class name to be loaded.
     * <br>&nbsp;&nbsp;{@code args[1..n]} are parameters passed
     *                      to the target {@code main()}.
     *
     * @see javassist.Loader#run(String[])
     */
    public static void main(String[] args) throws Throwable {
        Loader cl = new Loader();
        cl.run(args);
    }

    /**
     * Loads a class and calls <code>main()</code> in that class.
     *
     * @param args              command line parameters.
     *
     * <br>&nbsp;&nbsp;{@code args[0]} is the class name to be loaded.
     * <br>&nbsp;&nbsp;{@code args[1..n]} are parameters passed
     *                      to the target {@code main()}.
     */
    public void run(String[] args) throws Throwable {
        if (args.length >= 1)
            run(args[0], Arrays.copyOfRange(args, 1, args.length));
    }

    /**
     * Loads a class and calls <code>main()</code> in that class.
     *
     * @param classname         the loaded class.
     * @param args              parameters passed to <code>main()</code>.
     */
    public void run(String classname, String[] args) throws Throwable {
        Class<?> c = loadClass(classname);
        try {
            c.getDeclaredMethod("main", new Class<?>[] { String[].class }).invoke(
                null,
                new Object[] { args });
        }
        catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }

    /**
     * Requests the class loader to load a class.
     */
    @Override
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassFormatError, ClassNotFoundException {
        name = name.intern();
        synchronized (name) {
            Class<?> c = findLoadedClass(name);
            if (c == null)
                c = loadClassByDelegation(name);

            if (c == null)
                c = findClass(name);

            if (c == null)
                c = delegateToParent(name);

            if (resolve)
                resolveClass(c);

            return c;
        }
    }

    /**
     * Finds the specified class using <code>ClassPath</code>.
     * If the source throws an exception, this returns null.
     *
     * <p>This method can be overridden by a subclass of
     * <code>Loader</code>.  Note that the overridden method must not throw
     * an exception when it just fails to find a class file.
     *
     * @return      null if the specified class could not be found.
     * @throws ClassNotFoundException   if an exception is thrown while
     *                                  obtaining a class file.
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classfile;
        try {
            if (source != null) {
                if (translator != null)
                    translator.onLoad(source, name);

                try {
                    classfile = source.get(name).toBytecode();
                }
                catch (NotFoundException e) {
                    return null;
                }
            }
            else {
                String jarname = "/" + name.replace('.', '/') + ".class";
                InputStream in = this.getClass().getResourceAsStream(jarname);
                if (in == null)
                    return null;

                classfile = ClassPoolTail.readStream(in);
            }
        }
        catch (Exception e) {
            throw new ClassNotFoundException(
                "caught an exception while obtaining a class file for "
                + name, e);
        }

        int i = name.lastIndexOf('.');
        if (i != -1) {
            String pname = name.substring(0, i);
            if (isDefinedPackage(pname))
                try {
                    definePackage(
                        pname, null, null, null, null, null, null, null);
                }
                catch (IllegalArgumentException e) {
                    // ignore.  maybe the package object for the same
                    // name has been created just right away.
                }
        }

        if (domain == null)
            return defineClass(name, classfile, 0, classfile.length);
        return defineClass(name, classfile, 0, classfile.length, domain);
    }

    private boolean isDefinedPackage(String name) {
        if (ClassFile.MAJOR_VERSION >= ClassFile.JAVA_9)
            return getDefinedPackage(name) == null;
        else
            return getPackage(name) == null;
    }

    protected Class<?> loadClassByDelegation(String name)
        throws ClassNotFoundException
    {
        /* The swing components must be loaded by a system
         * class loader.
         * javax.swing.UIManager loads a (concrete) subclass
         * of LookAndFeel by a system class loader and cast
         * an instance of the class to LookAndFeel for
         * (maybe) a security reason.  To avoid failure of
         * type conversion, LookAndFeel must not be loaded
         * by this class loader.
         */

        Class<?> c = null;
        if (doDelegation)
            if (name.startsWith("java.")
                || name.startsWith("javax.")
                || name.startsWith("sun.")
                || name.startsWith("com.sun.")
                || name.startsWith("org.w3c.")
                || name.startsWith("org.xml.")
                || notDelegated(name))
                c = delegateToParent(name);

        return c;
    }

    private boolean notDelegated(String name) {
        if (notDefinedHere.containsKey(name))
            return true;

        for (String pack : notDefinedPackages)
            if (name.startsWith(pack))
                return true;

        return false;
    }

    protected Class<?> delegateToParent(String classname)
        throws ClassNotFoundException
    {
        ClassLoader cl = getParent();
        if (cl != null)
            return cl.loadClass(classname);
        return findSystemClass(classname);
    }
}
