/*
* JBoss, Home of Professional Open Source
* Copyright 2008-10 Red Hat and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
* @authors Andrew Dinn
*/
package org.jboss.byteman.rule.expression;
import org.jboss.byteman.rule.compiler.CompileContext;
import org.jboss.byteman.rule.exception.TypeWarningException;
import org.jboss.byteman.rule.type.Type;
import org.jboss.byteman.rule.type.TypeGroup;
import org.jboss.byteman.rule.exception.TypeException;
import org.jboss.byteman.rule.exception.ExecuteException;
import org.jboss.byteman.rule.exception.ThrowException;
import org.jboss.byteman.rule.exception.CompileException;
import org.jboss.byteman.rule.Rule;
import org.jboss.byteman.rule.helper.HelperAdapter;
import org.jboss.byteman.rule.grammar.ParseNode;
import org.jboss.byteman.rule.type.TypeHelper;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Expression which implements a throw from a rule action but only where the thrown exception is
* declared by the trigger method or is a runtime exception which does nto need ot be declared
*/
public class ThrowExpression extends Expression
{
private String typeName;
private List<Expression> arguments;
private List<Type> argumentTypes;
private List<Type> paramTypes;
private Constructor constructor;
public ThrowExpression(Rule rule, ParseNode token, List<Expression> arguments) {
super(rule, Type.UNDEFINED, token);
this.typeName = token.getText();
this.arguments = arguments;
this.argumentTypes = null;
this.constructor = null;
}
/**
* verify that variables mentioned in this expression are actually available in the supplied
* bindings list and infer/validate the type of this expression or its subexpressions
* where possible
*
* @return true if all variables in this expression are bound and no type mismatches have
* been detected during inference/validation.
*/
public void bind() throws TypeException
{
// check that the recipient and argument expressions have valid bindings
Iterator<Expression> iterator = arguments.iterator();
while (iterator.hasNext()) {
iterator.next().bind();
}
}
/**
* ensure that all type references in the expression and its component expressions
* can be resolved, that the type of the expression is well-defined and that it is
* compatible with the type expected in the context in which it occurs.
*
* @param expected the type expected for the expression in the contxt in which it occurs. this
* may be void but shoudl not be undefined at the point where type checking is performed.
* @return
* @throws org.jboss.byteman.rule.exception.TypeException
*
*/
public Type typeCheck(Type expected) throws TypeException {
// check the exception type is defined and then look for a relevant constructor
TypeGroup typeGroup = getTypeGroup();
type = Type.dereference(typeGroup.create(typeName));
if (type == null || type.isUndefined()) {
throw new TypeException("ThrowExpression.typeCheck : unknown exception type " + typeName + getPos());
}
if (!Throwable.class.isAssignableFrom(type.getTargetClass())) {
throw new TypeException("ThrowExpression.typeCheck : not an exception type " + typeName + getPos());
}
Class clazz = type.getTargetClass();
// if we can find a unique method then we can use it to type the parameters
// otherwise we do it the hard way
int arity = arguments.size();
Constructor[] constructors = clazz.getConstructors();
List<Constructor> candidates = new ArrayList<Constructor>();
boolean duplicates = false;
for (Constructor constructor : constructors) {
if (constructor.getParameterTypes().length == arity) {
candidates.add(constructor);
}
}
argumentTypes = new ArrayList<Type>();
// check each argument in turn -- if all candidates have the same argument type then
// use that as the type to check against
for (int i = 0; i < arguments.size() ; i++) {
if (candidates.isEmpty()) {
throw new TypeException("ThrowExpression.typeCheck : invalid constructor for target class " + typeName + getPos());
}
// TODO get and prune operations do not allow for coercion but type check does!
// e.g. the parameter type may be int and the arg type float
// or the parameter type may be String and the arg type class Foo
// reimplement this using type inter-assignability to do the pruning
Class candidateClass = getCandidateArgClass(candidates, i);
Type candidateType;
if (candidateClass != null) {
candidateType = typeGroup.ensureType(candidateClass);
} else {
candidateType = Type.UNDEFINED;
}
Type argType = arguments.get(i).typeCheck(candidateType);
argumentTypes.add(argType);
if (candidateType == Type.UNDEFINED) {
// we had several constructors to choose from
candidates = pruneCandidates(candidates, i, argType.getTargetClass());
}
}
if (candidates.isEmpty()) {
throw new TypeException("ThrowExpression.typeCheck : invalid constructor for target class " + typeName + getPos());
}
if (candidates.size() > 1) {
throw new TypeException("ThrowExpression.typeCheck : ambiguous constructor signature for target class " + typeName + getPos());
}
constructor = candidates.get(0);
// make sure we know the formal parameter types and have included them in the typegroup
paramTypes = new ArrayList<Type>();
Class<?>[] paramClasses = constructor.getParameterTypes();
for (int i = 0; i < arguments.size() ; i++) {
paramTypes.add(typeGroup.ensureType(paramClasses[i]));
}
checkThrownTypeIsValid();
return type;
}
public Class getCandidateArgClass(List<Constructor> candidates, int argIdx)
{
Class argClazz = null;
for (Constructor c : candidates) {
Class nextClazz = c.getParameterTypes()[argIdx];
if (argClazz == null) {
argClazz = nextClazz;
} else if (argClazz != nextClazz) {
return null;
}
}
return argClazz;
}
public List<Constructor> pruneCandidates(List<Constructor> candidates, int argIdx, Class argClazz)
{
for (int i = 0; i < candidates.size();) {
Constructor c = candidates.get(i);
Class nextClazz = c.getParameterTypes()[argIdx];
if (nextClazz != argClazz) {
candidates.remove(i);
} else {
i++;
}
}
return candidates;
}
/**
* evaluate the expression by interpreting the expression tree
*
* @param helper an execution context associated with the rule whcih contains a map of
* current bindings for rule variables and another map of their declared types both of which
* are indexed by varoable name. This includes entries for the helper (name "-1"), the
* recipient if the trigger method is not static (name "0") and the trigger method arguments
* (names "1", ...)
* @return the result of evaluation as an Object
* @throws org.jboss.byteman.rule.exception.ExecuteException
*
*/
public Object interpret(HelperAdapter helper) throws ExecuteException {
int l = arguments.size();
int i;
Object[] callArgs = new Object[l];
for (i =0; i < l; i++) {
callArgs[i] = arguments.get(i).interpret(helper);
}
try {
Throwable th = (Throwable) constructor.newInstance(callArgs);
ThrowException thex = new ThrowException(th);
throw thex;
} catch (InstantiationException e) {
throw new ExecuteException("ThrowExpression.interpret : unable to instantiate exception class " + typeName + getPos(), e);
} catch (IllegalAccessException e) {
throw new ExecuteException("ThrowExpression.interpret : unable to access exception class " + typeName + getPos(), e);
} catch (InvocationTargetException e) {
throw new ExecuteException("ThrowExpression.interpret : unable to invoke exception class constructor for " + typeName + getPos(), e);
}
}
public void compile(MethodVisitor mv, CompileContext compileContext) throws CompileException
{
// make sure we are at the right source line
compileContext.notifySourceLine(line);
int currentStack = compileContext.getStackCount();
int expected = 1;
int extraParams = 0;
// ok, we need to create the thrown exception instance and then
// initialise it.
// create the thrown exception instance -- adds 1 to stack
String exceptionClassName = type.getInternalName();
mv.visitTypeInsn(Opcodes.NEW, exceptionClassName);
compileContext.addStackCount(1);
// copy the exception so we can init it
mv.visitInsn(Opcodes.DUP);
compileContext.addStackCount(1);
int argCount = arguments.size();
// stack each of the arguments to the constructor
for (int i = 0; i < argCount; i++) {
Type argType = argumentTypes.get(i);
Type paramType = paramTypes.get(i);
int paramCount = (paramType.getNBytes() > 4 ? 2 : 1);
// track extra storage used after type conversion
extraParams += (paramCount);
arguments.get(i).compile(mv, compileContext);
compileTypeConversion(argType, paramType, mv, compileContext);
}
// construct the exception
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, exceptionClassName, "<init>", getDescriptor());
// modify the stack height to account for the removed exception and params
compileContext.addStackCount(-(extraParams+1));
if (compileContext.getStackCount() != currentStack + expected) {
throw new CompileException("ThrowExpression.compile : invalid stack height " + compileContext.getStackCount() + " expecting " + (currentStack + expected));
}
// now create a ThrowException to wrap the user exception
// create the thrown exception instance -- adds 1 to stack [UE] --> [UE, THE]
exceptionClassName = "org/jboss/byteman/rule/exception/ThrowException";
mv.visitTypeInsn(Opcodes.NEW, exceptionClassName);
compileContext.addStackCount(1);
// copy the ThrowException so we can init it [UE, THE] --> [THE, UE, THE]
mv.visitInsn(Opcodes.DUP_X1);
compileContext.addStackCount(1);
// reverse the order of the top two words [THE, UE, THE] --> [THE, THE, UE]
mv.visitInsn(Opcodes.SWAP);
// construct the exception [THE, THE, UE] --> [UE]
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, exceptionClassName, "<init>", "(Ljava/lang/Throwable;)V");
// we should now have just the ThrowException on the stack
compileContext.addStackCount(-2);
if (compileContext.getStackCount() != currentStack + expected) {
throw new CompileException("ThrowExpression.compile : invalid stack height " + compileContext.getStackCount() + " expecting " + (currentStack + expected));
}
// now throw the exception and decrement the stack height
mv.visitInsn(Opcodes.ATHROW);
compileContext.addStackCount(-1);
}
private String getDescriptor()
{
StringBuffer buffer = new StringBuffer();
buffer.append("(");
int nParams = paramTypes.size();
for (int i = 0; i < nParams; i++) {
buffer.append(paramTypes.get(i).getInternalName(true, true));
}
buffer.append(")V");
return buffer.toString();
}
public void writeTo(StringWriter stringWriter) {
stringWriter.write("throw ");
if (type == null || Type.UNDEFINED == type) {
stringWriter.write(typeName);
} else {
stringWriter.write(type.getName());
}
String separator = "";
stringWriter.write("(");
for (Expression argument : arguments) {
stringWriter.write(separator);
argument.writeTo(stringWriter);
separator = ",";
}
stringWriter.write(")");
}
/**
* check that it is legitimate to throw an exception of the type computed for this expression from the
* trtiggering method. if the computed type is a subtype of runtime exception then it is valid whatever
* the trigger method. if the computed type or one of its super types is declared by the trigger method
* as a checked exception then it is valid. if no such declaration is found then a typeexception is raised.
* However, in the case of an overriding rule whose target class includes a version of the method which
* does declares the exception a type warning exception is raised as this is not strictly an error. See
* issue BYTEMAN-156 for an explanation.
* @throws TypeWarningException if it is not legitimate to throw a value of the computed type from the
* trigger method but it is legitimate to throw a value of this type from the rule target method.
* @throws TypeException if it is otherwise not legitimate to throw a value of the computed type from the trigger
* method
*/
private void checkThrownTypeIsValid() throws TypeWarningException, TypeException
{
TypeGroup typeGroup = getTypeGroup();
// expected type should always be void since throw can only occur as a top level action
// however, we need to be sure that the trigering method throws this exception type or
// else that it is a subtype of runtime exception
if (!RuntimeException.class.isAssignableFrom(type.getTargetClass())) {
Iterator<Type> iterator = typeGroup.getExceptionTypes().iterator();
while (iterator.hasNext()) {
Type exceptionType = iterator.next();
if (Type.dereference(exceptionType).isAssignableFrom(type)) {
// ok we found a suitable declaration for the exception
return;
}
}
}
// look for a method declared on the target class which declares the computed type as a checked exception
ClassLoader loader = rule.getLoader();
String targetClassName = rule.getTargetClass();
String triggerClassName = rule.getTriggerClass();
String triggerMethodName = rule.getTriggerMethod();
String descriptor = rule.getTriggerDescriptor();
Class<?>[] paramTypes = null;
boolean isQualified = targetClassName.contains(".");
boolean isClass = !rule.isInterface();
try {
Class<?> triggerClass = loader.loadClass(triggerClassName);
SuperIterator iterator;
if (isClass) {
iterator = new ClassIterator(triggerClass.getSuperclass());
} else {
iterator = new InterfaceIterator(triggerClass);
}
while (iterator.hasNext()) {
Class<?> nextClass = iterator.next();
String nextClassName = nextClass.getName();
if (nextClassName.equals(targetClassName) ||
(!isQualified && nextClassName.endsWith("." + targetClassName))) {
// check whether the trigger method overrides a method on this class
if (paramTypes == null) {
paramTypes = createParamTypes(descriptor, loader);
}
try {
Method method = nextClass.getMethod(triggerMethodName, paramTypes);
Class<?>[] exceptionTypes = method.getExceptionTypes();
for (int i = 0; i < exceptionTypes.length; i++) {
if (exceptionTypes[i].isAssignableFrom(type.getTargetClass())) {
throw new TypeWarningException("ThrowExpression.typeCheck : exception type declared by rule target method but not by trigger method " + typeName + getPos());
}
}
} catch (NoSuchMethodException e) {
// ok, ignore
}
}
}
} catch (ClassNotFoundException e) {
// drop through
}
// no extenuating circumstances so this is a type error
throw new TypeException("ThrowExpression.typeCheck : exception type not declared by trigger method " + typeName + getPos());
}
public Class<?>[] createParamTypes(String descriptor, ClassLoader loader) throws TypeException
{
String external = TypeHelper.parseMethodDescriptor(descriptor).substring(1);
String typeNamesOnly = external.substring(0, external.indexOf(")")).trim();
if (typeNamesOnly.length() == 0) {
return new Class<?>[0];
} else {
String[] typeNameList = typeNamesOnly.split(",");
Class<?>[] classList = new Class<?>[typeNameList.length];
for (int i = 0; i < typeNameList.length; i++) {
String name = typeNameList[i].trim();
try {
classList[i] = loader.loadClass(name);
} catch (ClassNotFoundException e) {
throw new TypeException("ThrowExpression.createParamTypes : unexpected error looking up trigger method parameter type" + e);
}
}
return classList;
}
}
public abstract class SuperIterator implements Iterator<Class<?>>
{
}
public class ClassIterator extends SuperIterator
{
private Class<?> nextClass;
public ClassIterator(Class<?> startClass)
{
this.nextClass = startClass;
}
public boolean hasNext() {
return nextClass != null;
}
public Class<?> next() {
Class<?> next = nextClass;
nextClass = nextClass.getSuperclass();
return next;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public class InterfaceIterator extends SuperIterator
{
private LinkedList<Class<?>> visited;
private LinkedList<Class<?>> unvisited;
private Class<?> nextClass;
public InterfaceIterator(Class<?> startClass)
{
visited = new LinkedList<Class<?>>();
unvisited = new LinkedList<Class<?>>();
nextClass = startClass;
}
private void pushInterfaces()
{
if (nextClass != null) {
LinkedList<Class<?>> candidates = new LinkedList<Class<?>>();
Class<?>[] ifaces = nextClass.getInterfaces();
for (int i = 0; i < ifaces.length; i++) {
candidates.add(ifaces[i]);
}
while (!candidates.isEmpty()) {
Class<?> iface = candidates.pop();
if (!visited.contains(iface) && !unvisited.contains(iface)) {
unvisited.add(iface);
ifaces = iface.getInterfaces();
for (int i = 0; i < ifaces.length; i++) {
candidates.add(ifaces[i]);
}
}
}
nextClass = nextClass.getSuperclass();
}
return;
}
public boolean hasNext() {
if (unvisited.isEmpty()) {
pushInterfaces();
}
return !unvisited.isEmpty();
}
public Class<?> next() {
Class<?> next = unvisited.pop();
visited.add(next);
return next;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}