package abbot.script; import java.awt.*; import java.util.EmptyStackException; import java.lang.reflect.Field; import javax.swing.SwingUtilities; import abbot.Log; import abbot.Platform; import abbot.i18n.Strings; import abbot.util.*; /** * A custom class loader which installs itself as if it were the application * class loader. A classpath of null is equivalent to the system property * java.class.path.<p> * The class loader may optionally load a class <i>before</i> the parent class * loader gets a chance to look for the class (instead of the default * behavior, which always delegates to the parent class loader first). This * behavior enables the class to be reloaded simply by using a new instance of * this class loader with each launch of the app.<p> * This class mimics the behavior of sun.misc.Launcher$AppClassLoader as much * as possible.<p> * Bootstrap classes are always delegated to the bootstrap loader, with the * exception of the sun.applet package, which should never be delegated, since * it does not work properly unless it is reloaded.<p> * The parent of this class loader will be the normal, default AppClassLoader * (specifically, the class loader which loaded this class will be used). */ public class AppClassLoader extends NonDelegatingClassLoader { /** A new event queue installed for the lifetime of this class loader. */ private AppEventQueue eventQueue; /** This class loader is used to load bootstrap classes that must be * preloaded. */ private BootstrapClassLoader bootstrapLoader; /** For use in checking if a class is in the framework class path. */ private NonDelegatingClassLoader extensionsLoader; /** Whether the framework itself is being tested. */ private boolean frameworkIsUnderTest = false; /** Old class loader context for the thread where this loader was * installed. */ private ClassLoader oldClassLoader = null; private Thread installedThread = null; private String oldClassPath = null; private class InstallationLock {} private InstallationLock lock = new InstallationLock(); /** Constructs an AppClassLoader using the current classpath (as found in java.class.path). */ public AppClassLoader() { this(null); } /** * Constructs a AppClassLoader with a custom classpath, indicating * whether the class loader should delegate to its parent class loader * prior to searching for a given resource.<p> * The class path argument may use either a colon or semicolon to separate * its elements.<p> */ public AppClassLoader(String classPath) { super(classPath, AppClassLoader.class.getClassLoader()); bootstrapLoader = new BootstrapClassLoader(); // Use this one to look up extensions; we want to reload them, but may // need to look in the framework class path for them. // Make sure it *only* loads extensions, though, and defers all other // lookups to its parent. extensionsLoader = new NonDelegatingClassLoader(System.getProperty("java.class.path"), AppClassLoader.class. getClassLoader()) { protected boolean shouldDelegate(String name) { return !isExtension(name); } }; // Don't want to open a whole new can of class loading worms! /* // If we're testing the framework itself, then absolutely DO NOT // delegate those classes. if (getClassPath().indexOf("abbot.jar") != -1) { frameworkIsUnderTest = true; } */ } public boolean isEventDispatchThread() { return (eventQueue != null && Thread.currentThread() == eventQueue.thread) || (eventQueue == null && SwingUtilities.isEventDispatchThread()); } /** Should the parent class loader try to load this class first? */ // FIXME we should only need the delegate flag if stuff in the classpath // is also found on the system classpath, e.g. the framework itself // Maybe just set it internally in case the classpaths overlap? protected boolean shouldDelegate(String name) { return bootstrapLoader.shouldDelegate(name) && !isExtension(name) && !(frameworkIsUnderTest && isFrameworkClass(name)); } private boolean isFrameworkClass(String name) { return name.startsWith("abbot.") || name.startsWith("junit.extensions.abbot."); } private boolean isExtension(String name) { return name.startsWith("abbot.tester.extensions.") || name.startsWith("abbot.script.parsers.extensions."); } /** * Finds and loads the class with the specified name from the search * path. If the class is a bootstrap class and must be preloaded, use our * own bootstrap loader. If it is an extension class, use our own * extensions loader. * * @param name the name of the class * @return the resulting class * @exception ClassNotFoundException if the class could not be found */ public Class findClass(String name) throws ClassNotFoundException { if (isBootstrapClassRequiringReload(name)) { try { return bootstrapLoader.findClass(name); } catch(ClassNotFoundException cnf) { Log.warn(cnf); } } // Look for extensions first in the framework class path (with a // special loader), then in the app class path. // Extensions *must* have the same class loader as the corresponding // custom components try { return super.findClass(name); } catch(ClassNotFoundException cnf) { if (isExtension(name)) { return extensionsLoader.findClass(name); } throw cnf; } } /** Ensure that everything else subsequently loaded on the same thread or * any subsequently spawned threads uses the given class loader. Also * ensure that classes loaded by the event dispatch thread and threads it * spawns use the given class loader. */ public void install() { if (SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException(Strings.get("appcl.invalid_state")); } if (installedThread != null) { String msg = Strings.get("appcl.already_installed", new Object[] { installedThread }); throw new IllegalStateException(msg); } // Change the effective classpath, but make sure it's available if // someone needs to access it. oldClassPath = System.getProperty("java.class.path"); System.setProperty("abbot.class.path", oldClassPath); System.setProperty("java.class.path", getClassPath()); Log.debug("java.class.path set to " + System.getProperty("java.class.path")); // Install our own handler for catching exceptions on the event // dispatch thread. try { new EventExceptionHandler().install(); } catch(Exception e) { // ignore any exceptions, since they're not fatal } eventQueue = new AppEventQueue(); eventQueue.install(); Thread current = Thread.currentThread(); installedThread = current; oldClassLoader = installedThread.getContextClassLoader(); installedThread.setContextClassLoader(this); } public boolean isInstalled() { synchronized(lock) { return eventQueue != null; } } /** Reverse the effects of install. Has no effect if the class loader * has not been installed on any thread. */ public void uninstall() { // Ensure that no two threads attempt to uninstall synchronized(lock) { if (eventQueue != null) { eventQueue.uninstall(); eventQueue = null; } if (installedThread != null) { installedThread.setContextClassLoader(oldClassLoader); oldClassLoader = null; installedThread = null; System.setProperty("java.class.path", oldClassPath); oldClassPath = null; } } } private class AppEventQueue extends EventQueue { private Thread thread; /** Ensure the class loader for the event dispatch thread is the right one. */ public void install() { Runnable installer = new Runnable() { public void run() { Toolkit.getDefaultToolkit(). getSystemEventQueue().push(AppEventQueue.this); } }; // Avoid deadlock with the event queue, in case it has the tree // lock (pickens). AWT.invokeAndWait(installer); Runnable threadTagger = new Runnable() { public void run() { thread = Thread.currentThread(); thread.setContextClassLoader(AppClassLoader.this); thread.setName(thread.getName() + " (AppClassLoader)"); } }; AWT.invokeAndWait(threadTagger); } /** Pop this and any subsequently pushed event queues. */ public void uninstall() { Log.debug("Uninstalling AppEventQueue"); try { pop(); thread = null; } catch(EmptyStackException ese) { } Log.debug("AppEventQueue uninstalled"); } public String toString() { return "Abbot Event Queue: " + thread; } } /** List of bootstrap classes we most definitely want to be loaded by this * class loader, rather than any parent, or the bootstrap loader. */ private String[] mustReloadPrefixes = { "sun.applet.", // need the whole package, not just AppletViewer/Main }; /** Does the given class absolutely need to be preloaded? */ private boolean isBootstrapClassRequiringReload(String name) { for (int i=0;i < mustReloadPrefixes.length;i++) { if (name.startsWith(mustReloadPrefixes[i])) return true; } return false; } /** Returns the path to the primary JRE classes, not including any * extensions. This is primarily needed for loading * sun.applet.AppletViewer/Main, since most other classes in the bootstrap * path should <i>only</i> be loaded by the bootstrap loader. */ private static String getBootstrapPath() { return System.getProperty("sun.boot.class.path"); } /** Provide access to bootstrap classes that we need to be able to * reload. */ private class BootstrapClassLoader extends NonDelegatingClassLoader { public BootstrapClassLoader() { super(getBootstrapPath(), null); } protected boolean shouldDelegate(String name) { // Exclude all bootstrap classes, except for those we know we // *must* be reloaded on each run in order to have function // properly (e.g. applet) return !isBootstrapClassRequiringReload(name) && !"abbot.script.AppletSecurityManager".equals(name); } } public String toString() { return super.toString() + " (java.class.path=" + System.getProperty("java.class.path") + ")"; } }