org.jboss.byteman.agent.ScriptRepository Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of byteman Show documentation
Show all versions of byteman Show documentation
The byteman jar merges the byteman-agent jar contents with those of the
byteman-jigsaw and byteman-layer jars as a mutli-release jar. The contents
of the latter two jars are installed under META-INF/versions/9 ensuring
that they are only linked when Byteman is deployed on a JDK9+ JVM
/*
* JBoss, Home of Professional Open Source
* Copyright 2009-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.
*
* (C) 2009-10,
* @authors Andrew Dinn
*/
package org.jboss.byteman.agent;
import org.jboss.byteman.rule.helper.Helper;
import java.util.*;
import java.lang.reflect.Method;
/**
* Class to manage indexing and lookup of rule scripts by rule name and by either class or interface name
*/
public class ScriptRepository
{
public ScriptRepository(boolean skipOverrideRules)
{
targetClassIndex = new HashMap>();
targetInterfaceIndex = new HashMap>();
ruleNameIndex = new HashMap();
this.skipOverrideRules = skipOverrideRules;
}
/**
* Split the text of a script file into a list of individual rule scripts
* @param scriptText the text obtained from a script file
* @param scriptFile teh name of the file containing teh text
* @return a list of rule scripts
* @throws Exception if there is an error in the format of the script file tesxt
*/
public List processScripts(String scriptText, String scriptFile) throws Exception
{
List ruleScripts = new LinkedList();
if (scriptText != null) {
// split rules into separate lines
String[] lines = scriptText.split("\n");
List rules = new ArrayList();
String nextRule = "";
String sepr = "";
String name = null;
String targetClass = null;
String targetMethod = null;
String targetHelper = null;
String defaultHelper = null;
String[] targetImports = null;
String[] defaultImports = new String[0];
// script level compilation defaults to Transformer setting but may be overridden
boolean scriptCompileToBytecode = Transformer.isCompileToBytecode();
// rule level compilation defaults to script level but may be overridden
boolean ruleCompileToBytecode = scriptCompileToBytecode;
LocationType locationType = null;
Location targetLocation = null;
boolean isInterface = false;
boolean isOverride = false;
int lineNumber = 0;
int startNumber = -1;
int maxLines = lines.length;
boolean inRule = false;
for (String line : lines) {
line = line.trim();
lineNumber++;
if (line.startsWith("#")) {
if (inRule) {
// add a blank line in place of the comment so the line numbers
// are reported consistently during parsing
nextRule += sepr;
sepr = "\n";
} // else { // just drop comment line }
} else if (line.startsWith("RULE ")) {
inRule = true;
name = line.substring(5).trim();
if (name.equals("")) {
throw new Exception("org.jboss.byteman.agent.Transformer : RULE with no name at line " + lineNumber + " in script " + scriptFile);
}
} else if (line.startsWith("HELPER ")) {
if (inRule) {
targetHelper = line.substring(7).trim();
} else {
defaultHelper = line.substring(7).trim();
// empty classname resets to the default
if (defaultHelper.length() == 0) {
defaultHelper = null;
}
}
} else if (line.equals("COMPILE")) {
if (inRule) {
ruleCompileToBytecode = true;
} else {
scriptCompileToBytecode = true;
ruleCompileToBytecode = true;
}
} else if (line.equals("NOCOMPILE")) {
if (inRule) {
ruleCompileToBytecode = false;
} else {
scriptCompileToBytecode = false;
ruleCompileToBytecode = false;
}
} else if (line.startsWith("IMPORT ") || line.equals("IMPORT")) {
String imp = line.substring(6).trim();
if (inRule) {
if (imp.isEmpty()) {
// remove any globally defined imports
targetImports = new String[0];
} else {
// add to the existing rule imports if any, otherwise the global ones
if (targetImports == null) {
if (defaultImports == null)
targetImports = new String[1];
else
targetImports = Arrays.copyOf(defaultImports, defaultImports.length + 1);
} else {
targetImports = Arrays.copyOf(targetImports, targetImports.length + 1);
}
targetImports[targetImports.length - 1] = imp;
}
} else {
if (imp.isEmpty())
defaultImports = null;
else {
if (defaultImports == null)
defaultImports = new String[1];
else
defaultImports = Arrays.copyOf(defaultImports, defaultImports.length + 1);
defaultImports[defaultImports.length - 1] = imp;
}
}
} else if (!inRule) {
if (!line.equals("")) {
throw new Exception("org.jboss.byteman.agent.Transformer : invalid text outside of RULE/ENDRULE " + "at line " + lineNumber + " in script " + scriptFile);
}
} else if (line.startsWith("CLASS ")) {
targetClass = line.substring(6).trim();
if (targetClass.startsWith("^")) {
isOverride = true;
targetClass = targetClass.substring(1).trim();
}
if (Transformer.isBytemanClass(targetClass)) {
throw new Exception("org.jboss.byteman.agent.Transformer : invalid target class " + targetClass + " at line " + lineNumber + " in script " + scriptFile);
}
} else if (line.startsWith("INTERFACE ")) {
targetClass = line.substring(10).trim();
isInterface = true;
if (targetClass.startsWith("^")) {
isOverride = true;
targetClass = targetClass.substring(1).trim();
}
if (Transformer.isBytemanClass(targetClass)) {
throw new Exception("org.jboss.byteman.agent.Transformer : invalid target class " + targetClass + " at line " + lineNumber + " in script " + scriptFile);
}
} else if (line.startsWith("METHOD ")) {
targetMethod = line.substring(7).trim();
} else if ((locationType = LocationType.type(line)) != null) {
String parameters = LocationType.parameterText(line);
targetLocation = Location.create(locationType, parameters);
if (targetLocation == null) {
throw new Exception("org.jboss.byteman.agent.Transformer : invalid target location at line " + lineNumber + " in script " + scriptFile);
}
} else if (line.startsWith("ENDRULE")) {
if (name == null || "".equals(name)) {
throw new Exception("org.jboss.byteman.agent.Transformer : no matching RULE for ENDRULE at line " + lineNumber + " in script " + scriptFile);
} else if (targetClass == null || "".equals(targetClass)) {
throw new Exception("org.jboss.byteman.agent.Transformer : no CLASS for RULE " + name + " in script " + scriptFile);
} else if (targetMethod == null || "".equals(targetMethod)) {
throw new Exception("org.jboss.byteman.agent.Transformer : no METHOD for RULE " + name + " in script " + scriptFile);
} else {
if (targetLocation == null) {
targetLocation = Location.create(LocationType.ENTRY, "");
}
if (targetHelper == null) {
targetHelper = defaultHelper;
}
if (targetImports == null) {
targetImports = (defaultImports != null) ? defaultImports : new String[0];
}
RuleScript ruleScript = new RuleScript(name, targetClass, isInterface, isOverride, targetMethod, targetHelper, targetImports, targetLocation, nextRule, startNumber, scriptFile, ruleCompileToBytecode);
ruleScripts.add(ruleScript);
}
name = null;
targetClass = null;
targetMethod = null;
targetLocation = null;
targetHelper = null;
targetImports = null;
// reset rule level compilation to script level setting
ruleCompileToBytecode = scriptCompileToBytecode;
nextRule = "";
sepr = "";
inRule = false;
isInterface = false;
// reset start number so we pick up the next rule text line
startNumber = -1;
} else if (lineNumber == maxLines && !nextRule.trim().equals("")) {
throw new Exception("org.jboss.byteman.agent.Transformer : no matching ENDRULE for RULE " + name + " in script " + scriptFile);
} else {
// this is a line of rule text - see if it is the first one
if (startNumber < 0) {
startNumber = lineNumber;
}
nextRule += sepr + line;
sepr = "\n";
}
}
}
return ruleScripts;
}
/**
* add a rule script to the repository returning any existing script with the same name or null
* if no such script can be found. if a script is returned it will have been deactivated.
* @param script the script to be added to the repository
* @return any previous script with the same name or null
*/
public RuleScript addScript(RuleScript script)
{
String name = script.getName();
RuleScript previous = null;
// sanity check override rule setting and print warning if necessary
if (skipOverrideRules && script.isOverride()) {
Helper.err("ScriptRepository.addScript : injection into overriding methods disabled but found override rules " + script.getName());
}
// insert the script by name, invalidating any old script
synchronized (ruleNameIndex) {
previous = ruleNameIndex.put(name, script);
if (previous != null) {
boolean isDeleted = previous.setDeleted();
if (isDeleted) {
// it is some other thread's responsibility to remove the script
previous = null;
}
}
}
boolean isOverride = script.isOverride();
if (previous == null) {
// increment override count if necessary before indexing
if (isOverride) {
overrideRuleCount++;
}
// now index the new script
if (script.isInterface()) {
indexTarget(script, targetInterfaceIndex);
} else {
indexTarget(script, targetClassIndex);
}
} else {
boolean wasOverride = previous.isOverride();
// increment override count if necessary before indexing
if (isOverride) {
overrideRuleCount++;
}
boolean isInterface = script.isInterface();
boolean wasInterface = previous.isInterface();
if (isInterface == wasInterface) {
// both in the same index so try a reindex
Map> index = (isInterface ? targetInterfaceIndex : targetClassIndex);
reindexTarget(script, previous, index);
} else if (isInterface) {
// different indexes so unindex then index
unindexTarget(previous, targetClassIndex);
indexTarget(script, targetInterfaceIndex);
} else {
unindexTarget(previous, targetInterfaceIndex);
indexTarget(script, targetClassIndex);
}
// decrement count if necessary after unindexing
if (wasOverride) {
overrideRuleCount--;
}
}
return previous;
}
/**
* remove a rule script from the repository returning the script if it is found or null
* if is not found. if a script is returned it will have been deactivated.
* @param script the script to be removed from the repository.
* @return the script if it was found in the repository and removed successfully or null
* if it had already been removed.
*/
public RuleScript removeScript(RuleScript script)
{
String name = script.getName();
RuleScript current;
// check for the script by name
synchronized (ruleNameIndex) {
current = ruleNameIndex.get(name);
if (current == script) {
ruleNameIndex.remove(current.getName());
boolean isDeleted = current.setDeleted();
if (isDeleted) {
// it is some other thread's responsibility to remove the script
current = null;
}
} else {
// it is some other thread's responsibility to remove the script
current = null;
}
}
// if we found a script then we have to unindex it
if (current != null) {
Map> index = (current.isInterface() ? targetInterfaceIndex : targetClassIndex);
unindexTarget(current, index);
boolean wasOverride = current.isOverride();
// decrement count if necessary after unindexing
if (wasOverride) {
overrideRuleCount--;
}
}
return current;
}
/**
* remove a rule script from the repository by name returning the script if it is found or null
* if is not found. if a script is returned it will have been deactivated.
* @param name the name of the script to be removed from the repository
* @return the script if it was found in the repository or null if none was found
*/
public RuleScript removeScript(String name)
{
RuleScript current = scriptForRuleName(name);
if (current != null) {
current = removeScript(current);
}
return current;
}
/**
* locate a rule script with a given name
* @param name the name of the rule script
* @return the script with that name or null if no such script can be found
*/
public RuleScript scriptForRuleName(String name)
{
synchronized (ruleNameIndex) {
return ruleNameIndex.get(name);
}
}
/**
* return a list of all class scripts indexed using the supplied name. note that if name is
* package qualified then only scripts with the full package qualificaton will be returned
* whereas if name is not package qualified then only scripts with the unqualified name
* will be returned. Note that the returned list can be iterated safely but will not reflect
* later additions to or deletions from the list.
* @param name the name of the class for which scripts should be listed
* @return the list of scripts for that name
*/
public List scriptsForClassName(String name)
{
synchronized (targetClassIndex) {
return targetClassIndex.get(name);
}
}
/**
* return a list of all interface scripts indexed using the supplied name. note that if name is
* package qualified then only scripts with the full package qualificaton will be returned
* whereas if name is not package qualified then only scripts with the unqualified name
* will be returned. Note that the returned list can be iterated safely but will not reflect
* later additions to or deletions from the list.
* @param name the name of the interface for which scripts should be listed
* @return the list of scripts for that name
*/
public List scriptsForInterfaceName(String name)
{
synchronized (targetInterfaceIndex) {
return targetInterfaceIndex.get(name);
}
}
/**
* return true if there is a rule which applies to the supplied class otherwise false
* @param clazz the name of the class for which rules are being sought
* @return true if there is a rule which applies
* @throws Exception if an error occurs during class lookup
*/
public boolean matchClass(Class> clazz) throws Exception
{
// see if we have any scripts for the class or its supers
Class nextClazz = clazz;
boolean isOverride = false;
// we create these lazily to avoid unnecessary work
LinkedList visited = null;
LinkedList toVisit = null;
while (nextClazz != null) {
String name = nextClazz.getName();
if (matchTarget(name, clazz, false, isOverride)) {
return true;
}
int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
if (matchTarget(name.substring(lastDot + 1), clazz, false, isOverride)) {
return true;
}
}
// ok, now see if we need to inject via any interfaces that the class implements
if (checkInterfaces()) {
Class[] interfaces = nextClazz.getInterfaces();
int l = interfaces.length;
if (l > 0) {
// ok, so we have to create the lists here
if (visited == null) {
visited = new LinkedList();
toVisit = new LinkedList();
}
// add the implements list of this class as interfaces to consider
for (int i = 0; i < interfaces.length; i++) {
Class interfaze = interfaces[i];
if (!visited.contains(interfaze)) {
toVisit.add(interfaze);
}
}
while (!toVisit.isEmpty()) {
// check the next interface
Class interfaze = toVisit.pop();
name = interfaze.getName();
if (matchTarget(name, clazz, true, isOverride)) {
return true;
} else {
lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
if (matchTarget(name.substring(lastDot + 1), clazz, true, isOverride)) {
return true;
}
}
}
visited.add(interfaze);
// check the extends list of this interface for new interfaces to consider
interfaces = interfaze.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
interfaze = interfaces[i];
if (!visited.contains(interfaze)) {
toVisit.add(interfaze);
}
}
}
}
}
if (skipOverrideRules) {
return false;
}
nextClazz = nextClazz.getSuperclass();
isOverride = true;
}
return false;
}
/**
* return a list containing all the currently installed rule scripts.
* @return the list of all installed scripts
*/
public List currentRules()
{
return new ArrayList(ruleNameIndex.values());
}
/**
* return true if there are any scripts indexed under name which meet the required matching conditions
* @param name the name under which the scripts are indexed
* @param clazz a class which should be checked for a method whose name matches the script method name
* @param isInterface true if we are interested in matching interface rules false if we are interested in
* matching class rules
* @param isOverride true if we are only interested in rules which apply to overriding methods false
* if we are happy with any rule
* @return true if any scripts are found otherwise false
*/
private boolean matchTarget(String name, Class> clazz, boolean isInterface, boolean isOverride) {
Map> index = (isInterface ? targetInterfaceIndex : targetClassIndex);
synchronized (index) {
List ruleScripts = index.get(name);
if (ruleScripts != null) {
for (RuleScript ruleScript: ruleScripts) {
if (isOverride && !ruleScript.isOverride()) {
continue;
}
String methodName = ruleScript.getTargetMethod();
int signaturePos = methodName.indexOf("(");
if (signaturePos > 0) {
methodName = methodName.substring(0, signaturePos).trim();
}
int wsPos = methodName.indexOf(' ');
if (wsPos < 0) {
wsPos = methodName.indexOf('\t');
}
if (wsPos > 0) {
// ok, so METHOD spec must be in format "type methodname"
methodName = methodName.substring(wsPos).trim();
}
if ("".equals(methodName) || "".equals(methodName)) {
// every class has some sort of constructor so accept it
return true;
}
// this filters out cases where the class does not have a method with the correct name
try {
Method[] declaredMethods = clazz.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
Method method = declaredMethods[i];
if (method.getName().equals(methodName)) {
return true;
}
}
} catch (NoClassDefFoundError e) {
// we cam sometimes get an Error thrown if the class we are lookingb up has unresolved
// refernces ot a non-existent class. don't really know why such classes turn up
// in the inst allLoaddedClasses list but they do.
// ignore
}
}
}
}
return false;
}
/**
* insert a script into the index using the script target class name as the index key.
* @param script
* @param index
*/
private void indexTarget(RuleScript script, Map> index)
{
String key = script.getTargetClass();
// synchronize on the new script to avoid ant race with a deleting thread
synchronized(script) {
if (script.isDeleted()) {
return;
}
synchronized (index) {
List entry = index.get(key);
// always create a new list so that we don't affect any in progress iteration of the previous value
if (entry == null) {
entry = new ArrayList();
add(entry, script);
} else {
entry = new ArrayList(entry);
add(entry, script);
}
index.put(key, entry);
}
}
}
/**
* remove a script from the index using the script target class name as the index key.
* @param script
* @param index
*/
private void unindexTarget(RuleScript script, Map> index)
{
synchronized (index) {
String key = script.getTargetClass();
List entry = index.get(key);
// check it has not been deleted by another thread
if (entry != null && entry.contains(script)) {
if (entry.size() == 1) {
// removing the last one so reset entry to null
entry = null;
} else {
// always create a new list so that we don't affect any in progress iteration of the previous value
entry = new ArrayList(entry);
entry.remove(script);
}
index.put(key, entry);
}
}
}
/**
* replace a script in the index using the script target class name as the index key.
* @param script
* @param index
*/
private void reindexTarget(RuleScript script, RuleScript previous, Map> index)
{
// synchronize on the new script to avoid ant race with a deleting thread
synchronized (script) {
if (script.isDeleted()) {
// we just need to delete the old script
unindexTarget(previous, index);
return;
} else {
String key = script.getTargetClass();
String oldKey = previous.getTargetClass();
synchronized (index) {
if (key == oldKey) {
// both in the same list so do one update
List entry = index.get(key);
// check old one has not been deleted by another thread
if (entry == null || !entry.contains(previous)) {
// some other thread must have deleted the old one so just insert the new one
// always create a new list so that we don't affect any in progress iteration of the previous value
entry = new ArrayList(entry);
add(entry, script);
} else {
// always create a new list so that we don't affect any in progress iteration of the previous value
entry = new ArrayList(entry);
entry.remove(previous);
add(entry, script);
}
index.put(key, entry);
} else {
List entry = index.get(oldKey);
// check old one has not been deleted by another thread
if (entry != null && entry.contains(previous)) {
// unindex the previous script
if (entry.size() == 1) {
// removing the last one so reset entry to null
entry = null;
} else {
// always create a new list so that we don't affect any in progress iteration of the previous value
entry = new ArrayList(entry);
entry.remove(previous);
}
index.put(oldKey, entry);
}
// now index the new one
entry = index.get(key);
// always create a new list so that we don't affect any in progress iteration of the previous value
if (entry == null) {
entry = new ArrayList();
} else {
entry = new ArrayList(entry);
}
add(entry, script);
index.put(key, entry);
}
}
}
}
}
/**
* add a rule script to start or end of the index list according to its location type. AT ENTRY rules
* are pushed so they are sorted in reverse load order. other rules are appended so they are sorted
* in load order.
* @param entries
* @param script
*/
private void add(List entries, RuleScript script)
{
// ENTRY rules are pushed so they are sorted in reverse load order
// other rules are appended so they are sorted in load order
if (script.getTargetLocation().getLocationType() == LocationType.ENTRY) {
entries.add(0, script);
} else {
entries.add(script);
}
}
/**
* a 1-1 mapping from target class names which appear in rules to a script object holding the
* rule details
*/
private final Map> targetClassIndex;
/**
* a 1-m mapping from target interface names which appear in rules to a script object holding the
* rule details
*/
private final Map> targetInterfaceIndex;
/**
* a 1-m mapping from rule names which appear in rules to a script object holding the
* rule details
*/
private final Map ruleNameIndex;
/**
* a flag derived from the transformer which enables us to avoid testing superclass rules for
* matches if it is set
*/
private final boolean skipOverrideRules;
/**
* a count of how many rules there are in the script repository which employ injection into hierarchies
*/
private int overrideRuleCount = 0;
/**
* see if we need to do any transformation of interfaces
* @return true if there are any interface rules false if there are none
*/
public boolean checkInterfaces()
{
// n.b. this probably ought to be called synchronized rather than ddo its own synchronization
// so the caller can avoid the timing window between checking and responding to the check.
// but this is a grey area given that there is no way of knowing exactly when a transform
// request will be sent to the Transformer and the repsonse is not to ransform anything. if
// an uupdate affects a loaded class then it will get retransformed anyway so the risk here
// si that the rule gets applied alittle late. we still synchornize here anyway to ensure
// the isEmpty check does not get a partial view of the index.
synchronized (targetInterfaceIndex) {
return !targetInterfaceIndex.isEmpty();
}
}
/**
* check whether any overriding rules are currently loaded
* @return true if there are no overriding rules false if there are any
*/
public boolean skipOverrideRules() {
if (skipOverrideRules) {
return true;
} else {
return overrideRuleCount == 0;
}
}
}