package net.jxta.test.util; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.security.CodeSource; import java.security.SecureClassLoader; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Utility ClassLoader for use in situations where the standard delegation * model proves to be inadequate. Specifically, this class allows the * programmer to create a classloading decoupling point to prevent classes * being loaded from actually being defined by the ClassLoader containing * the class/resource definition, whether it be the system, parent, or * child class loader. This is useful to prevent a class from being loaded * too high up the delegation chain and thereby becoming immutable at too * high of a level. Likewise, since the child ClassLoaders are only used to * obtain the resource/class definition (as opposed to resolving and defining * it), this class can raise the definition scope of the target being * loaded. * <p/> * In general, the delegation model of class loading is both appropriate and * desirable. This class becomes of particular value only in environments * where class namespace isolation is desired and in situations where class * reloading may occur. * <p/> * NOTE: Since certain system class are not allowed to be redefined due to * security and stability concerns, all "java." and "javax." classes will * always be loaded by the system ClassLoader. */ public class DelegateClassLoader extends SecureClassLoader { /** * Log object for this class. */ private static final Logger LOG = Logger.getLogger(DelegateClassLoader.class.getName()); /** * Initial size of the buffer used to convert a class * data stream into a <code>Class</code> instance. */ private static final int BUFFER_SIZE = 4096; /** * Incremental size change when the buffer used to * convert the data stream into a <code>Class</code> * object is not large enough to contain all the * data. */ private static final int BUFFER_INCR = 4096; /** * Local reference to the system class loader. */ private static final ClassLoader SYS_LOADER = ClassLoader.getSystemClassLoader(); /** * Regular expression to select those classes which MUST be loaded using * the system ClassLoader. */ private static final Pattern PATTERN_SYS_CLASSES = Pattern.compile("^javax?\\..*"); /** * List of patterns which will cause a Class to be forcibly redefined by * this ClassLoader should the name of the Class match a pattern in this * list (and not match any patterns in the neverRedefine list). */ private final Set<Pattern> redefinePatterns = new CopyOnWriteArraySet<Pattern>(); /** * List of patterns which will prevent a Class from ever being forcibly * redefined, even if it matches one of the Patterns in the * redefinePatterns list. */ private final Set<Pattern> neverRedefinePatterns = new CopyOnWriteArraySet<Pattern>(); /** * Stores the order which should be used when * looking for a loader. */ private final List<ClassLoader> searchOrder = new CopyOnWriteArrayList<ClassLoader>(); /** * CodeSource to associate with all loaded classes. */ private final CodeSource codeSource; /** * Creates a class loader instance using the current * <code>ClassLoader</code> as the parent loader. */ public DelegateClassLoader() { this(null, null); } /** * Creates a class loader instance using the specified * loader as the parent loader. * * @param parent parent ClassLoader */ public DelegateClassLoader(final ClassLoader parent) { this(null, parent); } /** * Creates a class loader instance using the specified loader * as the parent loader and the specified code source instance for * all classes that we load. * * @param theCodeSource code source to assign to all loaded classes, or * {@code null} for the default, per-loader code source * @param parent parent class loader, or {@code null} to use only the * system class loader */ public DelegateClassLoader( final CodeSource theCodeSource, final ClassLoader parent) { super(parent); // Always search the system loader first searchOrder.add(SYS_LOADER); if (parent != null) { // Then search the parent loader, if provided if (!searchOrder.contains(parent)) { searchOrder.add(parent); } } // We can never redefine classes which are protected by Java spec neverRedefinePatterns.add(PATTERN_SYS_CLASSES); if (theCodeSource == null) { try { URL url = new URL("file:/" + getClass().getName() + "/" + toString()); codeSource = new CodeSource(url, (Certificate[]) null); } catch (MalformedURLException malx) { throw(new IllegalStateException("should never happen", malx)); } } else { codeSource = theCodeSource; } } /** * Adds a regular expression which will be used to test a class name * during a class loading attempt to see if the class name should be * loaded form the delegated class loader instances and redefined locally * by this class. This creates a local classloading confluence which will * keep as many subsequent loading requests as possible coming through * this ClassLoader. * * @param pattern pattern to add */ public void addClassRedefinePattern(Pattern pattern) { redefinePatterns.add(pattern); } /** * Adds a regular expression which will be used to test a class name * during a class loading attempt to see if the class name should * never be locally redefined by this class. This protects against * reloading attempts which would otherwise be illegal or undesirable, * such as reloading the Java APIs loaded by the ssytem class loader. * (Note that any class name starting with {@code java.} or {@code javax.} * are implicitly in this list, by default). * * @param pattern pattern to add */ public void addClassNeverRedefinePattern(Pattern pattern) { neverRedefinePatterns.add(pattern); } /** * Adds a ClassLoader to the class/resource search path * of this class loader. ClassLoaders are search in order of * addition. * * @param loader child ClassLoader to delegate requests to */ public void addClassLoader(final ClassLoader loader) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("addClassLoader(" + loader + ")"); } if (!searchOrder.contains(loader)) { searchOrder.add(loader); } } ////////////////////////////////////////////////////////////////////// // ClassLoader overrides: /** * {@inheritDoc} */ @Override public Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { Class result = null; URL res = null; long findStart=0; long findTime; if (LOG.isLoggable(Level.FINE)) { findStart = System.currentTimeMillis(); LOG.fine("findClass(" + name + ")"); } // Determine the resource name for this class name String resName = name.replace('.', '/').concat(".class"); boolean redefine = shouldRedefine(name); // See if we've already loaded this class result = this.findLoadedClass(name); if (result != null) { LOG.finer("Class was previously loaded"); return result; } // Always use the system loader first. try { /* * Only redefine if flag set and the class is not in the list of * classes which must be loaded by the system ClassLoader. */ if (redefine) { LOG.finer("Searching for system resource definition"); res = findResource(resName); if (res == null) { LOG.finer("Class resource not found for redefinition"); } else { LOG.finer("Redefining class resource"); result = defineClass(name, res); } } else { LOG.finer("Using system/parent ClassLoader"); result = super.loadClass(name, resolve); } } catch (ClassNotFoundException cnfx) { if (LOG.isLoggable(Level.FINER)) { LOG.finer("System/parent ClassLoader could not find class: " + name); } // Okay. Try our loader now. } // Get the first definition of this resource name if (result == null) { LOG.finer("Searching for child resource definition"); res = findResource(resName); if (res != null) { LOG.finer("Redefining child data"); result = defineClass(name, res); } } if (LOG.isLoggable(Level.FINEST)) { findTime = System.currentTimeMillis() - findStart; LOG.finest("findClass(" + name + ") result: " + result); LOG.finest("findClass(" + name + ") time: " + findTime); } if (result != null) { if (resolve) { resolveClass(result); } return result; } throw(new ClassNotFoundException("Class not found: " + name)); } /** * {@inheritDoc} */ @Override public URL getResource(final String name) { if (LOG.isLoggable(Level.FINE)) { LOG.fine("getResource(" + name + ")"); } return findResource(name); } /** * {@inheritDoc} * * @throws IOException on resource location error */ @Override public Enumeration<URL> findResources(final String name) throws IOException { if (LOG.isLoggable(Level.FINE)) { LOG.fine("findResources(" + name + ")"); } return findResources(name, false); } /** * {@inheritDoc} */ private Enumeration<URL> findResources( final String name, final boolean firstOnly) throws IOException { if (LOG.isLoggable(Level.FINE)) { LOG.fine("findResources(" + name + ", " + firstOnly + ")"); } // Remove any leading slashes. String normalizedName = name; while (normalizedName.charAt(0) == '/') { try { normalizedName = normalizedName.substring(1); } catch (IndexOutOfBoundsException ioobx) { normalizedName = ""; } } // Search through the search path for resource definitions ArrayList<URL> found = new ArrayList<URL>(); for (ClassLoader loader : searchOrder) { if (LOG.isLoggable(Level.FINEST)) { LOG.finest(" Searching loader: " + loader); } Enumeration<URL> anEnum = loader.getResources(normalizedName); while (anEnum.hasMoreElements()) { URL url = anEnum.nextElement(); if (LOG.isLoggable(Level.FINE)) { LOG.finer(" FOUND: " + url); } found.add(url); if (firstOnly) { return Collections.enumeration(found); } } } return Collections.enumeration(found); } /** * {@inheritDoc} */ @Override public URL findResource(final String name) { Enumeration anEnum; if (LOG.isLoggable(Level.FINE)) { LOG.fine("findResource(" + name + ")"); } try { anEnum = findResources(name, true); return (URL) anEnum.nextElement(); } catch (Exception x) { return null; } } ////////////////////////////////////////////////////////////////////// // Private methods: /** * Determines if the class name provided should be forcibly redefined * by this ClassLoader. * * @param className name of the class to evaluate * @return {@code true} if the class should eb forcibly redefined, * {@code false} otherwise */ private boolean shouldRedefine(final String className) { Iterator<Pattern> iter = redefinePatterns.iterator(); boolean redefine = false; while (iter.hasNext() && !redefine) { Pattern pattern = iter.next(); Matcher matcher = pattern.matcher(className); redefine = matcher.matches(); } if (redefine) { // Ensure we don't allow redefinition of forbidden classes iter = neverRedefinePatterns.iterator(); while (iter.hasNext() && redefine) { Pattern pattern = iter.next(); Matcher matcher = pattern.matcher(className); redefine = !matcher.matches(); } } return redefine; } /** * Loads a URL resource into a byte array so that it can be * directly used in a defineClass() call. * * @param res resource URL to read * @return byte array containing rw definition data * @throws IOException on data read failure */ private static byte[] loadResource(final URL res) throws IOException { InputStream input; byte[] data, tmp; int used; int len; long start=0; long time; // XXX: Rework this method with ByteArrayOutputStream usage if (LOG.isLoggable(Level.FINE)) { start = System.currentTimeMillis(); LOG.finer("loadResource(" + res + ")"); } // Initialize our buffer data = new byte[BUFFER_SIZE]; used = 0; // Read in the resource data input = res.openStream(); while (true) { len = data.length - used; len = input.read(data, used, len); if (len < 0) { // End of file break; } used += len; if (used == data.length) { // Increase buffer size tmp = new byte[data.length + BUFFER_INCR]; System.arraycopy(data, 0, tmp, 0, used); data = tmp; } } // Trim up the buffer tmp = new byte[used]; System.arraycopy(data, 0, tmp, 0, used); if (LOG.isLoggable(Level.FINEST)) { time = System.currentTimeMillis() - start; LOG.finest(" loadResource(" + res + ") time: " + time); } return tmp; } /** * Defines a class using the data found at the other end of the specified * URL. * * @param name class name to define * @param classDataURL class data resource URL * @return class definition */ private Class defineClass(final String name, final URL classDataURL) { byte[] data; Class result = null; // Load the class data try { data = loadResource(classDataURL); result = defineClass(name, data, 0, data.length, codeSource); } catch (Exception x) { if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Couldn't load resource data", x); } } return result; } }