/* * Copyright (C) 2009-2016 The Project Lombok Authors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package lombok.patcher; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.ProtectionDomain; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.jar.JarFile; public class ScriptManager { private final List<PatchScript> scripts = new ArrayList<PatchScript>(); private TransplantMapper transplantMapper = TransplantMapper.IDENTITY_MAPPER; private Filter filter = Filter.ALWAYS; public void addScript(PatchScript script) { scripts.add(script); } public void setFilter(Filter filter) { this.filter = filter == null ? Filter.ALWAYS : filter; } public void registerTransformer(Instrumentation instrumentation) { try { Method m = Instrumentation.class.getMethod("addTransformer", ClassFileTransformer.class, boolean.class); m.invoke(instrumentation, transformer, true); } catch (Throwable t) { //We're on java 1.5, or something even crazier happened. This one works in 1.5 as well: instrumentation.addTransformer(transformer); } } public void reloadClasses(Instrumentation instrumentation) { Set<String> toReload = new HashSet<String>(); for (PatchScript s : scripts) toReload.addAll(s.getClassesToReload()); for (Class<?> c : instrumentation.getAllLoadedClasses()) { if (toReload.contains(c.getName())) { try { //instrumentation.retransformClasses(c); - //not in java 1.5. Instrumentation.class.getMethod("retransformClasses", Class[].class).invoke(instrumentation, new Object[] { new Class[] {c }}); } catch ( InvocationTargetException e ) { throw new UnsupportedOperationException( "The " + c.getName() + " class is already loaded and cannot be modified. " + "You'll have to restart the application to patch it. Reason: " + e.getCause()); } catch ( Throwable t ) { throw new UnsupportedOperationException( "This appears to be a JVM v1.5, which cannot reload already loaded classes. " + "You'll have to restart the application to patch it."); } } } } private static final String DEBUG_PATCHING; static { DEBUG_PATCHING = System.getProperty("lombok.patcher.patchDebugDir", null); } private final OurClassFileTransformer transformer = new OurClassFileTransformer(); private class OurClassFileTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className == null) return null; if (!filter.shouldTransform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer)) return null; byte[] byteCode = classfileBuffer; boolean patched = false; for (PatchScript script : scripts) { byte[] transformed = null; try { transformed = script.patch(className, byteCode, transplantMapper); } catch (Throwable t) { //Exceptions get silently swallowed by instrumentation, so this is a slight improvement. System.err.printf("Transformer %s failed on %s. Trace:\n", script.getPatchScriptName(), className); t.printStackTrace(); transformed = null; } if (transformed != null) { patched = true; byteCode = transformed; } } if (patched && DEBUG_PATCHING != null) { try { writeArray(DEBUG_PATCHING, className + ".class", byteCode); writeArray(DEBUG_PATCHING, className + "_OLD.class", classfileBuffer); } catch (IOException e) { System.err.println("Can't log patch result."); e.printStackTrace(); } } return patched ? byteCode : null; } private void writeArray(String dir, String fileName, byte[] bytes) throws IOException { File f = new File(dir, fileName); f.getParentFile().mkdirs(); FileOutputStream fos = new FileOutputStream(f); fos.write(bytes); fos.close(); } }; private static boolean classpathContains(String property, String path) { String pathCanonical = new File(path).getAbsolutePath(); try { pathCanonical = new File(path).getCanonicalPath(); } catch (Exception ignore) {} for (String existingPath : System.getProperty(property, "").split(File.pathSeparator)) { String p = new File(existingPath).getAbsolutePath(); try { p = new File(existingPath).getCanonicalPath(); } catch (Throwable ignore) {} if (p.equals(pathCanonical)) return true; } return false; } /** * Adds the provided path (must be to a jar file!) to the system classpath. * * Will do nothing if the jar is already on either the system or the boot classpath. * * @throws IllegalArgumentException If {@code pathToJar} doesn't exist or isn't a jar file. * @throws IllegalStateException If you try this on a 1.5 VM - it requires 1.6 or up VM. */ public void addToSystemClasspath(Instrumentation instrumentation, String pathToJar) { if (pathToJar == null) throw new NullPointerException("pathToJar"); if (classpathContains("sun.boot.class.path", pathToJar)) return; if (classpathContains("java.class.path", pathToJar)) return; try { Method m = instrumentation.getClass().getMethod("appendToSystemClassLoaderSearch", JarFile.class); m.invoke(instrumentation, new JarFile(pathToJar)); } catch (NoSuchMethodException e) { throw new IllegalStateException("Adding to the classloader path is not possible on a v1.5 JVM"); } catch (IOException e) { throw new IllegalArgumentException("not found or not a jar file: " + pathToJar, e); } catch (IllegalAccessException e) { throw new IllegalStateException("appendToSystemClassLoaderSearch isn't public? This isn't a JVM..."); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException) throw (RuntimeException)cause; throw new IllegalArgumentException("Unknown issue: " + cause, cause); } } /** * Adds the provided path (must be to a jar file!) to the system classpath. * * Will do nothing if the jar is already on the boot classpath. * * @throws IllegalArgumentException If {@code pathToJar} doesn't exist or isn't a jar file. * @throws IllegalStateException If you try this on a 1.5 VM - it requires 1.6 or up VM. */ public void addToBootClasspath(Instrumentation instrumentation, String pathToJar) { if (pathToJar == null) throw new NullPointerException("pathToJar"); if (classpathContains("sun.boot.class.path", pathToJar)) return; try { Method m = instrumentation.getClass().getMethod("appendToBootstrapClassLoaderSearch", JarFile.class); m.invoke(instrumentation, new JarFile(pathToJar)); } catch (NoSuchMethodException e) { throw new IllegalStateException("Adding to the classloader path is not possible on a v1.5 JVM"); } catch (IOException e) { throw new IllegalArgumentException("not found or not a jar file: " + pathToJar, e); } catch (IllegalAccessException e) { throw new IllegalStateException("appendToSystemClassLoaderSearch isn't public? This isn't a JVM..."); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException) throw (RuntimeException)cause; throw new IllegalArgumentException("Unknown issue: " + cause, cause); } } public void setTransplantMapper(TransplantMapper transplantMapper) { this.transplantMapper = transplantMapper == null ? TransplantMapper.IDENTITY_MAPPER : transplantMapper; } }