/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
// CodeHacker.java
//
/*
* ImageJ software for multidimensional image processing and analysis.
*
* Copyright (c) 2010, ImageJDev.org.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the names of the ImageJDev.org developers nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package icy.system;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import icy.util.StringUtil;
import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
/**
* The code hacker provides a mechanism for altering the behavior of classes
* before they are loaded, for the purpose of injecting new methods and/or
* altering existing ones.
* <p>
* In ImageJ, this mechanism is used to provide new seams into legacy ImageJ1 code, so that (e.g.)
* the modern UI is aware of IJ1 events as they occur.
* </p>
*
* @author Curtis Rueden
* @author Rick Lentz
* @author Stephane Dallongeville
*/
public class ClassPatcher
{
private final static String ARG_RESULT = "result";
private final ClassPool pool;
private final String patchPackage;
private final String patchSuffix;
public ClassPatcher(ClassLoader classLoader, String patchPackage, String patchSuffix)
{
pool = ClassPool.getDefault();
pool.appendClassPath(new ClassClassPath(getClass()));
if (classLoader != null)
pool.appendClassPath(new LoaderClassPath(classLoader));
this.patchPackage = patchPackage;
this.patchSuffix = patchSuffix;
}
public ClassPatcher(String patchPackage, String patchSuffix)
{
this(null, patchPackage, patchSuffix);
}
/**
* Modifies a class by injecting additional code at the end of the specified
* method's body.
* <p>
* The extra code is defined in the imagej.legacy.patches package, as described in the
* documentation for {@link #insertMethod(String, String)}.
* </p>
*
* @param fullClass
* Fully qualified name of the class to modify.
* @param methodSig
* Method signature of the method to modify; e.g.,
* "public void updateAndDraw()"
*/
public void insertAfterMethod(final String fullClass, final String methodSig)
{
insertAfterMethod(fullClass, methodSig, newCode(fullClass, methodSig));
}
/**
* Modifies a class by injecting the provided code string at the end of the
* specified method's body.
*
* @param fullClass
* Fully qualified name of the class to modify.
* @param methodSig
* Method signature of the method to modify; e.g.,
* "public void updateAndDraw()"
* @param newCode
* The string of code to add; e.g., System.out.println(\"Hello
* World!\");
*/
public void insertAfterMethod(final String fullClass, final String methodSig, final String newCode)
{
try
{
getMethod(fullClass, methodSig).insertAfter(newCode);
}
catch (final CannotCompileException e)
{
throw new IllegalArgumentException("Cannot modify method: " + methodSig, e);
}
}
/**
* Modifies a class by injecting additional code at the start of the specified
* method's body.
* <p>
* The extra code is defined in the imagej.legacy.patches package, as described in the
* documentation for {@link #insertMethod(String, String)}.
* </p>
*
* @param fullClass
* Fully qualified name of the class to override.
* @param methodSig
* Method signature of the method to override; e.g.,
* "public void updateAndDraw()"
*/
public void insertBeforeMethod(final String fullClass, final String methodSig)
{
insertBeforeMethod(fullClass, methodSig, newCode(fullClass, methodSig));
}
/**
* Modifies a class by injecting the provided code string at the start of the
* specified method's body.
*
* @param fullClass
* Fully qualified name of the class to override.
* @param methodSig
* Method signature of the method to override; e.g.,
* "public void updateAndDraw()"
* @param newCode
* The string of code to add; e.g., System.out.println(\"Hello
* World!\");
*/
public void insertBeforeMethod(final String fullClass, final String methodSig, final String newCode)
{
try
{
getMethod(fullClass, methodSig).insertBefore(newCode);
}
catch (final CannotCompileException e)
{
throw new IllegalArgumentException("Cannot modify method: " + methodSig, e);
}
}
/**
* Modifies a class by injecting a new method.
* <p>
* The body of the method is defined in the imagej.legacy.patches package, as described in the
* {@link #insertMethod(String, String)} method documentation.
* <p>
* The new method implementation should be declared in the imagej.legacy.patches package, with
* the same name as the original class plus "Methods"; e.g., overridden ij.gui.ImageWindow
* methods should be placed in the imagej.legacy.patches.ImageWindowMethods class.
* </p>
* <p>
* New method implementations must be public static, with an additional first parameter: the
* instance of the class on which to operate.
* </p>
*
* @param fullClass
* Fully qualified name of the class to override.
* @param methodSig
* Method signature of the method to override; e.g.,
* "public void setVisible(boolean vis)"
*/
public void insertMethod(final String fullClass, final String methodSig)
{
insertMethod(fullClass, methodSig, newCode(fullClass, methodSig));
}
/**
* Modifies a class by injecting the provided code string as a new method.
*
* @param fullClass
* Fully qualified name of the class to override.
* @param methodSig
* Method signature of the method to override; e.g.,
* "public void updateAndDraw()"
* @param newCode
* The string of code to add; e.g., System.out.println(\"Hello
* World!\");
*/
public void insertMethod(final String fullClass, final String methodSig, final String newCode)
{
final CtClass classRef = getClass(fullClass);
final String methodBody = methodSig + " { " + newCode + " } ";
try
{
final CtMethod methodRef = CtNewMethod.make(methodBody, classRef);
classRef.addMethod(methodRef);
}
catch (final CannotCompileException e)
{
throw new IllegalArgumentException("Cannot add method: " + methodSig, e);
}
}
/**
* Modifies a class by replacing the specified method.
* <p>
* The new code is defined in the imagej.legacy.patches package, as described in the
* documentation for {@link #insertMethod(String, String)}.
* </p>
*
* @param fullClass
* Fully qualified name of the class to override.
* @param methodSig
* Method signature of the method to replace; e.g.,
* "public void setVisible(boolean vis)"
*/
public void replaceMethod(final String fullClass, final String methodSig)
{
replaceMethod(fullClass, methodSig, newCode(fullClass, methodSig));
}
/**
* Modifies a class by replacing the specified method with the provided code
* string.
*
* @param fullClass
* Fully qualified name of the class to override.
* @param methodSig
* Method signature of the method to replace; e.g.,
* "public void setVisible(boolean vis)"
* @param newCode
* The string of code to add; e.g., System.out.println(\"Hello
* World!\");
*/
public void replaceMethod(final String fullClass, final String methodSig, final String newCode)
{
try
{
getMethod(fullClass, methodSig).setBody(newCode);
}
catch (final CannotCompileException e)
{
throw new IllegalArgumentException("Cannot modify method: " + methodSig, e);
}
}
/**
* Loads the given, possibly modified, class.
* <p>
* This method must be called to confirm any changes made with {@link #insertAfterMethod},
* {@link #insertBeforeMethod}, {@link #insertMethod} or {@link #replaceMethod}.
* </p>
*
* @param fullClass
* Fully qualified class name to load.
* @return the loaded class
*/
public Class<?> loadClass(final String fullClass)
{
final CtClass classRef = getClass(fullClass);
try
{
return classRef.toClass();
}
catch (final CannotCompileException e)
{
IcyExceptionHandler.showErrorMessage(e, false);
System.err.println("Cannot load class: " + fullClass);
return null;
}
}
/**
* Loads the given, possibly modified, class.
* <p>
* This method must be called to confirm any changes made with {@link #insertAfterMethod},
* {@link #insertBeforeMethod}, {@link #insertMethod} or {@link #replaceMethod}.
* </p>
*
* @param fullClass
* Fully qualified class name to load.
* @return the loaded class
*/
public Class<?> loadClass(final String fullClass, ClassLoader classLoader, ProtectionDomain protectionDomain)
{
final CtClass classRef = getClass(fullClass);
try
{
return classRef.toClass(classLoader, protectionDomain);
}
catch (final CannotCompileException e)
{
IcyExceptionHandler.showErrorMessage(e, false);
System.err.println("Cannot load class: " + fullClass);
return null;
}
}
/** Gets the Javassist class object corresponding to the given class name. */
private CtClass getClass(final String fullClass)
{
try
{
return pool.get(fullClass);
}
catch (final NotFoundException e)
{
throw new IllegalArgumentException("No such class: " + fullClass, e);
}
}
/**
* Gets the Javassist method object corresponding to the given method
* signature of the specified class name.
*/
private CtMethod getMethod(final String fullClass, final String methodSig)
{
final CtClass cc = getClass(fullClass);
final String name = getMethodName(methodSig);
final String[] argTypes = getMethodArgTypes(methodSig, false);
final CtClass[] params = new CtClass[argTypes.length];
for (int i = 0; i < params.length; i++)
{
params[i] = getClass(argTypes[i]);
}
try
{
return cc.getDeclaredMethod(name, params);
}
catch (final NotFoundException e)
{
throw new IllegalArgumentException("No such method: " + methodSig, e);
}
}
/**
* Generates a new line of code calling the {@link imagej.legacy.patches} class and method
* corresponding to the given method signature.
*/
private String newCode(final String fullClass, final String methodSig)
{
final int dotIndex = fullClass.lastIndexOf(".");
final String className = fullClass.substring(dotIndex + 1);
final String methodName = getMethodName(methodSig);
final boolean isStatic = isStatic(methodSig);
final boolean isVoid = isVoid(methodSig);
final StringBuilder newCode = new StringBuilder(
(isVoid ? "" : "return ") + patchPackage + "." + className + patchSuffix + "." + methodName + "(");
boolean firstArg = true;
if (!isStatic)
{
newCode.append("this");
firstArg = false;
}
int i = 1;
for (String argName : getMethodArgNames(methodSig, true))
{
if (firstArg)
firstArg = false;
else
newCode.append(", ");
if (StringUtil.equals(argName, ARG_RESULT))
newCode.append("$_");
else
{
newCode.append("$" + i);
i++;
}
}
newCode.append(");");
return newCode.toString();
}
/** Extracts the method name from the given method signature. */
private String getMethodName(final String methodSig)
{
final int parenIndex = methodSig.indexOf("(");
final int spaceIndex = methodSig.lastIndexOf(" ", parenIndex);
return methodSig.substring(spaceIndex + 1, parenIndex);
}
private String[] getMethodArgs(final String methodSig, final boolean wantResult)
{
final ArrayList<String> result = new ArrayList<String>();
final int parenIndex = methodSig.indexOf("(");
final String methodArgs = methodSig.substring(parenIndex + 1, methodSig.length() - 1);
final String[] args = methodArgs.equals("") ? new String[0] : methodArgs.split(",");
for (String arg : args)
{
final String a = arg.trim();
if (!StringUtil.equals(a.split(" ")[1], ARG_RESULT) || wantResult)
result.add(a);
}
return result.toArray(new String[result.size()]);
}
private String[] getMethodArgTypes(final String methodSig, final boolean wantResult)
{
final String[] args = getMethodArgs(methodSig, wantResult);
for (int i = 0; i < args.length; i++)
args[i] = args[i].split(" ")[0];
return args;
}
private String[] getMethodArgNames(final String methodSig, final boolean wantResult)
{
final String[] args = getMethodArgs(methodSig, wantResult);
for (int i = 0; i < args.length; i++)
args[i] = args[i].split(" ")[1];
return args;
}
/** Returns true if the given method signature is static. */
private boolean isStatic(final String methodSig)
{
final int parenIndex = methodSig.indexOf("(");
final String methodPrefix = methodSig.substring(0, parenIndex);
for (final String token : methodPrefix.split(" "))
{
if (token.equals("static"))
return true;
}
return false;
}
/** Returns true if the given method signature returns void. */
private boolean isVoid(final String methodSig)
{
final int parenIndex = methodSig.indexOf("(");
final String methodPrefix = methodSig.substring(0, parenIndex);
return methodPrefix.startsWith("void ") || methodPrefix.indexOf(" void ") > 0;
}
}