/*
 * 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.util.proxy;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import java.util.*;
import java.lang.ref.WeakReference;

import javassist.CannotCompileException;
import javassist.NotFoundException;
import javassist.bytecode.*;

/*
 * This class is implemented only with the lower-level API of Javassist.
 * This design decision is for maximizing performance.
 */

/**
 * Factory of dynamic proxy classes.
 *
 * <p>This factory generates a class that extends the given super class and implements
 * the given interfaces.  The calls of the methods inherited from the super class are
 * forwarded and then <code>invoke()</code> is called on the method handler
 * associated with instances of the generated class.  The calls of the methods from
 * the interfaces are also forwarded to the method handler.
 *
 * <p>For example, if the following code is executed,
 * 
 * <ul><pre>
 * ProxyFactory f = new ProxyFactory();
 * f.setSuperclass(Foo.class);
 * f.setFilter(new MethodFilter() {
 *     public boolean isHandled(Method m) {
 *         // ignore finalize()
 *         return !m.getName().equals("finalize");
 *     }
 * });
 * Class c = f.createClass();
 * MethodHandler mi = new MethodHandler() {
 *     public Object invoke(Object self, Method m, Method proceed,
 *                          Object[] args) throws Throwable {
 *         System.out.println("Name: " + m.getName());
 *         return proceed.invoke(self, args);  // execute the original method.
 *     }
 * };
 * Foo foo = (Foo)c.newInstance();
 * ((Proxy)foo).setHandler(mi);
 * </pre></ul>
 *
 * <p>Here, <code>Method</code> is <code>java.lang.reflect.Method</code>.</p>
 *
 * <p>Then, the following method call will be forwarded to MethodHandler
 * <code>mi</code> and prints a message before executing the originally called method
 * <code>bar()</code> in <code>Foo</code>.
 *
 * <ul><pre>
 * foo.bar();
 * </pre></ul>
 *
 * <p>The last three lines of the code shown above can be replaced with a call to
 * the helper method <code>create</code>, which generates a proxy class, instantiates
 * it, and sets the method handler of the instance:
 *
 * <ul><pre>
 *     :
 * Foo foo = (Foo)f.create(new Class[0], new Object[0], mi);
 * </pre></ul>
 *
 * <p>To change the method handler during runtime,
 * execute the following code:
 *
 * <ul><pre>
 * MethodHandler mi = ... ;    // alternative handler
 * ((Proxy)foo).setHandler(mi);
 * </pre></ul>
 *
 * <p> If setHandler is never called for a proxy instance then it will
 * employ the default handler which proceeds by invoking the original method.
 * The behaviour of the default handler is identical to the following
 * handler:
 *
 * <ul><pre>
 * class EmptyHandler implements MethodHandler {
 *     public Object invoke(Object self, Method m,
 *                          Method proceed, Object[] args) throws Exception {
 *         return proceed.invoke(self, args);
 *     }
 * }
 * </pre></ul>
 *
 * <p>A proxy factory caches and reuses proxy classes by default. It is possible to reset
 * this default globally by setting static field {@link ProxyFactory#useCache} to false.
 * Caching may also be configured for a specific factory by calling instance method
 * {@link ProxyFactory#setUseCache(boolean)}. It is strongly recommended that new clients
 * of class ProxyFactory enable caching. Failure to do so may lead to exhaustion of
 * the heap memory area used to store classes.
 *
 * <p>Caching is automatically disabled for any given proxy factory if deprecated instance
 * method {@link ProxyFactory#setHandler(MethodHandler)} is called. This method was
 * used to specify a default handler which newly created proxy classes should install
 * when they create their instances. It is only retained to provide backward compatibility
 * with previous releases of javassist. Unfortunately,this legacy behaviour makes caching
 * and reuse of proxy classes impossible. The current programming model expects javassist
 * clients to set the handler of a proxy instance explicitly by calling method
 * {@link Proxy#setHandler(MethodHandler)} as shown in the sample code above. New
 * clients are strongly recommended to use this model rather than calling
 * {@link ProxyFactory#setHandler(MethodHandler)}.
 *
 * <p>A proxy object generated by <code>ProxyFactory</code> is serializable
 * if its super class or any of its interfaces implement <code>java.io.Serializable</code>.
 * However, a serialized proxy object may not be compatible with future releases.
 * The serialization support should be used for short-term storage or RMI.
 *
 * <p>For compatibility with older releases serialization of proxy objects is implemented by
 * adding a writeReplace method to the proxy class. This allows a proxy to be serialized
 * to a conventional {@link java.io.ObjectOutputStream} and deserialized from a corresponding
 * {@link java.io.ObjectInputStream}. However this method suffers from several problems, the most
 * notable one being that it fails to serialize state inherited from the proxy's superclass.
 * <p>
 * An alternative method of serializing proxy objects is available which fixes these problems. It
 * requires inhibiting generation of the writeReplace method and instead using instances of
 * {@link javassist.util.proxy.ProxyObjectOutputStream} and {@link javassist.util.proxy.ProxyObjectInputStream}
 * (which are subclasses of {@link java.io.ObjectOutputStream} and  {@link java.io.ObjectInputStream})
 * to serialize and deserialize, respectively, the proxy. These streams recognise javassist proxies and ensure
 * that they are serialized and deserialized without the need for the proxy class to implement special methods
 * such as writeReplace. Generation of the writeReplace method can be disabled globally by setting static field
 * {@link ProxyFactory#useWriteReplace} to false. Alternatively, it may be
 * configured per factory by calling instance method {@link ProxyFactory#setUseWriteReplace(boolean)}.
 *
 * @see MethodHandler
 * @since 3.1
 * @author Muga Nishizawa
 * @author Shigeru Chiba
 * @author Andrew Dinn
 */
public class ProxyFactory {
    private Class superClass;
    private Class[] interfaces;
    private MethodFilter methodFilter;
    private MethodHandler handler;  // retained for legacy usage
    private List signatureMethods;
    private boolean hasGetHandler;
    private byte[] signature;
    private String classname;
    private String basename;
    private String superName;
    private Class thisClass;
    /**
     * per factory setting initialised from current setting for useCache but able to be reset before each create call
     */
    private boolean factoryUseCache;
    /**
     * per factory setting initialised from current setting for useWriteReplace but able to be reset before each create call
     */
    private boolean factoryWriteReplace;


    /**
     * If the value of this variable is not null, the class file of
     * the generated proxy class is written under the directory specified
     * by this variable.  For example, if the value is 
     * <code>"."</code>, then the class file is written under the current
     * directory.  This method is for debugging.
     *
     * <p>The default value is null.
     */
    public String writeDirectory;

    private static final Class OBJECT_TYPE = Object.class;

    private static final String HOLDER = "_methods_";
    private static final String HOLDER_TYPE = "[Ljava/lang/reflect/Method;";
    private static final String FILTER_SIGNATURE_FIELD = "_filter_signature";
    private static final String FILTER_SIGNATURE_TYPE = "[B";
    private static final String HANDLER = "handler";
    private static final String NULL_INTERCEPTOR_HOLDER = "javassist.util.proxy.RuntimeSupport";
    private static final String DEFAULT_INTERCEPTOR = "default_interceptor";
    private static final String HANDLER_TYPE
        = 'L' + MethodHandler.class.getName().replace('.', '/') + ';';
    private static final String HANDLER_SETTER = "setHandler";
    private static final String HANDLER_SETTER_TYPE = "(" + HANDLER_TYPE + ")V";

    private static final String HANDLER_GETTER = "getHandler";
    private static final String HANDLER_GETTER_TYPE = "()" + HANDLER_TYPE;

    private static final String SERIAL_VERSION_UID_FIELD = "serialVersionUID";
    private static final String SERIAL_VERSION_UID_TYPE = "J";
    private static final long SERIAL_VERSION_UID_VALUE = -1L;

    /**
     * If true, a generated proxy class is cached and it will be reused
     * when generating the proxy class with the same properties is requested.
     * The default value is true.
     *
     * Note that this value merely specifies the initial setting employed by any newly created
     * proxy factory. The factory setting may be overwritten by calling factory instance method
     * {@link #setUseCache(boolean)}
     *
     * @since 3.4
     */
    public static volatile boolean useCache = true;

    /**
     * If true, a generated proxy class will implement method writeReplace enabling
     * serialization of its proxies to a conventional ObjectOutputStream. this (default)
     * setting retains the old javassist behaviour which has the advantage that it
     * retains compatibility with older  releases and requires no extra work on the part
     * of the client performing the serialization. However, it has the disadvantage that
     * state inherited from the superclasses of the proxy is lost during serialization.
     * if false then serialization/deserialization of the proxy instances will preserve
     * all fields. However, serialization must be performed via a {@link ProxyObjectOutputStream}
     * and deserialization must be via {@link ProxyObjectInputStream}. Any attempt to serialize
     * proxies whose class was created with useWriteReplace set to false via a normal
     * {@link java.io.ObjectOutputStream} will fail.
     *
     * Note that this value merely specifies the initial setting employed by any newly created
     * proxy factory. The factory setting may be overwritten by calling factory instance method
     * {@link #setUseWriteReplace(boolean)}
     *
     * @since 3.4
     */
    public static volatile boolean useWriteReplace = true;

    /*
     * methods allowing individual factory settings for factoryUseCache and factoryWriteReplace to be reset
     */

    /**
     * test whether this factory uses the proxy cache
     * @return true if this factory uses the proxy cache otherwise false
     */
    public boolean isUseCache()
    {
        return factoryUseCache;
    }

    /**
     * configure whether this factory should use the proxy cache
     * @param useCache true if this factory should use the proxy cache and false if it should not use the cache
     * @throws RuntimeException if a default interceptor has been set for the factory
     */
    public void setUseCache(boolean useCache)
    {
        // we cannot allow caching to be used if the factory is configured to install a default interceptor
        // field into generated classes
        if (handler != null && useCache) {
            throw new RuntimeException("caching cannot be enabled if the factory default interceptor has been set");
        }
        factoryUseCache = useCache;
    }

    /**
     * test whether this factory installs a writeReplace method in created classes
     * @return true if this factory installs a writeReplace method in created classes otherwise false
     */
    public boolean isUseWriteReplace()
    {
        return factoryWriteReplace;
    }

    /**
     * configure whether this factory should add a writeReplace method to created classes
     * @param useWriteReplace true if this factory should add a writeReplace method to created classes and false if it
     * should not add a writeReplace method
     */
    public void setUseWriteReplace(boolean useWriteReplace)
    {
        factoryWriteReplace = useWriteReplace;
    }

    private static WeakHashMap proxyCache = new WeakHashMap();

    /**
     * determine if a class is a javassist proxy class
     * @param cl
     * @return true if the class is a javassist proxy class otherwise false
     */
    public static boolean isProxyClass(Class cl)
    {
        // all proxies implement Proxy or ProxyObject. nothing else should. 
        return (Proxy.class.isAssignableFrom(cl));
    }

    /**
     * used to store details of a specific proxy class in the second tier of the proxy cache. this entry
     * will be located in a hashmap keyed by the unique identifying name of the proxy class. the hashmap is
     * located in a weak hashmap keyed by the classloader common to all proxy classes in the second tier map.
     */
    static class ProxyDetails {
        /**
         * the unique signature of any method filter whose behaviour will be met by this class. each bit in
         * the byte array is set if the filter redirects the corresponding super or interface method and clear
         * if it does not redirect it.
         */
        byte[] signature;
        /**
         * a hexadecimal string representation of the signature bit sequence. this string also forms part
         * of the proxy class name.
         */
        WeakReference proxyClass;
        /**
         * a flag which is true this class employs writeReplace to perform serialization of its instances
         * and false if serialization must employ of a ProxyObjectOutputStream and ProxyObjectInputStream
         */
        boolean isUseWriteReplace;

        ProxyDetails(byte[] signature, Class proxyClass, boolean isUseWriteReplace)
        {
            this.signature = signature;
            this.proxyClass = new WeakReference(proxyClass);
            this.isUseWriteReplace = isUseWriteReplace;
        }
    }

    /**
     * Constructs a factory of proxy class.
     */
    public ProxyFactory() {
        superClass = null;
        interfaces = null;
        methodFilter = null;
        handler = null;
        signature = null;
        signatureMethods = null;
        hasGetHandler = false;
        thisClass = null;
        writeDirectory = null;
        factoryUseCache = useCache;
        factoryWriteReplace = useWriteReplace;
    }

    /**
     * Sets the super class of a proxy class.
     */
    public void setSuperclass(Class clazz) {
        superClass = clazz;
        // force recompute of signature
        signature = null;
    }

    /**
     * Obtains the super class set by <code>setSuperclass()</code>.
     *
     * @since 3.4
     */
    public Class getSuperclass() { return superClass; }

    /**
     * Sets the interfaces of a proxy class.
     */
    public void setInterfaces(Class[] ifs) {
        interfaces = ifs;
        // force recompute of signature
        signature = null;
    }

    /**
     * Obtains the interfaces set by <code>setInterfaces</code>.
     *
     * @since 3.4
     */
    public Class[] getInterfaces() { return interfaces; }

    /**
     * Sets a filter that selects the methods that will be controlled by a handler.
     */
    public void setFilter(MethodFilter mf) {
        methodFilter = mf;
        // force recompute of signature
        signature = null;
    }

    /**
     * Generates a proxy class using the current filter.
     */
    public Class createClass() {
        if (signature == null) {
            computeSignature(methodFilter);
        }
        return createClass1();
    }

    /**
     * Generates a proxy class using the supplied filter.
     */
    public Class createClass(MethodFilter filter) {
        computeSignature(filter);
        return createClass1();
    }

    /**
     * Generates a proxy class with a specific signature.
     * access is package local so ProxyObjectInputStream can use this
     * @param signature
     * @return
     */
    Class createClass(byte[] signature)
    {
        installSignature(signature);
        return createClass1();
    }

    private Class createClass1() {
        if (thisClass == null) {
            ClassLoader cl = getClassLoader();
            synchronized (proxyCache) {
                if (factoryUseCache)
                    createClass2(cl);
                else 
                    createClass3(cl);
            }
        }

        // don't retain any unwanted references
        Class result = thisClass;
        thisClass = null;

        return result;
    }

    private static char[] hexDigits =
            { '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    public String getKey(Class superClass, Class[] interfaces, byte[] signature, boolean useWriteReplace)
    {
        StringBuffer sbuf = new StringBuffer();
        if (superClass != null){
            sbuf.append(superClass.getName());
        }
        sbuf.append(":");
        for (int i = 0; i < interfaces.length; i++) {
            sbuf.append(interfaces[i].getName());
            sbuf.append(":");
        }
        for (int i = 0; i < signature.length; i++) {
            byte b = signature[i];
            int lo = b & 0xf;
            int hi = (b >> 4) & 0xf;
            sbuf.append(hexDigits[lo]);
            sbuf.append(hexDigits[hi]);
        }
        if (useWriteReplace) {
            sbuf.append(":w");
        }

        return sbuf.toString();
    }

    private void createClass2(ClassLoader cl) {
        String key = getKey(superClass, interfaces, signature, factoryWriteReplace);
        /*
         * Excessive concurrency causes a large memory footprint and slows the
         * execution speed down (with JDK 1.5).  Thus, we use a jumbo lock for
         * reducing concrrency.
         */
        // synchronized (proxyCache) {
            HashMap cacheForTheLoader = (HashMap)proxyCache.get(cl);
            ProxyDetails details;
            if (cacheForTheLoader == null) {
                cacheForTheLoader = new HashMap();
                proxyCache.put(cl, cacheForTheLoader);
            }
            details = (ProxyDetails)cacheForTheLoader.get(key);
            if (details != null) {
                WeakReference reference = details.proxyClass;
                thisClass = (Class)reference.get();
                if (thisClass != null) {
                    return;
                }
            }
            createClass3(cl);
            details = new  ProxyDetails(signature, thisClass, factoryWriteReplace);
            cacheForTheLoader.put(key, details);
        // }
    }

    private void createClass3(ClassLoader cl) {
        // we need a new class so we need a new class name
        allocateClassName();

        try {
            ClassFile cf = make();
            if (writeDirectory != null)
                FactoryHelper.writeFile(cf, writeDirectory);

            thisClass = FactoryHelper.toClass(cf, cl, getDomain());
            setField(FILTER_SIGNATURE_FIELD, signature);
            // legacy behaviour : we only set the default interceptor static field if we are not using the cache
            if (!factoryUseCache) {
                setField(DEFAULT_INTERCEPTOR, handler);
            }
        }
        catch (CannotCompileException e) {
            throw new RuntimeException(e.getMessage(), e);
        }

    }

    private void setField(String fieldName, Object value) {
        if (thisClass != null && value != null)
            try {
                Field f = thisClass.getField(fieldName);
                SecurityActions.setAccessible(f, true);
                f.set(null, value);
                SecurityActions.setAccessible(f, false);
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
    }

    static byte[] getFilterSignature(Class clazz) {
        return (byte[])getField(clazz, FILTER_SIGNATURE_FIELD);
    }

    private static Object getField(Class clazz, String fieldName) {
        try {
            Field f = clazz.getField(fieldName);
            f.setAccessible(true);
            Object value = f.get(null);
            f.setAccessible(false);
            return value;
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Obtains the method handler of the given proxy object.
     * 
     * @param p     a proxy object.
     * @return the method handler.
     * @since 3.16
     */
    public static MethodHandler getHandler(Proxy p) {
        try {
            Field f = p.getClass().getDeclaredField(HANDLER);
            f.setAccessible(true);
            Object value = f.get(p);
            f.setAccessible(false);
            return (MethodHandler)value;
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * A provider of class loaders.
     *
     * @see #classLoaderProvider
     * @since 3.4
     */
    public static interface ClassLoaderProvider {
        /**
         * Returns a class loader.
         *
         * @param pf    a proxy factory that is going to obtain a class loader.
         */
        public ClassLoader get(ProxyFactory pf);
    }

    /**
     * A provider used by <code>createClass()</code> for obtaining
     * a class loader.
     * <code>get()</code> on this <code>ClassLoaderProvider</code> object
     * is called to obtain a class loader.
     *
     * <p>The value of this field can be updated for changing the default
     * implementation.
     *
     * <p>Example:
     * <ul><pre>
     * ProxyFactory.classLoaderProvider = new ProxyFactory.ClassLoaderProvider() {
     *     public ClassLoader get(ProxyFactory pf) {
     *         return Thread.currentThread().getContextClassLoader();
     *     }
     * };
     * </pre></ul>
     *
     * @since 3.4
     */
    public static ClassLoaderProvider classLoaderProvider
        = new ClassLoaderProvider() {
              public ClassLoader get(ProxyFactory pf) {
                  return pf.getClassLoader0();
              }
          };

    protected ClassLoader getClassLoader() {
        return classLoaderProvider.get(this);
    }

    protected ClassLoader getClassLoader0() {
        ClassLoader loader = null;
        if (superClass != null && !superClass.getName().equals("java.lang.Object"))
            loader = superClass.getClassLoader();
        else if (interfaces != null && interfaces.length > 0)
            loader = interfaces[0].getClassLoader();
 
        if (loader == null) {
            loader = getClass().getClassLoader();
            // In case javassist is in the endorsed dir
            if (loader == null) {
                loader = Thread.currentThread().getContextClassLoader();
                if (loader == null)
                    loader = ClassLoader.getSystemClassLoader();
            }
        }

        return loader;
    }

    protected ProtectionDomain getDomain() {
        Class clazz;
        if (superClass != null && !superClass.getName().equals("java.lang.Object"))
            clazz = superClass;
        else if (interfaces != null && interfaces.length > 0)
            clazz = interfaces[0];
        else
            clazz = this.getClass();

        return clazz.getProtectionDomain();
    }

    /**
     * Creates a proxy class and returns an instance of that class.
     *
     * @param paramTypes    parameter types for a constructor.
     * @param args          arguments passed to a constructor.
     * @param mh            the method handler for the proxy class.
     * @since 3.4
     */
    public Object create(Class[] paramTypes, Object[] args, MethodHandler mh)
        throws NoSuchMethodException, IllegalArgumentException,
               InstantiationException, IllegalAccessException, InvocationTargetException
    {
        Object obj = create(paramTypes, args);
        ((Proxy)obj).setHandler(mh);
        return obj;
    }

    /**
     * Creates a proxy class and returns an instance of that class.
     *
     * @param paramTypes    parameter types for a constructor.
     * @param args          arguments passed to a constructor.
     */
    public Object create(Class[] paramTypes, Object[] args)
        throws NoSuchMethodException, IllegalArgumentException,
               InstantiationException, IllegalAccessException, InvocationTargetException
    {
        Class c = createClass();
        Constructor cons = c.getConstructor(paramTypes);
        return cons.newInstance(args);
    }

    /**
     * Sets the default invocation handler.  This invocation handler is shared
     * among all the instances of a proxy class unless another is explicitly
     * specified.
     * @deprecated since 3.12
     * use of this method is incompatible  with proxy class caching.
     * instead clients should call method {@link Proxy#setHandler(MethodHandler)} to set the handler
     * for each newly created  proxy instance.
     * calling this method will automatically disable caching of classes created by the proxy factory.
     */
    public void setHandler(MethodHandler mi) {
        // if we were using the cache and the handler is non-null then we must stop caching
        if (factoryUseCache && mi != null)  {
            factoryUseCache = false;
            // clear any currently held class so we don't try to reuse it or set its handler field
          thisClass  = null;
        }
        handler = mi;
        // this retains the behaviour of the old code which resets any class we were holding on to
        // this is probably not what is wanted
        setField(DEFAULT_INTERCEPTOR, handler);
    }

    /**
     * A unique class name generator.
     */
    public static interface UniqueName {
        /**
         * Returns a unique class name.
         *
         * @param classname     the super class name of the proxy class.
         */
        String get(String classname);
    }

    /**
     * A unique class name generator.
     * Replacing this generator changes the algorithm to generate a
     * unique name. The <code>get</code> method does not have to be
     * a <code>synchronized</code> method since the access to this field
     * is mutually exclusive and thus thread safe.
     */
    public static UniqueName nameGenerator = new UniqueName() {
        private final String sep = "_$$_jvst" + Integer.toHexString(this.hashCode() & 0xfff) + "_";
        private int counter = 0;

        public String get(String classname) {
            return classname + sep + Integer.toHexString(counter++);
        }
    };

    private static String makeProxyName(String classname) {
        synchronized (nameGenerator) {
            return nameGenerator.get(classname);
        }
    }

    private ClassFile make() throws CannotCompileException {
        ClassFile cf = new ClassFile(false, classname, superName);
        cf.setAccessFlags(AccessFlag.PUBLIC);
        setInterfaces(cf, interfaces, hasGetHandler ? Proxy.class : ProxyObject.class);
        ConstPool pool = cf.getConstPool();

        // legacy: we only add the static field for the default interceptor if caching is disabled
        if  (!factoryUseCache) {
            FieldInfo finfo = new FieldInfo(pool, DEFAULT_INTERCEPTOR, HANDLER_TYPE);
            finfo.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
            cf.addField(finfo);
        }

        // handler is per instance
        FieldInfo finfo2 = new FieldInfo(pool, HANDLER, HANDLER_TYPE);
        finfo2.setAccessFlags(AccessFlag.PRIVATE);
        cf.addField(finfo2);

        // filter signature is per class
        FieldInfo finfo3 = new FieldInfo(pool, FILTER_SIGNATURE_FIELD, FILTER_SIGNATURE_TYPE);
        finfo3.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC);
        cf.addField(finfo3);

        // the proxy class serial uid must always be a fixed value
        FieldInfo finfo4 = new FieldInfo(pool, SERIAL_VERSION_UID_FIELD, SERIAL_VERSION_UID_TYPE);
        finfo4.setAccessFlags(AccessFlag.PUBLIC | AccessFlag.STATIC| AccessFlag.FINAL);
        cf.addField(finfo4);
        
        // HashMap allMethods = getMethods(superClass, interfaces);
        // int size = allMethods.size();
        makeConstructors(classname, cf, pool, classname);

        ArrayList forwarders = new ArrayList();
        int s = overrideMethods(cf, pool, classname, forwarders);
        addClassInitializer(cf, pool, classname, s, forwarders);
        addSetter(classname, cf, pool);
        if (!hasGetHandler)
            addGetter(classname, cf, pool);

        if (factoryWriteReplace) {
            try {
                cf.addMethod(makeWriteReplace(pool));
            }
            catch (DuplicateMemberException e) {
                // writeReplace() is already declared in the super class/interfaces.
            }
        }

        thisClass = null;
        return cf;
    }

    private void checkClassAndSuperName() {
        if (interfaces == null)
            interfaces = new Class[0];

        if (superClass == null) {
            superClass = OBJECT_TYPE;
            superName = superClass.getName();
            basename = interfaces.length == 0 ? superName
                                               : interfaces[0].getName();
        } else {
            superName = superClass.getName();
            basename = superName;
        }

        if (Modifier.isFinal(superClass.getModifiers()))
            throw new RuntimeException(superName + " is final");
        
        if (basename.startsWith("java."))
            basename = "org.javassist.tmp." + basename;
    }

    private void allocateClassName() {
        classname = makeProxyName(basename);
    }

    private static Comparator sorter = new Comparator() {

        public int compare(Object o1, Object o2) {
            Map.Entry e1 = (Map.Entry)o1;
            Map.Entry e2 = (Map.Entry)o2;
            String key1 = (String)e1.getKey();
            String key2 = (String)e2.getKey();
            return key1.compareTo(key2);
        }
    };

    private void makeSortedMethodList() {
        checkClassAndSuperName();

        hasGetHandler = false;      // getMethods() may set this to true.
        HashMap allMethods = getMethods(superClass, interfaces);
        signatureMethods = new ArrayList(allMethods.entrySet());
        Collections.sort(signatureMethods, sorter);
    }

    private void computeSignature(MethodFilter filter) // throws CannotCompileException
    {
        makeSortedMethodList();

        int l = signatureMethods.size();
        int maxBytes = ((l + 7) >> 3);
        signature = new byte[maxBytes];
        for (int idx = 0; idx < l; idx++)
        {
            Map.Entry e = (Map.Entry)signatureMethods.get(idx);
            Method m = (Method)e.getValue();
            int mod = m.getModifiers();
            if (!Modifier.isFinal(mod) && !Modifier.isStatic(mod)
                    && isVisible(mod, basename, m) && (filter == null || filter.isHandled(m))) {
                setBit(signature, idx);
            }
        }
    }

    private void installSignature(byte[] signature) // throws CannotCompileException
    {
        makeSortedMethodList();

        int l = signatureMethods.size();
        int maxBytes = ((l + 7) >> 3);
        if (signature.length != maxBytes) {
            throw new RuntimeException("invalid filter signature length for deserialized proxy class");
        }

        this.signature =  signature;
    }

    private boolean testBit(byte[] signature, int idx) {
        int byteIdx = idx >> 3;
        if (byteIdx > signature.length) {
            return false;
        } else {
            int bitIdx = idx & 0x7;
            int mask = 0x1 << bitIdx;
            int sigByte = signature[byteIdx];
            return ((sigByte & mask) != 0);
        }
    }

    private void setBit(byte[] signature, int idx) {
        int byteIdx = idx >> 3;
        if (byteIdx < signature.length) {
            int bitIdx = idx & 0x7;
            int mask = 0x1 << bitIdx;
            int sigByte = signature[byteIdx];
            signature[byteIdx] = (byte)(sigByte | mask);
        }
    }

    private static void setInterfaces(ClassFile cf, Class[] interfaces, Class proxyClass) {
        String setterIntf = proxyClass.getName();
        String[] list;
        if (interfaces == null || interfaces.length == 0)
            list = new String[] { setterIntf };
        else {
            list = new String[interfaces.length + 1];
            for (int i = 0; i < interfaces.length; i++)
                list[i] = interfaces[i].getName();

            list[interfaces.length] = setterIntf;
        }

        cf.setInterfaces(list);
    }

    private static void addClassInitializer(ClassFile cf, ConstPool cp,
                                    String classname, int size, ArrayList forwarders)
        throws CannotCompileException
    {
        FieldInfo finfo = new FieldInfo(cp, HOLDER, HOLDER_TYPE);
        finfo.setAccessFlags(AccessFlag.PRIVATE | AccessFlag.STATIC);
        cf.addField(finfo);
        MethodInfo minfo = new MethodInfo(cp, "<clinit>", "()V");
        minfo.setAccessFlags(AccessFlag.STATIC);
        setThrows(minfo, cp, new Class[] { ClassNotFoundException.class });

        Bytecode code = new Bytecode(cp, 0, 2);
        code.addIconst(size * 2);
        code.addAnewarray("java.lang.reflect.Method");
        final int varArray = 0;
        code.addAstore(varArray);

        // forName() must be called here.  Otherwise, the class might be
        // invisible.
        code.addLdc(classname);
        code.addInvokestatic("java.lang.Class",
                "forName", "(Ljava/lang/String;)Ljava/lang/Class;");
        final int varClass = 1;
        code.addAstore(varClass);

        Iterator it = forwarders.iterator();
        while (it.hasNext()) {
            Find2MethodsArgs args = (Find2MethodsArgs)it.next();
            callFind2Methods(code, args.methodName, args.delegatorName,
                             args.origIndex, args.descriptor, varClass, varArray);
        }

        code.addAload(varArray);
        code.addPutstatic(classname, HOLDER, HOLDER_TYPE);

        code.addLconst(SERIAL_VERSION_UID_VALUE);
        code.addPutstatic(classname, SERIAL_VERSION_UID_FIELD, SERIAL_VERSION_UID_TYPE);
        code.addOpcode(Bytecode.RETURN);
        minfo.setCodeAttribute(code.toCodeAttribute());
        cf.addMethod(minfo);
    }

    /**
     * @param thisMethod        might be null.
     */
    private static void callFind2Methods(Bytecode code, String superMethod, String thisMethod,
                                         int index, String desc, int classVar, int arrayVar) {
        String findClass = RuntimeSupport.class.getName();
        String findDesc
            = "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;[Ljava/lang/reflect/Method;)V";

        code.addAload(classVar);
        code.addLdc(superMethod);
        if (thisMethod == null)
            code.addOpcode(Opcode.ACONST_NULL);
        else
            code.addLdc(thisMethod);

        code.addIconst(index);
        code.addLdc(desc);
        code.addAload(arrayVar);
        code.addInvokestatic(findClass, "find2Methods", findDesc);
    }

    private static void addSetter(String classname, ClassFile cf, ConstPool cp)
        throws CannotCompileException
    {
        MethodInfo minfo = new MethodInfo(cp, HANDLER_SETTER,
                                          HANDLER_SETTER_TYPE);
        minfo.setAccessFlags(AccessFlag.PUBLIC);
        Bytecode code = new Bytecode(cp, 2, 2);
        code.addAload(0);
        code.addAload(1);
        code.addPutfield(classname, HANDLER, HANDLER_TYPE);
        code.addOpcode(Bytecode.RETURN);
        minfo.setCodeAttribute(code.toCodeAttribute());
        cf.addMethod(minfo);
    }

    private static void addGetter(String classname, ClassFile cf, ConstPool cp)
        throws CannotCompileException
    {
        MethodInfo minfo = new MethodInfo(cp, HANDLER_GETTER,
                                          HANDLER_GETTER_TYPE);
        minfo.setAccessFlags(AccessFlag.PUBLIC);
        Bytecode code = new Bytecode(cp, 1, 1);
        code.addAload(0);
        code.addGetfield(classname, HANDLER, HANDLER_TYPE);
        code.addOpcode(Bytecode.ARETURN);
        minfo.setCodeAttribute(code.toCodeAttribute());
        cf.addMethod(minfo);
    }

    private int overrideMethods(ClassFile cf, ConstPool cp, String className, ArrayList forwarders)
        throws CannotCompileException
    {
        String prefix = makeUniqueName("_d", signatureMethods);
        Iterator it = signatureMethods.iterator();
        int index = 0;
        while (it.hasNext()) {
            Map.Entry e = (Map.Entry)it.next();
            String key = (String)e.getKey();
            Method meth = (Method)e.getValue();
            if (ClassFile.MAJOR_VERSION < ClassFile.JAVA_5 || !isBridge(meth))
            	if (testBit(signature, index)) {
            		override(className, meth, prefix, index,
            				 keyToDesc(key, meth), cf, cp, forwarders);
            	}

            index++;
        }

        return index;
    }

    private static boolean isBridge(Method m) {
    	return m.isBridge();
    }

    private void override(String thisClassname, Method meth, String prefix,
                          int index, String desc, ClassFile cf, ConstPool cp, ArrayList forwarders)
        throws CannotCompileException
    {
        Class declClass = meth.getDeclaringClass();
        String delegatorName = prefix + index + meth.getName();
        if (Modifier.isAbstract(meth.getModifiers()))
            delegatorName = null;
        else {
            MethodInfo delegator
                = makeDelegator(meth, desc, cp, declClass, delegatorName);
            // delegator is not a bridge method.  See Sec. 15.12.4.5 of JLS 3rd Ed.
            delegator.setAccessFlags(delegator.getAccessFlags() & ~AccessFlag.BRIDGE);
            cf.addMethod(delegator);
        }

        MethodInfo forwarder
            = makeForwarder(thisClassname, meth, desc, cp, declClass,
                            delegatorName, index, forwarders);
        cf.addMethod(forwarder);
    }

    private void makeConstructors(String thisClassName, ClassFile cf,
            ConstPool cp, String classname) throws CannotCompileException
    {
        Constructor[] cons = SecurityActions.getDeclaredConstructors(superClass);
        // legacy: if we are not caching then we need to initialise the default handler
        boolean doHandlerInit = !factoryUseCache;
        for (int i = 0; i < cons.length; i++) {
            Constructor c = cons[i];
            int mod = c.getModifiers();
            if (!Modifier.isFinal(mod) && !Modifier.isPrivate(mod)
                    && isVisible(mod, basename, c)) {
                MethodInfo m = makeConstructor(thisClassName, c, cp, superClass, doHandlerInit);
                cf.addMethod(m);
            }
        }
    }

    private static String makeUniqueName(String name, List sortedMethods) {
        if (makeUniqueName0(name, sortedMethods.iterator()))
            return name;

        for (int i = 100; i < 999; i++) {
            String s = name + i;
            if (makeUniqueName0(s, sortedMethods.iterator()))
                return s;
        }

        throw new RuntimeException("cannot make a unique method name");
    }

    private static boolean makeUniqueName0(String name, Iterator it) {
        while (it.hasNext()) {
            Map.Entry e = (Map.Entry)it.next();
            String key = (String)e.getKey();
            if (key.startsWith(name))
                return false;
        }

        return true;
    }

    /**
     * Returns true if the method is visible from the package.
     *
     * @param mod       the modifiers of the method. 
     */
    private static boolean isVisible(int mod, String from, Member meth) {
        if ((mod & Modifier.PRIVATE) != 0)
            return false;
        else if ((mod & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0)
            return true;
        else {
            String p = getPackageName(from);
            String q = getPackageName(meth.getDeclaringClass().getName());
            if (p == null)
                return q == null;
            else
                return p.equals(q);
        }
    }

    private static String getPackageName(String name) {
        int i = name.lastIndexOf('.');
        if (i < 0)
            return null;
        else
            return name.substring(0, i);
    }

    /* getMethods() may set hasGetHandler to true.
     */
    private HashMap getMethods(Class superClass, Class[] interfaceTypes) {
        HashMap hash = new HashMap();
        HashSet set = new HashSet();
        for (int i = 0; i < interfaceTypes.length; i++)
            getMethods(hash, interfaceTypes[i], set);

        getMethods(hash, superClass, set);
        return hash;
    }

    private void getMethods(HashMap hash, Class clazz, Set visitedClasses) {
        // This both speeds up scanning by avoiding duplicate interfaces and is needed to
        // ensure that superinterfaces are always scanned before subinterfaces.
        if (!visitedClasses.add(clazz))
            return;

        Class[] ifs = clazz.getInterfaces();
        for (int i = 0; i < ifs.length; i++)
            getMethods(hash, ifs[i], visitedClasses);

        Class parent = clazz.getSuperclass();
        if (parent != null)
            getMethods(hash, parent, visitedClasses);

        /* Java 5 or later allows covariant return types.
         * It also allows contra-variant parameter types
         * if a super class is a generics with concrete type arguments
         * such as Foo<String>.  So the method-overriding rule is complex.
         */
        Method[] methods = SecurityActions.getDeclaredMethods(clazz);
        for (int i = 0; i < methods.length; i++)
            if (!Modifier.isPrivate(methods[i].getModifiers())) {
                Method m = methods[i];
                String key = m.getName() + ':' + RuntimeSupport.makeDescriptor(m);	// see keyToDesc().
                if (key.startsWith(HANDLER_GETTER_KEY))
                    hasGetHandler = true;

                // JIRA JASSIST-85
                // put the method to the cache, retrieve previous definition (if any) 
                Method oldMethod = (Method)hash.put(key, methods[i]); 

                // check if visibility has been reduced 
                if (null != oldMethod && Modifier.isPublic(oldMethod.getModifiers())
                                      && !Modifier.isPublic(methods[i].getModifiers()) ) { 
                    // we tried to overwrite a public definition with a non-public definition,
                    // use the old definition instead. 
                    hash.put(key, oldMethod); 
                }
            }
    }

    private static final String HANDLER_GETTER_KEY
    	= HANDLER_GETTER + ":()";

    private static String keyToDesc(String key, Method m) {
        return key.substring(key.indexOf(':') + 1);
    }

    private static MethodInfo makeConstructor(String thisClassName, Constructor cons,
                                              ConstPool cp, Class superClass, boolean doHandlerInit) {
        String desc = RuntimeSupport.makeDescriptor(cons.getParameterTypes(),
                                                    Void.TYPE);
        MethodInfo minfo = new MethodInfo(cp, "<init>", desc);
        minfo.setAccessFlags(Modifier.PUBLIC);      // cons.getModifiers() & ~Modifier.NATIVE
        setThrows(minfo, cp, cons.getExceptionTypes());
        Bytecode code = new Bytecode(cp, 0, 0);

        // legacy: if we are not using caching then we initialise the instance's handler
        // from the class's static default interceptor and skip the next few instructions if
        // it is non-null
        if (doHandlerInit) {
            code.addAload(0);
            code.addGetstatic(thisClassName, DEFAULT_INTERCEPTOR, HANDLER_TYPE);
            code.addPutfield(thisClassName, HANDLER, HANDLER_TYPE);
            code.addGetstatic(thisClassName, DEFAULT_INTERCEPTOR, HANDLER_TYPE);
            code.addOpcode(Opcode.IFNONNULL);
            code.addIndex(10);
        }
        // if caching is enabled then we don't have a handler to initialise so this else branch will install
        // the handler located in the static field of class RuntimeSupport.
        code.addAload(0);
        code.addGetstatic(NULL_INTERCEPTOR_HOLDER, DEFAULT_INTERCEPTOR, HANDLER_TYPE);
        code.addPutfield(thisClassName, HANDLER, HANDLER_TYPE);
        int pc = code.currentPc();

        code.addAload(0);
        int s = addLoadParameters(code, cons.getParameterTypes(), 1);
        code.addInvokespecial(superClass.getName(), "<init>", desc);
        code.addOpcode(Opcode.RETURN);
        code.setMaxLocals(s + 1);
        CodeAttribute ca = code.toCodeAttribute();
        minfo.setCodeAttribute(ca);

        StackMapTable.Writer writer = new StackMapTable.Writer(32);
        writer.sameFrame(pc);
        ca.setAttribute(writer.toStackMapTable(cp));
        return minfo;
    }

    private static MethodInfo makeDelegator(Method meth, String desc,
                ConstPool cp, Class declClass, String delegatorName) {
        MethodInfo delegator = new MethodInfo(cp, delegatorName, desc);
        delegator.setAccessFlags(Modifier.FINAL | Modifier.PUBLIC
                | (meth.getModifiers() & ~(Modifier.PRIVATE
                                           | Modifier.PROTECTED
                                           | Modifier.ABSTRACT
                                           | Modifier.NATIVE
                                           | Modifier.SYNCHRONIZED)));
        setThrows(delegator, cp, meth);
        Bytecode code = new Bytecode(cp, 0, 0);
        code.addAload(0);
        int s = addLoadParameters(code, meth.getParameterTypes(), 1);
        code.addInvokespecial(declClass.getName(), meth.getName(), desc);
        addReturn(code, meth.getReturnType());
        code.setMaxLocals(++s);
        delegator.setCodeAttribute(code.toCodeAttribute());
        return delegator;
    }

    /**
     * @param delegatorName     null if the original method is abstract.
     */
    private static MethodInfo makeForwarder(String thisClassName,
                    Method meth, String desc, ConstPool cp,
                    Class declClass, String delegatorName, int index,
                    ArrayList forwarders) {
        MethodInfo forwarder = new MethodInfo(cp, meth.getName(), desc);
        forwarder.setAccessFlags(Modifier.FINAL
                    | (meth.getModifiers() & ~(Modifier.ABSTRACT
                                               | Modifier.NATIVE
                                               | Modifier.SYNCHRONIZED)));
        setThrows(forwarder, cp, meth);
        int args = Descriptor.paramSize(desc);
        Bytecode code = new Bytecode(cp, 0, args + 2);
        /*
         * static {
         *   methods[index * 2]
         *     = RuntimeSupport.findSuperMethod(this, <overridden name>, <desc>);
         *   methods[index * 2 + 1]
         *     = RuntimeSupport.findMethod(this, <delegator name>, <desc>);
         *     or = null // the original method is abstract.
         * }
         *     :
         * return ($r)handler.invoke(this, methods[index * 2],
         *                methods[index * 2 + 1], $args);
         */
        int origIndex = index * 2;
        int delIndex = index * 2 + 1;
        int arrayVar = args + 1;
        code.addGetstatic(thisClassName, HOLDER, HOLDER_TYPE);
        code.addAstore(arrayVar);

        forwarders.add(new Find2MethodsArgs(meth.getName(), delegatorName, desc, origIndex));

        code.addAload(0);
        code.addGetfield(thisClassName, HANDLER, HANDLER_TYPE);
        code.addAload(0);

        code.addAload(arrayVar);
        code.addIconst(origIndex);
        code.addOpcode(Opcode.AALOAD);

        code.addAload(arrayVar);
        code.addIconst(delIndex);
        code.addOpcode(Opcode.AALOAD);

        makeParameterList(code, meth.getParameterTypes());
        code.addInvokeinterface(MethodHandler.class.getName(), "invoke",
            "(Ljava/lang/Object;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;",
            5);
        Class retType = meth.getReturnType();
        addUnwrapper(code, retType);
        addReturn(code, retType);

        CodeAttribute ca = code.toCodeAttribute();
        forwarder.setCodeAttribute(ca);
        return forwarder;
    }

    static class Find2MethodsArgs {
        String methodName, delegatorName, descriptor;
        int origIndex;

        Find2MethodsArgs(String mname, String dname, String desc, int index) {
            methodName = mname;
            delegatorName = dname;
            descriptor = desc;
            origIndex = index;
        }
    }

    private static void setThrows(MethodInfo minfo, ConstPool cp, Method orig) {
        Class[] exceptions = orig.getExceptionTypes();
        setThrows(minfo, cp, exceptions);
    }

    private static void setThrows(MethodInfo minfo, ConstPool cp,
                                  Class[] exceptions) {
        if (exceptions.length == 0)
            return;

        String[] list = new String[exceptions.length];
        for (int i = 0; i < exceptions.length; i++)
            list[i] = exceptions[i].getName();

        ExceptionsAttribute ea = new ExceptionsAttribute(cp);
        ea.setExceptions(list);
        minfo.setExceptionsAttribute(ea);
    }

    private static int addLoadParameters(Bytecode code, Class[] params,
                                         int offset) {
        int stacksize = 0;
        int n = params.length;
        for (int i = 0; i < n; ++i)
            stacksize += addLoad(code, stacksize + offset, params[i]);

        return stacksize;
    }

    private static int addLoad(Bytecode code, int n, Class type) {
        if (type.isPrimitive()) {
            if (type == Long.TYPE) {
                code.addLload(n);
                return 2;
            }
            else if (type == Float.TYPE)
                code.addFload(n);
            else if (type == Double.TYPE) {
                code.addDload(n);
                return 2;
            }
            else
                code.addIload(n);
        }
        else
            code.addAload(n);

        return 1;
    }

    private static int addReturn(Bytecode code, Class type) {
        if (type.isPrimitive()) {
            if (type == Long.TYPE) {
                code.addOpcode(Opcode.LRETURN);
                return 2;
            }
            else if (type == Float.TYPE)
                code.addOpcode(Opcode.FRETURN);
            else if (type == Double.TYPE) {
                code.addOpcode(Opcode.DRETURN);
                return 2;
            }
            else if (type == Void.TYPE) {
                code.addOpcode(Opcode.RETURN);
                return 0;
            }
            else
                code.addOpcode(Opcode.IRETURN);
        }
        else
            code.addOpcode(Opcode.ARETURN);

        return 1;
    }

    private static void makeParameterList(Bytecode code, Class[] params) {
        int regno = 1;
        int n = params.length;
        code.addIconst(n);
        code.addAnewarray("java/lang/Object");
        for (int i = 0; i < n; i++) {
            code.addOpcode(Opcode.DUP);
            code.addIconst(i);
            Class type = params[i];
            if (type.isPrimitive())
                regno = makeWrapper(code, type, regno);
            else {
                code.addAload(regno);
                regno++;
            }

            code.addOpcode(Opcode.AASTORE);
        }
    }

    private static int makeWrapper(Bytecode code, Class type, int regno) {
        int index = FactoryHelper.typeIndex(type);
        String wrapper = FactoryHelper.wrapperTypes[index]; 
        code.addNew(wrapper);
        code.addOpcode(Opcode.DUP);
        addLoad(code, regno, type);
        code.addInvokespecial(wrapper, "<init>",
                              FactoryHelper.wrapperDesc[index]);
        return regno + FactoryHelper.dataSize[index];
    }

    private static void addUnwrapper(Bytecode code, Class type) {
        if (type.isPrimitive()) {
            if (type == Void.TYPE)
                code.addOpcode(Opcode.POP);
            else {
                int index = FactoryHelper.typeIndex(type);
                String wrapper = FactoryHelper.wrapperTypes[index];
                code.addCheckcast(wrapper);
                code.addInvokevirtual(wrapper,
                                      FactoryHelper.unwarpMethods[index],
                                      FactoryHelper.unwrapDesc[index]);
            }
        }
        else       
            code.addCheckcast(type.getName());
    }

    private static MethodInfo makeWriteReplace(ConstPool cp) {
        MethodInfo minfo = new MethodInfo(cp, "writeReplace", "()Ljava/lang/Object;");
        String[] list = new String[1];
        list[0] = "java.io.ObjectStreamException";
        ExceptionsAttribute ea = new ExceptionsAttribute(cp);
        ea.setExceptions(list);
        minfo.setExceptionsAttribute(ea);
        Bytecode code = new Bytecode(cp, 0, 1);
        code.addAload(0);
        code.addInvokestatic("javassist.util.proxy.RuntimeSupport",
                             "makeSerializedProxy",
                             "(Ljava/lang/Object;)Ljavassist/util/proxy/SerializedProxy;");
        code.addOpcode(Opcode.ARETURN);
        minfo.setCodeAttribute(code.toCodeAttribute());
        return minfo;
    }
}
