Package org.jplastic.internal

Source Code of org.jplastic.internal.PlasticClassImpl

// Copyright 2010, 2011 Howard M. Lewis Ship
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.jplastic.internal;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import org.jplastic.core.AnnotationAccess;
import org.jplastic.core.ClassInstantiator;
import org.jplastic.core.ComputedValue;
import org.jplastic.core.FieldConduit;
import org.jplastic.core.FieldHandle;
import org.jplastic.core.InstanceContext;
import org.jplastic.core.InstructionBuilder;
import org.jplastic.core.InstructionBuilderCallback;
import org.jplastic.core.MethodAdvice;
import org.jplastic.core.MethodDescription;
import org.jplastic.core.MethodHandle;
import org.jplastic.core.MethodInvocation;
import org.jplastic.core.MethodInvocationResult;
import org.jplastic.core.MethodParameter;
import org.jplastic.core.PlasticClass;
import org.jplastic.core.PlasticField;
import org.jplastic.core.PlasticMethod;
import org.jplastic.core.PlasticUtils;
import org.jplastic.core.PropertyAccessType;
import org.jplastic.core.SwitchBlock;
import org.jplastic.core.SwitchCallback;
import org.jplastic.core.TryCatchBlock;
import org.jplastic.core.TryCatchCallback;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;

@SuppressWarnings("all")
public class PlasticClassImpl extends Lockable implements PlasticClass, PlasticClassTransformation, Opcodes
{
    private static final String NOTHING_TO_VOID = "()V";

    private static final String CONSTRUCTOR_NAME = "<init>";

    private static final String OBJECT_INT_TO_OBJECT = "(Ljava/lang/Object;I)Ljava/lang/Object;";

    private static final String OBJECT_INT_OBJECT_TO_VOID = "(Ljava/lang/Object;ILjava/lang/Object;)V";

    private static final String OBJECT_INT_OBJECT_ARRAY_TO_METHOD_INVOCATION_RESULT = String.format(
            "(Ljava/lang/Object;I[Ljava/lang/Object;)%s", toDesc(Type.getInternalName(MethodInvocationResult.class)));

    private static final String ABSTRACT_METHOD_INVOCATION_INTERNAL_NAME = PlasticInternalUtils
            .toInternalName(AbstractMethodInvocation.class.getName());

    private static final String OBJECT_INTERNAL_NAME = Type.getInternalName(Object.class);

    private static final String HANDLE_SHIM_BASE_CLASS_INTERNAL_NAME = Type
            .getInternalName(PlasticClassHandleShim.class);

    private static final String STATIC_CONTEXT_INTERNAL_NAME = Type.getInternalName(StaticContext.class);

    private static final String INSTANCE_CONTEXT_INTERNAL_NAME = Type.getInternalName(InstanceContext.class);

    private static final String INSTANCE_CONTEXT_DESC = toDesc(INSTANCE_CONTEXT_INTERNAL_NAME);

    private static final String CONSTRUCTOR_DESC = String.format("(L%s;L%s;)V", STATIC_CONTEXT_INTERNAL_NAME,
            INSTANCE_CONTEXT_INTERNAL_NAME);

    private static final MethodDescription TO_STRING_METHOD_DESCRIPTION = new MethodDescription(String.class.getName(),
            "toString");

    private static String toDesc(String internalName)
    {
        return "L" + internalName + ";";
    }

    private class PlasticMember implements AnnotationAccess
    {
        private final AnnotationAccess annotationAccess;

        PlasticMember(List<AnnotationNode> visibleAnnotations)
        {
            annotationAccess = pool.createAnnotationAccess(visibleAnnotations);
        }

        public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType)
        {
            check();

            return annotationAccess.hasAnnotation(annotationType);
        }

        public <T extends Annotation> T getAnnotation(Class<T> annotationType)
        {
            check();

            return annotationAccess.getAnnotation(annotationType);
        }

    }

    private class MethodParameterImpl extends PlasticMember implements MethodParameter
    {
        private final String type;

        private final int index;

        MethodParameterImpl(List<AnnotationNode> visibleAnnotations, String type, int index)
        {
            super(visibleAnnotations);

            this.type = type;
            this.index = index;
        }

        public String getType()
        {
            check();

            return type;
        }

        public int getIndex()
        {
            check();

            return index;
        }
    }

    private class PlasticMethodImpl extends PlasticMember implements PlasticMethod, Comparable<PlasticMethodImpl>
    {
        private final MethodNode node;

        private MethodDescription description;

        private MethodHandleImpl handle;

        private MethodAdviceManager adviceManager;

        private List<MethodParameter> parameters;

        private int methodIndex = -1;

        public PlasticMethodImpl(MethodNode node)
        {
            super(node.visibleAnnotations);

            this.node = node;
            this.description = PlasticInternalUtils.toMethodDescription(node);
        }

        public String toString()
        {
            return String.format("PlasticMethod[%s in class %s]", description, className);
        }

        public MethodDescription getDescription()
        {
            check();

            return description;
        }

        public int compareTo(PlasticMethodImpl o)
        {
            return description.compareTo(o.description);
        }

        public MethodHandle getHandle()
        {
            check();

            if (handle == null)
            {
                methodIndex = nextMethodIndex++;
                handle = new MethodHandleImpl(className, description.toString(), methodIndex);
                shimMethods.add(this);
            }

            return handle;
        }

        public PlasticMethod changeImplementation(InstructionBuilderCallback callback)
        {
            check();

            // If the method is currently abstract, clear that flag.
            if (Modifier.isAbstract(node.access))
            {
                node.access = node.access & ~ACC_ABSTRACT;
                description = description.withModifiers(node.access);
            }

            node.instructions.clear();

            newBuilder(description, node).doCallback(callback);

            // With the implementation changed, it is necessary to intercept field reads/writes.
            // The node may not already have been in the fieldTransformMethods Set if it was
            // an introduced method.

            fieldTransformMethods.add(node);

            return this;
        }

        public PlasticMethod addAdvice(MethodAdvice advice)
        {
            check();

            assert advice != null;

            if (adviceManager == null)
            {
                adviceManager = new MethodAdviceManager(description, node);
                advisedMethods.add(this);
            }

            adviceManager.add(advice);

            return this;
        }

        public PlasticMethod delegateTo(final PlasticField field)
        {
            check();

            assert field != null;

            // TODO: Ensure that the field is a field of this class.

            // TODO: Better handling error case where delegating to a primitive or object array.

            // TODO: Is there a easy way to ensure that the type has the necessary method? I don't
            // like that errors along those lines may be deferred until execution time.

            changeImplementation(new InstructionBuilderCallback()
            {
                public void doBuild(InstructionBuilder builder)
                {
                    // Load the field

                    String delegateType = field.getTypeName();

                    builder.loadThis().getField(className, field.getName(), delegateType);
                    builder.loadArguments();

                    invokeDelegateAndReturnResult(builder, delegateType);
                }
            });

            return this;
        }

        public PlasticMethod delegateTo(PlasticMethod delegateProvidingMethod)
        {
            check();

            assert delegateProvidingMethod != null;

            // TODO: ensure same class, ensure not primitive/array type
            final MethodDescription providerDescriptor = delegateProvidingMethod.getDescription();
            final String delegateType = providerDescriptor.returnType;

            if (delegateType.equals("void") || providerDescriptor.argumentTypes.length > 0)
                throw new IllegalArgumentException(
                        String.format(
                                "Method %s is not usable as a delegate provider; it must be a void method that takes no arguments.",
                                delegateProvidingMethod));

            changeImplementation(new InstructionBuilderCallback()
            {
                public void doBuild(InstructionBuilder builder)
                {
                    // Load the field

                    builder.loadThis();

                    if (Modifier.isPrivate(providerDescriptor.modifiers))
                    {
                        builder.invokeSpecial(className, providerDescriptor);
                    }
                    else
                    {
                        builder.invokeVirtual(className, delegateType, providerDescriptor.methodName);
                    }

                    builder.loadArguments();

                    invokeDelegateAndReturnResult(builder, delegateType);
                }
            });

            return this;
        }

        public List<MethodParameter> getParameters()
        {
            if (parameters == null)
            {
                parameters = PlasticInternalUtils.newList();

                for (int i = 0; i < description.argumentTypes.length; i++)
                {
                    parameters.add(new MethodParameterImpl(node.visibleParameterAnnotations[i],
                            description.argumentTypes[i], i));
                }
            }

            return parameters;
        }

        private void rewriteMethodForAdvice()
        {
            adviceManager.rewriteOriginalMethod();
        }

        private boolean isPrivate()
        {
            return Modifier.isPrivate(node.access);
        }

        /**
         * If a MethodHandle has been requested and the method itself is private, then create a non-private
         * access method. Returns the access method name (which is different from the method name itself only
         * for private methods, where an access method is created).
         */
        private String setupMethodHandleAccess()
        {
            if (isPrivate())
                return createAccessMethod();
            else
                return node.name;
        }

        private String createAccessMethod()
        {
            String name = String.format("%s$access%s", node.name, PlasticUtils.nextUID());

            // Kind of awkward that exceptions are specified as String[] when what we have handy is List<String>
            MethodNode mn = new MethodNode(ACC_SYNTHETIC | ACC_FINAL, name, node.desc, node.signature, null);
            // But it is safe enough for the two nodes to share
            mn.exceptions = node.exceptions;

            InstructionBuilder builder = newBuilder(mn);

            builder.loadThis();
            builder.loadArguments();
            builder.invokeSpecial(className, description);
            builder.returnResult();

            classNode.methods.add(mn);

            return name;
        }

        void installShim(PlasticClassHandleShim shim)
        {
            handle.shim = shim;
        }

        /**
         * The properly cast target instance will be on the stack.
         */
        void extendShimInvoke(SwitchBlock block)
        {
            final String accessMethodName = setupMethodHandleAccess();

            block.addCase(methodIndex, false, new InstructionBuilderCallback()
            {
                public void doBuild(InstructionBuilder builder)
                {
                    builder.startTryCatch(new TryCatchCallback()
                    {
                        public void doBlock(TryCatchBlock block)
                        {
                            block.addTry(new InstructionBuilderCallback()
                            {
                                public void doBuild(InstructionBuilder builder)
                                {
                                    // The third argument is an Object array; get each
                                    for (int i = 0; i < description.argumentTypes.length; i++)
                                    {
                                        String argumentType = description.argumentTypes[i];

                                        builder.loadArgument(2);
                                        builder.loadArrayElement(i, Object.class.getName());
                                        builder.castOrUnbox(argumentType);
                                    }

                                    builder.invokeVirtual(className, description.returnType, accessMethodName,
                                            description.argumentTypes);

                                    // TODO: hate see "void" just there.

                                    if (description.returnType.equals("void"))
                                        builder.loadNull();
                                    else
                                        builder.boxPrimitive(description.returnType);

                                    builder.newInstance(SuccessMethodInvocationResult.class).dupe(1).swap();
                                    builder.invokeConstructor(SuccessMethodInvocationResult.class, Object.class);
                                    builder.returnResult();
                                }
                            });

                            for (String exceptionType : description.checkedExceptionTypes)
                            {
                                block.addCatch(exceptionType, new InstructionBuilderCallback()
                                {
                                    public void doBuild(InstructionBuilder builder)
                                    {
                                        builder.newInstance(FailureMethodInvocationResult.class).dupe(1).swap();
                                        builder.invokeConstructor(FailureMethodInvocationResult.class, Throwable.class);
                                        builder.returnResult();
                                    }
                                });
                            }
                        }
                    });
                }
            });
        }

        private void invokeDelegateAndReturnResult(InstructionBuilder builder, String delegateType)
        {
            // The trick is that you have to be careful to use the right opcode based on the field type
            // (interface vs. ordinary object).

            final TypeCategory typeCategory = pool.getTypeCategory(delegateType);

            if (typeCategory == TypeCategory.INTERFACE)
                builder.invokeInterface(delegateType, description.returnType, description.methodName,
                        description.argumentTypes);
            else
                builder.invokeVirtual(delegateType, description.returnType, description.methodName,
                        description.argumentTypes);

            builder.returnResult();
        }

    }

    private class PlasticFieldImpl extends PlasticMember implements PlasticField, Comparable<PlasticFieldImpl>
    {
        private final FieldNode node;

        private final String typeName;

        private Object tag;

        private FieldHandleImpl handle;

        // Names of methods to get or set the value of the field, invoked
        // from the generated FieldAccess object. With a FieldConduit,
        // these also represent the names of the methods that replace field access
        // in non-introduced methods

        private String getAccessName, setAccessName;

        private FieldState state = FieldState.INITIAL;

        private int fieldIndex = -1;

        public PlasticFieldImpl(FieldNode node)
        {
            super(node.visibleAnnotations);

            this.node = node;
            this.typeName = Type.getType(node.desc).getClassName();
        }

        public String toString()
        {
            return String.format("PlasticField[%s %s %s (in class %s)]", Modifier.toString(node.access), typeName,
                    node.name, className);
        }

        public int compareTo(PlasticFieldImpl o)
        {
            return this.node.name.compareTo(o.node.name);
        }

        public FieldHandle getHandle()
        {
            check();

            if (handle == null)
            {
                fieldIndex = nextFieldIndex++;

                // The shim gets assigned later

                handle = new FieldHandleImpl(className, node.name, fieldIndex);

                shimFields.add(this);
            }

            return handle;
        }

        public PlasticField claim(Object tag)
        {
            assert tag != null;

            check();

            if (this.tag != null)
                throw new IllegalStateException(String.format(
                        "Field %s of class %s can not be claimed by %s as it is already claimed by %s.", node.name,
                        className, tag, this.tag));

            this.tag = tag;

            // Force the list of unclaimed fields to be recomputed on next access

            unclaimedFields = null;

            return this;
        }

        public boolean isClaimed()
        {
            check();

            return tag != null;
        }

        public String getName()
        {
            check();

            return node.name;
        }

        public String getTypeName()
        {
            check();

            return typeName;
        }

        private void verifyInitialState(String operation)
        {
            if (state != FieldState.INITIAL)
                throw new IllegalStateException(String.format("Unable to %s field %s of class %s, as it already %s.",
                        operation, node.name, className, state.description));
        }

        public PlasticField inject(Object value)
        {
            check();

            verifyInitialState("inject a value into");

            assert value != null;

            initializeFieldFromStaticContext(node.name, typeName, value);

            makeReadOnly();

            state = FieldState.INJECTED;

            return this;
        }

        public PlasticField injectComputed(ComputedValue<?> computedValue)
        {
            check();

            verifyInitialState("inject a computed value into");

            assert computedValue != null;

            initializeComputedField(computedValue);

            makeReadOnly();

            state = FieldState.INJECTED;

            return this;
        }

        private void initializeComputedField(ComputedValue<?> computedValue)
        {
            int index = staticContext.store(computedValue);

            constructorBuilder.loadThis(); // for the putField()

            // Get the ComputedValue out of the StaticContext and onto the stack

            constructorBuilder.loadArgument(0).loadConstant(index);
            constructorBuilder.invoke(StaticContext.class, Object.class, "get", int.class).checkcast(
                    ComputedValue.class);

            // Add the InstanceContext to the stack

            constructorBuilder.loadArgument(1);
            constructorBuilder.invoke(ComputedValue.class, Object.class, "get", InstanceContext.class).castOrUnbox(
                    typeName);

            constructorBuilder.putField(className, node.name, typeName);
        }

        public PlasticField setConduit(FieldConduit<?> conduit)
        {
            assert conduit != null;

            check();

            verifyInitialState("set the FieldConduit for");

            // First step: define a field to store the conduit and add constructor logic
            // to initialize it

            String conduitFieldName = createAndInitializeFieldFromStaticContext(node.name + "_FieldConduit",
                    FieldConduit.class.getName(), conduit);

            replaceFieldReadAccess(conduitFieldName);
            replaceFieldWriteAccess(conduitFieldName);

            // TODO: Do we keep the field or not? It will now always be null/0/false.

            state = FieldState.CONDUIT;

            return null;
        }

        public PlasticField createAccessors(PropertyAccessType accessType)
        {
            check();

            return createAccessors(accessType, PlasticInternalUtils.toPropertyName(node.name));
        }

        public PlasticField createAccessors(PropertyAccessType accessType, String propertyName)
        {
            check();

            assert accessType != null;
            assert PlasticInternalUtils.isNonBlank(propertyName);

            String capitalized = PlasticInternalUtils.capitalize(propertyName);

            if (accessType != PropertyAccessType.WRITE_ONLY)
            {
                introduceMethod(new MethodDescription(getTypeName(), "get" + capitalized, null)).changeImplementation(
                        new InstructionBuilderCallback()
                        {
                            public void doBuild(InstructionBuilder builder)
                            {
                                builder.loadThis().getField(className, node.name, getTypeName()).returnResult();
                            }
                        });
            }

            if (accessType != PropertyAccessType.READ_ONLY)
            {
                introduceMethod(new MethodDescription("void", "set" + capitalized, getTypeName()))
                        .changeImplementation(new InstructionBuilderCallback()
                        {
                            public void doBuild(InstructionBuilder builder)
                            {
                                builder.loadThis().loadArgument(0);
                                builder.putField(className, node.name, getTypeName());
                                builder.returnResult();
                            }
                        });
            }

            return this;
        }

        private void replaceFieldWriteAccess(String conduitFieldName)
        {
            setAccessName = makeUnique(methodNames, "set_" + node.name);

            MethodNode mn = new MethodNode(ACC_SYNTHETIC | ACC_FINAL, setAccessName, "(" + node.desc + ")V", null, null);

            InstructionBuilder builder = newBuilder(mn);

            pushFieldConduitOntoStack(conduitFieldName, builder);
            pushInstanceContextFieldOntoStack(builder);

            // Take the value passed to this method and push it onto the stack.

            builder.loadArgument(0);
            builder.boxPrimitive(typeName);

            builder.invoke(FieldConduit.class, void.class, "set", InstanceContext.class, Object.class);

            builder.returnResult();

            addMethod(mn);

            fieldToWriteMethod.put(node.name, setAccessName);
        }

        private void replaceFieldReadAccess(String conduitFieldName)
        {
            getAccessName = makeUnique(methodNames, "getfieldvalue_" + node.name);

            MethodNode mn = new MethodNode(ACC_SYNTHETIC | ACC_FINAL, getAccessName, "()" + node.desc, null, null);

            InstructionBuilder builder = newBuilder(mn);

            // Get the correct FieldConduit object on the stack

            pushFieldConduitOntoStack(conduitFieldName, builder);

            // Now push the instance context on the stack

            pushInstanceContextFieldOntoStack(builder);

            builder.invoke(FieldConduit.class, Object.class, "get", InstanceContext.class).castOrUnbox(typeName);

            builder.returnResult();

            addMethod(mn);

            fieldToReadMethod.put(node.name, getAccessName);
        }

        private void pushFieldConduitOntoStack(String conduitFileName, InstructionBuilder builder)
        {
            builder.loadThis();
            builder.getField(className, conduitFileName, FieldConduit.class);
        }

        private void makeReadOnly()
        {
            setAccessName = makeUnique(methodNames, "setfieldvalue_" + node.name);

            MethodNode mn = new MethodNode(ACC_SYNTHETIC | ACC_FINAL, setAccessName, "(" + node.desc + ")V", null, null);

            String message = String.format("Field %s of class %s is read-only.", node.name, className);

            newBuilder(mn).throwException(IllegalStateException.class, message);

            addMethod(mn);

            fieldToWriteMethod.put(node.name, setAccessName);

            node.access |= ACC_FINAL;
        }

        /**
         * Adds a static setter method, allowing an external FieldAccess implementation
         * to directly set the value of the field.
         */
        private MethodNode addSetAccessMethod()
        {
            String name = makeUnique(methodNames, "directset_" + node.name);

            // Takes two Object parameters (instance, and value) and returns void.

            MethodNode mn = new MethodNode(ACC_SYNTHETIC | ACC_FINAL, name, "(" + node.desc + ")V", null, null);

            InstructionBuilder builder = newBuilder(mn);

            builder.loadThis().loadArgument(0).putField(className, node.name, typeName);
            builder.returnResult();

            addMethod(mn);

            return mn;
        }

        private MethodNode addGetAccessMethod()
        {
            String name = makeUnique(methodNames, "directget_" + node.name);

            MethodNode mn = new MethodNode(ACC_SYNTHETIC | ACC_FINAL, name, "()" + node.desc, null, null);

            InstructionBuilder builder = newBuilder(mn);

            builder.loadThis().getField(className, node.name, typeName).returnResult();

            addMethod(mn);

            return mn;
        }

        void installShim(PlasticClassHandleShim shim)
        {
            if (handle != null)
            {
                handle.shim = shim;
            }
        }

        /** Invoked with the object instance on the stack and cast to the right type. */
        void extendShimGet(SwitchBlock switchBlock)
        {
            String accessMethodName = getAccessName;

            if (accessMethodName == null)
            {
                MethodNode method = addGetAccessMethod();
                fieldTransformMethods.add(method);
                accessMethodName = method.name;
            }

            final String methodToInvoke = accessMethodName;

            switchBlock.addCase(fieldIndex, false, new InstructionBuilderCallback()
            {
                public void doBuild(InstructionBuilder builder)
                {
                    builder.invokeVirtual(className, typeName, methodToInvoke).boxPrimitive(typeName).returnResult();
                }
            });
        }

        /**
         * Invoked with the object instance on the stack and cast to the right type, then the
         * new field value (as Object, needing to be cast or unboxed).
         */
        void extendShimSet(SwitchBlock switchBlock)
        {
            String accessMethodName = setAccessName;

            // If no conduit has yet been specified, then we need a set access method for the shim to invoke.

            if (accessMethodName == null)
            {
                MethodNode method = addSetAccessMethod();
                fieldTransformMethods.add(method);
                accessMethodName = method.name;
            }

            final String methodToInvoke = accessMethodName;

            switchBlock.addCase(fieldIndex, true, new InstructionBuilderCallback()
            {

                public void doBuild(InstructionBuilder builder)
                {
                    builder.castOrUnbox(typeName);
                    builder.invokeVirtual(className, "void", methodToInvoke, typeName);
                    // Should not be necessary, as its always a void method, and we can
                    // drop to the bottom of the method.
                    // builder.returnResult();
                }
            });
        }
    }

    /**
     * Responsible for tracking the advice added to a method, as well as creating the MethodInvocation
     * class for the method and ultimately rewriting the original method to instnatiate the MethodInvocation
     * and handle the success or failure result.
     */
    class MethodAdviceManager
    {
        private final static String RETURN_VALUE = "returnValue";

        private final MethodDescription description;

        /**
         * The method to which advice is added; it must be converted to instantiate the
         * MethodInvocation subclass, then unpack the return value and/or re-throw checked exceptions.
         */
        private final MethodNode advisedMethodNode;

        private final ClassNode invocationClassNode;

        private final List<MethodAdvice> advice = new ArrayList<MethodAdvice>();

        private final boolean isVoid;

        private final String invocationClassName;

        /** The new method that uses the original instructions from the advisedMethodNode. */
        private final String newMethodName;

        private final String[] constructorTypes;

        protected MethodAdviceManager(MethodDescription description, MethodNode methodNode)
        {
            this.description = description;
            this.advisedMethodNode = methodNode;

            isVoid = description.returnType.equals("void");

            invocationClassName = String.format("%s$Invocation_%s_%s", className, description.methodName,
                    PlasticUtils.nextUID());

            invocationClassNode = new ClassNode();

            invocationClassNode.visit(V1_5, ACC_PUBLIC | ACC_FINAL, nameCache.toInternalName(invocationClassName),
                    null, ABSTRACT_METHOD_INVOCATION_INTERNAL_NAME, new String[]
                    { nameCache.toInternalName(MethodInvocation.class) });

            constructorTypes = createFieldsAndConstructor();

            createReturnValueAccessors();

            createSetParameter();

            createGetParameter();

            newMethodName = String.format("advised$%s_%s", description.methodName, PlasticUtils.nextUID());

            createNewMethod();

            createProceedToAdvisedMethod();
        }

        private String[] createFieldsAndConstructor()
        {
            if (!isVoid)
                invocationClassNode.visitField(ACC_PUBLIC, RETURN_VALUE, nameCache.toDesc(description.returnType),
                        null, null);

            List<String> consTypes = new ArrayList<String>();
            consTypes.add(Object.class.getName());
            consTypes.add(InstanceContext.class.getName());
            consTypes.add(MethodAdvice.class.getName() + "[]");

            for (int i = 0; i < description.argumentTypes.length; i++)
            {
                String type = description.argumentTypes[i];

                invocationClassNode.visitField(ACC_PRIVATE, "p" + i, nameCache.toDesc(type), null, null);

                consTypes.add(type);
            }

            String[] constructorTypes = consTypes.toArray(new String[consTypes.size()]);

            MethodNode cons = new MethodNode(ACC_PUBLIC, CONSTRUCTOR_NAME, nameCache.toMethodDescriptor("void",
                    constructorTypes), null, null);

            InstructionBuilder builder = newBuilder(cons);

            // First three arguments go to the super-class

            builder.loadThis();
            builder.loadArgument(0);
            builder.loadArgument(1);
            builder.loadArgument(2);
            builder.invokeConstructor(AbstractMethodInvocation.class, Object.class, InstanceContext.class,
                    MethodAdvice[].class);

            for (int i = 0; i < description.argumentTypes.length; i++)
            {
                String name = "p" + i;
                String type = description.argumentTypes[i];

                builder.loadThis();
                builder.loadArgument(3 + i);
                builder.putField(invocationClassName, name, type);
            }

            builder.returnResult();

            invocationClassNode.methods.add(cons);

            return constructorTypes;
        }

        public void add(MethodAdvice advice)
        {
            this.advice.add(advice);
        }

        private void createReturnValueAccessors()
        {
            addReturnValueSetter();
            createReturnValueGetter();
        }

        private InstructionBuilder newMethod(String name, Class returnType, Class... argumentTypes)
        {
            MethodNode mn = new MethodNode(ACC_PUBLIC, name, nameCache.toMethodDescriptor(returnType, argumentTypes),
                    null, null);

            invocationClassNode.methods.add(mn);

            return newBuilder(mn);
        }

        private void createReturnValueGetter()
        {
            InstructionBuilder builder = newMethod("getReturnValue", Object.class);

            if (isVoid)
            {
                builder.loadNull().returnResult();
            }
            else
            {
                builder.loadThis().getField(invocationClassName, RETURN_VALUE, description.returnType)
                        .boxPrimitive(description.returnType).returnResult();
            }
        }

        private void addReturnValueSetter()
        {
            InstructionBuilder builder = newMethod("setReturnValue", MethodInvocation.class, Object.class);

            if (isVoid)
            {
                builder.throwException(IllegalArgumentException.class, String
                        .format("Method %s of class %s is void, setting a return value is not allowed.", description,
                                className));
            }
            else
            {
                builder.loadThis().loadArgument(0);
                builder.castOrUnbox(description.returnType);
                builder.putField(invocationClassName, RETURN_VALUE, description.returnType);

                builder.loadThis().returnResult();
            }
        }

        private void createGetParameter()
        {
            InstructionBuilder builder = newMethod("getParameter", Object.class, int.class);

            if (description.argumentTypes.length == 0)
            {
                indexOutOfRange(builder);
            }
            else
            {
                builder.loadArgument(0);
                builder.startSwitch(0, description.argumentTypes.length - 1, new SwitchCallback()
                {

                    public void doSwitch(SwitchBlock block)
                    {
                        for (int i = 0; i < description.argumentTypes.length; i++)
                        {
                            final int index = i;

                            block.addCase(i, false, new InstructionBuilderCallback()
                            {

                                public void doBuild(InstructionBuilder builder)
                                {
                                    String type = description.argumentTypes[index];

                                    builder.loadThis();
                                    builder.getField(invocationClassName, "p" + index, type).boxPrimitive(type)
                                            .returnResult();
                                }
                            });
                        }
                    }
                });
            }
        }

        private void indexOutOfRange(InstructionBuilder builder)
        {
            builder.throwException(IllegalArgumentException.class, "Parameter index out of range.");
        }

        private void createSetParameter()
        {
            InstructionBuilder builder = newMethod("setParameter", MethodInvocation.class, int.class, Object.class);

            if (description.argumentTypes.length == 0)
            {
                indexOutOfRange(builder);
            }
            else
            {
                builder.loadArgument(0).startSwitch(0, description.argumentTypes.length - 1, new SwitchCallback()
                {

                    public void doSwitch(SwitchBlock block)
                    {
                        for (int i = 0; i < description.argumentTypes.length; i++)
                        {
                            final int index = i;

                            block.addCase(i, true, new InstructionBuilderCallback()
                            {

                                public void doBuild(InstructionBuilder builder)
                                {
                                    String type = description.argumentTypes[index];

                                    builder.loadThis();
                                    builder.loadArgument(1).castOrUnbox(type);
                                    builder.putField(invocationClassName, "p" + index, type);
                                }
                            });
                        }
                    }
                });

                builder.loadThis().returnResult();
            }
        }

        private void createNewMethod()
        {
            String[] exceptions = (String[]) (advisedMethodNode.exceptions == null ? null
                    : advisedMethodNode.exceptions.toArray(new String[0]));

            // Remove the private flag, so that the MethodInvocation implementation (in the same package)
            // can directly access the method without an additional access method.

            MethodNode mn = new MethodNode(advisedMethodNode.access & ~ACC_PRIVATE, newMethodName,
                    advisedMethodNode.desc, advisedMethodNode.signature, exceptions);

            // Copy everything else about the advisedMethodNode over to the new node

            advisedMethodNode.accept(mn);

            // Add this new method, with the same implementation as the original method, to the
            // PlasticClass

            classNode.methods.add(mn);
        }

        /** Invoke the "new" method, and deal with the return value and/or thrown exceptions. */
        private void createProceedToAdvisedMethod()
        {
            InstructionBuilder builder = newMethod("proceedToAdvisedMethod", void.class);

            if (!isVoid)
                builder.loadThis();

            builder.loadThis().invoke(AbstractMethodInvocation.class, Object.class, "getInstance").checkcast(className);

            // Load up each parameter
            for (int i = 0; i < description.argumentTypes.length; i++)
            {
                String type = description.argumentTypes[i];

                builder.loadThis().getField(invocationClassName, "p" + i, type);
            }

            builder.startTryCatch(new TryCatchCallback()
            {
                public void doBlock(TryCatchBlock block)
                {
                    block.addTry(new InstructionBuilderCallback()
                    {

                        public void doBuild(InstructionBuilder builder)
                        {
                            builder.invokeVirtual(className, description.returnType, newMethodName,
                                    description.argumentTypes);

                            if (!isVoid)
                                builder.putField(invocationClassName, RETURN_VALUE, description.returnType);

                            builder.returnResult();
                        }
                    });

                    for (String exceptionName : description.checkedExceptionTypes)
                    {
                        block.addCatch(exceptionName, new InstructionBuilderCallback()
                        {
                            public void doBuild(InstructionBuilder builder)
                            {
                                builder.loadThis().swap();
                                builder.invoke(AbstractMethodInvocation.class, MethodInvocation.class,
                                        "setCheckedException", Exception.class);

                                builder.returnResult();
                            }
                        });
                    }
                }
            });
        }

        private void rewriteOriginalMethod()
        {
            pool.realize(invocationClassNode);

            String fieldName = String.format("advice_%s_%s", description.methodName, PlasticUtils.nextUID());

            MethodAdvice[] adviceArray = advice.toArray(new MethodAdvice[advice.size()]);

            classNode.visitField(ACC_PRIVATE | ACC_FINAL, fieldName, nameCache.toDesc(constructorTypes[2]), null, null);
            initializeFieldFromStaticContext(fieldName, constructorTypes[2], adviceArray);

            // Ok, here's the easy part: replace the method invocation with instantiating the invocation class

            advisedMethodNode.instructions.clear();
            advisedMethodNode.tryCatchBlocks.clear();

            if (advisedMethodNode.localVariables != null)
                advisedMethodNode.localVariables.clear();

            InstructionBuilder builder = newBuilder(description, advisedMethodNode);

            builder.newInstance(invocationClassName).dupe(0);

            // Now load up the parameters to the constructor

            builder.loadThis();
            builder.loadThis().getField(className, getInstanceContextFieldName(), constructorTypes[1]);
            builder.loadThis().getField(className, fieldName, constructorTypes[2]);

            // Load up the actual method parameters

            builder.loadArguments();
            builder.invokeConstructor(invocationClassName, constructorTypes);

            // That leaves an instance of the invocation class on the stack. If the method is void
            // and throws no checked exceptions, then the variable actually isn't used. This code
            // should be refactored a bit once there are tests for those cases.

            builder.startVariable("invocation", invocationClassName, new InstructionBuilderCallback()
            {
                public void doBuild(InstructionBuilder builder)
                {
                    builder.dupe(0).storeVariable("invocation");

                    builder.invoke(AbstractMethodInvocation.class, MethodInvocation.class, "proceed");

                    if (description.checkedExceptionTypes.length > 0)
                    {
                        builder.invoke(MethodInvocation.class, boolean.class, "didThrowCheckedException");

                        builder.ifZero(null, new InstructionBuilderCallback()
                        {
                            public void doBuild(InstructionBuilder builder)
                            {
                                builder.loadVariable("invocation").loadTypeConstant(Exception.class);
                                builder.invokeVirtual(invocationClassName, Throwable.class.getName(),
                                        "getCheckedException", Class.class.getName());
                                builder.throwException();
                            }
                        });
                    }

                    if (!isVoid)
                        builder.loadVariable("invocation").getField(invocationClassName, RETURN_VALUE,
                                description.returnType);

                    builder.returnResult();
                }
            });
        }
    }

    // Now past the inner classes; these are the instance variables of PlasticClassImpl proper:

    private final ClassNode classNode;

    private final PlasticClassPool pool;

    private final String className;

    private final String superClassName;

    private final AnnotationAccess annotationAccess;

    // All the non-introduced (and non-constructor) methods, in sorted order

    private final List<PlasticMethodImpl> methods;

    private final Map<MethodDescription, PlasticMethod> description2method = new HashMap<MethodDescription, PlasticMethod>();

    private final Set<String> methodNames = new HashSet<String>();

    // All non-introduced instance fields

    private final List<PlasticFieldImpl> fields;

    /**
     * Methods that require special attention inside {@link #createInstantiator()} because they
     * have method advice.
     */
    private final Set<PlasticMethodImpl> advisedMethods = PlasticInternalUtils.newSet();;

    private final NameCache nameCache = new NameCache();

    // This is generated from fields, as necessary
    private List<PlasticField> unclaimedFields;

    private final Set<String> fieldNames = PlasticInternalUtils.newSet();

    private final StaticContext staticContext;

    private final MethodBundle methodBundle;

    // MethodNodes in which field transformations should occur; this is most existing and
    // introduced methods, outside of special access methods.

    private final Set<MethodNode> fieldTransformMethods = PlasticInternalUtils.newSet();

    /**
     * Maps a field name to a replacement method that should be invoked instead of reading the
     * field.
     */
    private final Map<String, String> fieldToReadMethod = new HashMap<String, String>();

    /**
     * Maps a field name to a replacement method that should be invoked instead of writing the
     * field.
     */
    private final Map<String, String> fieldToWriteMethod = new HashMap<String, String>();

    /**
     * This normal no-arguments constructor, or null. By the end of the transformation
     * this will be converted into an ordinary method.
     */
    private MethodNode originalConstructor;

    private final MethodNode newConstructor;

    private final InstructionBuilder constructorBuilder;

    private String instanceContextFieldName;

    private Class<?> transformedClass;

    // Indexes used to identify fields or methods in the shim
    private int nextFieldIndex = 0;

    private int nextMethodIndex = 0;

    // Set of fields that need to contribute to the shim and gain access to it

    private final Set<PlasticFieldImpl> shimFields = PlasticInternalUtils.newSet();

    // Set of methods that need to contribute to the shim and gain access to it

    private final Set<PlasticMethodImpl> shimMethods = PlasticInternalUtils.newSet();

    /**
     * @param classNode
     * @param pool
     * @param parentMethodBundle
     * @param parentStaticContext
     */
    public PlasticClassImpl(ClassNode classNode, PlasticClassPool pool, MethodBundle parentMethodBundle,
            StaticContext parentStaticContext)
    {
        this.classNode = classNode;
        this.pool = pool;

        staticContext = parentStaticContext.dupe();

        annotationAccess = pool.createAnnotationAccess(classNode.visibleAnnotations);

        className = PlasticInternalUtils.toClassName(classNode.name);
        superClassName = PlasticInternalUtils.toClassName(classNode.superName);

        methodBundle = parentMethodBundle.createChild(className);

        methods = new ArrayList(classNode.methods.size());

        String invalidConstructorMessage = String.format(
                "Class %s has been transformed and may not be directly instantiated.", className);

        for (MethodNode node : (List<MethodNode>) classNode.methods)
        {
            if (node.name.equals(CONSTRUCTOR_NAME))
            {
                if (node.desc.equals(NOTHING_TO_VOID))
                {
                    originalConstructor = node;
                    fieldTransformMethods.add(node);
                }
                else
                {
                    node.instructions.clear();

                    newBuilder(node).throwException(IllegalStateException.class, invalidConstructorMessage);
                }

                continue;
            }

            if (Modifier.isStatic(node.access))
                continue;

            if (!Modifier.isAbstract(node.access))
                fieldTransformMethods.add(node);

            PlasticMethodImpl pmi = new PlasticMethodImpl(node);

            methods.add(pmi);
            description2method.put(pmi.getDescription(), pmi);

            if (isInheritableMethod(node))
                methodBundle.addMethod(node.name, node.desc);

            methodNames.add(node.name);
        }

        Collections.sort(methods);

        fields = new ArrayList(classNode.fields.size());

        for (FieldNode node : (List<FieldNode>) classNode.fields)
        {
            fieldNames.add(node.name);

            // Ignore static fields.

            if (Modifier.isStatic(node.access))
                continue;

            // TODO: Perhaps we should defer this private check until it is needed,
            // i.e., when we do some work on the field that requires it to be private?
            // However, given class loading issues, public fields are likely to cause
            // their own problems when passing the ClassLoader boundrary.

            if (!Modifier.isPrivate(node.access))
                throw new IllegalArgumentException(
                        String.format(
                                "Field %s of class %s is not private. Class transformation requires that all instance fields be private.",
                                node.name, className));

            fields.add(new PlasticFieldImpl(node));
        }

        Collections.sort(fields);

        // TODO: Make the output class's constructor protected, and create a shim class to instantiate it
        // efficiently (without reflection).
        newConstructor = new MethodNode(ACC_PUBLIC, CONSTRUCTOR_NAME, CONSTRUCTOR_DESC, null, null);
        constructorBuilder = newBuilder(newConstructor);

        // Start by calling the super-class no args constructor

        if (parentMethodBundle.isTransformed())
        {
            // If the parent is transformed, our first step is always to invoke its constructor.

            constructorBuilder.loadThis().loadArgument(0).loadArgument(1);
            constructorBuilder.invokeConstructor(superClassName, StaticContext.class.getName(),
                    InstanceContext.class.getName());
        }
        else
        {
            // Assumes the base class includes a visible constructor that takes no arguments.
            // TODO: Do a proper check for this case and throw a meaningful exception
            // if not present.

            constructorBuilder.loadThis().invokeConstructor(superClassName);
        }

        // During the transformation, we'll be adding code to the constructor to pull values
        // out of the static or instance context and assign them to fields.

        // Later on, we'll add the RETURN opcode
    }

    public <T extends Annotation> boolean hasAnnotation(Class<T> annotationType)
    {
        check();

        return annotationAccess.hasAnnotation(annotationType);
    }

    public <T extends Annotation> T getAnnotation(Class<T> annotationType)
    {
        check();

        return annotationAccess.getAnnotation(annotationType);
    }

    public PlasticClass proxyInterface(Class interfaceType, PlasticField field)
    {
        check();

        assert field != null;

        introduceInterface(interfaceType);

        for (Method m : interfaceType.getMethods())
        {
            introduceMethod(m).delegateTo(field);
        }

        return this;
    }

    public ClassInstantiator createInstantiator()
    {
        lock();

        interceptFieldAccess();

        createShimIfNeeded();

        rewriteAdvisedMethods();

        completeConstructor();

        transformedClass = pool.realizeTransformedClass(classNode, methodBundle, staticContext);

        return createInstantiatorFromClass(transformedClass);
    }

    private ClassInstantiator createInstantiatorFromClass(Class clazz)
    {
        try
        {
            Constructor ctor = clazz.getConstructor(StaticContext.class, InstanceContext.class);

            return new ClassInstantiatorImpl(clazz, ctor, staticContext);
        }
        catch (Exception ex)
        {
            throw new RuntimeException(String.format("Unable to create ClassInstantiator for class %s: %s",
                    clazz.getName(), PlasticInternalUtils.toMessage(ex)), ex);
        }
    }

    private void completeConstructor()
    {
        if (originalConstructor != null)
        {
            // Convert the original constructor into a private method invoked from the
            // generated constructor.

            String initializerName = makeUnique(methodNames, "initializeInstance");

            originalConstructor.access = ACC_PRIVATE;
            originalConstructor.name = initializerName;

            stripOutSuperConstructorCall(originalConstructor);

            constructorBuilder.loadThis().invokeVirtual(className, "void", initializerName);
        }

        constructorBuilder.returnResult();

        classNode.methods.add(newConstructor);
    }

    private void stripOutSuperConstructorCall(MethodNode cons)
    {
        InsnList ins = cons.instructions;

        ListIterator li = ins.iterator();

        // Look for the ALOAD 0 (i.e., push this on the stack)
        while (li.hasNext())
        {
            AbstractInsnNode node = (AbstractInsnNode) li.next();

            if (node.getOpcode() == ALOAD)
            {
                VarInsnNode varNode = (VarInsnNode) node;

                assert varNode.var == 0;

                // Remove the ALOAD
                li.remove();
                break;
            }
        }

        // Look for the call to the super-class, an INVOKESPECIAL
        while (li.hasNext())
        {
            AbstractInsnNode node = (AbstractInsnNode) li.next();

            if (node.getOpcode() == INVOKESPECIAL)
            {
                MethodInsnNode mnode = (MethodInsnNode) node;

                assert mnode.owner.equals(classNode.superName);
                assert mnode.name.equals(CONSTRUCTOR_NAME);
                assert mnode.desc.equals(cons.desc);

                li.remove();
                return;
            }
        }

        throw new AssertionError("Could not convert constructor to simple method.");
    }

    public <T extends Annotation> List<PlasticField> getFieldsWithAnnotation(Class<T> annotationType)
    {
        check();

        List<PlasticField> result = getUnclaimedFields();

        Iterator<PlasticField> iterator = result.iterator();

        while (iterator.hasNext())
        {
            PlasticField plasticField = iterator.next();

            if (!plasticField.hasAnnotation(annotationType))
                iterator.remove();
        }

        return result;
    }

    public List<PlasticField> getAllFields()
    {
        check();

        return new ArrayList<PlasticField>(fields);
    }

    public List<PlasticField> getUnclaimedFields()
    {
        check();

        // Initially null, and set back to null by PlasticField.claim().

        if (unclaimedFields == null)
        {
            unclaimedFields = new ArrayList<PlasticField>(fields.size());

            for (PlasticField f : fields)
            {
                if (!f.isClaimed())
                    unclaimedFields.add(f);
            }
        }

        return unclaimedFields;
    }

    public PlasticField introduceField(String className, String suggestedName)
    {
        check();

        assert PlasticInternalUtils.isNonBlank(className);
        assert PlasticInternalUtils.isNonBlank(suggestedName);

        String name = makeUnique(fieldNames, suggestedName);

        // No signature and no initial value

        FieldNode fieldNode = new FieldNode(ACC_PRIVATE, name, PlasticInternalUtils.toDescriptor(className), null, null);

        classNode.fields.add(fieldNode);

        fieldNames.add(name);

        PlasticFieldImpl newField = new PlasticFieldImpl(fieldNode);

        return newField;
    }

    public PlasticField introduceField(Class fieldType, String suggestedName)
    {
        assert fieldType != null;

        return introduceField(fieldType.getName(), suggestedName);
    }

    private String makeUnique(Set<String> values, String input)
    {
        return values.contains(input) ? input + "$" + PlasticUtils.nextUID() : input;
    }

    public <T extends Annotation> List<PlasticMethod> getMethodsWithAnnotation(Class<T> annotationType)
    {
        check();

        List<PlasticMethod> result = getMethods();
        Iterator<PlasticMethod> iterator = result.iterator();

        while (iterator.hasNext())
        {
            PlasticMethod method = iterator.next();

            if (!method.hasAnnotation(annotationType))
                iterator.remove();
        }

        return result;
    }

    public List<PlasticMethod> getMethods()
    {
        check();

        return new ArrayList<PlasticMethod>(methods);
    }

    public PlasticMethod introduceMethod(MethodDescription description)
    {
        check();

        PlasticMethod result = description2method.get(description);

        if (result == null)
        {
            result = createNewMethod(description);

            description2method.put(description, result);
        }

        // Note that is it not necessary to add the new MethodNode to
        // fieldTransformMethods (the default implementations provided by introduceMethod() do not
        // ever access instance fields) ... unless the caller invokes changeImplementation().

        return result;
    }

    public PlasticMethod introduceMethod(Method method)
    {
        check();

        return introduceMethod(new MethodDescription(method).withModifiers(method.getModifiers() & ~Modifier.ABSTRACT));
    }

    private void addMethod(MethodNode methodNode)
    {
        classNode.methods.add(methodNode);

        methodNames.add(methodNode.name);
    }

    private PlasticMethod createNewMethod(MethodDescription description)
    {
        if (Modifier.isAbstract(description.modifiers) || Modifier.isStatic(description.modifiers))
            throw new IllegalArgumentException(String.format(
                    "Unable to introduce method '%s' into class %s: introduced methods may not be abstract or static.",
                    description, className));

        String desc = nameCache.toDesc(description);

        String[] exceptions = new String[description.checkedExceptionTypes.length];
        for (int i = 0; i < exceptions.length; i++)
        {
            exceptions[i] = PlasticInternalUtils.toInternalName(description.checkedExceptionTypes[i]);
        }

        MethodNode methodNode = new MethodNode(description.modifiers, description.methodName, desc, null, exceptions);
        boolean isOverride = methodBundle.isImplemented(methodNode.name, desc);

        if (isOverride)
            createOverrideOfBaseClassImpl(description, methodNode);
        else
            createNewMethodImpl(description, methodNode);

        classNode.methods.add(methodNode);

        if (!Modifier.isPrivate(description.modifiers))
            methodBundle.addMethod(description.methodName, desc);

        return new PlasticMethodImpl(methodNode);
    }

    private void createNewMethodImpl(MethodDescription methodDescription, MethodNode methodNode)
    {
        newBuilder(methodDescription, methodNode).returnDefaultValue();
    }

    private void createOverrideOfBaseClassImpl(MethodDescription methodDescription, MethodNode methodNode)
    {
        InstructionBuilder builder = newBuilder(methodDescription, methodNode);

        builder.loadThis();
        builder.loadArguments();
        builder.invokeSpecial(superClassName, methodDescription);
        builder.returnResult();
    }

    /**
     * Iterates over all non-introduced methods, including the original constructor. For each
     * method, the bytecode is
     * scanned for field reads and writes. When a match is found against an intercepted field, the
     * operation is
     * replaced with a method invocation.
     */
    private void interceptFieldAccess()
    {
        for (MethodNode node : fieldTransformMethods)
        {
            interceptFieldAccess(node);
        }
    }

    /**
     * Determines if any fields or methods have provided FieldHandles or MethodHandles; if so
     * a shim class must be created to facilitate read/write access to fields, or invocation of methods.
     */
    private void createShimIfNeeded()
    {
        if (shimFields.isEmpty() && shimMethods.isEmpty())
            return;

        PlasticClassHandleShim shim = createShimInstance();

        installShim(shim);
    }

    public void installShim(PlasticClassHandleShim shim)
    {
        for (PlasticFieldImpl f : shimFields)
        {
            f.installShim(shim);
        }

        for (PlasticMethodImpl m : shimMethods)
        {
            m.installShim(shim);
        }
    }

    public PlasticClassHandleShim createShimInstance()
    {
        String shimClassName = String.format("%s$Shim_%s", classNode.name, PlasticUtils.nextUID());

        ClassNode shimClassNode = new ClassNode();

        shimClassNode.visit(V1_5, ACC_PUBLIC | ACC_FINAL, shimClassName, null, HANDLE_SHIM_BASE_CLASS_INTERNAL_NAME,
                null);

        implementConstructor(shimClassNode);

        if (!shimFields.isEmpty())
        {
            implementShimGet(shimClassNode);
            implementShimSet(shimClassNode);
        }

        if (!shimMethods.isEmpty())
        {
            implementShimInvoke(shimClassNode);
        }

        return instantiateShim(shimClassNode);
    }

    private void implementConstructor(ClassNode shimClassNode)
    {
        MethodNode mn = new MethodNode(ACC_PUBLIC, CONSTRUCTOR_NAME, NOTHING_TO_VOID, null, null);

        InstructionBuilder builder = newBuilder(mn);

        builder.loadThis().invokeConstructor(PlasticClassHandleShim.class).returnResult();

        shimClassNode.methods.add(mn);

    }

    private PlasticClassHandleShim instantiateShim(ClassNode shimClassNode)
    {
        Class shimClass = pool.realize(shimClassNode);

        try
        {
            return (PlasticClassHandleShim) shimClass.newInstance();
        }
        catch (Exception ex)
        {
            throw new RuntimeException(
                    String.format("Unable to instantiate shim class %s for plastic class %s: %s",
                            PlasticInternalUtils.toClassName(shimClassNode.name), className,
                            PlasticInternalUtils.toMessage(ex)), ex);
        }
    }

    private void implementShimGet(ClassNode shimClassNode)
    {
        MethodNode mn = new MethodNode(ACC_PUBLIC, "get", OBJECT_INT_TO_OBJECT, null, null);

        InstructionBuilder builder = newBuilder(mn);

        // Arg 0 is the target instance
        // Arg 1 is the index

        builder.loadArgument(0).checkcast(className);
        builder.loadArgument(1);

        builder.startSwitch(0, nextFieldIndex - 1, new SwitchCallback()
        {
            public void doSwitch(SwitchBlock block)
            {
                for (PlasticFieldImpl f : shimFields)
                {
                    f.extendShimGet(block);
                }
            }
        });

        shimClassNode.methods.add(mn);
    }

    private void implementShimSet(ClassNode shimClassNode)
    {
        MethodNode mn = new MethodNode(ACC_PUBLIC, "set", OBJECT_INT_OBJECT_TO_VOID, null, null);

        InstructionBuilder builder = newBuilder(mn);

        // Arg 0 is the target instance
        // Arg 1 is the index
        // Arg 2 is the new value

        builder.loadArgument(0).checkcast(className);
        builder.loadArgument(2);

        builder.loadArgument(1);

        builder.startSwitch(0, nextFieldIndex - 1, new SwitchCallback()
        {
            public void doSwitch(SwitchBlock block)
            {
                for (PlasticFieldImpl f : shimFields)
                {
                    f.extendShimSet(block);
                }
            }
        });

        builder.returnResult();

        shimClassNode.methods.add(mn);
    }

    private void implementShimInvoke(ClassNode shimClassNode)
    {
        MethodNode mn = new MethodNode(ACC_PUBLIC, "invoke", OBJECT_INT_OBJECT_ARRAY_TO_METHOD_INVOCATION_RESULT, null,
                null);

        InstructionBuilder builder = newBuilder(mn);

        // Arg 0 is the target instance
        // Arg 1 is the index
        // Arg 2 is the object array of parameters

        builder.loadArgument(0).checkcast(className);

        builder.loadArgument(1);

        builder.startSwitch(0, nextMethodIndex - 1, new SwitchCallback()
        {
            public void doSwitch(SwitchBlock block)
            {
                for (PlasticMethodImpl m : shimMethods)
                {
                    m.extendShimInvoke(block);
                }
            }
        });

        shimClassNode.methods.add(mn);
    }

    private void rewriteAdvisedMethods()
    {
        for (PlasticMethodImpl method : advisedMethods)
        {
            method.rewriteMethodForAdvice();
        }
    }

    private void interceptFieldAccess(MethodNode methodNode)
    {
        InsnList insns = methodNode.instructions;

        ListIterator it = insns.iterator();

        while (it.hasNext())
        {
            AbstractInsnNode node = (AbstractInsnNode) it.next();

            int opcode = node.getOpcode();

            if (opcode != GETFIELD && opcode != PUTFIELD)
                continue;

            // Make sure we're talking about access to a field of this class, not some other
            // visible field of another class.

            FieldInsnNode fnode = (FieldInsnNode) node;

            if (!fnode.owner.equals(classNode.name))
                continue;

            Map<String, String> fieldToMethod = opcode == GETFIELD ? fieldToReadMethod : fieldToWriteMethod;

            String methodName = fieldToMethod.get(fnode.name);

            if (methodName == null)
                continue;

            String methodDescription = opcode == GETFIELD ? "()" + fnode.desc : "(" + fnode.desc + ")V";

            // Replace the field access node with the appropriate method invocation.

            insns.insertBefore(fnode, new MethodInsnNode(INVOKEVIRTUAL, fnode.owner, methodName, methodDescription));

            it.remove();
        }
    }

    private String getInstanceContextFieldName()
    {
        if (instanceContextFieldName == null)
        {
            instanceContextFieldName = makeUnique(fieldNames, "instanceContext");

            // TODO: Once we support inheritance, we could use a protected field and only initialize
            // it once, in the first base class where it is needed.

            FieldNode node = new FieldNode(ACC_PRIVATE | ACC_FINAL, instanceContextFieldName, INSTANCE_CONTEXT_DESC,
                    null, null);

            classNode.fields.add(node);

            // Extend the constructor to store the context in a field.

            constructorBuilder.loadThis().loadArgument(1)
                    .putField(className, instanceContextFieldName, InstanceContext.class);
        }

        return instanceContextFieldName;
    }

    /** Creates a new private final field and initializes its value (using the StaticContext). */
    private String createAndInitializeFieldFromStaticContext(String suggestedFieldName, String fieldType,
            Object injectedFieldValue)
    {
        String name = makeUnique(fieldNames, suggestedFieldName);

        FieldNode field = new FieldNode(ACC_PRIVATE | ACC_FINAL, name, nameCache.toDesc(fieldType), null, null);

        classNode.fields.add(field);

        initializeFieldFromStaticContext(name, fieldType, injectedFieldValue);

        return name;
    }

    /**
     * Initializes a field from the static context. The injected value is added to the static
     * context and the class constructor updated to assign the value from the context (which includes casting and
     * possibly unboxing).
     */
    private void initializeFieldFromStaticContext(String fieldName, String fieldType, Object injectedFieldValue)
    {
        int index = staticContext.store(injectedFieldValue);

        // Although it feels nicer to do the loadThis() later and then swap() that breaks
        // on primitive longs and doubles, so its just easier to do the loadThis() first
        // so its at the right place on the stack for the putField().

        constructorBuilder.loadThis();

        constructorBuilder.loadArgument(0).loadConstant(index);
        constructorBuilder.invoke(StaticContext.class, Object.class, "get", int.class);
        constructorBuilder.castOrUnbox(fieldType);

        constructorBuilder.putField(className, fieldName, fieldType);
    }

    private void pushInstanceContextFieldOntoStack(InstructionBuilder builder)
    {
        builder.loadThis().getField(className, getInstanceContextFieldName(), InstanceContext.class);
    }

    public PlasticClass getPlasticClass()
    {
        return this;
    }

    public Class<?> getTransformedClass()
    {
        if (transformedClass == null)
            throw new IllegalStateException(String.format(
                    "Transformed class %s is not yet available because the transformation is not yet complete.",
                    className));

        return transformedClass;
    }

    private boolean isInheritableMethod(MethodNode node)
    {
        return (node.access & (ACC_ABSTRACT | ACC_PRIVATE)) == 0;
    }

    public String getClassName()
    {
        return className;
    }

    private InstructionBuilderImpl newBuilder(MethodNode mn)
    {
        return newBuilder(PlasticInternalUtils.toMethodDescription(mn), mn);
    }

    private InstructionBuilderImpl newBuilder(MethodDescription description, MethodNode mn)
    {
        return new InstructionBuilderImpl(description, mn, nameCache);
    }

    public Set<PlasticMethod> introduceInterface(Class interfaceType)
    {
        check();

        assert interfaceType != null;

        if (!interfaceType.isInterface())
            throw new IllegalArgumentException(String.format(
                    "Class %s is not an interface; ony interfaces may be introduced.", interfaceType.getName()));

        String interfaceName = nameCache.toInternalName(interfaceType);

        // I suppose this means that a subclass may restate that it implements an interface from a base class.

        if (!classNode.interfaces.contains(interfaceName))
            classNode.interfaces.add(interfaceName);

        Set<PlasticMethod> introducedMethods = new HashSet<PlasticMethod>();

        for (Method m : interfaceType.getMethods())
        {
            MethodDescription description = new MethodDescription(m);

            if (!isMethodImplemented(description))
            {
                introducedMethods.add(introduceMethod(m));
            }
        }

        return introducedMethods;
    }

    public PlasticClass addToString(final String toStringValue)
    {
        check();

        if (!isMethodImplemented(TO_STRING_METHOD_DESCRIPTION))
        {
            introduceMethod(TO_STRING_METHOD_DESCRIPTION).changeImplementation(new InstructionBuilderCallback()
            {
                public void doBuild(InstructionBuilder builder)
                {
                    builder.loadConstant(toStringValue).returnResult();
                }
            });
        }

        return this;
    }

    /**
     * Returns true if this class has an implementation of the indicated method, or a super-class provides
     * a non-abstract implementation.
     */
    private boolean isMethodImplemented(MethodDescription description)
    {
        return methodBundle.isImplemented(description.methodName, nameCache.toDesc(description));
    }

}
TOP

Related Classes of org.jplastic.internal.PlasticClassImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.