/*
* 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;
import org.jboss.byteman.agent.HelperManager;
import org.jboss.byteman.rule.type.TypeGroup;
import org.jboss.byteman.rule.type.Type;
import org.jboss.byteman.rule.exception.*;
import org.jboss.byteman.rule.binding.Bindings;
import org.jboss.byteman.rule.binding.Binding;
import org.jboss.byteman.rule.grammar.ECATokenLexer;
import org.jboss.byteman.rule.grammar.ECAGrammarParser;
import org.jboss.byteman.rule.grammar.ParseNode;
import org.jboss.byteman.rule.helper.HelperAdapter;
import org.jboss.byteman.rule.helper.Helper;
import org.jboss.byteman.rule.helper.InterpretedHelper;
import org.jboss.byteman.agent.Location;
import org.jboss.byteman.agent.Transformer;
import org.jboss.byteman.agent.RuleScript;
import org.objectweb.asm.Opcodes;
import org.jboss.byteman.rule.compiler.Compiler;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java_cup.runtime.Symbol;
/**
* A rule ties together an event, condition and action. It also maintains a TypeGroup
* identifying type information derived from these components.
*/
public class Rule
{
/**
* the script defining this rule
*/
private RuleScript ruleScript;
/**
* the name of this rule supplied in the rule script
*/
private String name;
/**
* the class loader for the target class
*/
private ClassLoader loader;
/**
* the parsed event derived from the script for this rule
*/
private Event event;
/**
* the parsed condition derived from the script for this rule
*/
private Condition condition;
/**
* the parsed condition derived from the script for this rule
*/
private Action action;
/**
* the set of bindings derived from the event supplemented, post type checking, with bindings
* derived from the trigger method. we may eventually also be able to install bindings for
* method local variables.
*
* Note that use of the name bindings is slightly misleading since this instance identifies the
* name and type of each of the available bound variables and, in the case of an event binding,
* the expression to be evaluated in order to initialise the variable. It does not identify
* any bound values for the variable. These are stored <em>per rule-firing</em> in a set
* attached to the Helper instance used to implement the execute method for the rule.
*/
private Bindings bindings;
/**
* the fully qualified name of the class to which this rule has been attached by the code
* transformation package. note that this may not be the same as targetClass since the
* latter may not specify a package.
*/
private String triggerClass;
/**
* the name of the trigger method in which a trigger call for this rule has been inserted by
* the code transformation package, not including the descriptor component. note that this
* may not be the same as the targetMethod since the latter may include an argument list.
*/
private String triggerMethod;
/**
* the descriptor of the trigger method in which a trigger call for this rule has been inserted
* by the code transformation package. note that this will be in encoded format e.g. "(IZ)V"
* rather than declaration format e.g. "void (int, boolean)"
*/
private String triggerDescriptor;
/**
* the name sof all the exceptions declared by the trigger method in which a trigger call for
* this rule has been inserted by the code transformation package.
*/
private String[] triggerExceptions;
/**
* the access mode for the target method defined using flag bits defined in the asm Opcodes
* class.
*/
private int triggerAccess;
/**
* the set of types employed by the rule, inlcuding types referenced by abbreviated name (without
* mentioning the package), array type sand/or their base types and standard builtin types.
*/
private TypeGroup typeGroup;
/**
* flag set to true only after the rule has been type checked
*/
private boolean checked;
/**
* flag set to true only after the rule has been type checked successfully
*/
private boolean checkFailed;
/**
* return type of the rule's trigger method
*/
private Type returnType;
/**
* the key under which this rule is indexed in the rule key map.
*/
private String key;
/**
* lifecycle event manager for rule helpers
*/
private HelperManager helperManager;
/**
* a list of field objects used by compiled code to enable rule code to access non-public fields
*/
private List<Field> accessibleFields;
/**
* a list of method objects used by compiled code to enable rule code to access non-public methods
*/
private List<Method> accessibleMethods;
private Rule(RuleScript ruleScript, ClassLoader loader, HelperManager helperManager)
throws ParseException, TypeException, CompileException
{
ParseNode ruleTree;
this.ruleScript = ruleScript;
this.helperClass = null;
this.loader = loader;
typeGroup = new TypeGroup(loader);
bindings = new Bindings();
checked = false;
triggerClass = null;
triggerMethod = null;
triggerDescriptor = null;
triggerAccess = 0;
returnType = null;
accessibleFields = null;
accessibleMethods = null;
// this is only set when the rule is created via a real installed transformer
this.helperManager = helperManager;
ECAGrammarParser parser = null;
try {
String file = getFile();
ECATokenLexer lexer = new ECATokenLexer(new StringReader(ruleScript.getRuleText()));
lexer.setStartLine(getLine());
lexer.setFile(file);
parser = new ECAGrammarParser(lexer);
parser.setFile(file);
Symbol parse = (debugParse ? parser.debug_parse() : parser.parse());
if (parser.getErrorCount() != 0) {
String message = "rule " + ruleScript.getName();
message += parser.getErrors();
throw new ParseException(message);
}
ruleTree = (ParseNode) parse.value;
} catch (ParseException pe) {
throw pe;
} catch (Throwable th) {
String message = "rule " + ruleScript.getName();
if (parser != null && parser.getErrorCount() != 0) {
message += parser.getErrors();
}
message += "\n" + th.getMessage();
throw new ParseException(message);
}
ParseNode eventTree = (ParseNode)ruleTree.getChild(0);
ParseNode conditionTree = (ParseNode)ruleTree.getChild(1);
ParseNode actionTree = (ParseNode)ruleTree.getChild(2);
event = Event.create(this, eventTree);
condition = Condition.create(this, conditionTree);
action = Action.create(this, actionTree);
key = null;
}
public TypeGroup getTypeGroup()
{
return typeGroup;
}
public Bindings getBindings()
{
return bindings;
}
public String getName() {
return ruleScript.getName();
}
public String getTargetClass() {
return ruleScript.getTargetClass();
}
public String getTargetMethod() {
return ruleScript.getTargetMethod();
}
public Location getTargetLocation() {
return ruleScript.getTargetLocation();
}
public boolean isOverride()
{
return ruleScript.isOverride();
}
public boolean isInterface()
{
return ruleScript.isInterface();
}
/**
* retrieve the start line for the rule
* @return the start line for the rule
*/
public int getLine()
{
return ruleScript.getLine();
}
/**
* retrieve the name of the file containing this rule
* @return the name of the file containing this rule
*/
public String getFile()
{
return ruleScript.getFile();
}
public Event getEvent()
{
return event;
}
public Condition getCondition() {
return condition;
}
public Action getAction() {
return action;
}
public String getTriggerClass() {
return triggerClass;
}
public String getTriggerMethod() {
return triggerMethod;
}
public String getTriggerDescriptor() {
return triggerDescriptor;
}
public Type getReturnType()
{
return returnType;
}
/**
* get the class loader of the target class for the rule
* @return
*/
public ClassLoader getLoader()
{
return loader;
}
public static Rule create(RuleScript ruleScript, ClassLoader loader, HelperManager helperManager)
throws ParseException, TypeException, CompileException
{
return new Rule(ruleScript, loader, helperManager);
}
public void setEvent(String eventSpec) throws ParseException, TypeException
{
if (event == null) {
event = Event.create(this, eventSpec);
}
}
public void setCondition(String conditionSpec) throws ParseException, TypeException
{
if (event != null & condition == null) {
condition = Condition.create(this, conditionSpec);
}
}
public void setAction(String actionSpec) throws ParseException, TypeException
{
if (event != null & condition != null && action == null) {
action = Action.create(this, actionSpec);
}
}
public void setTypeInfo(final String className, final int access, final String methodName, final String desc, String[] exceptions)
{
triggerClass = className;
triggerAccess = access;
triggerMethod = methodName;
triggerDescriptor = desc;
triggerExceptions = exceptions;
}
/**
* has this rule been typechecked and/or compiled
* @return true if this rule has been typechecked and/or compiled otherwise false
*/
public boolean isChecked()
{
return checked;
}
/**
* has this rule failed to typecheck or compile
* @return true if this rule has failed to typecheck or compile otherwise false
*/
public boolean isCheckFailed()
{
return checkFailed;
}
/**
* has this rule been typechecked and compiled without error.
* @return true if this rule has been typechecked and compiled without error otherwise false
*/
public boolean isCheckedOk()
{
return (checked && !checkFailed);
}
/**
* disable triggering of rules inside the current thread. this is the version called internally
* after returning from a method call in a rule binding, condition or action.
* @return true if triggering was previously enabled and false if it was already disabled
*/
public static boolean disableTriggersInternal()
{
return Transformer.disableTriggers(false);
}
/**
* enable triggering of rules inside the current thread n.b. this is called internally by the rule
* engine before it executes a method call in a rule binding, condition or action. it will not
* enable triggers if they have been switched off by an earlier call to userDisableTriggers.
* @return true if triggering was previously enabled and false if it was already disabled
*/
public static boolean enableTriggersInternal()
{
return Transformer.enableTriggers(false);
}
/**
* disable triggering of rules inside the current thread. this is the version which should be
* called from a Helper class to ensure that subsequent method invocatiosn during execution of
* the current rule bindings, condition or action do not recursively trigger rules. It ensures
* that subsequent calls to enableTriggers have no effect. The effect lasts until the end of
* processing for the current rule when resetTriggers is called.
* @return true if triggering was previously enabled and false if it was already disabled
*/
public static boolean disableTriggers()
{
return Transformer.disableTriggers(true);
}
/**
* enable triggering of rules inside the current thread. this is called internally by the rule
* engine after rule execution has completed. it will re-enable triggers even if they have been
* switched off by an earlier call to userDisableTriggers. It is also called by the default helper
* to reverse the effect of calling userDisableTriggers.
* @return true if triggering was previously enabled and false if it was already disabled
*/
public static boolean enableTriggers()
{
return Transformer.enableTriggers(true);
}
/**
* check if triggering of rules is enabled inside the current thread
* @return true if triggering is enabled and false if it is disabled
*/
public static boolean isTriggeringEnabled()
{
return Transformer.isTriggeringEnabled();
}
/**
* typecheck and then compile this rule unless either action has been tried before
* @return true if the rule successfully type checks and then compiles under this call or a previous
* call or false if either operation has previously failed or fails under this call.
*/
private synchronized boolean ensureTypeCheckedCompiled()
{
if (checkFailed) {
return false;
}
if (!checked) {
// ensure we don't trigger any code inside the type check or compile
// n.b. we may still allow recursive triggering while executing
boolean triggerEnabled = false;
String detail = "";
try {
typeCheck();
compile();
checked = true;
installed();
} catch (TypeWarningException te) {
checkFailed = true;
if (Transformer.isVerbose()) {
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
writer.println("Rule.ensureTypeCheckedCompiled : warning type checking rule " + getName());
te.printStackTrace(writer);
detail = stringWriter.toString();
System.out.println(detail);
}
} catch (TypeException te) {
checkFailed = true;
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
writer.println("Rule.ensureTypeCheckedCompiled : error type checking rule " + getName());
te.printStackTrace(writer);
detail = stringWriter.toString();
System.out.println(detail);
} catch (CompileException ce) {
checkFailed = true;
StringWriter stringWriter = new StringWriter();
PrintWriter writer = new PrintWriter(stringWriter);
writer.println("Rule.ensureTypeCheckedCompiled : error compiling rule " + getName());
ce.printStackTrace(writer);
detail = stringWriter.toString();
System.out.println(detail);
}
ruleScript.recordCompile(triggerClass, loader, !checkFailed, detail);
return !checkFailed;
}
return true;
}
/**
* type check this rule
* @throws TypeException if the ruele contains type errors
*/
public void typeCheck()
throws TypeException
{
String helperName = ruleScript.getTargetHelper();
// ensure that we have a valid helper class
if (helperName != null) {
try {
helperClass = loader.loadClass(helperName);
} catch (ClassNotFoundException e) {
throw new TypeException("Rule.typecheck : unknown helper class " + helperName + " for rule " + getName());
}
} else {
helperClass = Helper.class;
}
if (triggerExceptions != null) {
// ensure that the type group includes the exception types
typeGroup.addExceptionTypes(triggerExceptions);
}
// try to resolve all types in the type group to classes
typeGroup.resolveTypes();
// use the descriptor to derive the method argument types and type any bindings for them
// that are located in the bindings list
installParameters((triggerAccess & Opcodes.ACC_STATIC) != 0, triggerClass);
event.typeCheck(Type.VOID);
condition.typeCheck(Type.Z);
action.typeCheck(Type.VOID);
}
/**
* install helper class used to execute this rule. this may involve generating a compiled helper class
* for the rule and, if compilation to bytecode is enabled, generating bytecode for a method of this class
* used to execute the rule binding, condition and action expressions. If the rule employ sthe default helper
* without enabling compilation to bytecode then no class need be generated. the installed helper class will
* be the predefined class InterpretedHelper.
* @throws CompileException if the rule cannot be compiled
*/
public void compile()
throws CompileException
{
boolean compileToBytecode = isCompileToBytecode();
if (helperClass == Helper.class && !compileToBytecode) {
// we can use the builtin interpreted helper adapter for class Helper
helperImplementationClass = InterpretedHelper.class;
} else {
// we need to generate a helper adapter class which either interprets or compiles
helperImplementationClass = Compiler.getHelperAdapter(this, helperClass, compileToBytecode);
}
}
/**
* should rules be compiled to bytecode
* @return true if rules should be compiled to bytecode otherwise false
*/
private boolean isCompileToBytecode()
{
return Transformer.isCompileToBytecode();
}
private void installParameters(boolean isStatic, String className)
throws TypeException
{
Type type;
// add a binding for the helper so we can call builtin static methods
type = typeGroup.create(helperClass.getName());
Binding ruleBinding = bindings.lookup("$$");
if (ruleBinding != null) {
ruleBinding.setType(type);
} else {
bindings.append(new Binding(this, "$$", type));
}
if (!isStatic) {
Binding recipientBinding = bindings.lookup("$0");
if (recipientBinding != null) {
type = typeGroup.create(className);
if (type.isUndefined()) {
throw new TypeException("Rule.installParameters : Rule " + name + " unable to load class " + className);
}
recipientBinding.setType(type);
}
}
String returnTypeName = Type.parseMethodReturnType(triggerDescriptor);
returnType = typeGroup.create(returnTypeName);
Iterator<Binding> iterator = bindings.iterator();
while (iterator.hasNext()) {
Binding binding = iterator.next();
// these bindings are typed via the descriptor installed during trigger injection
// note that the return type has to be done this way because it may represent the
// trigger method return type or the invoke method return type
if (binding.isParam() || binding.isLocalVar() || binding.isReturn()) {
String typeName = binding.getDescriptor();
String[] typeAndArrayBounds = typeName.split("\\[");
Type baseType = typeGroup.create(typeAndArrayBounds[0]);
Type fullType = baseType;
if (baseType.isUndefined()) {
throw new TypeException("Rule.installParameters : Rule " + name + " unable to load class " + baseType);
}
for (int i = 1; i < typeAndArrayBounds.length ; i++) {
fullType = typeGroup.createArray(fullType);
}
binding.setType(fullType);
} else if (binding.isThrowable()) {
// TODO -- enable a more precise specification of the throwable type
// we need to be able to obtain the type descriptor for the throw operation
binding.setType(typeGroup.ensureType(Throwable.class));
} else if (binding.isParamCount()) {
binding.setType(Type.I);
} else if (binding.isParamArray() || binding.isInvokeParamArray()) {
binding.setType(Type.OBJECT.arrayType());
} else if (binding.isTriggerClass() || binding.isTriggerMethod()) {
binding.setType(Type.STRING);
}
}
}
/**
* forward an execute request a rule identified by its unique key
* @param key a string key identifying the rule instance to be fired
* @param recipient the recipient of the method from which execution of the rule was
* triggered or null if it was a static method
* @param args the arguments of the method from which execution of the rule was
* triggered
*/
public static void execute(String key, Object recipient, Object[] args) throws ExecuteException
{
boolean enabled = isTriggeringEnabled();
if (!enabled) {
// we don't trigger code while we are doing rule housekeeping
return;
}
// disable triggering until we get into actual rule code
disableTriggersInternal();
try {
Rule rule = ruleKeyMap.get(key);
if (Transformer.isVerbose()) {
System.out.println("Rule.execute called for " + key);
}
// if the key is no longer present it just means the rule has been decommissioned so return
if (rule == null) {
if (Transformer.isVerbose()) {
System.out.println("Rule.execute for decommissioned key " + key);
}
return;
}
rule.execute(recipient, args);
} finally {
// restore the status quo -- we must have been enabled if we got to this method
enableTriggers();
}
}
/**
* forward an execute request to a helper instance associated with the rule
* @param recipient the recipient of the method from which execution of this rule was
* triggered or null if it was a static method
* @param args the arguments of the method from which execution of this rule was
* triggered
*/
private void execute(Object recipient, Object[] args) throws ExecuteException
{
// type check and createHelperAdapter the rule now if it has not already been done
if (ensureTypeCheckedCompiled()) {
// create a helper and get it to execute the rule
// eventually we will create a subclass of helper for each rule and createHelperAdapter
// an implementation of execute from the rule source. for now we create a generic
// helper and call the generic execute method which interprets the rule
HelperAdapter helper;
try {
Constructor constructor = helperImplementationClass.getConstructor(Rule.class);
helper = (HelperAdapter)constructor.newInstance(this);
//helper = (RuleHelper)helperClass.newInstance();
//helper.setRule(this);
helper.execute(recipient, args);
} catch (NoSuchMethodException e) {
// should not happen!!!
System.out.println("cannot find constructor " + helperImplementationClass.getCanonicalName() + "(Rule) for helper class");
e.printStackTrace(System.out);
return;
} catch (InvocationTargetException e) {
e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
} catch (InstantiationException e) {
// should not happen
System.out.println("cannot create instance of " + helperImplementationClass.getCanonicalName());
e.printStackTrace(System.out);
return;
} catch (IllegalAccessException e) {
// should not happen
System.out.println("cannot access " + helperImplementationClass.getCanonicalName());
e.printStackTrace(System.out);
return;
} catch (ClassCastException e) {
// should not happen
System.out.println("cast exception " + helperImplementationClass.getCanonicalName());
e.printStackTrace(System.out);
return;
} catch (EarlyReturnException e) {
throw e;
} catch (ThrowException e) {
throw e;
} catch (ExecuteException e) {
System.out.println(getName() + " : " + e);
throw e;
} catch (Throwable throwable) {
System.out.println(getName() + " : " + throwable);
throw new ExecuteException(getName() + " : caught " + throwable, throwable);
}
}
}
/**
* called when a trigger is compiled for the rule to provide a String key which can be used
* at execution time to obtain a handle on the rule instance
*
* @return a key which can be used later to obtain a reference to the rule
*/
public String getKey()
{
if (key != null) {
return key;
}
String key = getName() + "_" + nextId();
this.key = key;
ruleKeyMap.put(key, this);
return key;
}
/**
* return the key under which this rule has been indexed in the rule key map
* @return
*/
public String lookupKey()
{
return key;
}
/**
* delete any reference to the rule from the rule map
*/
public void purge()
{
// nothing to do unless we actually allocated a key
if (key != null) {
ruleKeyMap.remove(key);
if (checked) {
uninstalled();
}
}
}
/**
* a hash map used to identify rules from their keys
*/
private static HashMap<String, Rule> ruleKeyMap = new HashMap<String, Rule>();
/**
* a counter used to ensure rule identifiers are unique
*/
private static int nextId = 0;
/**
* a method to return the next available counter for use in constructing a key for the rule
* @return
*/
private synchronized static int nextId()
{
return nextId++;
}
private static boolean compileRules()
{
return Transformer.isCompileToBytecode();
}
/**
* generate a string representation of the rule
*
* @return a string representation of the rule
*/
public String toString()
{
StringWriter stringWriter = new StringWriter();
stringWriter.write("RULE ");
stringWriter.write(getName());
stringWriter.write("\n");
if (isInterface()) {
stringWriter.write("INTERFACE ");
} else {
stringWriter.write("CLASS ");
}
if (isOverride()) {
stringWriter.write("^");
}
stringWriter.write(getTargetClass());
stringWriter.write('\n');
stringWriter.write("METHOD ");
stringWriter.write(getTargetMethod());
stringWriter.write('\n');
stringWriter.write(getTargetLocation().toString());
stringWriter.write('\n');
if (event != null) {
event.writeTo(stringWriter);
} else {
stringWriter.write("BIND NOTHING\n");
}
if (condition != null) {
condition.writeTo(stringWriter);
} else {
stringWriter.write("COND TRUE\n");
}
if (action != null) {
action.writeTo(stringWriter);
} else {
stringWriter.write("DO NOTHING\n");
}
return stringWriter.toString();
}
/**
* a helper class which defines the builtin methods available to this rule -- by default Helper
*/
private Class helperClass;
/**
* an extension of the helper class which implements the methods of interface RuleHelper -- by default
* InterpretedHelper. This is the class which is instantiated and used as the target for an execute
* operation.
*/
private Class helperImplementationClass;
/**
* a getter allowing the helper class for the rule to be identified
*
* @return
*/
public Class getHelperClass()
{
return helperClass;
}
/**
* flag true if debugging of rule parsing is desired and false if it should not be performed
*/
private static boolean debugParse = (System.getProperty("org.jboss.byteman.rule.debug") != null ? true : false);
/**
* method called when the rule has been successfully injected into a class, type checked and compiled. it passes
* the message on to the Transformer so it can perform helper lifecycle management.
*/
private void installed()
{
helperManager.installed(this);
}
/**
* method called when the rule has been uninstalled after previously being successfully injected into a class,
* type checked and compiled. it passes the message on to the Transformer so it can perform helper lifecycle
* management.
*/
private void uninstalled()
{
helperManager.uninstalled(this);
}
public int addAccessibleField(Field field) {
if (accessibleFields == null) {
accessibleFields = new ArrayList<Field>();
}
int index = accessibleFields.size();
accessibleFields.add(field);
return index;
}
public int addAccessibleMethod(Method method) {
if (accessibleMethods == null) {
accessibleMethods = new ArrayList<Method>();
}
int index = accessibleMethods.size();
accessibleMethods.add(method);
return index;
}
public Object getAccessibleField(Object owner, int fieldIndex) throws ExecuteException
{
try {
Field field = accessibleFields.get(fieldIndex);
return field.get(owner);
} catch (Exception e) {
throw new ExecuteException("Rule.getAccessibleField : unexpected error getting non-public field in rule " + getName(), e);
}
}
public void setAccessibleField(Object owner, Object value, int fieldIndex) throws ExecuteException
{
try {
Field field = accessibleFields.get(fieldIndex);
field.set(owner, value);
} catch (Exception e) {
throw new ExecuteException("Rule.setAccessibleField : unexpected error setting non-public field in rule " + getName(), e);
}
}
public Object invokeAccessibleMethod(Object target, Object[] args, int methodIndex)
{
try {
Method method = accessibleMethods.get(methodIndex);
return method.invoke(target, args);
} catch (Exception e) {
throw new ExecuteException("Rule.invokeAccessibleMethod : unexpected error invoking non-public method in rule " + getName(), e);
}
}
public long getObjectSize(Object o)
{
return helperManager.getObjectSize(o);
}
}