diff options
author | Reinier Zwitserloot <reinier@zwitserloot.com> | 2014-12-04 02:54:22 +0100 |
---|---|---|
committer | Reinier Zwitserloot <reinier@zwitserloot.com> | 2014-12-04 02:54:22 +0100 |
commit | 2e78a2e03f8fdba4d3d468b2900d30f7e7317641 (patch) | |
tree | 617a4f36b044fa7b1f87cd2e525bd7308b6f0163 /src | |
parent | 815f7d0fe82df761c038907043abd1a33d491f5d (diff) | |
parent | 9e7c75a0fef387c173af289626aa04d5c2942710 (diff) | |
download | lombok-2e78a2e03f8fdba4d3d468b2900d30f7e7317641.tar.gz lombok-2e78a2e03f8fdba4d3d468b2900d30f7e7317641.tar.bz2 lombok-2e78a2e03f8fdba4d3d468b2900d30f7e7317641.zip |
Merge branch 'shadowLauncher'
Diffstat (limited to 'src')
-rw-r--r-- | src/core/lombok/core/AgentLauncher.java (renamed from src/core/lombok/core/Agent.java) | 74 | ||||
-rw-r--r-- | src/core/lombok/core/Main.java | 1 | ||||
-rw-r--r-- | src/core/lombok/core/PostCompiler.java | 8 | ||||
-rw-r--r-- | src/core/lombok/core/Version.java | 2 | ||||
-rw-r--r-- | src/core/lombok/eclipse/handlers/HandlePrintAST.java | 2 | ||||
-rw-r--r-- | src/core/lombok/javac/handlers/HandlePrintAST.java | 2 | ||||
-rw-r--r-- | src/delombok/lombok/delombok/DelombokApp.java | 16 | ||||
-rw-r--r-- | src/eclipseAgent/lombok/eclipse/agent/EclipseLoaderPatcher.java | 106 | ||||
-rw-r--r-- | src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java | 16 | ||||
-rw-r--r-- | src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java | 3 | ||||
-rw-r--r-- | src/installer/lombok/installer/InstallerGUI.java | 5 | ||||
-rw-r--r-- | src/installer/lombok/installer/eclipse/EclipseLocation.java | 5 | ||||
-rw-r--r-- | src/launch/lombok/launch/Agent.java | 47 | ||||
-rw-r--r-- | src/launch/lombok/launch/AnnotationProcessor.java | 77 | ||||
-rw-r--r-- | src/launch/lombok/launch/Main.java | 40 | ||||
-rw-r--r-- | src/launch/lombok/launch/ShadowClassLoader.java | 399 |
16 files changed, 714 insertions, 89 deletions
diff --git a/src/core/lombok/core/Agent.java b/src/core/lombok/core/AgentLauncher.java index 49c03bf3..1d5ab3e6 100644 --- a/src/core/lombok/core/Agent.java +++ b/src/core/lombok/core/AgentLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Project Lombok Authors. + * Copyright (C) 2009-2014 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 @@ -21,45 +21,32 @@ */ package lombok.core; -import java.lang.instrument.ClassFileTransformer; -import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; -import java.security.ProtectionDomain; import java.util.Arrays; import java.util.Collections; import java.util.List; -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; - -public abstract class Agent { - protected abstract void runAgent(String agentArgs, Instrumentation instrumentation, boolean injected) throws Exception; - - public static void agentmain(String agentArgs, Instrumentation instrumentation) throws Throwable { - runAgents(agentArgs, instrumentation, true); +public class AgentLauncher { + public interface AgentLaunchable { + void runAgent(String agentArgs, Instrumentation instrumentation, boolean injected, Class<?> launchingContext) throws Exception; } - public static void premain(String agentArgs, Instrumentation instrumentation) throws Throwable { - runAgents(agentArgs, instrumentation, false); - } - - private static final List<AgentInfo> AGENTS = Collections.unmodifiableList(Arrays.asList( - new NetbeansPatcherInfo(), - new EclipsePatcherInfo() - )); - - private static void runAgents(String agentArgs, Instrumentation instrumentation, boolean injected) throws Throwable { + public static void runAgents(String agentArgs, Instrumentation instrumentation, boolean injected, Class<?> launchingContext) throws Throwable { for (AgentInfo info : AGENTS) { try { Class<?> agentClass = Class.forName(info.className()); - Agent agent = (Agent) agentClass.newInstance(); - agent.runAgent(agentArgs, instrumentation, injected); + AgentLaunchable agent = (AgentLaunchable) agentClass.newInstance(); + agent.runAgent(agentArgs, instrumentation, injected, launchingContext); } catch (Throwable t) { info.problem(t, instrumentation); } } } + private static final List<AgentInfo> AGENTS = Collections.unmodifiableList(Arrays.<AgentInfo>asList( + new EclipsePatcherInfo() + )); + private static abstract class AgentInfo { abstract String className(); @@ -91,45 +78,6 @@ public abstract class Agent { } } - private static class NetbeansPatcherInfo extends AgentInfo { - @Override String className() { - return "lombok.netbeans.agent.NetbeansPatcher"; - } - - @Override void problem(Throwable in, Instrumentation instrumentation) throws Throwable { - try { - super.problem(in, instrumentation); - } catch (InternalError ie) { - throw ie; - } catch (Throwable t) { - final String error; - - if (t instanceof UnsupportedClassVersionError) { - error = "Lombok only works on netbeans if you start netbeans using a 1.6 or higher JVM.\n" + - "Change your platform's default JVM, or edit etc/netbeans.conf\n" + - "and explicitly tell netbeans your 1.6 JVM's location."; - } else { - error = "Lombok disabled due to error: " + t; - } - - instrumentation.addTransformer(new ClassFileTransformer() { - @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { - if ("org/netbeans/modules/java/source/parsing/JavacParser".equals(className)) { - //If that class gets loaded, this is definitely a netbeans(-esque) environment, and thus we SHOULD tell the user that lombok is not in fact loaded. - SwingUtilities.invokeLater(new Runnable() { - @Override public void run() { - JOptionPane.showMessageDialog(null, error, "Lombok Disabled", JOptionPane.ERROR_MESSAGE); - } - }); - } - - return null; - } - }); - } - } - } - private static class EclipsePatcherInfo extends AgentInfo { @Override String className() { return "lombok.eclipse.agent.EclipsePatcher"; diff --git a/src/core/lombok/core/Main.java b/src/core/lombok/core/Main.java index d62fe3e4..0856d3b3 100644 --- a/src/core/lombok/core/Main.java +++ b/src/core/lombok/core/Main.java @@ -38,6 +38,7 @@ public class Main { )); public static void main(String[] args) throws IOException { + Thread.currentThread().setContextClassLoader(Main.class.getClassLoader()); int err = new Main(SpiLoadUtil.readAllFromIterator( SpiLoadUtil.findServices(LombokApp.class)), Arrays.asList(args)).go(); System.exit(err); diff --git a/src/core/lombok/core/PostCompiler.java b/src/core/lombok/core/PostCompiler.java index 11091cb8..237867cb 100644 --- a/src/core/lombok/core/PostCompiler.java +++ b/src/core/lombok/core/PostCompiler.java @@ -24,6 +24,8 @@ package lombok.core; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.Collections; import java.util.List; @@ -55,10 +57,12 @@ public final class PostCompiler { transformations = SpiLoadUtil.readAllFromIterator(SpiLoadUtil.findServices(PostCompilerTransformation.class, PostCompilerTransformation.class.getClassLoader())); } catch (IOException e) { transformations = Collections.emptyList(); - diagnostics.addWarning("Could not load post-compile transformers: " + e.getMessage()); + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw, true)); + diagnostics.addWarning("Could not load post-compile transformers: " + e.getMessage() + "\n" + sw.toString()); } } - + public static OutputStream wrapOutputStream(final OutputStream originalStream, final String fileName, final DiagnosticsReceiver diagnostics) throws IOException { if (System.getProperty("lombok.disablePostCompiler", null) != null) return originalStream; return new ByteArrayOutputStream() { diff --git a/src/core/lombok/core/Version.java b/src/core/lombok/core/Version.java index 9f810525..02bc1af0 100644 --- a/src/core/lombok/core/Version.java +++ b/src/core/lombok/core/Version.java @@ -30,7 +30,7 @@ public class Version { // ** CAREFUL ** - this class must always compile with 0 dependencies (it must not refer to any other sources or libraries). // Note: In 'X.Y.Z', if Z is odd, its a snapshot build built from the repository, so many different 0.10.3 versions can exist, for example. // Official builds always end in an even number. (Since 0.10.2). - private static final String VERSION = "1.14.9"; + private static final String VERSION = "1.14.9.shadow"; private static final String RELEASE_NAME = "Edgy Guinea Pig"; // private static final String RELEASE_NAME = "Branching Cobra"; diff --git a/src/core/lombok/eclipse/handlers/HandlePrintAST.java b/src/core/lombok/eclipse/handlers/HandlePrintAST.java index 0b61bc4d..234e29b8 100644 --- a/src/core/lombok/eclipse/handlers/HandlePrintAST.java +++ b/src/core/lombok/eclipse/handlers/HandlePrintAST.java @@ -59,7 +59,7 @@ public class HandlePrintAST extends EclipseAnnotationHandler<PrintAST> { try { stream.close(); } catch (Exception e) { - Lombok.sneakyThrow(e); + throw Lombok.sneakyThrow(e); } } } diff --git a/src/core/lombok/javac/handlers/HandlePrintAST.java b/src/core/lombok/javac/handlers/HandlePrintAST.java index 9a52b9d9..0826d1d1 100644 --- a/src/core/lombok/javac/handlers/HandlePrintAST.java +++ b/src/core/lombok/javac/handlers/HandlePrintAST.java @@ -59,7 +59,7 @@ public class HandlePrintAST extends JavacAnnotationHandler<PrintAST> { try { stream.close(); } catch (Exception e) { - Lombok.sneakyThrow(e); + throw Lombok.sneakyThrow(e); } } } diff --git a/src/delombok/lombok/delombok/DelombokApp.java b/src/delombok/lombok/delombok/DelombokApp.java index 276bd7de..aa753fc8 100644 --- a/src/delombok/lombok/delombok/DelombokApp.java +++ b/src/delombok/lombok/delombok/DelombokApp.java @@ -88,7 +88,7 @@ public class DelombokApp extends LombokApp { // Since we only read from it, not closing it should not be a problem. @SuppressWarnings({"resource", "all"}) final JarFile toolsJarFile = new JarFile(toolsJar); - ClassLoader loader = new ClassLoader() { + ClassLoader loader = new ClassLoader(DelombokApp.class.getClassLoader()) { private Class<?> loadStreamAsClass(String name, boolean resolve, InputStream in) throws ClassNotFoundException { try { try { @@ -107,16 +107,24 @@ public class DelombokApp extends LombokApp { } finally { in.close(); } - } catch (IOException e2) { + } catch (Exception e2) { throw new ClassNotFoundException(name, e2); } } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { - String rawName = name.replace(".", "/") + ".class"; + String rawName, altName; { + String binName = name.replace(".", "/"); + rawName = binName + ".class"; + altName = binName + ".SCL.lombok"; + } JarEntry entry = toolsJarFile.getJarEntry(rawName); if (entry == null) { - if (name.startsWith("lombok.")) return loadStreamAsClass(name, resolve, super.getResourceAsStream(rawName)); + if (name.startsWith("lombok.")) { + InputStream res = getParent().getResourceAsStream(rawName); + if (res == null) res = getParent().getResourceAsStream(altName); + return loadStreamAsClass(name, resolve, res); + } return super.loadClass(name, resolve); } diff --git a/src/eclipseAgent/lombok/eclipse/agent/EclipseLoaderPatcher.java b/src/eclipseAgent/lombok/eclipse/agent/EclipseLoaderPatcher.java new file mode 100644 index 00000000..0d21c212 --- /dev/null +++ b/src/eclipseAgent/lombok/eclipse/agent/EclipseLoaderPatcher.java @@ -0,0 +1,106 @@ +package lombok.eclipse.agent; + +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import lombok.patcher.ClassRootFinder; +import lombok.patcher.Hook; +import lombok.patcher.MethodTarget; +import lombok.patcher.ScriptManager; +import lombok.patcher.StackRequest; +import lombok.patcher.scripts.ScriptBuilder; + +public class EclipseLoaderPatcher { + public static boolean overrideLoadDecide(ClassLoader original, String name, boolean resolve) { + return name.startsWith("lombok."); + } + + public static Class<?> overrideLoadResult(ClassLoader original, String name, boolean resolve) throws ClassNotFoundException { + try { + Field shadowLoaderField = original.getClass().getField("lombok$shadowLoader"); + ClassLoader shadowLoader = (ClassLoader) shadowLoaderField.get(original); + if (shadowLoader == null) { + String jarLoc = (String) original.getClass().getField("lombok$location").get(null); + JarFile jf = new JarFile(jarLoc); + InputStream in = null; + try { + ZipEntry entry = jf.getEntry("lombok/launch/ShadowClassLoader.class"); + in = jf.getInputStream(entry); + byte[] bytes = new byte[65536]; + int len = 0; + while (true) { + int r = in.read(bytes, len, bytes.length - len); + if (r == -1) break; + len += r; + if (len == bytes.length) throw new IllegalStateException("lombok.launch.ShadowClassLoader too large."); + } + in.close(); + Class<?> shadowClassLoaderClass; { + Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); + defineClassMethod.setAccessible(true); + shadowClassLoaderClass = (Class<?>) defineClassMethod.invoke(original, "lombok.launch.ShadowClassLoader", bytes, 0, len); + } + Constructor<?> constructor = shadowClassLoaderClass.getDeclaredConstructor(ClassLoader.class, String.class, String.class, String[].class); + constructor.setAccessible(true); + shadowLoader = (ClassLoader) constructor.newInstance(original, "lombok", jarLoc, new String[] {"lombok."}); + shadowLoaderField.set(original, shadowLoader); + } finally { + if (in != null) in.close(); + jf.close(); + } + } + + if (resolve) { + Method m = shadowLoader.getClass().getDeclaredMethod("loadClass", String.class, boolean.class); + m.setAccessible(true); + return (Class<?>) m.invoke(shadowLoader, name, true); + } else { + return shadowLoader.loadClass(name); + } + } catch (Exception ex) { + Throwable t = ex; + if (t instanceof InvocationTargetException) t = t.getCause(); + if (t instanceof RuntimeException) throw (RuntimeException) t; + if (t instanceof Error) throw (Error) t; + throw new RuntimeException(t); + } + } + + private static final String SELF_NAME = "lombok.eclipse.agent.EclipseLoaderPatcher"; + + public static void patchEquinoxLoaders(ScriptManager sm, Class<?> launchingContext) { + sm.addScript(ScriptBuilder.exitEarly() + .target(new MethodTarget("org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader", "loadClass", + "java.lang.Class", "java.lang.String", "boolean")) + .target(new MethodTarget("org.eclipse.osgi.framework.adapter.core.AbstractClassLoader", "loadClass", + "java.lang.Class", "java.lang.String", "boolean")) + .target(new MethodTarget("org.eclipse.osgi.internal.loader.ModuleClassLoader", "loadClass", + "java.lang.Class", "java.lang.String", "boolean")) + .decisionMethod(new Hook(SELF_NAME, "overrideLoadDecide", "boolean", "java.lang.ClassLoader", "java.lang.String", "boolean")) + .valueMethod(new Hook(SELF_NAME, "overrideLoadResult", "java.lang.Class", "java.lang.ClassLoader", "java.lang.String", "boolean")) + .transplant() + .request(StackRequest.THIS, StackRequest.PARAM1, StackRequest.PARAM2).build()); + + sm.addScript(ScriptBuilder.addField().setPublic() + .fieldType("Ljava/lang/ClassLoader;") + .fieldName("lombok$shadowLoader") + .targetClass("org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader") + .targetClass("org.eclipse.osgi.framework.adapter.core.AbstractClassLoader") + .targetClass("org.eclipse.osgi.internal.loader.ModuleClassLoader") + .build()); + + sm.addScript(ScriptBuilder.addField().setPublic().setStatic().setFinal() + .fieldType("Ljava/lang/String;") + .fieldName("lombok$location") + .targetClass("org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader") + .targetClass("org.eclipse.osgi.framework.adapter.core.AbstractClassLoader") + .targetClass("org.eclipse.osgi.internal.loader.ModuleClassLoader") + .value(ClassRootFinder.findClassRootOfClass(launchingContext)) + .build()); + } +} diff --git a/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java b/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java index e14d1367..96f253f2 100644 --- a/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java +++ b/src/eclipseAgent/lombok/eclipse/agent/EclipsePatcher.java @@ -28,13 +28,13 @@ import java.util.Collection; import java.util.Collections; import java.util.List; -import lombok.core.Agent; +import lombok.core.AgentLauncher; + import lombok.patcher.Hook; import lombok.patcher.MethodTarget; import lombok.patcher.ScriptManager; import lombok.patcher.StackRequest; import lombok.patcher.TargetMatcher; -import lombok.patcher.equinox.EquinoxClassLoader; import lombok.patcher.scripts.ScriptBuilder; /** @@ -44,13 +44,12 @@ import lombok.patcher.scripts.ScriptBuilder; * classes in this package for more information about which classes are transformed and how they are * transformed. */ -public class EclipsePatcher extends Agent { +public class EclipsePatcher implements AgentLauncher.AgentLaunchable { // At some point I'd like the agent to be capable of auto-detecting if its on eclipse or on ecj. This class is a sure sign we're not in ecj but in eclipse. -ReinierZ @SuppressWarnings("unused") private static final String ECLIPSE_SIGNATURE_CLASS = "org/eclipse/core/runtime/adaptor/EclipseStarter"; - @Override - public void runAgent(String agentArgs, Instrumentation instrumentation, boolean injected) throws Exception { + @Override public void runAgent(String agentArgs, Instrumentation instrumentation, boolean injected, Class<?> launchingContext) throws Exception { String[] args = agentArgs == null ? new String[0] : agentArgs.split(":"); boolean forceEcj = false; boolean forceEclipse = false; @@ -69,15 +68,14 @@ public class EclipsePatcher extends Agent { else if (forceEclipse) ecj = false; else ecj = injected; - registerPatchScripts(instrumentation, injected, ecj); + registerPatchScripts(instrumentation, injected, ecj, launchingContext); } - private static void registerPatchScripts(Instrumentation instrumentation, boolean reloadExistingClasses, boolean ecjOnly) { + private static void registerPatchScripts(Instrumentation instrumentation, boolean reloadExistingClasses, boolean ecjOnly, Class<?> launchingContext) { ScriptManager sm = new ScriptManager(); sm.registerTransformer(instrumentation); if (!ecjOnly) { - EquinoxClassLoader.addPrefix("lombok."); - EquinoxClassLoader.registerScripts(sm); + EclipseLoaderPatcher.patchEquinoxLoaders(sm, launchingContext); } if (!ecjOnly) { diff --git a/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java b/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java index 8eec27fb..44adb333 100644 --- a/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java +++ b/src/eclipseAgent/lombok/eclipse/agent/PatchExtensionMethod.java @@ -224,7 +224,8 @@ public class PatchExtensionMethod { if (methodCall.arguments != null) arguments.addAll(Arrays.asList(methodCall.arguments)); List<TypeBinding> argumentTypes = new ArrayList<TypeBinding>(); for (Expression argument : arguments) { - argumentTypes.add(argument.resolvedType); + if (argument.resolvedType != null) argumentTypes.add(argument.resolvedType); + // TODO: Instead of just skipping nulls entirely, there is probably a 'unresolved type' placeholder. THAT is what we ought to be adding here! } MethodBinding fixedBinding = scope.getMethod(extensionMethod.declaringClass, methodCall.selector, argumentTypes.toArray(new TypeBinding[0]), methodCall); if (fixedBinding instanceof ProblemMethodBinding) { diff --git a/src/installer/lombok/installer/InstallerGUI.java b/src/installer/lombok/installer/InstallerGUI.java index 5717948a..41832e5d 100644 --- a/src/installer/lombok/installer/InstallerGUI.java +++ b/src/installer/lombok/installer/InstallerGUI.java @@ -820,9 +820,8 @@ public class InstallerGUI { private static final String HOW_I_WORK_EXPLANATION = "<html><h2>Eclipse</h2><ol>" + "<li>First, I copy myself (lombok.jar) to your Eclipse install directory.</li>" + - "<li>Then, I edit the <i>eclipse.ini</i> file to add the following two entries:<br>" + - "<pre>-Xbootclasspath/a:lombok.jar<br>" + - "-javaagent:lombok.jar</pre></li></ol>" + + "<li>Then, I edit the <i>eclipse.ini</i> file to add the following entry:<br>" + + "<pre>-javaagent:lombok.jar</pre></li></ol>" + "On Mac OS X, eclipse.ini is hidden in<br>" + "<code>Eclipse.app/Contents/MacOS</code> so that's where I place the jar files.</html>"; diff --git a/src/installer/lombok/installer/eclipse/EclipseLocation.java b/src/installer/lombok/installer/eclipse/EclipseLocation.java index e347cd98..0fe60c05 100644 --- a/src/installer/lombok/installer/eclipse/EclipseLocation.java +++ b/src/installer/lombok/installer/eclipse/EclipseLocation.java @@ -337,8 +337,6 @@ public class EclipseLocation extends IdeLocation { newContents.append(String.format( "-javaagent:%s", escapePath(fullPathToLombok + "lombok.jar"))).append(OS_NEWLINE); - newContents.append(String.format( - "-Xbootclasspath/a:%s", escapePath(fullPathToLombok + "lombok.jar"))).append(OS_NEWLINE); FileOutputStream fos = new FileOutputStream(eclipseIniPath); try { @@ -360,8 +358,7 @@ public class EclipseLocation extends IdeLocation { } return "If you start " + getTypeName() + " with a custom -vm parameter, you'll need to add:<br>" + - "<code>-vmargs -Xbootclasspath/a:lombok.jar -javaagent:lombok.jar</code><br>" + - "as parameter as well."; + "<code>-vmargs -javaagent:lombok.jar</code><br>as parameter as well."; } @Override public URL getIdeIcon() { diff --git a/src/launch/lombok/launch/Agent.java b/src/launch/lombok/launch/Agent.java new file mode 100644 index 00000000..7989e51f --- /dev/null +++ b/src/launch/lombok/launch/Agent.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 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.launch; + +import java.lang.instrument.Instrumentation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +final class Agent { + public static void agentmain(String agentArgs, Instrumentation instrumentation) throws Throwable { + runLauncher(agentArgs, instrumentation, true); + } + + public static void premain(String agentArgs, Instrumentation instrumentation) throws Throwable { + runLauncher(agentArgs, instrumentation, false); + } + + private static void runLauncher(String agentArgs, Instrumentation instrumentation, boolean injected) throws Throwable { + ClassLoader cl = Main.createShadowClassLoader(); + try { + Class<?> c = cl.loadClass("lombok.core.AgentLauncher"); + Method m = c.getDeclaredMethod("runAgents", String.class, Instrumentation.class, boolean.class, Class.class); + m.invoke(null, agentArgs, instrumentation, injected, Agent.class); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/src/launch/lombok/launch/AnnotationProcessor.java b/src/launch/lombok/launch/AnnotationProcessor.java new file mode 100644 index 00000000..35c26b7c --- /dev/null +++ b/src/launch/lombok/launch/AnnotationProcessor.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 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.launch; + +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Completion; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +class AnnotationProcessorHider { + public static class AnnotationProcessor extends AbstractProcessor { + private final AbstractProcessor instance = createWrappedInstance(); + + @Override public Set<String> getSupportedOptions() { + return instance.getSupportedOptions(); + } + + @Override public Set<String> getSupportedAnnotationTypes() { + return instance.getSupportedAnnotationTypes(); + } + + @Override public SourceVersion getSupportedSourceVersion() { + return instance.getSupportedSourceVersion(); + } + + @Override public void init(ProcessingEnvironment processingEnv) { + instance.init(processingEnv); + super.init(processingEnv); + } + + @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + return instance.process(annotations, roundEnv); + } + + @Override public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) { + return instance.getCompletions(element, annotation, member, userText); + } + + private static AbstractProcessor createWrappedInstance() { + ClassLoader cl = Main.createShadowClassLoader(); + try { + Class<?> mc = cl.loadClass("lombok.core.AnnotationProcessor"); + return (AbstractProcessor) mc.newInstance(); + } catch (Throwable t) { + if (t instanceof Error) throw (Error) t; + if (t instanceof RuntimeException) throw (RuntimeException) t; + throw new RuntimeException(t); + } + } + } +} diff --git a/src/launch/lombok/launch/Main.java b/src/launch/lombok/launch/Main.java new file mode 100644 index 00000000..63d97d48 --- /dev/null +++ b/src/launch/lombok/launch/Main.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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.launch; + +import java.lang.reflect.InvocationTargetException; + +class Main { + static ClassLoader createShadowClassLoader() { + return new ShadowClassLoader(Main.class.getClassLoader(), "lombok"); + } + + public static void main(String[] args) throws Throwable { + ClassLoader cl = createShadowClassLoader(); + Class<?> mc = cl.loadClass("lombok.core.Main"); + try { + mc.getMethod("main", String[].class).invoke(null, new Object[] {args}); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/src/launch/lombok/launch/ShadowClassLoader.java b/src/launch/lombok/launch/ShadowClassLoader.java new file mode 100644 index 00000000..79809b2f --- /dev/null +++ b/src/launch/lombok/launch/ShadowClassLoader.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2014 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.launch; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; +import java.util.WeakHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * The shadow classloader serves to completely hide almost all classes in a given jar file by using a different file ending. + * + * The shadow classloader also serves to link in a project as it is being developed (a 'bin' dir from an IDE for example). + * <p> + * Classes loaded by the shadowloader use ".SCL.<em>sclSuffix</em>" in addition to ".class". In other words, most of the class files in a given jar end in this suffix, which + * serves to hide them from any tool that isn't aware of the suffix (such as IDEs generating auto-complete dialogs, and javac's classpath in general). Only shadowloader can actually + * load these classes. + * <p> + * The shadowloader will pick up an alternate (priority) classpath, using normal class files, from the system property "<code>shadow.override.<em>sclSuffix</em></code>". + * This shadow classpath looks just like a normal java classpath; the path separator is applied (semi-colon on windows, colon elsewhere), and entries can consist of directories, + * jar files, or directories ending in "/*" to pick up all jars inside it. + * <p> + * Load order is as follows if at least one override is present: + * <li>First, if the resource is found in one of the paths stated in the shadow classpath, find that. + * <li>Next, ask the <code>parent</code> loader, which is passed during construction of the ShadowClassLoader. + * <li>Notably, this jar's contents are always skipped! (The idea of the shadow classpath is that this jar only functions as a launcher, not as a source of your actual application). + * </ul> + * + * If no overrides are present, the load order is as follows: + * <li>First, if the resource is found in our own jar (trying ".SCL.<em>sclSuffix</em>" first for any resource request ending in ".class"), return that. + * <li>Next, ask the <code>parent</code> loader. + * </ul> + * + * Use ShadowClassLoader to accomplish the following things:<ul> + * <li>Avoid contaminating the namespace of any project using an SCL-based jar. Autocompleters in IDEs will NOT suggest anything other than actual public API. + * <li>Like jarjar, allows folding in dependencies such as ASM without foisting these dependencies on projects that use this jar. shadowloader obviates the need for jarjar. + * <li>Allows an agent (which MUST be in jar form) to still load everything except this loader infrastructure from class files generated by the IDE, which should + * considerably help debugging, as you can now rely on the IDE's built-in auto-recompile features instead of having to run a full build everytime, and it should help + * with hot code replace and the like (this is what the {@code shadow.override} feature is for). + * </ul> + * + * Implementation note: {@code lombok.eclipse.agent.EclipseLoaderPatcher} <em>relies</em> on this class having no dependencies on any other class except the JVM boot class, notably + * including any other classes in this package, <strong>including</strong> inner classes. So, don't write closures, anonymous inner class literals, + * enums, or anything else that could cause the compilation of this file to produce more than 1 class file. In general, actually passing load control to this loader is a bit tricky + * so ensure that this class has zero dependencies on anything except java core classes. + */ +class ShadowClassLoader extends ClassLoader { + private static final String SELF_NAME = "lombok/launch/ShadowClassLoader.class"; + private final String SELF_BASE; + private final File SELF_BASE_FILE; + private final int SELF_BASE_LENGTH; + + private final List<File> override = new ArrayList<File>(); + private final String sclSuffix; + private final List<String> parentExclusion = new ArrayList<String>(); + + /** + * Calls the {@link ShadowClassLoader(ClassLoader, String, String, String[]) constructor with no exclusions and the source of this class as base. + */ + ShadowClassLoader(ClassLoader source, String sclSuffix) { + this(source, sclSuffix, null); + } + + /** + * @param source The 'parent' classloader. + * @param sclSuffix The suffix of the shadowed class files in our own jar. For example, if this is {@code lombok}, then the class files in your jar should be {@code foo/Bar.SCL.lombok} and not {@code foo/Bar.class}. + * @param selfBase The (preferably absolute) path to our own jar. This jar will be searched for class/SCL.sclSuffix files. + * @param parentExclusion For example {@code "lombok."}; upon invocation of loadClass of this loader, the parent loader ({@code source}) will NOT be invoked if the class to be loaded begins with anything in the parent exclusion list. No exclusion is applied for getResource(s). + */ + ShadowClassLoader(ClassLoader source, String sclSuffix, String selfBase, String... parentExclusion) { + super(source); + this.sclSuffix = sclSuffix; + if (parentExclusion != null) for (String pe : parentExclusion) { + pe = pe.replace(".", "/"); + if (!pe.endsWith("/")) pe = pe + "/"; + this.parentExclusion.add(pe); + } + + if (selfBase != null) { + SELF_BASE = selfBase; + SELF_BASE_LENGTH = selfBase.length(); + } else { + String sclClassUrl = ShadowClassLoader.class.getResource("ShadowClassLoader.class").toString(); + if (!sclClassUrl.endsWith(SELF_NAME)) throw new InternalError("ShadowLoader can't find itself."); + SELF_BASE_LENGTH = sclClassUrl.length() - SELF_NAME.length(); + SELF_BASE = sclClassUrl.substring(0, SELF_BASE_LENGTH); + } + + if (SELF_BASE.startsWith("jar:file:") && SELF_BASE.endsWith("!/")) SELF_BASE_FILE = new File(SELF_BASE.substring(9, SELF_BASE.length() - 2)); + else if (SELF_BASE.startsWith("file:")) SELF_BASE_FILE = new File(SELF_BASE.substring(5)); + else SELF_BASE_FILE = new File(SELF_BASE); + String scl = System.getProperty("shadow.override." + sclSuffix); + if (scl != null && !scl.isEmpty()) { + for (String part : scl.split("\\s*" + (File.pathSeparatorChar == ';' ? ";" : ":") + "\\s*")) { + if (part.endsWith("/*") || part.endsWith(File.separator + "*")) { + addOverrideJarDir(part.substring(0, part.length() - 2)); + } else { + addOverrideClasspathEntry(part); + } + } + } + } + + private static final String EMPTY_MARKER = new String("--EMPTY JAR--"); + private Map<String, Object> jarContentsCacheTrackers = new HashMap<String, Object>(); + private static WeakHashMap<Object, String> trackerCache = new WeakHashMap<Object, String>(); + private static WeakHashMap<Object, List<String>> jarContentsCache = new WeakHashMap<Object, List<String>>(); + + /** + * This cache ensures that any given jar file is only opened once in order to determine the full contents of it. + * We use 'trackers' to make sure that the bulk of the memory taken up by this cache (the list of strings representing the content of a jar file) + * gets garbage collected if all ShadowClassLoaders that ever tried to request a listing of this jar file, are garbage collected. + */ + private List<String> getOrMakeJarListing(String absolutePathToJar) { + List<String> list = retrieveFromCache(absolutePathToJar); + synchronized (list) { + if (list.isEmpty()) { + try { + JarFile jf = new JarFile(absolutePathToJar); + try { + Enumeration<JarEntry> entries = jf.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + if (!jarEntry.isDirectory()) list.add(jarEntry.getName()); + } + } finally { + jf.close(); + } + } catch (Exception ignore) {} + if (list.isEmpty()) list.add(EMPTY_MARKER); + } + } + + if (list.size() == 1 && list.get(0) == EMPTY_MARKER) return Collections.emptyList(); + return list; + } + + private List<String> retrieveFromCache(String absolutePathToJar) { + synchronized (trackerCache) { + Object tracker = jarContentsCacheTrackers.get(absolutePathToJar); + if (tracker != null) return jarContentsCache.get(tracker); + + for (Map.Entry<Object, String> entry : trackerCache.entrySet()) { + if (entry.getValue().equals(absolutePathToJar)) { + tracker = entry.getKey(); + break; + } + } + List<String> result = null; + if (tracker != null) result = jarContentsCache.get(tracker); + if (result != null) return result; + + tracker = new Object(); + List<String> list = new ArrayList<String>(); + jarContentsCache.put(tracker, list); + trackerCache.put(tracker, absolutePathToJar); + jarContentsCacheTrackers.put(absolutePathToJar, tracker); + return list; + } + } + + /** + * Looks up {@code altName} in {@code location}, and if that isn't found, looks up {@code name}; {@code altName} can be null in which case it is skipped. + */ + private URL getResourceFromLocation(String name, String altName, File location) { + if (location.isDirectory()) { + try { + if (altName != null) { + File f = new File(location, altName); + if (f.isFile() && f.canRead()) return f.toURI().toURL(); + } + + File f = new File(location, name); + if (f.isFile() && f.canRead()) return f.toURI().toURL(); + return null; + } catch (MalformedURLException e) { + return null; + } + } + + if (!location.isFile() || !location.canRead()) return null; + + String absolutePath; { + try { + absolutePath = location.getCanonicalPath(); + } catch (Exception e) { + absolutePath = location.getAbsolutePath(); + } + } + List<String> jarContents = getOrMakeJarListing(absolutePath); + + try { + if (jarContents.contains(altName)) { + return new URI("jar:file:" + absolutePath + "!/" + altName).toURL(); + } + } catch (Exception e) {} + + try { + if (jarContents.contains(name)) { + return new URI("jar:file:" + absolutePath + "!/" + name).toURL(); + } + } catch(Exception e) {} + + return null; + } + + /** + * Checks if the stated item is located inside the same classpath root as the jar that hosts ShadowClassLoader.class. {@code item} and {@code name} refer to the same thing. + */ + private boolean inOwnBase(URL item, String name) { + if (item == null) return false; + String itemString = item.toString(); + return (itemString.length() == SELF_BASE_LENGTH + name.length()) && SELF_BASE.regionMatches(0, itemString, 0, SELF_BASE_LENGTH); + } + + @Override public Enumeration<URL> getResources(String name) throws IOException { + String altName = null; + if (name.endsWith(".class")) altName = name.substring(0, name.length() - 6) + ".SCL." + sclSuffix; + + // Vector? Yes, we need one: + // * We can NOT make inner classes here (this class is loaded with special voodoo magic in eclipse, as a one off, it's not a full loader. + // * We need to return an enumeration. + // * We can't make one on the fly. + // * ArrayList can't make these. + Vector<URL> vector = new Vector<URL>(); + + for (File ce : override) { + URL url = getResourceFromLocation(name, altName, ce); + if (url != null) vector.add(url); + } + + if (override.isEmpty()) { + URL fromSelf = getResourceFromLocation(name, altName, SELF_BASE_FILE); + if (fromSelf != null) vector.add(fromSelf); + } + + Enumeration<URL> sec = super.getResources(name); + while (sec.hasMoreElements()) { + URL item = sec.nextElement(); + if (!inOwnBase(item, name)) vector.add(item); + } + + if (altName != null) { + Enumeration<URL> tern = super.getResources(altName); + while (tern.hasMoreElements()) { + URL item = tern.nextElement(); + if (!inOwnBase(item, altName)) vector.add(item); + } + } + + return vector.elements(); + } + + @Override public URL getResource(String name) { + return getResource_(name, false); + } + + private URL getResource_(String name, boolean noSuper) { + String altName = null; + if (name.endsWith(".class")) altName = name.substring(0, name.length() - 6) + ".SCL." + sclSuffix; + for (File ce : override) { + URL url = getResourceFromLocation(name, altName, ce); + if (url != null) return url; + } + + if (!override.isEmpty()) { + if (noSuper) return null; + if (altName != null) { + try { + URL res = getResourceSkippingSelf(altName); + if (res != null) return res; + } catch (IOException ignore) {} + } + + try { + return getResourceSkippingSelf(name); + } catch (IOException e) { + return null; + } + } + + URL url = getResourceFromLocation(name, altName, SELF_BASE_FILE); + if (url != null) return url; + + if (altName != null) { + URL res = super.getResource(altName); + if (res != null && (!noSuper || inOwnBase(res, altName))) return res; + } + + URL res = super.getResource(name); + if (res != null && (!noSuper || inOwnBase(res, name))) return res; + return null; + } + + private boolean exclusionListMatch(String name) { + for (String pe : parentExclusion) { + if (name.startsWith(pe)) return true; + } + return false; + } + + private URL getResourceSkippingSelf(String name) throws IOException { + URL candidate = super.getResource(name); + if (candidate == null) return null; + if (!inOwnBase(candidate, name)) return candidate; + + Enumeration<URL> en = super.getResources(name); + while (en.hasMoreElements()) { + candidate = en.nextElement(); + if (!inOwnBase(candidate, name)) return candidate; + } + + return null; + } + + @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + { + Class<?> alreadyLoaded = findLoadedClass(name); + if (alreadyLoaded != null) return alreadyLoaded; + } + + String fileNameOfClass = name.replace(".", "/") + ".class"; + URL res = getResource_(fileNameOfClass, true); + if (res == null) { + if (!exclusionListMatch(fileNameOfClass)) return super.loadClass(name, resolve); + throw new ClassNotFoundException(name); + } + + byte[] b; + int p = 0; + try { + InputStream in = res.openStream(); + + try { + b = new byte[65536]; + while (true) { + int r = in.read(b, p, b.length - p); + if (r == -1) break; + p += r; + if (p == b.length) { + byte[] nb = new byte[b.length * 2]; + System.arraycopy(b, 0, nb, 0, p); + b = nb; + } + } + } finally { + in.close(); + } + } catch (IOException e) { + throw new ClassNotFoundException("I/O exception reading class " + name, e); + } + + Class<?> c = defineClass(name, b, 0, p); + if (resolve) resolveClass(c); + return c; + } + + public void addOverrideJarDir(String dir) { + File f = new File(dir); + for (File j : f.listFiles()) { + if (j.getName().toLowerCase().endsWith(".jar") && j.canRead() && j.isFile()) override.add(j); + } + } + + public void addOverrideClasspathEntry(String entry) { + override.add(new File(entry)); + } +} |