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}