/* * This file is part of the Scriba source distribution. This is free, open-source * software. For full licensing information, please see the LicensingInformation file * at the root level of the distribution. * * Copyright (c) 2006-2007 Kobrix Software, Inc. */ package org.sharegov.cirm.utils; /* * This class slightly modified to allow optional resolving classes from this * classloader before system/ parent class loader. */ /* * Copyright (c) 1997-1999 The Java Apache Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. All advertising materials mentioning features or use of this software must * display the following acknowledgment: "This product includes software * developed by the Java Apache Project for use in the Apache JServ servlet * engine project <http://java.apache.org/>." * * 4. The fontNames "Apache JServ", "Apache JServ Servlet Engine" and "Java * Apache Project" must not be used to endorse or promote products derived from * this software without prior written permission. * * 5. Products derived from this software may not be called "Apache JServ" nor * may "Apache" nor "Apache JServ" appear in their fontNames without prior * written permission of the Java Apache Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: "This product includes software developed by the Java Apache * Project for use in the Apache JServ servlet engine project * <http://java.apache.org/>." * * THIS SOFTWARE IS PROVIDED BY THE JAVA APACHE PROJECT "AS IS" AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE JAVA APACHE PROJECT OR ITS CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals on * behalf of the Java Apache Group. For more information on the Java Apache * Project and the Apache JServ Servlet Engine project, * * please see <http://java.apache.org/>. * */ import java.util.Hashtable; import java.util.Vector; import java.util.Enumeration; import java.io.*; import java.net.URL; import java.util.zip.ZipFile; import java.util.zip.ZipException; import java.util.zip.ZipEntry; /** * * A class loader that loads classes from directories and/or zip-format * file such as JAR file. It tracks the modification time of the classes * it loads to permit reloading through re-instantiation. * * <P> * When the classloader reports its creator that one of the classes it * has loaded has changed on disk, it should discard the classloader * and create a new instance using <CODE>reinstantiate</CODE>. * The classes are then reloaded into the new classloader as required. * * <P> * The classloader can also load resources, which are a means * for packaging application data such as images within a jar file * or directory. * * <P> * The classloader always first tries to load classes and resources * from the system, and uses it's own path if that fails. This is also * done if an empty repository is passed at construction. * * <P> * <B>How autoreload works:</B> * </P> * * <P> * The Java VM considers two classes the same if they have the same * fully-qualified name <B>and</B> if they were loaded from the same * <CODE>ClassLoader</CODE>. * * <P> * There is no way for a classloader to 'undefine' a class once it * has been loaded. However, the servlet engine can discard a * classloader and the classes it contains, causing the * * <P> * The <CODE>JServServletManager</CODE> creates a new instance of * the classloader each time it detects that any of the loaded classes * have changed. * * <P> * Before terminating, all servlets are destroyed. * <P> * According to the Java Language Specification (JLS), classes may * be garbage-collected when there are no longer any instances of that * class and the <CODE>java.lang.Class</CODE> object is finalizable. * It is intended that this be the case when a <CODE>JServClassLoader</CODE> * is discarded. * <P> * Many VM releases did not implement class garbage collection * properly. In such a VM, the memory usage will continue to grow if * autoreloading is enable. Running the VM with * <CODE>-verbosegc</CODE> (or the corresponding option for * non-Javasoft VMs) may give some debugging information. * * <P> * It is important that the <CODE>destroy</CODE> method be * implemented properly, as servlets may be destroyed and * reinitialized several times in the life of a VM. * * @author Dawid Weiss (modifications) * @author Francis J. Lacoste * @author Martin Pool * @author Jim Heintz * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a> * @see java.lang.ClassLoader * */ public class AdaptiveClassLoader extends ClassLoader { /** * Generation counter, incremented for each classloader as they are * created. */ static private int generationCounter = 0; private ClassLoader delegateOnFail = null; /** * Generation number of the classloader, used to distinguish between * different instances. */ private int generation; /** * Cache of the loaded classes. This contains ClassCacheEntry keyed * by class fontNames. */ private Hashtable cache; /** * Save our class loader for chaining, and speed purposes. */ private ClassLoader myParentClassLoader; /** * The classpath which this classloader searches for class definitions. * Each element of the vector should be either a directory, a .zip * file, or a .jar file. * <p> * It may be empty when only system classes are controlled. */ private Vector repository; /** * Order of class resolving - if true, parent class loader is checked after * this class loader attempted to load a class. */ private boolean parentClassLoaderLast; /** * Private class used to maintain information about the classes that * we loaded. */ private static class ClassCacheEntry { /** * * The actual loaded class */ Class loadedClass; /** * * The file from which this class was loaded; or null if * * it was loaded from the system. * */ File origin; /** * * The time at which the class was loaded from the origin * * file, in ms since the epoch. * */ long lastModified; /** * * Check whether this class was loaded from the system. * */ public boolean isSystemClass() { return origin == null; } } // ------------------------------------------------------- Constructors /** * * Creates a new class loader that will load classes from specified * class repositories. * * @param classRepository An set of File classes indicating * directories and/or zip/jar files. It may be empty when * only system classes are loaded. * * @param parentClassLoaderLast * if set to true, classes are resolved using * this classloader before attempting the parent classloader. This * feature can be used for dynamic reloading of classes, however * it slows down performance and requires additional memory. * @throw java.lang.IllegalArgumentException if the objects contained * in the vector are not a file instance or the file is not * a valid directory or a zip/jar file. */ public AdaptiveClassLoader(Vector classRepository, boolean parentClassLoaderLast) throws IllegalArgumentException { this(classRepository, null, parentClassLoaderLast); } /** * * Creates a new class loader that will load classes from specified * * class repositories. * * * * @param classRepository * An set of File classes indicating * * directories and/or zip/jar files. It may be empty when * * only system classes are loaded. * * @param chainedClassLoader * A class loader to attempt to load classes * * as resources thru before falling back on the default system * * loaders. * * @param parentClassLoaderLast * if set to true, classes are resolved using * * this classloader before attempting the parent classloader. This * * feature can be used for dynamic reloading of classes, however * * it slows down performance and requires additional memory. * * @throw java.lang.IllegalArgumentException if the objects contained * * in the vector are not a file instance or the file is not * * a valid directory or a zip/jar file. * */ public AdaptiveClassLoader(Vector classRepository, ClassLoader chainedClassLoader, boolean parentClassLoaderLast) throws IllegalArgumentException { this.parentClassLoaderLast = parentClassLoaderLast; myParentClassLoader = chainedClassLoader; // Create the cache of loaded classes cache = new Hashtable(); // Verify that all the repository are valid. Enumeration e = classRepository.elements(); while (e.hasMoreElements()) { Object o = e.nextElement(); File file; File[] files; int i; // Check to see if element is a File instance. try { file = (File) o; } catch (ClassCastException objectIsNotFile) { throw new IllegalArgumentException("Object " + o + "is not a valid \"File\" instance"); } // Check to see if we have proper access. if (!file.exists()) { throw new IllegalArgumentException("Repository " + file.getAbsolutePath() + " doesn't exist!"); } else if (!file.canRead()) { throw new IllegalArgumentException( "Do not have read access for file " + file.getAbsolutePath()); } // Check that it is a directory or zip/jar file if (!(file.isDirectory() || isZipOrJarArchive(file))) { throw new IllegalArgumentException( file.getAbsolutePath() + " is not a directory or zip/jar file" + " or if it's a zip/jar file then it is corrupted."); } } // Store the class repository for use this.repository = classRepository; // Increment and store generation counter this.generation = generationCounter++; } // ------------------------------------------------------- Methods /** * * Test if a file is a ZIP or JAR archive. * * * * @param file * the file to be tested. * * @return true if the file is a ZIP/JAR archive, false otherwise. * */ private boolean isZipOrJarArchive(File file) { boolean isArchive = true; ZipFile zipFile = null; try { zipFile = new ZipFile(file); } catch (ZipException zipCurrupted) { isArchive = false; } catch (IOException anyIOError) { isArchive = false; } finally { if (zipFile != null) { try { zipFile.close(); } catch (IOException ignored) { } } } return isArchive; } /** * * Check to see if a given class should be reloaded because of a * * modification to the original class. * * * * @param className * The name of the class to check for modification. * */ public synchronized boolean shouldReload(String classname) { ClassCacheEntry entry = (ClassCacheEntry) cache.get(classname); if (entry == null) { // class wasn't even loaded return false; } else if (entry.isSystemClass()) { // System classes cannot be reloaded return false; } else { boolean reload = (entry.origin.lastModified() != entry.lastModified); return reload; } } /** * * Check whether the classloader should be reinstantiated. * * <P> * * The classloader must be replaced if there is any class whose * * origin file has changed since it was last loaded. * */ public synchronized boolean shouldReload() { // Check whether any class has changed Enumeration e = cache.elements(); while (e.hasMoreElements()) { ClassCacheEntry entry = (ClassCacheEntry) e.nextElement(); if (entry.isSystemClass()) continue; // XXX: Because we want the classloader to be an accurate // reflection of the contents of the repository, we also // reload if a class origin file is now missing. This // probably makes things a bit more fragile, but is OK in // a servlet development situation. <mbp@pharos.com.au> long msOrigin = entry.origin.lastModified(); if (msOrigin == 0) { // class no longer exists return true; } if (msOrigin != entry.lastModified) { // class is modified return true; } } // No changes, no need to reload return false; } /** * * Re-instantiate this class loader. * * <p> * * This method creates a new instance * * of the class loader that will load classes form the same path * * as this one. * */ public AdaptiveClassLoader reinstantiate() { return new AdaptiveClassLoader(repository, myParentClassLoader, parentClassLoaderLast); } // ------------------------------------ Implementation of Classloader /* * * XXX: The javadoc for java.lang.ClassLoader says that the * * ClassLoader should cache classes so that it can handle repeated * * requests for the same class. On the other hand, the JLS seems * * to imply that each classloader is only asked to load each class * * once. Is this a contradiction? * * * * Perhaps the second call only applies to classes which have been * * garbage-collected? * */ /** * * Resolves the specified name to a Class. The method loadClass() * is called by the virtual machine. As an abstract method, * loadClass() must be defined in a subclass of ClassLoader. * * @param name the name of the desired Class. * @param resolve true if the Class needs to be resolved; * false if the virtual machine just wants to determine * whether the class exists or not * * @return the resulting Class. * @exception ClassNotFoundException if the class loader cannot * find a the requested class. * */ protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // The class object that will be returned. Class<?> c = null; // Use the cached value, if this class is already loaded into // this classloader. ClassCacheEntry entry = (ClassCacheEntry) cache.get(name); if (entry != null) { // Class found in our cache c = entry.loadedClass; if (resolve) resolveClass(c); return c; } if (!securityAllowsClass(name)) { return loadSystemClass(name, resolve); } if (parentClassLoaderLast == false) { // Attempt to load the class from the system try { c = loadSystemClass(name, resolve); if (c != null) { if (resolve) resolveClass(c); return c; } } catch (Exception e) { c = null; } } // Try to load it from each repository Enumeration repEnum = repository.elements(); // Cache entry. ClassCacheEntry classCache = new ClassCacheEntry(); while (repEnum.hasMoreElements()) { byte[] classData = null; File file = (File) repEnum.nextElement(); try { if (file.isDirectory()) { classData = loadClassFromDirectory(file, name, classCache); } else { classData = loadClassFromZipfile(file, name, classCache); } } catch (IOException ioe) { // Error while reading in data, consider it as not found classData = null; } if (classData != null) { // Define the class c = defineClass(name, classData, 0, classData.length); // Cache the result; classCache.loadedClass = c; // Origin is set by the specific loader classCache.lastModified = classCache.origin.lastModified(); cache.put(name, classCache); // Resolve it if necessary if (resolve) resolveClass(c); return c; } } if (parentClassLoaderLast == true) { // Attempt to load the class from the system try { c = loadSystemClass(name, resolve); if (c != null) { if (resolve) resolveClass(c); return c; } } catch (Exception e) { c = null; } } // If not found in any repository throw new ClassNotFoundException(name); } /** * Load a class using the system classloader. * * @exception ClassNotFoundException * if the class loader cannot * find a the requested class. * @exception NoClassDefFoundError * if the class loader cannot * find a definition for the class. */ protected Class<?> loadSystemClass(String name, boolean resolve) throws NoClassDefFoundError, ClassNotFoundException { if (myParentClassLoader != null) return myParentClassLoader.loadClass(name); Class<?> c = findSystemClass(name); // Throws if not found. // Add cache entry ClassCacheEntry cacheEntry = new ClassCacheEntry(); cacheEntry.origin = null; cacheEntry.loadedClass = c; cacheEntry.lastModified = Long.MAX_VALUE; cache.put(name, cacheEntry); if (resolve) resolveClass(c); return c; } /** * * Checks whether a classloader is allowed to define a given class, * within the security manager restrictions. * */ // XXX: Should we perhaps also not allow classes to be dynamically // loaded from org.apache.jserv.*? Would it introduce security // problems if people could override classes here? // <mbp@humbug.org.au 1998-07-29> private boolean securityAllowsClass(String className) { try { SecurityManager security = System.getSecurityManager(); if (security == null) { // if there's no security manager then all classes // are allowed to be loaded return true; } int lastDot = className.lastIndexOf('.'); // Check if we are allowed to load the class' package security.checkPackageDefinition((lastDot > -1) ? className.substring(0, lastDot) : ""); // Throws if not allowed return true; } catch (SecurityException e) { return false; } } /** * * Tries to load the class from a directory. * * @param dir The directory that contains classes. * @param name The classname * @param cache The cache entry to set the file if successful. */ private byte[] loadClassFromDirectory(File dir, String name, ClassCacheEntry cache) throws IOException { // Translate class name to file name String classFileName = name.replace('.', File.separatorChar) + ".class"; // Check for garbage input at beginning of file name // i.e. ../ or similar if (!Character.isJavaIdentifierStart(classFileName.charAt(0))) { // Find real beginning of class name int start = 1; while (!Character.isJavaIdentifierStart(classFileName.charAt(start++))) ; classFileName = classFileName.substring(start); } File classFile = new File(dir, classFileName); if (classFile.exists()) { cache.origin = classFile; InputStream in = new FileInputStream(classFile); try { return loadBytesFromStream(in, (int) classFile.length()); } finally { in.close(); } } else { // Not found return null; } } /** * * Tries to load the class from a zip file. * * * * @param file * The zipfile that contains classes. * * @param name * The classname * * @param cache * The cache entry to set the file if successful. * */ private byte[] loadClassFromZipfile(File file, String name, ClassCacheEntry cache) throws IOException { // Translate class name to file name String classFileName = name.replace('.', '/') + ".class"; ZipFile zipfile = new ZipFile(file); try { ZipEntry entry = zipfile.getEntry(classFileName); if (entry != null) { cache.origin = file; return loadBytesFromStream(zipfile.getInputStream(entry), (int) entry.getSize()); } else { // Not found return null; } } finally { zipfile.close(); } } /** * * Loads all the bytes of an InputStream. * */ private byte[] loadBytesFromStream(InputStream in, int length) throws IOException { byte[] buf = new byte[length]; int nRead, count = 0; while ((length > 0) && ((nRead = in.read(buf, count, length)) != -1)) { count += nRead; length -= nRead; } return buf; } /** * * Get an InputStream on a given resource. Will return null if no * * resource with this name is found. * * <p> * * The JServClassLoader translate the resource's name to a file * * or a zip entry. It looks for the resource in all its repository * * entry. * * * * @see java.lang.Class#getResourceAsStream(String) * * @param name * the name of the resource, to be used as is. * * @return an InputStream on the resource, or null if not found. * */ public InputStream getResourceAsStream(String name) { // Try to load it from the system class InputStream s = null; if (myParentClassLoader != null) { s = myParentClassLoader.getResourceAsStream(name); } if (s == null) { s = getSystemResourceAsStream(name); } if (s == null) { // Try to find it from every repository Enumeration repEnum = repository.elements(); while (repEnum.hasMoreElements()) { File file = (File) repEnum.nextElement(); if (file.isDirectory()) { s = loadResourceFromDirectory(file, name); } else if (name.endsWith(".initArgs")) { String parentFile = file.getParent(); if (parentFile != null) { File dir = new File(parentFile); s = loadResourceFromDirectory(dir, name); } } else { s = loadResourceFromZipfile(file, name); } if (s != null) { break; } } } return s; } /** * * Loads resource from a directory. * */ private InputStream loadResourceFromDirectory(File dir, String name) { // Name of resources are always separated by / String fileName = name.replace('/', File.separatorChar); File resFile = new File(dir, fileName); if (resFile.exists()) { try { return new FileInputStream(resFile); } catch (FileNotFoundException shouldnothappen) { return null; } } else { return null; } } /** * * Loads resource from a zip file * */ private InputStream loadResourceFromZipfile(File file, String name) { ZipFile zipfile = null; InputStream resourceStream = null; try { zipfile = new ZipFile(file); ZipEntry entry = zipfile.getEntry(name); if (entry != null) { long length = entry.getSize(); resourceStream = zipfile.getInputStream(entry); byte[] data = loadBytesFromStream(resourceStream, (int) length); return new ByteArrayInputStream(data); } else { return null; } } catch (IOException e) { return null; } finally { if (resourceStream != null) { try { resourceStream.close(); } catch (IOException ignored) { } } if (zipfile != null) { try { zipfile.close(); } catch (IOException ignored) { } } } } /** * * Find a resource with a given name. The return is a URL to the * * resource. Doing a getContent() on the URL may return an Image, * * an AudioClip,or an InputStream. * * <p> * * This classloader looks for the resource only in the directory * * repository for this resource. * * * * @param name * the name of the resource, to be used as is. * * @return an URL on the resource, or null if not found. * */ public URL getResource(String name) { if (name == null) { return null; } // First ask the primordial class loader to fetch it from the classpath URL u = null; if (myParentClassLoader != null) { u = myParentClassLoader.getResource(name); } if (u == null) { u = getSystemResource(name); } if (u != null) { return u; } // We got here so we have to look for the resource in our list of // repository elements Enumeration repEnum = repository.elements(); while (repEnum.hasMoreElements()) { File file = (File) repEnum.nextElement(); // Construct a file://-URL if the repository is a directory if (file.isDirectory()) { String fileName = name.replace('/', File.separatorChar); File resFile = new File(file, fileName); if (resFile.exists()) { // Build a file:// URL form the file name try { return new URL("file", null, resFile.getAbsolutePath()); } catch (java.net.MalformedURLException badurl) { badurl.printStackTrace(); return null; } } } else { // a jar:-URL *could* change even between minor releases, but // didn't between JVM's 1.1.6 and 1.3beta. Tested on JVM's from // IBM, Blackdown, Microsoft, Sun @ Windows and Sun @ Solaris try { ZipFile zf = new ZipFile(file.getAbsolutePath()); ZipEntry ze = zf.getEntry(name); if (ze != null) { try { return new URL("jar:file:" + file.getAbsolutePath() + "!/" + name); } catch (java.net.MalformedURLException badurl) { badurl.printStackTrace(); return null; } } } catch (IOException ioe) { ioe.printStackTrace(); return null; } } } // Not found return null; } /** * * Return the last modified time for a class in the * * ClassCache. * * * * @throws ClassNotFoundException * if class is not found * */ public long lastModified(String name) throws ClassNotFoundException { ClassCacheEntry entry = (ClassCacheEntry) cache.get(name); if (entry == null) { throw new ClassNotFoundException("Could not find class: " + name); } else { return entry.lastModified; } } }