blob: 6c977b51d209281f81c056b089980aba4e663df4 [file] [log] [blame]
// Copyright 2011, 2012 The Apache Software Foundation
//
// 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.apache.tapestry5.internal.plastic;
import org.apache.tapestry5.internal.plastic.asm.Opcodes;
import org.apache.tapestry5.internal.plastic.asm.tree.ClassNode;
import org.apache.tapestry5.internal.plastic.asm.tree.MethodNode;
import org.apache.tapestry5.plastic.*;
import java.util.List;
/**
* 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 instantiate 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 = PlasticInternalUtils.newList();
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;
private PlasticClassImpl plasticClass;
MethodAdviceManager(PlasticClassImpl plasticClass, MethodDescription description, MethodNode methodNode)
{
this.plasticClass = plasticClass;
this.description = description;
this.advisedMethodNode = methodNode;
isVoid = description.returnType.equals("void");
invocationClassName = String.format("%s$Invocation_%s_%s", plasticClass.className, description.methodName,
PlasticUtils.nextUID());
invocationClassNode = new ClassNode();
invocationClassNode.visit(PlasticConstants.DEFAULT_VERSION_OPCODE, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, plasticClass.nameCache.toInternalName(invocationClassName),
null, PlasticClassImpl.ABSTRACT_METHOD_INVOCATION_INTERNAL_NAME, new String[]
{plasticClass.nameCache.toInternalName(MethodInvocation.class)});
constructorTypes = createFieldsAndConstructor();
createReturnValueAccessors();
createSetParameter();
createGetParameter();
newMethodName = String.format("advised$%s_%s", description.methodName, PlasticUtils.nextUID());
createProceedToAdvisedMethod();
}
private String[] createFieldsAndConstructor()
{
if (!isVoid)
invocationClassNode.visitField(Opcodes.ACC_PUBLIC, RETURN_VALUE, plasticClass.nameCache.toDesc(description.returnType),
null, null);
List<String> consTypes = PlasticInternalUtils.newList();
consTypes.add(Object.class.getName());
consTypes.add(InstanceContext.class.getName());
consTypes.add(MethodInvocationBundle.class.getName());
for (int i = 0; i < description.argumentTypes.length; i++)
{
String type = description.argumentTypes[i];
invocationClassNode.visitField(Opcodes.ACC_PRIVATE, "p" + i, plasticClass.nameCache.toDesc(type), null, null);
consTypes.add(type);
}
String[] constructorTypes = consTypes.toArray(new String[consTypes.size()]);
MethodNode cons = new MethodNode(Opcodes.ACC_PUBLIC, PlasticClassImpl.CONSTRUCTOR_NAME, plasticClass.nameCache.toMethodDescriptor("void",
constructorTypes), null, null);
InstructionBuilder builder = plasticClass.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,
MethodInvocationBundle.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(Opcodes.ACC_PUBLIC, name, plasticClass.nameCache.toMethodDescriptor(returnType, argumentTypes),
null, null);
invocationClassNode.methods.add(mn);
return plasticClass.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,
plasticClass.className));
} else
{
builder.loadThis().loadArgument(0);
builder.castOrUnbox(description.returnType);
builder.putField(invocationClassName, RETURN_VALUE, description.returnType);
builder.loadThis().invoke(AbstractMethodInvocation.class, void.class, "clearCheckedException");
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()
{
@Override
public void doSwitch(SwitchBlock block)
{
for (int i = 0; i < description.argumentTypes.length; i++)
{
final int index = i;
block.addCase(i, false, new InstructionBuilderCallback()
{
@Override
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()
{
@Override
public void doSwitch(SwitchBlock block)
{
for (int i = 0; i < description.argumentTypes.length; i++)
{
final int index = i;
block.addCase(i, true, new InstructionBuilderCallback()
{
@Override
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 = 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 & ~Opcodes.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
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(plasticClass.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()
{
@Override
public void doBlock(TryCatchBlock block)
{
block.addTry(new InstructionBuilderCallback()
{
@Override
public void doBuild(InstructionBuilder builder)
{
builder.invokeVirtual(plasticClass.className, description.returnType, newMethodName,
description.argumentTypes);
if (!isVoid)
builder.putField(invocationClassName, RETURN_VALUE, description.returnType);
builder.returnResult();
}
});
for (String exceptionName : description.checkedExceptionTypes)
{
if (plasticClass.pool.isCheckedException(exceptionName))
{
block.addCatch(exceptionName, new InstructionBuilderCallback()
{
@Override
public void doBuild(InstructionBuilder builder)
{
builder.loadThis().swap();
builder.invoke(AbstractMethodInvocation.class, MethodInvocation.class,
"setCheckedException", Exception.class);
builder.returnResult();
}
});
}
}
}
});
}
/**
* Creates a new method containing the advised method's original implementation, then rewrites the
* advised method to create the MethodInvocation subclass, invoke proceed() on it, and handle
* the return value and/or checked exceptions.
*/
void rewriteOriginalMethod()
{
createNewMethod();
plasticClass.pool.realize(plasticClass.className, ClassType.METHOD_INVOCATION, invocationClassNode);
String fieldName = String.format("methodinvocationbundle_%s_%s", description.methodName,
PlasticUtils.nextUID());
MethodAdvice[] adviceArray = advice.toArray(new MethodAdvice[advice.size()]);
MethodInvocationBundle bundle = new MethodInvocationBundle(plasticClass.className, description, adviceArray);
plasticClass.classNode.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_FINAL, fieldName, plasticClass.nameCache.toDesc(constructorTypes[2]), null, null);
plasticClass.initializeFieldFromStaticContext(fieldName, constructorTypes[2], bundle);
// 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 = plasticClass.newBuilder(description, advisedMethodNode);
builder.newInstance(invocationClassName).dupe();
// Now load up the parameters to the constructor
builder.loadThis();
builder.loadThis().getField(plasticClass.className, plasticClass.getInstanceContextFieldName(), constructorTypes[1]);
builder.loadThis().getField(plasticClass.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(invocationClassName, new LocalVariableCallback()
{
@Override
public void doBuild(final LocalVariable invocation, InstructionBuilder builder)
{
builder.dupe().storeVariable(invocation);
builder.invoke(AbstractMethodInvocation.class, MethodInvocation.class, "proceed");
if (description.checkedExceptionTypes.length > 0)
{
builder.invoke(MethodInvocation.class, boolean.class, "didThrowCheckedException");
builder.when(Condition.NON_ZERO, new InstructionBuilderCallback()
{
@Override
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();
}
});
}
}