diff --git a/src/main/java/plugins/tprovoost/Microscopy/MicroManager/patch/ClassPatcher.java b/src/main/java/plugins/tprovoost/Microscopy/MicroManager/patch/ClassPatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..e638f91b3b05a952a88585845cd2656167808ff6 --- /dev/null +++ b/src/main/java/plugins/tprovoost/Microscopy/MicroManager/patch/ClassPatcher.java @@ -0,0 +1,521 @@ +package plugins.tprovoost.Microscopy.MicroManager.patch; + +/* + * 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. + */ + +import java.security.ProtectionDomain; +import java.util.ArrayList; + +import icy.system.IcyExceptionHandler; +import icy.system.SystemUtil; +import icy.util.StringUtil; +import javassist.CannotCompileException; +import javassist.ClassClassPath; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtField; +import javassist.CtMethod; +import javassist.CtNewMethod; +import javassist.LoaderClassPath; +import javassist.NotFoundException; +import javassist.bytecode.AccessFlag; +import javassist.bytecode.FieldInfo; + +/** + * 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. + * @param neighbor + * A class belonging to the same package that this + * class belongs to. It is used to load the class. + * @return the loaded class + */ + public Class<?> loadClass(final String fullClass, Class<?> neighbor) + { + final CtClass classRef = getClass(fullClass); + try + { + if (SystemUtil.getJavaVersionAsNumber() >= 9) + return pool.toClass(classRef, neighbor); + + // old deprecated way of doing + return pool.toClass(classRef); + } + 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. + * @param neighbor + * A class belonging to the same package that this + * class belongs to. It is used to load the class. + * @param classLoader + * @param protectionDomain + * @return the loaded class + */ + public Class<?> loadClass(final String fullClass, Class<?> neighbor, ClassLoader classLoader, + ProtectionDomain protectionDomain) + { + final CtClass classRef = getClass(fullClass); + try + { + if (SystemUtil.getJavaVersionAsNumber() >= 9) + return pool.toClass(classRef, neighbor, classLoader, protectionDomain); + + // old deprecated way of doing + return pool.toClass(classRef, 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); + } + } + + private CtField getField(final String fullClass, final String name) + { + final CtClass cc = getClass(fullClass); + try + { + return cc.getDeclaredField(name); + } + catch (final NotFoundException e) + { + throw new IllegalArgumentException("No such field: " + name, e); + } + } + + public void setFieldPublic(final String fullClass, final String name) + { + final CtField field = getField(fullClass, name); + final FieldInfo info = field.getFieldInfo(); + info.setAccessFlags(AccessFlag.PUBLIC); + } + + /** + * 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; + } +}