001/* 002 * Copyright 2010-2015 Institut Pasteur. 003 * 004 * This file is part of Icy. 005 * 006 * Icy is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Icy is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Icy. If not, see <http://www.gnu.org/licenses/>. 018 */ 019// CodeHacker.java 020// 021 022/* 023 * ImageJ software for multidimensional image processing and analysis. 024 * 025 * Copyright (c) 2010, ImageJDev.org. 026 * All rights reserved. 027 * 028 * Redistribution and use in source and binary forms, with or without 029 * modification, are permitted provided that the following conditions are met: 030 * Redistributions of source code must retain the above copyright 031 * notice, this list of conditions and the following disclaimer. 032 * Redistributions in binary form must reproduce the above copyright 033 * notice, this list of conditions and the following disclaimer in the 034 * documentation and/or other materials provided with the distribution. 035 * Neither the names of the ImageJDev.org developers nor the 036 * names of its contributors may be used to endorse or promote products 037 * derived from this software without specific prior written permission. 038 * 039 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 040 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 041 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 042 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE 043 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 044 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 045 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 046 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 047 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 048 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 049 * POSSIBILITY OF SUCH DAMAGE. 050 */ 051 052package icy.system; 053 054import icy.util.StringUtil; 055 056import java.security.ProtectionDomain; 057import java.util.ArrayList; 058 059import javassist.CannotCompileException; 060import javassist.ClassClassPath; 061import javassist.ClassPool; 062import javassist.CtClass; 063import javassist.CtMethod; 064import javassist.CtNewMethod; 065import javassist.LoaderClassPath; 066import javassist.NotFoundException; 067 068/** 069 * The code hacker provides a mechanism for altering the behavior of classes 070 * before they are loaded, for the purpose of injecting new methods and/or 071 * altering existing ones. 072 * <p> 073 * In ImageJ, this mechanism is used to provide new seams into legacy ImageJ1 code, so that (e.g.) 074 * the modern UI is aware of IJ1 events as they occur. 075 * </p> 076 * 077 * @author Curtis Rueden 078 * @author Rick Lentz 079 * @author Stephane Dallongeville 080 */ 081public class ClassPatcher 082{ 083 private final static String ARG_RESULT = "result"; 084 085 private final ClassPool pool; 086 private final String patchPackage; 087 private final String patchSuffix; 088 089 public ClassPatcher(ClassLoader classLoader, String patchPackage, String patchSuffix) 090 { 091 pool = ClassPool.getDefault(); 092 pool.appendClassPath(new ClassClassPath(getClass())); 093 if (classLoader != null) 094 pool.appendClassPath(new LoaderClassPath(classLoader)); 095 this.patchPackage = patchPackage; 096 this.patchSuffix = patchSuffix; 097 } 098 099 public ClassPatcher(String patchPackage, String patchSuffix) 100 { 101 this(null, patchPackage, patchSuffix); 102 } 103 104 /** 105 * Modifies a class by injecting additional code at the end of the specified 106 * method's body. 107 * <p> 108 * The extra code is defined in the imagej.legacy.patches package, as described in the 109 * documentation for {@link #insertMethod(String, String)}. 110 * </p> 111 * 112 * @param fullClass 113 * Fully qualified name of the class to modify. 114 * @param methodSig 115 * Method signature of the method to modify; e.g., 116 * "public void updateAndDraw()" 117 */ 118 public void insertAfterMethod(final String fullClass, final String methodSig) 119 { 120 insertAfterMethod(fullClass, methodSig, newCode(fullClass, methodSig)); 121 } 122 123 /** 124 * Modifies a class by injecting the provided code string at the end of the 125 * specified method's body. 126 * 127 * @param fullClass 128 * Fully qualified name of the class to modify. 129 * @param methodSig 130 * Method signature of the method to modify; e.g., 131 * "public void updateAndDraw()" 132 * @param newCode 133 * The string of code to add; e.g., System.out.println(\"Hello 134 * World!\"); 135 */ 136 public void insertAfterMethod(final String fullClass, final String methodSig, final String newCode) 137 { 138 try 139 { 140 getMethod(fullClass, methodSig).insertAfter(newCode); 141 } 142 catch (final CannotCompileException e) 143 { 144 throw new IllegalArgumentException("Cannot modify method: " + methodSig, e); 145 } 146 } 147 148 /** 149 * Modifies a class by injecting additional code at the start of the specified 150 * method's body. 151 * <p> 152 * The extra code is defined in the imagej.legacy.patches package, as described in the 153 * documentation for {@link #insertMethod(String, String)}. 154 * </p> 155 * 156 * @param fullClass 157 * Fully qualified name of the class to override. 158 * @param methodSig 159 * Method signature of the method to override; e.g., 160 * "public void updateAndDraw()" 161 */ 162 public void insertBeforeMethod(final String fullClass, final String methodSig) 163 { 164 insertBeforeMethod(fullClass, methodSig, newCode(fullClass, methodSig)); 165 } 166 167 /** 168 * Modifies a class by injecting the provided code string at the start of the 169 * specified method's body. 170 * 171 * @param fullClass 172 * Fully qualified name of the class to override. 173 * @param methodSig 174 * Method signature of the method to override; e.g., 175 * "public void updateAndDraw()" 176 * @param newCode 177 * The string of code to add; e.g., System.out.println(\"Hello 178 * World!\"); 179 */ 180 public void insertBeforeMethod(final String fullClass, final String methodSig, final String newCode) 181 { 182 try 183 { 184 getMethod(fullClass, methodSig).insertBefore(newCode); 185 } 186 catch (final CannotCompileException e) 187 { 188 throw new IllegalArgumentException("Cannot modify method: " + methodSig, e); 189 } 190 } 191 192 /** 193 * Modifies a class by injecting a new method. 194 * <p> 195 * The body of the method is defined in the imagej.legacy.patches package, as described in the 196 * {@link #insertMethod(String, String)} method documentation. 197 * <p> 198 * The new method implementation should be declared in the imagej.legacy.patches package, with 199 * the same name as the original class plus "Methods"; e.g., overridden ij.gui.ImageWindow 200 * methods should be placed in the imagej.legacy.patches.ImageWindowMethods class. 201 * </p> 202 * <p> 203 * New method implementations must be public static, with an additional first parameter: the 204 * instance of the class on which to operate. 205 * </p> 206 * 207 * @param fullClass 208 * Fully qualified name of the class to override. 209 * @param methodSig 210 * Method signature of the method to override; e.g., 211 * "public void setVisible(boolean vis)" 212 */ 213 public void insertMethod(final String fullClass, final String methodSig) 214 { 215 insertMethod(fullClass, methodSig, newCode(fullClass, methodSig)); 216 } 217 218 /** 219 * Modifies a class by injecting the provided code string as a new method. 220 * 221 * @param fullClass 222 * Fully qualified name of the class to override. 223 * @param methodSig 224 * Method signature of the method to override; e.g., 225 * "public void updateAndDraw()" 226 * @param newCode 227 * The string of code to add; e.g., System.out.println(\"Hello 228 * World!\"); 229 */ 230 public void insertMethod(final String fullClass, final String methodSig, final String newCode) 231 { 232 final CtClass classRef = getClass(fullClass); 233 final String methodBody = methodSig + " { " + newCode + " } "; 234 try 235 { 236 final CtMethod methodRef = CtNewMethod.make(methodBody, classRef); 237 classRef.addMethod(methodRef); 238 } 239 catch (final CannotCompileException e) 240 { 241 throw new IllegalArgumentException("Cannot add method: " + methodSig, e); 242 } 243 } 244 245 /** 246 * Modifies a class by replacing the specified method. 247 * <p> 248 * The new code is defined in the imagej.legacy.patches package, as described in the 249 * documentation for {@link #insertMethod(String, String)}. 250 * </p> 251 * 252 * @param fullClass 253 * Fully qualified name of the class to override. 254 * @param methodSig 255 * Method signature of the method to replace; e.g., 256 * "public void setVisible(boolean vis)" 257 */ 258 public void replaceMethod(final String fullClass, final String methodSig) 259 { 260 replaceMethod(fullClass, methodSig, newCode(fullClass, methodSig)); 261 } 262 263 /** 264 * Modifies a class by replacing the specified method with the provided code 265 * string. 266 * 267 * @param fullClass 268 * Fully qualified name of the class to override. 269 * @param methodSig 270 * Method signature of the method to replace; e.g., 271 * "public void setVisible(boolean vis)" 272 * @param newCode 273 * The string of code to add; e.g., System.out.println(\"Hello 274 * World!\"); 275 */ 276 public void replaceMethod(final String fullClass, final String methodSig, final String newCode) 277 { 278 try 279 { 280 getMethod(fullClass, methodSig).setBody(newCode); 281 } 282 catch (final CannotCompileException e) 283 { 284 throw new IllegalArgumentException("Cannot modify method: " + methodSig, e); 285 } 286 } 287 288 /** 289 * Loads the given, possibly modified, class. 290 * <p> 291 * This method must be called to confirm any changes made with {@link #insertAfterMethod}, 292 * {@link #insertBeforeMethod}, {@link #insertMethod} or {@link #replaceMethod}. 293 * </p> 294 * 295 * @param fullClass 296 * Fully qualified class name to load. 297 * @return the loaded class 298 */ 299 public Class<?> loadClass(final String fullClass) 300 { 301 final CtClass classRef = getClass(fullClass); 302 try 303 { 304 return classRef.toClass(); 305 } 306 catch (final CannotCompileException e) 307 { 308 IcyExceptionHandler.showErrorMessage(e, false); 309 System.err.println("Cannot load class: " + fullClass); 310 return null; 311 } 312 } 313 314 /** 315 * Loads the given, possibly modified, class. 316 * <p> 317 * This method must be called to confirm any changes made with {@link #insertAfterMethod}, 318 * {@link #insertBeforeMethod}, {@link #insertMethod} or {@link #replaceMethod}. 319 * </p> 320 * 321 * @param fullClass 322 * Fully qualified class name to load. 323 * @return the loaded class 324 */ 325 public Class<?> loadClass(final String fullClass, ClassLoader classLoader, ProtectionDomain protectionDomain) 326 { 327 final CtClass classRef = getClass(fullClass); 328 try 329 { 330 return classRef.toClass(classLoader, protectionDomain); 331 } 332 catch (final CannotCompileException e) 333 { 334 IcyExceptionHandler.showErrorMessage(e, false); 335 System.err.println("Cannot load class: " + fullClass); 336 return null; 337 } 338 } 339 340 /** Gets the Javassist class object corresponding to the given class name. */ 341 private CtClass getClass(final String fullClass) 342 { 343 try 344 { 345 return pool.get(fullClass); 346 } 347 catch (final NotFoundException e) 348 { 349 throw new IllegalArgumentException("No such class: " + fullClass, e); 350 } 351 } 352 353 /** 354 * Gets the Javassist method object corresponding to the given method 355 * signature of the specified class name. 356 */ 357 private CtMethod getMethod(final String fullClass, final String methodSig) 358 { 359 final CtClass cc = getClass(fullClass); 360 final String name = getMethodName(methodSig); 361 final String[] argTypes = getMethodArgTypes(methodSig, false); 362 final CtClass[] params = new CtClass[argTypes.length]; 363 for (int i = 0; i < params.length; i++) 364 { 365 params[i] = getClass(argTypes[i]); 366 } 367 try 368 { 369 return cc.getDeclaredMethod(name, params); 370 } 371 catch (final NotFoundException e) 372 { 373 throw new IllegalArgumentException("No such method: " + methodSig, e); 374 } 375 } 376 377 /** 378 * Generates a new line of code calling the {@link imagej.legacy.patches} class and method 379 * corresponding to the given method signature. 380 */ 381 private String newCode(final String fullClass, final String methodSig) 382 { 383 final int dotIndex = fullClass.lastIndexOf("."); 384 final String className = fullClass.substring(dotIndex + 1); 385 386 final String methodName = getMethodName(methodSig); 387 final boolean isStatic = isStatic(methodSig); 388 final boolean isVoid = isVoid(methodSig); 389 390 final StringBuilder newCode = new StringBuilder( 391 (isVoid ? "" : "return ") + patchPackage + "." + className + patchSuffix + "." + methodName + "("); 392 boolean firstArg = true; 393 if (!isStatic) 394 { 395 newCode.append("this"); 396 firstArg = false; 397 } 398 int i = 1; 399 for (String argName : getMethodArgNames(methodSig, true)) 400 { 401 if (firstArg) 402 firstArg = false; 403 else 404 newCode.append(", "); 405 406 if (StringUtil.equals(argName, ARG_RESULT)) 407 newCode.append("$_"); 408 else 409 { 410 newCode.append("$" + i); 411 i++; 412 } 413 } 414 newCode.append(");"); 415 416 return newCode.toString(); 417 } 418 419 /** Extracts the method name from the given method signature. */ 420 private String getMethodName(final String methodSig) 421 { 422 final int parenIndex = methodSig.indexOf("("); 423 final int spaceIndex = methodSig.lastIndexOf(" ", parenIndex); 424 return methodSig.substring(spaceIndex + 1, parenIndex); 425 } 426 427 private String[] getMethodArgs(final String methodSig, final boolean wantResult) 428 { 429 final ArrayList<String> result = new ArrayList<String>(); 430 431 final int parenIndex = methodSig.indexOf("("); 432 final String methodArgs = methodSig.substring(parenIndex + 1, methodSig.length() - 1); 433 final String[] args = methodArgs.equals("") ? new String[0] : methodArgs.split(","); 434 for (String arg : args) 435 { 436 final String a = arg.trim(); 437 if (!StringUtil.equals(a.split(" ")[1], ARG_RESULT) || wantResult) 438 result.add(a); 439 } 440 441 return result.toArray(new String[result.size()]); 442 } 443 444 private String[] getMethodArgTypes(final String methodSig, final boolean wantResult) 445 { 446 final String[] args = getMethodArgs(methodSig, wantResult); 447 for (int i = 0; i < args.length; i++) 448 args[i] = args[i].split(" ")[0]; 449 return args; 450 } 451 452 private String[] getMethodArgNames(final String methodSig, final boolean wantResult) 453 { 454 final String[] args = getMethodArgs(methodSig, wantResult); 455 for (int i = 0; i < args.length; i++) 456 args[i] = args[i].split(" ")[1]; 457 return args; 458 } 459 460 /** Returns true if the given method signature is static. */ 461 private boolean isStatic(final String methodSig) 462 { 463 final int parenIndex = methodSig.indexOf("("); 464 final String methodPrefix = methodSig.substring(0, parenIndex); 465 for (final String token : methodPrefix.split(" ")) 466 { 467 if (token.equals("static")) 468 return true; 469 } 470 return false; 471 } 472 473 /** Returns true if the given method signature returns void. */ 474 private boolean isVoid(final String methodSig) 475 { 476 final int parenIndex = methodSig.indexOf("("); 477 final String methodPrefix = methodSig.substring(0, parenIndex); 478 return methodPrefix.startsWith("void ") || methodPrefix.indexOf(" void ") > 0; 479 } 480 481}