--- java/org/apache/catalina/loader/LocalStrings.properties (revision 1522674) +++ java/org/apache/catalina/loader/LocalStrings.properties (working copy) @@ -38,6 +38,11 @@ webappClassLoader.warnThread=The web application [{0}] appears to have started a thread named [{1}] but has failed to stop it. This is very likely to create a memory leak. webappClassLoader.warnTimerThread=The web application [{0}] appears to have started a TimerThread named [{1}] via the java.util.Timer API but has failed to stop it. To prevent a memory leak, the timer (and hence the associated thread) has been forcibly canceled. webappClassLoader.wrongVersion=(unable to load class {0}) +webappClassLoader.addTransformer.illegalArgument=The web application {0} attempted to add a null class file transformer. +webappClassLoader.addTransformer.duplicate=Duplicate call to add class file transformer {0} to web application {1} ignored. +webappClassLoader.addTransformer=Added class file transformer {0} to web application {1}. +webappClassLoader.removeTransformer=Removed class file transformer {0} from web application {1}. +webappClassLoader.transformError=Instrumentation error: could not transform class {0} because it its class file format is not legal. webappLoader.addRepository=Adding repository {0} webappLoader.deploy=Deploying class repositories to work directory {0} webappLoader.jarDeploy=Deploy JAR {0} to {1} --- java/org/apache/catalina/loader/WebappClassLoader.java (revision 1522674) +++ java/org/apache/catalina/loader/WebappClassLoader.java (working copy) @@ -22,6 +22,8 @@ import java.io.FilePermission; import java.io.IOException; import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.lang.reflect.Field; @@ -47,11 +49,15 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.JarEntry; @@ -65,6 +71,7 @@ import org.apache.catalina.LifecycleState; import org.apache.catalina.WebResource; import org.apache.catalina.WebResourceRoot; +import org.apache.tomcat.InstrumentableClassLoader; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.IntrospectionUtils; import org.apache.tomcat.util.res.StringManager; @@ -104,6 +111,12 @@ * IMPLEMENTATION NOTE - No check for sealing violations or * security is made unless a security manager is present. *

+ * IMPLEMENTATION NOTE - As of 8.0 and 7.0.43, this class + * loader implements {@link InstrumentableClassLoader}, permitting web + * application classes to instrument other classes in the same web + * application. It does not permit instrumentation of system or container + * classes or classes in other web apps. + *

* TODO: Is there any requirement to provide a proper Lifecycle implementation * rather than the current stubbed implementation? * @@ -111,10 +124,8 @@ * @author Craig R. McClanahan * @version $Id$ */ -public class WebappClassLoader - extends URLClassLoader - implements Lifecycle - { +public class WebappClassLoader extends URLClassLoader + implements Lifecycle, InstrumentableClassLoader { private static final org.apache.juli.logging.Log log= org.apache.juli.logging.LogFactory.getLog( WebappClassLoader.class ); @@ -127,6 +138,8 @@ private static final String JVN_THREAD_GROUP_SYSTEM = "system"; + private static final String CLASS_FILE_SUFFIX = ".class"; + static { JVM_THREAD_GROUP_NAMES.add(JVN_THREAD_GROUP_SYSTEM); JVM_THREAD_GROUP_NAMES.add("RMI Runtime"); @@ -462,7 +475,23 @@ */ private boolean clearReferencesHttpClientKeepAliveThread = true; + /** + * Holds the class file transformers decorating this class loader. The + * LinkedList implementation has faster inserts than an ArrayList. + */ + private final LinkedList transformers = new LinkedList<>(); + /** + * These locks control access to the transformers list in a way that + * performs better than simple synchronization. Multiple threads will be + * able to read the list at the same time, but only one may update the list + * at a time, and no other thread may read the list while it's being updated. + */ + private final ReadWriteLock transformersMutex = new ReentrantReadWriteLock(); + private final Lock transformersReadLock = transformersMutex.readLock(); + private final Lock transformersWriteLock = transformersMutex.writeLock(); + + // ------------------------------------------------------------- Properties /** @@ -736,8 +765,119 @@ // ------------------------------------------------------- Reloader Methods + /** + * Adds the specified class file transformer to this class loader. The + * transformer will then be able to modify the bytecode of any classes + * loaded by this class loader after the invocation of this method. + * + * @param classFileTransformer The transformer to add to the class loader + */ + @Override + public void addTransformer(ClassFileTransformer transformer) { + if (transformer == null) { + throw new IllegalArgumentException(sm.getString( + "webappClassLoader.addTransformer.illegalArgument", getContextName())); + } + + this.transformersWriteLock.lock(); + try { + if (this.transformers.contains(transformer)) { + // if the same instance of this transformer was already added, bail out + log.warn(sm.getString("webappClassLoader.addTransformer.duplicate", + transformer, getContextName())); + return; + } + this.transformers.add(transformer); + } finally { + this.transformersWriteLock.unlock(); + } + + log.info(sm.getString("webappClassLoader.addTransformer", transformer, getContextName())); + + } + /** + * Removes the specified class file transformer from this class loader. + * It will no longer be able to modify the byte code of any classes + * loaded by the class loader after the invocation of this method. + * However, any classes already modified by this transformer will + * remain transformed. + * + * @param classFileTransformer The transformer to remove + */ + @Override + public void removeTransformer(ClassFileTransformer transformer) { + + if (transformer == null) { + return; + } + + this.transformersWriteLock.lock(); + try { + if (this.transformers.remove(transformer)) { + log.info(sm.getString("webappClassLoader.removeTransformer", + transformer, getContextName())); + return; + } + } finally { + this.transformersWriteLock.unlock(); + } + + } + + /** + * Returns a copy of this class loader without any class file + * transformers. This is a tool often used by Java Persistence API + * providers to inspect entity classes in the absence of any + * instrumentation, something that can't be guaranteed within the + * context of a {@link ClassFileTransformer}'s + * {@link ClassFileTransformer#transform() transform} method. + *

+ * The returned class loader's resource cache will have been cleared + * so that classes already instrumented will not be retained or + * returned. + * + * @return the transformer-free copy of this class loader. + */ + @Override + public WebappClassLoader copyWithoutTransformers() { + + WebappClassLoader loader = new WebappClassLoader(this.parent); + + loader.antiJARLocking = this.antiJARLocking; + loader.resources = this.resources; + loader.delegate = this.delegate; + loader.lastJarAccessed = this.lastJarAccessed; + loader.repository = this.repository; + loader.file = this.file; + loader.jarPath = this.jarPath; + loader.loaderDir = this.loaderDir; + loader.canonicalLoaderDir = this.canonicalLoaderDir; + loader.started = this.started; + loader.needConvert = this.needConvert; + loader.clearReferencesStatic = this.clearReferencesStatic; + loader.clearReferencesStopThreads = this.clearReferencesStopThreads; + loader.clearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads; + loader.clearReferencesLogFactoryRelease = this.clearReferencesLogFactoryRelease; + loader.clearReferencesHttpClientKeepAliveThread = this.clearReferencesHttpClientKeepAliveThread; + + loader.repositoryURLs = this.repositoryURLs.clone(); + loader.jarFiles = this.jarFiles.clone(); + loader.jarRealFiles = this.jarRealFiles.clone(); + loader.jarNames = this.jarNames.clone(); + loader.lastModifiedDates = this.lastModifiedDates.clone(); + loader.paths = this.paths.clone(); + + loader.notFoundResources.putAll(this.notFoundResources); + loader.permissionList.addAll(this.permissionList); + loader.loaderPC.putAll(this.loaderPC); + + return loader; + + } + + /** * Set the place this ClassLoader can look for classes to be loaded. * * @param repository Name of a source of classes to be loaded, such as a @@ -928,6 +1068,17 @@ sb.append(this.parent.toString()); sb.append("\r\n"); } + if (this.transformers.size() > 0) { + sb.append("----------> Class file transformers:\r\n"); + this.transformersReadLock.lock(); // prevent a concurrency exception in toString + try { + for (ClassFileTransformer transformer : this.transformers) { + sb.append(transformer.getClass().getName()).append("\r\n"); + } + } finally { + this.transformersReadLock.unlock(); + } + } return (sb.toString()); } @@ -1183,7 +1334,7 @@ try { String repository = entry.codeBase.toString(); if ((repository.endsWith(".jar")) - && (!(name.endsWith(".class")))) { + && (!(name.endsWith(CLASS_FILE_SUFFIX)))) { // Copy binary content to the work directory if not present File resourceFile = new File(loaderDir, name); url = getURI(resourceFile); @@ -2552,7 +2703,7 @@ throw new ClassNotFoundException(name); String tempPath = name.replace('.', '/'); - String classPath = tempPath + ".class"; + String classPath = tempPath + CLASS_FILE_SUFFIX; ResourceEntry entry = null; @@ -2669,7 +2820,7 @@ * * @return the loaded resource, or null if the resource isn't found */ - protected ResourceEntry findResourceInternal(String name, String path) { + protected ResourceEntry findResourceInternal(final String name, final String path) { if (!started) { log.info(sm.getString("webappClassLoader.stopped", name)); @@ -2685,7 +2836,7 @@ int contentLength = -1; InputStream binaryStream = null; - boolean isClassResource = path.endsWith(".class"); + boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX); int jarFilesLength = jarFiles.length; @@ -2782,7 +2933,7 @@ } // Extract resources contained in JAR to the workdir - if (antiJARLocking && !(path.endsWith(".class"))) { + if (antiJARLocking && !(path.endsWith(CLASS_FILE_SUFFIX))) { byte[] buf = new byte[1024]; File resourceFile = new File (loaderDir, jarEntry.getName()); @@ -2793,7 +2944,7 @@ JarEntry jarEntry2 = entries.nextElement(); if (!(jarEntry2.isDirectory()) && (!jarEntry2.getName().endsWith - (".class"))) { + (CLASS_FILE_SUFFIX))) { resourceFile = new File (loaderDir, jarEntry2.getName()); try { @@ -2923,6 +3074,34 @@ } } + if (isClassResource && entry != null && entry.binaryContent != null && + this.transformers.size() > 0) { + // If the resource is a class just being loaded, decorate it + // with any attached transformers + String className = name.endsWith(CLASS_FILE_SUFFIX) ? + name.substring(0, name.length() - CLASS_FILE_SUFFIX.length()) : name; + String internalName = className.replace(".", "/"); + + this.transformersReadLock.lock(); + try { + for (ClassFileTransformer transformer : this.transformers) { + try { + byte[] transformed = transformer.transform( + this, internalName, null, null, entry.binaryContent + ); + if (transformed != null) { + entry.binaryContent = transformed; + } + } catch (IllegalClassFormatException e) { + log.error(sm.getString("webappClassLoader.transformError", name), e); + return null; + } + } + } finally { + this.transformersReadLock.unlock(); + } + } + // Add the entry in the local resource repository synchronized (resourceEntries) { // Ensures that all the threads which may be in a race to load @@ -3126,7 +3305,7 @@ } if (clazz == null) continue; - String name = triggers[i].replace('.', '/') + ".class"; + String name = triggers[i].replace('.', '/') + CLASS_FILE_SUFFIX; if (log.isDebugEnabled()) log.debug(" Checking for " + name); JarEntry jarEntry = jarFile.getJarEntry(name); --- java/org/apache/tomcat/InstrumentableClassLoader.java (revision 0) +++ java/org/apache/tomcat/InstrumentableClassLoader.java (working copy) @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat; + +import java.lang.instrument.ClassFileTransformer; + +/** + * Specifies a class loader capable of being decorated with + * {@link ClassFileTransformer}s. These transformers can instrument + * (or weave) the byte code of classes loaded through this class loader + * to alter their behavior. Currently only + * {@link org.apache.catalina.loader.WebappClassLoader} implements this + * interface. This allows web application frameworks or JPA providers + * bundled with a web application to instrument web application classes + * as necessary. + *

+ * You should always program against the methods of this interface + * (whether using reflection or otherwise). The methods in + * {@code WebappClassLoader} are protected by the default security + * manager if one is in use. + * + * @since 8.0, 7.0.43 + */ +public interface InstrumentableClassLoader { + + /** + * Adds the specified class file transformer to this class loader. The + * transformer will then be able to instrument the bytecode of any + * classes loaded by this class loader after the invocation of this + * method. + * + * @param classFileTransformer The transformer to add to the class loader + * @throws IllegalArgumentException if the {@literal transformer} is null. + */ + void addTransformer(ClassFileTransformer transformer); + + /** + * Removes the specified class file transformer from this class loader. + * It will no longer be able to instrument the byte code of any classes + * loaded by the class loader after the invocation of this method. + * However, any classes already instrumented by this transformer before + * this method call will remain in their instramented state. + * + * @param classFileTransformer The transformer to remove + */ + void removeTransformer(ClassFileTransformer transformer); + + /** + * Returns a copy of this class loader without any class file + * transformers. This is a tool often used by Java Persistence API + * providers to inspect entity classes in the absence of any + * instrumentation, something that can't be guaranteed within the + * context of a {@link ClassFileTransformer}'s + * {@link ClassFileTransformer#transform() transform} method. + *

+ * The returned class loader's resource cache will have been cleared + * so that classes already instrumented will not be retained or + * returned. + * + * @return the transformer-free copy of this class loader. + */ + ClassLoader copyWithoutTransformers(); + +} --- test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java (revision 0) +++ test/org/apache/catalina/loader/TestWebappClassLoaderWeaving.java (working copy) @@ -0,0 +1,430 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.http.fileupload.FileUtils; + +public class TestWebappClassLoaderWeaving extends TomcatBaseTest { + + private static final String PACKAGE_PREFIX = "org/apache/catalina/loader"; + + private static String WEBAPP_DOC_BASE; + + @BeforeClass + public static void setUpClass() throws Exception { + + WEBAPP_DOC_BASE = System.getProperty("java.io.tmpdir") + "/TestWebappClassLoaderWeaving"; + File classes = new File(WEBAPP_DOC_BASE + "/WEB-INF/classes/" + PACKAGE_PREFIX); + classes.mkdirs(); + + copyResource(PACKAGE_PREFIX + "/TesterNeverWeavedClass.class", + new File(classes, "TesterNeverWeavedClass.class")); + copyResource(PACKAGE_PREFIX + "/TesterUnweavedClass.class", + new File(classes, "TesterUnweavedClass.class")); + + } + + @AfterClass + public static void tearDownClass() throws Exception { + + FileUtils.deleteDirectory(new File(WEBAPP_DOC_BASE)); + + } + + private Tomcat tomcat; + private Context context; + private WebappClassLoader loader; + + @Before + @Override + public void setUp() throws Exception { + + super.setUp(); + + this.tomcat = getTomcatInstance(); + this.context = this.tomcat.addContext("/weaving", WEBAPP_DOC_BASE); + this.tomcat.start(); + + ClassLoader loader = this.context.getLoader().getClassLoader(); + assertNotNull("The class loader should not be null.", loader); + assertSame("The class loader is not correct.", WebappClassLoader.class, loader.getClass()); + + this.loader = (WebappClassLoader) loader; + + } + + @After + @Override + public void tearDown() throws Exception { + + try { + this.loader = null; + + this.context.stop(); + this.tomcat.getHost().removeChild(this.context); + this.context = null; + } finally { + super.tearDown(); + } + + } + + @Test + public void testNoWeaving() throws Exception { + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testAddingNullTransformerThrowsException() throws Exception { + + try { + this.loader.addTransformer(null); + fail("Expected exception IllegalArgumentException, got no exception."); + } catch (IllegalArgumentException ignore) { + // good + } + + // class loading should still work, no weaving + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testAddedTransformerInstrumentsClass1() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testAddedTransformerInstrumentsClass2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testTransformersExecuteInOrderAdded1() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testTransformersExecuteInOrderAdded2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments1() throws Exception { + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_1); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Unweaved World!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments2() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_2); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #1!", result); + + } + + @Test + public void testRemovedTransformerNoLongerInstruments3() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + ReplacementTransformer removed = new ReplacementTransformer(WEAVED_REPLACEMENT_1); + this.loader.addTransformer(removed); + this.loader.removeTransformer(removed); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + } + + @Test + public void testCopiedClassLoaderExcludesResourcesAndTransformers() throws Exception { + + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_1)); + this.loader.addTransformer(new ReplacementTransformer(WEAVED_REPLACEMENT_2)); + + String result = invokeDoMethodOnClass(this.loader, "TesterNeverWeavedClass"); + assertEquals("The first result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(this.loader, "TesterUnweavedClass"); + assertEquals("The second result is not correct.", "Hello, Weaver #2!", result); + + WebappClassLoader copiedLoader = this.loader.copyWithoutTransformers(); + + result = invokeDoMethodOnClass(copiedLoader, "TesterNeverWeavedClass"); + assertEquals("The third result is not correct.", "This will never be weaved.", result); + + result = invokeDoMethodOnClass(copiedLoader, "TesterUnweavedClass"); + assertEquals("The fourth result is not correct.", "Hello, Unweaved World!", result); + + assertEquals("getAntiJARLocking did not match.", this.loader.getAntiJARLocking(), + copiedLoader.getAntiJARLocking()); + assertEquals("getClearReferencesHttpClientKeepAliveThread did not match.", + this.loader.getClearReferencesHttpClientKeepAliveThread(), + copiedLoader.getClearReferencesHttpClientKeepAliveThread()); + assertEquals("getClearReferencesLogFactoryRelease did not match.", + this.loader.getClearReferencesLogFactoryRelease(), + copiedLoader.getClearReferencesLogFactoryRelease()); + assertEquals("getClearReferencesStatic did not match.", + this.loader.getClearReferencesStatic(), + copiedLoader.getClearReferencesStatic()); + assertEquals("getClearReferencesStopThreads did not match.", + this.loader.getClearReferencesStopThreads(), + copiedLoader.getClearReferencesStopThreads()); + assertEquals("getClearReferencesStopTimerThreads did not match.", + this.loader.getClearReferencesStopTimerThreads(), + copiedLoader.getClearReferencesStopTimerThreads()); + assertEquals("getContextName did not match.", this.loader.getContextName(), + copiedLoader.getContextName()); + assertEquals("getDelegate did not match.", this.loader.getDelegate(), + copiedLoader.getDelegate()); + assertEquals("getJarPath did not match.", this.loader.getJarPath(), + copiedLoader.getJarPath()); + assertEquals("getURLs did not match.", this.loader.getURLs().length, + copiedLoader.getURLs().length); + assertSame("getParent did not match.", this.loader.getParent(), copiedLoader.getParent()); + + } + + private static void copyResource(String name, File file) throws Exception { + + InputStream is = TestWebappClassLoaderWeaving.class.getClassLoader() + .getResourceAsStream(name); + if (is == null) { + throw new IOException("Resource " + name + " not found on classpath."); + } + + FileOutputStream os = new FileOutputStream(file); + try { + for (int b = is.read(); b >= 0; b = is.read()) { + os.write(b); + } + } finally { + is.close(); + os.close(); + } + + } + + private static String invokeDoMethodOnClass(WebappClassLoader loader, String className) + throws Exception { + + Class c = loader.findClass("org.apache.catalina.loader." + className); + assertNotNull("The loaded class should not be null.", c); + + Method m = c.getMethod("doMethod"); + + Object o = c.newInstance(); + return (String) m.invoke(o); + + } + + private static class ReplacementTransformer implements ClassFileTransformer { + + private static final String CLASS_TO_WEAVE = PACKAGE_PREFIX + "/TesterUnweavedClass"; + + private final byte[] replacement; + + ReplacementTransformer(byte[] replacement) throws IOException { + + this.replacement = replacement; + + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class x, + ProtectionDomain y, byte[] b) { + + if (CLASS_TO_WEAVE.equals(className)) { + return this.replacement; + } + + return null; + + } + + } + + /** + * Compiled version of org.apache.catalina.loader.TesterUnweavedClass, except that + * the doMethod method returns "Hello, Weaver #1!". Compiled with Oracle Java 1.6.0_51. + */ + private static final byte[] WEAVED_REPLACEMENT_1 = new byte[] { + -54, -2, -70, -66, 0, 0, 0, 50, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, + 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, + 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 100, + 111, 77, 101, 116, 104, 111, 100, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, + 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, + 105, 108, 101, 1, 0, 24, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, 101, + 100, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 17, 72, 101, + 108, 108, 111, 44, 32, 87, 101, 97, 118, 101, 114, 32, 35, 49, 33, 1, 0, 46, 111, 114, + 103, 47, 97, 112, 97, 99, 104, 101, 47, 99, 97, 116, 97, 108, 105, 110, 97, 47, 108, + 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, + 101, 100, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, + 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, + 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, + 0, 6, 0, 1, 0, 0, 0, 19, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, + 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 22, 0, 1, 0, 11, 0, 0, 0, + 2, 0, 12 + }; + + /** + * Compiled version of org.apache.catalina.loader.TesterUnweavedClass, except that + * the doMethod method returns "Hello, Weaver #2!". Compiled with Oracle Java 1.6.0_51. + */ + private static final byte[] WEAVED_REPLACEMENT_2 = new byte[] { + -54, -2, -70, -66, 0, 0, 0, 50, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0, 16, 1, + 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, + 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 8, 100, + 111, 77, 101, 116, 104, 111, 100, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, + 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, + 105, 108, 101, 1, 0, 24, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, 101, + 100, 67, 108, 97, 115, 115, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 17, 72, 101, + 108, 108, 111, 44, 32, 87, 101, 97, 118, 101, 114, 32, 35, 50, 33, 1, 0, 46, 111, 114, + 103, 47, 97, 112, 97, 99, 104, 101, 47, 99, 97, 116, 97, 108, 105, 110, 97, 47, 108, + 111, 97, 100, 101, 114, 47, 84, 101, 115, 116, 101, 114, 85, 110, 119, 101, 97, 118, + 101, 100, 67, 108, 97, 115, 115, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, + 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, + 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 8, 0, 0, + 0, 6, 0, 1, 0, 0, 0, 19, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1, 0, 1, 0, 0, + 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 22, 0, 1, 0, 11, 0, 0, 0, + 2, 0, 12 + }; + + /* + * The WEAVED_REPLACEMENT_1 and WEAVED_REPLACEMENT_2 field contents are generated using the + * following code. To regenerate them, alter the TesterUnweavedClass code as desired, recompile, + * and run this main method. + */ + /*public static void main(String... arguments) throws Exception { + InputStream input = Translator.class.getClassLoader() + .getResourceAsStream("org/apache/catalina/loader/TesterUnweavedClass.class"); + + StringBuilder builder = new StringBuilder(); + builder.append(" "); + + System.out.println(" private static final byte[] WEAVED_REPLACEMENT_1 = new byte[] {"); + try { + for (int i = 0, b = input.read(); b >= 0; i++, b = input.read()) { + String value = "" + ((byte)b); + if (builder.length() + value.length() > 97) { + builder.append(","); + System.out.println(builder.toString()); + builder = new StringBuilder(); + builder.append(" ").append(value); + } else { + if (i > 0) { + builder.append(", "); + } + builder.append(value); + } + } + System.out.println(builder.toString()); + } finally { + input.close(); + } + System.out.println(" }"); + }*/ + +} --- test/org/apache/catalina/loader/TesterNeverWeavedClass.java (revision 0) +++ test/org/apache/catalina/loader/TesterNeverWeavedClass.java (working copy) @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.loader; + +public class TesterNeverWeavedClass { + + public String doMethod() { + return "This will never be weaved."; + } +} --- test/org/apache/catalina/loader/TesterUnweavedClass.java (revision 0) +++ test/org/apache/catalina/loader/TesterUnweavedClass.java (working copy) @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.loader; + +public class TesterUnweavedClass { + + public String doMethod() { + return "Hello, Unweaved World!"; + } +}