Original, unmodfied copy of Tomcat's WebappClassLoader. git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/1.6@4510 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/org/apache/catalina/loader/WebappClassLoader.java b/dev/core/src/org/apache/catalina/loader/WebappClassLoader.java new file mode 100644 index 0000000..465fbb0 --- /dev/null +++ b/dev/core/src/org/apache/catalina/loader/WebappClassLoader.java
@@ -0,0 +1,2165 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.AccessControlException; +import java.security.AccessController; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Policy; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Vector; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.jar.Attributes.Name; + +import javax.naming.NameClassPair; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.util.StringManager; +import org.apache.naming.JndiPermission; +import org.apache.naming.resources.Resource; +import org.apache.naming.resources.ResourceAttributes; +import org.apache.tomcat.util.compat.JdkCompat; + +/** + * Specialized web application class loader. + * <p> + * This class loader is a full reimplementation of the + * <code>URLClassLoader</code> from the JDK. It is desinged to be fully + * compatible with a normal <code>URLClassLoader</code>, although its internal + * behavior may be completely different. + * <p> + * <strong>IMPLEMENTATION NOTE</strong> - This class loader faithfully follows + * the delegation model recommended in the specification. The system class + * loader will be queried first, then the local repositories, and only then + * delegation to the parent class loader will occur. This allows the web + * application to override any shared class except the classes from J2SE. + * Special handling is provided from the JAXP XML parser interfaces, the JNDI + * interfaces, and the classes from the servlet API, which are never loaded + * from the webapp repository. + * <p> + * <strong>IMPLEMENTATION NOTE</strong> - Due to limitations in Jasper + * compilation technology, any repository which contains classes from + * the servlet API will be ignored by the class loader. + * <p> + * <strong>IMPLEMENTATION NOTE</strong> - The class loader generates source + * URLs which include the full JAR URL when a class is loaded from a JAR file, + * which allows setting security permission at the class level, even when a + * class is contained inside a JAR. + * <p> + * <strong>IMPLEMENTATION NOTE</strong> - Local repositories are searched in + * the order they are added via the initial constructor and/or any subsequent + * calls to <code>addRepository()</code> or <code>addJar()</code>. + * <p> + * <strong>IMPLEMENTATION NOTE</strong> - No check for sealing violations or + * security is made unless a security manager is present. + * + * @author Remy Maucherat + * @author Craig R. McClanahan + * @version $Revision: 1.34 $ $Date: 2004/05/26 15:47:40 $ + */ +public class WebappClassLoader + extends URLClassLoader + implements Reloader, Lifecycle + { + + private static org.apache.commons.logging.Log log= + org.apache.commons.logging.LogFactory.getLog( WebappClassLoader.class ); + + protected class PrivilegedFindResource + implements PrivilegedAction { + + private File file; + private String path; + + PrivilegedFindResource(File file, String path) { + this.file = file; + this.path = path; + } + + public Object run() { + return findResourceInternal(file, path); + } + + } + + + // ------------------------------------------------------- Static Variables + + + /** + * The set of trigger classes that will cause a proposed repository not + * to be added if this class is visible to the class loader that loaded + * this factory class. Typically, trigger classes will be listed for + * components that have been integrated into the JDK for later versions, + * but where the corresponding JAR files are required to run on + * earlier versions. + */ + private static final String[] triggers = { + "javax.servlet.Servlet" // Servlet API + }; + + /** + * Jdk Compatibility Support. + */ + private static JdkCompat jdkCompat = JdkCompat.getJdkCompat(); + + /** + * Set of package names which are not allowed to be loaded from a webapp + * class loader without delegating first. + */ + private static final String[] packageTriggers = { + "javax", // Java extensions + "org.xml.sax", // SAX 1 & 2 + "org.w3c.dom", // DOM 1 & 2 + "org.apache.xerces", // Xerces 1 & 2 + "org.apache.xalan" // Xalan + }; + + + /** + * The string manager for this package. + */ + protected static final StringManager sm = + StringManager.getManager(Constants.Package); + + + // ----------------------------------------------------------- Constructors + + + /** + * Construct a new ClassLoader with no defined repositories and no + * parent ClassLoader. + */ + public WebappClassLoader() { + + super(new URL[0]); + this.parent = getParent(); + system = getSystemClassLoader(); + securityManager = System.getSecurityManager(); + + if (securityManager != null) { + refreshPolicy(); + } + + } + + + /** + * Construct a new ClassLoader with no defined repositories and no + * parent ClassLoader. + */ + public WebappClassLoader(ClassLoader parent) { + + super(new URL[0], parent); + + this.parent = getParent(); + + system = getSystemClassLoader(); + securityManager = System.getSecurityManager(); + + if (securityManager != null) { + refreshPolicy(); + } + } + + + // ----------------------------------------------------- Instance Variables + + + /** + * Associated directory context giving access to the resources in this + * webapp. + */ + protected DirContext resources = null; + + + /** + * The cache of ResourceEntry for classes and resources we have loaded, + * keyed by resource name. + */ + protected HashMap resourceEntries = new HashMap(); + + + /** + * The list of not found resources. + */ + protected HashMap notFoundResources = new HashMap(); + + + /** + * The debugging detail level of this component. + */ + protected int debug = 0; + + + /** + * Should this class loader delegate to the parent class loader + * <strong>before</strong> searching its own repositories (i.e. the + * usual Java2 delegation model)? If set to <code>false</code>, + * this class loader will search its own repositories first, and + * delegate to the parent only if the class or resource is not + * found locally. + */ + protected boolean delegate = false; + + + /** + * Last time a JAR was accessed. + */ + protected long lastJarAccessed = 0L; + + + /** + * The list of local repositories, in the order they should be searched + * for locally loaded classes or resources. + */ + protected String[] repositories = new String[0]; + + + /** + * Repositories URLs, used to cache the result of getURLs. + */ + protected URL[] repositoryURLs = null; + + + /** + * Repositories translated as path in the work directory (for Jasper + * originally), but which is used to generate fake URLs should getURLs be + * called. + */ + protected File[] files = new File[0]; + + + /** + * The list of JARs, in the order they should be searched + * for locally loaded classes or resources. + */ + protected JarFile[] jarFiles = new JarFile[0]; + + + /** + * The list of JARs, in the order they should be searched + * for locally loaded classes or resources. + */ + protected File[] jarRealFiles = new File[0]; + + + /** + * The path which will be monitored for added Jar files. + */ + protected String jarPath = null; + + + /** + * The list of JARs, in the order they should be searched + * for locally loaded classes or resources. + */ + protected String[] jarNames = new String[0]; + + + /** + * The list of JARs last modified dates, in the order they should be + * searched for locally loaded classes or resources. + */ + protected long[] lastModifiedDates = new long[0]; + + + /** + * The list of resources which should be checked when checking for + * modifications. + */ + protected String[] paths = new String[0]; + + + /** + * A list of read File and Jndi Permission's required if this loader + * is for a web application context. + */ + private ArrayList permissionList = new ArrayList(); + + + /** + * Path where resources loaded from JARs will be extracted. + */ + private File loaderDir = null; + + + /** + * The PermissionCollection for each CodeSource for a web + * application context. + */ + private HashMap loaderPC = new HashMap(); + + + /** + * Instance of the SecurityManager installed. + */ + private SecurityManager securityManager = null; + + + /** + * The parent class loader. + */ + private ClassLoader parent = null; + + + /** + * The system class loader. + */ + private ClassLoader system = null; + + + /** + * Has this component been started? + */ + protected boolean started = false; + + + /** + * Has external repositories. + */ + protected boolean hasExternalRepositories = false; + + + /** + * All permission. + */ + private Permission allPermission = new java.security.AllPermission(); + + + // ------------------------------------------------------------- Properties + + + /** + * Get associated resources. + */ + public DirContext getResources() { + + return this.resources; + + } + + + /** + * Set associated resources. + */ + public void setResources(DirContext resources) { + + this.resources = resources; + + } + + + /** + * Return the debugging detail level for this component. + */ + public int getDebug() { + + return (this.debug); + + } + + + /** + * Set the debugging detail level for this component. + * + * @param debug The new debugging detail level + */ + public void setDebug(int debug) { + + this.debug = debug; + + } + + + /** + * Return the "delegate first" flag for this class loader. + */ + public boolean getDelegate() { + + return (this.delegate); + + } + + + /** + * Set the "delegate first" flag for this class loader. + * + * @param delegate The new "delegate first" flag + */ + public void setDelegate(boolean delegate) { + + this.delegate = delegate; + + } + + + /** + * If there is a Java SecurityManager create a read FilePermission + * or JndiPermission for the file directory path. + * + * @param path file directory path + */ + public void addPermission(String path) { + if (path == null) { + return; + } + + if (securityManager != null) { + Permission permission = null; + if( path.startsWith("jndi:") || path.startsWith("jar:jndi:") ) { + if (!path.endsWith("/")) { + path = path + "/"; + } + permission = new JndiPermission(path + "*"); + addPermission(permission); + } else { + if (!path.endsWith(File.separator)) { + permission = new FilePermission(path, "read"); + addPermission(permission); + path = path + File.separator; + } + permission = new FilePermission(path + "-", "read"); + addPermission(permission); + } + } + } + + + /** + * If there is a Java SecurityManager create a read FilePermission + * or JndiPermission for URL. + * + * @param url URL for a file or directory on local system + */ + public void addPermission(URL url) { + if (url != null) { + addPermission(url.toString()); + } + } + + + /** + * If there is a Java SecurityManager create a Permission. + * + * @param url URL for a file or directory on local system + */ + public void addPermission(Permission permission) { + if ((securityManager != null) && (permission != null)) { + permissionList.add(permission); + } + } + + + /** + * Return the JAR path. + */ + public String getJarPath() { + + return this.jarPath; + + } + + + /** + * Change the Jar path. + */ + public void setJarPath(String jarPath) { + + this.jarPath = jarPath; + + } + + + /** + * Change the work directory. + */ + public void setWorkDir(File workDir) { + this.loaderDir = new File(workDir, "loader"); + } + + + // ------------------------------------------------------- Reloader Methods + + + /** + * Add a new repository to the set of places this ClassLoader can look for + * classes to be loaded. + * + * @param repository Name of a source of classes to be loaded, such as a + * directory pathname, a JAR file pathname, or a ZIP file pathname + * + * @exception IllegalArgumentException if the specified repository is + * invalid or does not exist + */ + public void addRepository(String repository) { + + // Ignore any of the standard repositories, as they are set up using + // either addJar or addRepository + if (repository.startsWith("/WEB-INF/lib") + || repository.startsWith("/WEB-INF/classes")) + return; + + // Add this repository to our underlying class loader + try { + URL url = new URL(repository); + super.addURL(url); + hasExternalRepositories = true; + repositoryURLs = null; + } catch (MalformedURLException e) { + IllegalArgumentException iae = new IllegalArgumentException + ("Invalid repository: " + repository); + jdkCompat.chainException(iae, e); + throw iae; + } + + } + + + /** + * Add a new repository to the set of places this ClassLoader can look for + * classes to be loaded. + * + * @param repository Name of a source of classes to be loaded, such as a + * directory pathname, a JAR file pathname, or a ZIP file pathname + * + * @exception IllegalArgumentException if the specified repository is + * invalid or does not exist + */ + synchronized void addRepository(String repository, File file) { + + // Note : There should be only one (of course), but I think we should + // keep this a bit generic + + if (repository == null) + return; + + if (log.isDebugEnabled()) + log.debug("addRepository(" + repository + ")"); + + int i; + + // Add this repository to our internal list + String[] result = new String[repositories.length + 1]; + for (i = 0; i < repositories.length; i++) { + result[i] = repositories[i]; + } + result[repositories.length] = repository; + repositories = result; + + // Add the file to the list + File[] result2 = new File[files.length + 1]; + for (i = 0; i < files.length; i++) { + result2[i] = files[i]; + } + result2[files.length] = file; + files = result2; + + } + + + synchronized void addJar(String jar, JarFile jarFile, File file) + throws IOException { + + if (jar == null) + return; + if (jarFile == null) + return; + if (file == null) + return; + + if (log.isDebugEnabled()) + log.debug("addJar(" + jar + ")"); + + int i; + + if ((jarPath != null) && (jar.startsWith(jarPath))) { + + String jarName = jar.substring(jarPath.length()); + while (jarName.startsWith("/")) + jarName = jarName.substring(1); + + String[] result = new String[jarNames.length + 1]; + for (i = 0; i < jarNames.length; i++) { + result[i] = jarNames[i]; + } + result[jarNames.length] = jarName; + jarNames = result; + + } + + try { + + // Register the JAR for tracking + + long lastModified = + ((ResourceAttributes) resources.getAttributes(jar)) + .getLastModified(); + + String[] result = new String[paths.length + 1]; + for (i = 0; i < paths.length; i++) { + result[i] = paths[i]; + } + result[paths.length] = jar; + paths = result; + + long[] result3 = new long[lastModifiedDates.length + 1]; + for (i = 0; i < lastModifiedDates.length; i++) { + result3[i] = lastModifiedDates[i]; + } + result3[lastModifiedDates.length] = lastModified; + lastModifiedDates = result3; + + } catch (NamingException e) { + // Ignore + } + + // If the JAR currently contains invalid classes, don't actually use it + // for classloading + if (!validateJarFile(file)) + return; + + JarFile[] result2 = new JarFile[jarFiles.length + 1]; + for (i = 0; i < jarFiles.length; i++) { + result2[i] = jarFiles[i]; + } + result2[jarFiles.length] = jarFile; + jarFiles = result2; + + // Add the file to the list + File[] result4 = new File[jarRealFiles.length + 1]; + for (i = 0; i < jarRealFiles.length; i++) { + result4[i] = jarRealFiles[i]; + } + result4[jarRealFiles.length] = file; + jarRealFiles = result4; + } + + + /** + * Return a String array of the current repositories for this class + * loader. If there are no repositories, a zero-length array is + * returned.For security reason, returns a clone of the Array (since + * String are immutable). + */ + public String[] findRepositories() { + + return ((String[])repositories.clone()); + + } + + + /** + * Have one or more classes or resources been modified so that a reload + * is appropriate? + */ + public boolean modified() { + + if (log.isDebugEnabled()) + log.debug("modified()"); + + // Checking for modified loaded resources + int length = paths.length; + + // A rare race condition can occur in the updates of the two arrays + // It's totally ok if the latest class added is not checked (it will + // be checked the next time + int length2 = lastModifiedDates.length; + if (length > length2) + length = length2; + + for (int i = 0; i < length; i++) { + try { + long lastModified = + ((ResourceAttributes) resources.getAttributes(paths[i])) + .getLastModified(); + if (lastModified != lastModifiedDates[i]) { + if( log.isDebugEnabled() ) + log.debug(" Resource '" + paths[i] + + "' was modified; Date is now: " + + new java.util.Date(lastModified) + " Was: " + + new java.util.Date(lastModifiedDates[i])); + return (true); + } + } catch (NamingException e) { + log.error(" Resource '" + paths[i] + "' is missing"); + return (true); + } + } + + length = jarNames.length; + + // Check if JARs have been added or removed + if (getJarPath() != null) { + + try { + NamingEnumeration enum = resources.listBindings(getJarPath()); + int i = 0; + while (enum.hasMoreElements() && (i < length)) { + NameClassPair ncPair = (NameClassPair) enum.nextElement(); + String name = ncPair.getName(); + // Ignore non JARs present in the lib folder + if (!name.endsWith(".jar")) + continue; + if (!name.equals(jarNames[i])) { + // Missing JAR + log.info(" Additional JARs have been added : '" + + name + "'"); + return (true); + } + i++; + } + if (enum.hasMoreElements()) { + while (enum.hasMoreElements()) { + NameClassPair ncPair = + (NameClassPair) enum.nextElement(); + String name = ncPair.getName(); + // Additional non-JAR files are allowed + if (name.endsWith(".jar")) { + // There was more JARs + log.info(" Additional JARs have been added"); + return (true); + } + } + } else if (i < jarNames.length) { + // There was less JARs + log.info(" Additional JARs have been added"); + return (true); + } + } catch (NamingException e) { + if (log.isDebugEnabled()) + log.debug(" Failed tracking modifications of '" + + getJarPath() + "'"); + } catch (ClassCastException e) { + log.error(" Failed tracking modifications of '" + + getJarPath() + "' : " + e.getMessage()); + } + + } + + // No classes have been modified + return (false); + + } + + + /** + * Render a String representation of this object. + */ + public String toString() { + + StringBuffer sb = new StringBuffer("WebappClassLoader\r\n"); + sb.append(" delegate: "); + sb.append(delegate); + sb.append("\r\n"); + sb.append(" repositories:\r\n"); + if (repositories != null) { + for (int i = 0; i < repositories.length; i++) { + sb.append(" "); + sb.append(repositories[i]); + sb.append("\r\n"); + } + } + if (this.parent != null) { + sb.append("----------> Parent Classloader:\r\n"); + sb.append(this.parent.toString()); + sb.append("\r\n"); + } + return (sb.toString()); + + } + + + // ---------------------------------------------------- ClassLoader Methods + + + /** + * Add the specified URL to the classloader. + */ + protected void addURL(URL url) { + super.addURL(url); + hasExternalRepositories = true; + repositoryURLs = null; + } + + + /** + * Find the specified class in our local repositories, if possible. If + * not found, throw <code>ClassNotFoundException</code>. + * + * @param name Name of the class to be loaded + * + * @exception ClassNotFoundException if the class was not found + */ + public Class findClass(String name) throws ClassNotFoundException { + + if (log.isDebugEnabled()) + log.debug(" findClass(" + name + ")"); + + // (1) Permission to define this class when using a SecurityManager + if (securityManager != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + try { + if (log.isTraceEnabled()) + log.trace(" securityManager.checkPackageDefinition"); + securityManager.checkPackageDefinition(name.substring(0,i)); + } catch (Exception se) { + if (log.isTraceEnabled()) + log.trace(" -->Exception-->ClassNotFoundException", se); + throw new ClassNotFoundException(name, se); + } + } + } + + // Ask our superclass to locate this class, if possible + // (throws ClassNotFoundException if it is not found) + Class clazz = null; + try { + if (log.isTraceEnabled()) + log.trace(" findClassInternal(" + name + ")"); + try { + clazz = findClassInternal(name); + } catch(ClassNotFoundException cnfe) { + if (!hasExternalRepositories) { + throw cnfe; + } + } catch(AccessControlException ace) { + throw new ClassNotFoundException(name, ace); + } catch (RuntimeException e) { + if (log.isTraceEnabled()) + log.trace(" -->RuntimeException Rethrown", e); + throw e; + } + if ((clazz == null) && hasExternalRepositories) { + try { + clazz = super.findClass(name); + } catch(AccessControlException ace) { + throw new ClassNotFoundException(name, ace); + } catch (RuntimeException e) { + if (log.isTraceEnabled()) + log.trace(" -->RuntimeException Rethrown", e); + throw e; + } + } + if (clazz == null) { + if (log.isDebugEnabled()) + log.debug(" --> Returning ClassNotFoundException"); + throw new ClassNotFoundException(name); + } + } catch (ClassNotFoundException e) { + if (log.isTraceEnabled()) + log.trace(" --> Passing on ClassNotFoundException"); + throw e; + } + + // Return the class we have located + if (log.isTraceEnabled()) + log.debug(" Returning class " + clazz); + if ((log.isTraceEnabled()) && (clazz != null)) + log.debug(" Loaded by " + clazz.getClassLoader()); + return (clazz); + + } + + + /** + * Find the specified resource in our local repository, and return a + * <code>URL</code> refering to it, or <code>null</code> if this resource + * cannot be found. + * + * @param name Name of the resource to be found + */ + public URL findResource(final String name) { + + if (log.isDebugEnabled()) + log.debug(" findResource(" + name + ")"); + + URL url = null; + + ResourceEntry entry = (ResourceEntry) resourceEntries.get(name); + if (entry == null) { + entry = findResourceInternal(name, name); + } + if (entry != null) { + url = entry.source; + } + + if ((url == null) && hasExternalRepositories) + url = super.findResource(name); + + if (log.isDebugEnabled()) { + if (url != null) + log.debug(" --> Returning '" + url.toString() + "'"); + else + log.debug(" --> Resource not found, returning null"); + } + return (url); + + } + + + /** + * Return an enumeration of <code>URLs</code> representing all of the + * resources with the given name. If no resources with this name are + * found, return an empty enumeration. + * + * @param name Name of the resources to be found + * + * @exception IOException if an input/output error occurs + */ + public Enumeration findResources(String name) throws IOException { + + if (log.isDebugEnabled()) + log.debug(" findResources(" + name + ")"); + + Vector result = new Vector(); + + int jarFilesLength = jarFiles.length; + int repositoriesLength = repositories.length; + + int i; + + // Looking at the repositories + for (i = 0; i < repositoriesLength; i++) { + try { + String fullPath = repositories[i] + name; + resources.lookup(fullPath); + // Note : Not getting an exception here means the resource was + // found + try { + result.addElement(getURI(new File(files[i], name))); + } catch (MalformedURLException e) { + // Ignore + } + } catch (NamingException e) { + } + } + + // Looking at the JAR files + synchronized (jarFiles) { + openJARs(); + for (i = 0; i < jarFilesLength; i++) { + JarEntry jarEntry = jarFiles[i].getJarEntry(name); + if (jarEntry != null) { + try { + String jarFakeUrl = getURI(jarRealFiles[i]).toString(); + jarFakeUrl = "jar:" + jarFakeUrl + "!/" + name; + result.addElement(new URL(jarFakeUrl)); + } catch (MalformedURLException e) { + // Ignore + } + } + } + } + + // Adding the results of a call to the superclass + if (hasExternalRepositories) { + + Enumeration otherResourcePaths = super.findResources(name); + + while (otherResourcePaths.hasMoreElements()) { + result.addElement(otherResourcePaths.nextElement()); + } + + } + + return result.elements(); + + } + + + /** + * Find the resource with the given name. A resource is some data + * (images, audio, text, etc.) that can be accessed by class code in a + * way that is independent of the location of the code. The name of a + * resource is a "/"-separated path name that identifies the resource. + * If the resource cannot be found, return <code>null</code>. + * <p> + * This method searches according to the following algorithm, returning + * as soon as it finds the appropriate URL. If the resource cannot be + * found, returns <code>null</code>. + * <ul> + * <li>If the <code>delegate</code> property is set to <code>true</code>, + * call the <code>getResource()</code> method of the parent class + * loader, if any.</li> + * <li>Call <code>findResource()</code> to find this resource in our + * locally defined repositories.</li> + * <li>Call the <code>getResource()</code> method of the parent class + * loader, if any.</li> + * </ul> + * + * @param name Name of the resource to return a URL for + */ + public URL getResource(String name) { + + if (log.isDebugEnabled()) + log.debug("getResource(" + name + ")"); + URL url = null; + + // (1) Delegate to parent if requested + if (delegate) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader " + parent); + ClassLoader loader = parent; + if (loader == null) + loader = system; + url = loader.getResource(name); + if (url != null) { + if (log.isDebugEnabled()) + log.debug(" --> Returning '" + url.toString() + "'"); + return (url); + } + } + + // (2) Search local repositories + url = findResource(name); + if (url != null) { + // Locating the repository for special handling in the case + // of a JAR + ResourceEntry entry = (ResourceEntry) resourceEntries.get(name); + try { + String repository = entry.codeBase.toString(); + if ((repository.endsWith(".jar")) + && (!(name.endsWith(".class")))) { + // Copy binary content to the work directory if not present + File resourceFile = new File(loaderDir, name); + url = resourceFile.toURL(); + } + } catch (Exception e) { + // Ignore + } + if (log.isDebugEnabled()) + log.debug(" --> Returning '" + url.toString() + "'"); + return (url); + } + + // (3) Delegate to parent unconditionally if not already attempted + if( !delegate ) { + ClassLoader loader = parent; + if (loader == null) + loader = system; + url = loader.getResource(name); + if (url != null) { + if (log.isDebugEnabled()) + log.debug(" --> Returning '" + url.toString() + "'"); + return (url); + } + } + + // (4) Resource was not found + if (log.isDebugEnabled()) + log.debug(" --> Resource not found, returning null"); + return (null); + + } + + + /** + * Find the resource with the given name, and return an input stream + * that can be used for reading it. The search order is as described + * for <code>getResource()</code>, after checking to see if the resource + * data has been previously cached. If the resource cannot be found, + * return <code>null</code>. + * + * @param name Name of the resource to return an input stream for + */ + public InputStream getResourceAsStream(String name) { + + if (log.isDebugEnabled()) + log.debug("getResourceAsStream(" + name + ")"); + InputStream stream = null; + + // (0) Check for a cached copy of this resource + stream = findLoadedResource(name); + if (stream != null) { + if (log.isDebugEnabled()) + log.debug(" --> Returning stream from cache"); + return (stream); + } + + // (1) Delegate to parent if requested + if (delegate) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader " + parent); + ClassLoader loader = parent; + if (loader == null) + loader = system; + stream = loader.getResourceAsStream(name); + if (stream != null) { + // FIXME - cache??? + if (log.isDebugEnabled()) + log.debug(" --> Returning stream from parent"); + return (stream); + } + } + + // (2) Search local repositories + if (log.isDebugEnabled()) + log.debug(" Searching local repositories"); + URL url = findResource(name); + if (url != null) { + // FIXME - cache??? + if (log.isDebugEnabled()) + log.debug(" --> Returning stream from local"); + stream = findLoadedResource(name); + try { + if (hasExternalRepositories && (stream == null)) + stream = url.openStream(); + } catch (IOException e) { + ; // Ignore + } + if (stream != null) + return (stream); + } + + // (3) Delegate to parent unconditionally + if (!delegate) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader unconditionally " + parent); + ClassLoader loader = parent; + if (loader == null) + loader = system; + stream = loader.getResourceAsStream(name); + if (stream != null) { + // FIXME - cache??? + if (log.isDebugEnabled()) + log.debug(" --> Returning stream from parent"); + return (stream); + } + } + + // (4) Resource was not found + if (log.isDebugEnabled()) + log.debug(" --> Resource not found, returning null"); + return (null); + + } + + + /** + * Load the class with the specified name. This method searches for + * classes in the same manner as <code>loadClass(String, boolean)</code> + * with <code>false</code> as the second argument. + * + * @param name Name of the class to be loaded + * + * @exception ClassNotFoundException if the class was not found + */ + public Class loadClass(String name) throws ClassNotFoundException { + + return (loadClass(name, false)); + + } + + + /** + * Load the class with the specified name, searching using the following + * algorithm until it finds and returns the class. If the class cannot + * be found, returns <code>ClassNotFoundException</code>. + * <ul> + * <li>Call <code>findLoadedClass(String)</code> to check if the + * class has already been loaded. If it has, the same + * <code>Class</code> object is returned.</li> + * <li>If the <code>delegate</code> property is set to <code>true</code>, + * call the <code>loadClass()</code> method of the parent class + * loader, if any.</li> + * <li>Call <code>findClass()</code> to find this class in our locally + * defined repositories.</li> + * <li>Call the <code>loadClass()</code> method of our parent + * class loader, if any.</li> + * </ul> + * If the class was found using the above steps, and the + * <code>resolve</code> flag is <code>true</code>, this method will then + * call <code>resolveClass(Class)</code> on the resulting Class object. + * + * @param name Name of the class to be loaded + * @param resolve If <code>true</code> then resolve the class + * + * @exception ClassNotFoundException if the class was not found + */ + public Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + + if (log.isDebugEnabled()) + log.debug("loadClass(" + name + ", " + resolve + ")"); + Class clazz = null; + + // Don't load classes if class loader is stopped + if (!started) { + log.info(sm.getString("webappClassLoader.stopped")); + throw new ThreadDeath(); + } + + // (0) Check our previously loaded local class cache + clazz = findLoadedClass0(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Returning class from cache"); + if (resolve) + resolveClass(clazz); + return (clazz); + } + + // (0.1) Check our previously loaded class cache + clazz = findLoadedClass(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Returning class from cache"); + if (resolve) + resolveClass(clazz); + return (clazz); + } + + // (0.2) Try loading the class with the system class loader, to prevent + // the webapp from overriding J2SE classes + try { + clazz = system.loadClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return (clazz); + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // (0.5) Permission to access this class when using a SecurityManager + if (securityManager != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + try { + securityManager.checkPackageAccess(name.substring(0,i)); + } catch (SecurityException se) { + String error = "Security Violation, attempt to use " + + "Restricted Class: " + name; + log.info(error, se); + throw new ClassNotFoundException(error, se); + } + } + } + + boolean delegateLoad = delegate || filter(name); + + // (1) Delegate to our parent if requested + if (delegateLoad) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader1 " + parent); + ClassLoader loader = parent; + if (loader == null) + loader = system; + try { + clazz = loader.loadClass(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Loading class from parent"); + if (resolve) + resolveClass(clazz); + return (clazz); + } + } catch (ClassNotFoundException e) { + ; + } + } + + // (2) Search local repositories + if (log.isDebugEnabled()) + log.debug(" Searching local repositories"); + try { + clazz = findClass(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Loading class from local repository"); + if (resolve) + resolveClass(clazz); + return (clazz); + } + } catch (ClassNotFoundException e) { + ; + } + + // (3) Delegate to parent unconditionally + if (!delegateLoad) { + if (log.isDebugEnabled()) + log.debug(" Delegating to parent classloader at end: " + parent); + ClassLoader loader = parent; + if (loader == null) + loader = system; + try { + clazz = loader.loadClass(name); + if (clazz != null) { + if (log.isDebugEnabled()) + log.debug(" Loading class from parent"); + if (resolve) + resolveClass(clazz); + return (clazz); + } + } catch (ClassNotFoundException e) { + ; + } + } + + throw new ClassNotFoundException(name); + } + + + /** + * Get the Permissions for a CodeSource. If this instance + * of WebappClassLoader is for a web application context, + * add read FilePermission or JndiPermissions for the base + * directory (if unpacked), + * the context URL, and jar file resources. + * + * @param codeSource where the code was loaded from + * @return PermissionCollection for CodeSource + */ + protected PermissionCollection getPermissions(CodeSource codeSource) { + + String codeUrl = codeSource.getLocation().toString(); + PermissionCollection pc; + if ((pc = (PermissionCollection)loaderPC.get(codeUrl)) == null) { + pc = super.getPermissions(codeSource); + if (pc != null) { + Iterator perms = permissionList.iterator(); + while (perms.hasNext()) { + Permission p = (Permission)perms.next(); + pc.add(p); + } + loaderPC.put(codeUrl,pc); + } + } + return (pc); + + } + + + /** + * Returns the search path of URLs for loading classes and resources. + * This includes the original list of URLs specified to the constructor, + * along with any URLs subsequently appended by the addURL() method. + * @return the search path of URLs for loading classes and resources. + */ + public URL[] getURLs() { + + if (repositoryURLs != null) { + return repositoryURLs; + } + + URL[] external = super.getURLs(); + + int filesLength = files.length; + int jarFilesLength = jarRealFiles.length; + int length = filesLength + jarFilesLength + external.length; + int i; + + try { + + URL[] urls = new URL[length]; + for (i = 0; i < length; i++) { + if (i < filesLength) { + urls[i] = getURL(files[i]); + } else if (i < filesLength + jarFilesLength) { + urls[i] = getURL(jarRealFiles[i - filesLength]); + } else { + urls[i] = external[i - filesLength - jarFilesLength]; + } + } + + repositoryURLs = urls; + + } catch (MalformedURLException e) { + repositoryURLs = new URL[0]; + } + + return repositoryURLs; + + } + + + // ------------------------------------------------------ Lifecycle Methods + + + /** + * Add a lifecycle event listener to this component. + * + * @param listener The listener to add + */ + public void addLifecycleListener(LifecycleListener listener) { + } + + + /** + * Get the lifecycle listeners associated with this lifecycle. If this + * Lifecycle has no listeners registered, a zero-length array is returned. + */ + public LifecycleListener[] findLifecycleListeners() { + return new LifecycleListener[0]; + } + + + /** + * Remove a lifecycle event listener from this component. + * + * @param listener The listener to remove + */ + public void removeLifecycleListener(LifecycleListener listener) { + } + + + /** + * Start the class loader. + * + * @exception LifecycleException if a lifecycle error occurs + */ + public void start() throws LifecycleException { + + started = true; + + } + + + /** + * Stop the class loader. + * + * @exception LifecycleException if a lifecycle error occurs + */ + public void stop() throws LifecycleException { + + started = false; + + int length = files.length; + for (int i = 0; i < length; i++) { + files[i] = null; + } + + length = jarFiles.length; + for (int i = 0; i < length; i++) { + try { + if (jarFiles[i] != null) { + jarFiles[i].close(); + } + } catch (IOException e) { + // Ignore + } + jarFiles[i] = null; + } + + notFoundResources.clear(); + resourceEntries.clear(); + resources = null; + repositories = null; + repositoryURLs = null; + files = null; + jarFiles = null; + jarRealFiles = null; + jarPath = null; + jarNames = null; + lastModifiedDates = null; + paths = null; + hasExternalRepositories = false; + parent = null; + + permissionList.clear(); + loaderPC.clear(); + + if (loaderDir != null) { + deleteDir(loaderDir); + } + + org.apache.commons.logging.LogFactory.release(this); + + } + + + /** + * Used to periodically signal to the classloader to release + * JAR resources. + */ + public void closeJARs(boolean force) { + if (jarFiles.length > 0) { + try { + synchronized (jarFiles) { + if (force || (System.currentTimeMillis() + > (lastJarAccessed + 90000))) { + for (int i = 0; i < jarFiles.length; i++) { + if (jarFiles[i] != null) { + jarFiles[i].close(); + jarFiles[i] = null; + } + } + } + } + } catch (IOException e) { + log("Failed to close JAR", e); + } + } + } + + + // ------------------------------------------------------ Protected Methods + + + /** + * Used to periodically signal to the classloader to release JAR resources. + */ + protected void openJARs() { + if (started && (jarFiles.length > 0)) { + lastJarAccessed = System.currentTimeMillis(); + if (jarFiles[0] == null) { + try { + for (int i = 0; i < jarFiles.length; i++) { + jarFiles[i] = new JarFile(jarRealFiles[i]); + } + } catch (IOException e) { + log("Failed to open JAR", e); + } + } + } + } + + + /** + * Find specified class in local repositories. + * + * @return the loaded class, or null if the class isn't found + */ + protected Class findClassInternal(String name) + throws ClassNotFoundException { + + if (!validate(name)) + throw new ClassNotFoundException(name); + + String tempPath = name.replace('.', '/'); + String classPath = tempPath + ".class"; + + ResourceEntry entry = null; + + entry = findResourceInternal(name, classPath); + + if ((entry == null) || (entry.binaryContent == null)) + throw new ClassNotFoundException(name); + + Class clazz = entry.loadedClass; + if (clazz != null) + return clazz; + + // Looking up the package + String packageName = null; + int pos = name.lastIndexOf('.'); + if (pos != -1) + packageName = name.substring(0, pos); + + Package pkg = null; + + if (packageName != null) { + + pkg = getPackage(packageName); + + // Define the package (if null) + if (pkg == null) { + if (entry.manifest == null) { + definePackage(packageName, null, null, null, null, null, + null, null); + } else { + definePackage(packageName, entry.manifest, entry.codeBase); + } + } + + } + + // Create the code source object + CodeSource codeSource = + new CodeSource(entry.codeBase, entry.certificates); + + if (securityManager != null) { + + // Checking sealing + if (pkg != null) { + boolean sealCheck = true; + if (pkg.isSealed()) { + sealCheck = pkg.isSealed(entry.codeBase); + } else { + sealCheck = (entry.manifest == null) + || !isPackageSealed(packageName, entry.manifest); + } + if (!sealCheck) + throw new SecurityException + ("Sealing violation loading " + name + " : Package " + + packageName + " is sealed."); + } + + } + + if (entry.loadedClass == null) { + synchronized (this) { + if (entry.loadedClass == null) { + clazz = defineClass(name, entry.binaryContent, 0, + entry.binaryContent.length, + codeSource); + entry.loadedClass = clazz; + entry.binaryContent = null; + entry.source = null; + entry.codeBase = null; + entry.manifest = null; + entry.certificates = null; + } else { + clazz = entry.loadedClass; + } + } + } else { + clazz = entry.loadedClass; + } + + return clazz; + + } + + /** + * Find specified resource in local repositories. This block + * will execute under an AccessControl.doPrivilege block. + * + * @return the loaded resource, or null if the resource isn't found + */ + private ResourceEntry findResourceInternal(File file, String path){ + ResourceEntry entry = new ResourceEntry(); + try { + entry.source = getURI(new File(file, path)); + entry.codeBase = getURL(new File(file, path)); + } catch (MalformedURLException e) { + return null; + } + return entry; + } + + + /** + * Find specified resource in local repositories. + * + * @return the loaded resource, or null if the resource isn't found + */ + protected ResourceEntry findResourceInternal(String name, String path) { + + if (!started) { + log.info(sm.getString("webappClassLoader.stopped")); + return null; + } + + if ((name == null) || (path == null)) + return null; + + ResourceEntry entry = (ResourceEntry) resourceEntries.get(name); + if (entry != null) + return entry; + + int contentLength = -1; + InputStream binaryStream = null; + + int jarFilesLength = jarFiles.length; + int repositoriesLength = repositories.length; + + int i; + + Resource resource = null; + + for (i = 0; (entry == null) && (i < repositoriesLength); i++) { + try { + + String fullPath = repositories[i] + path; + + Object lookupResult = resources.lookup(fullPath); + if (lookupResult instanceof Resource) { + resource = (Resource) lookupResult; + } + + // Note : Not getting an exception here means the resource was + // found + if (securityManager != null) { + PrivilegedAction dp = + new PrivilegedFindResource(files[i], path); + entry = (ResourceEntry)AccessController.doPrivileged(dp); + } else { + entry = findResourceInternal(files[i], path); + } + + ResourceAttributes attributes = + (ResourceAttributes) resources.getAttributes(fullPath); + contentLength = (int) attributes.getContentLength(); + entry.lastModified = attributes.getLastModified(); + + if (resource != null) { + + try { + binaryStream = resource.streamContent(); + } catch (IOException e) { + return null; + } + + // Register the full path for modification checking + // Note: Only syncing on a 'constant' object is needed + synchronized (allPermission) { + + int j; + + long[] result2 = + new long[lastModifiedDates.length + 1]; + for (j = 0; j < lastModifiedDates.length; j++) { + result2[j] = lastModifiedDates[j]; + } + result2[lastModifiedDates.length] = entry.lastModified; + lastModifiedDates = result2; + + String[] result = new String[paths.length + 1]; + for (j = 0; j < paths.length; j++) { + result[j] = paths[j]; + } + result[paths.length] = fullPath; + paths = result; + + } + + } + + } catch (NamingException e) { + } + } + + if ((entry == null) && (notFoundResources.containsKey(name))) + return null; + + JarEntry jarEntry = null; + + synchronized (jarFiles) { + + openJARs(); + for (i = 0; (entry == null) && (i < jarFilesLength); i++) { + + jarEntry = jarFiles[i].getJarEntry(path); + + if (jarEntry != null) { + + entry = new ResourceEntry(); + try { + entry.codeBase = getURL(jarRealFiles[i]); + String jarFakeUrl = getURI(jarRealFiles[i]).toString(); + jarFakeUrl = "jar:" + jarFakeUrl + "!/" + path; + entry.source = new URL(jarFakeUrl); + entry.lastModified = jarRealFiles[i].lastModified(); + } catch (MalformedURLException e) { + return null; + } + contentLength = (int) jarEntry.getSize(); + try { + entry.manifest = jarFiles[i].getManifest(); + binaryStream = jarFiles[i].getInputStream(jarEntry); + } catch (IOException e) { + return null; + } + + // Extract resources contained in JAR to the workdir + if (!(path.endsWith(".class"))) { + byte[] buf = new byte[1024]; + File resourceFile = new File + (loaderDir, jarEntry.getName()); + if (!resourceFile.exists()) { + Enumeration entries = jarFiles[i].entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry2 = + (JarEntry) entries.nextElement(); + if (!(jarEntry2.isDirectory()) + && (!jarEntry2.getName().endsWith + (".class"))) { + resourceFile = new File + (loaderDir, jarEntry2.getName()); + resourceFile.getParentFile().mkdirs(); + FileOutputStream os = null; + InputStream is = null; + try { + is = jarFiles[i].getInputStream + (jarEntry2); + os = new FileOutputStream + (resourceFile); + while (true) { + int n = is.read(buf); + if (n <= 0) { + break; + } + os.write(buf, 0, n); + } + } catch (IOException e) { + // Ignore + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + } + try { + if (os != null) { + os.close(); + } + } catch (IOException e) { + } + } + } + } + } + } + + } + + } + + if (entry == null) { + synchronized (notFoundResources) { + notFoundResources.put(name, name); + } + return null; + } + + if (binaryStream != null) { + + byte[] binaryContent = new byte[contentLength]; + + try { + int pos = 0; + + while (true) { + int n = binaryStream.read(binaryContent, pos, + binaryContent.length - pos); + if (n <= 0) + break; + pos += n; + } + binaryStream.close(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + entry.binaryContent = binaryContent; + + // The certificates are only available after the JarEntry + // associated input stream has been fully read + if (jarEntry != null) { + entry.certificates = jarEntry.getCertificates(); + } + + } + + } + + // Add the entry in the local resource repository + synchronized (resourceEntries) { + // Ensures that all the threads which may be in a race to load + // a particular class all end up with the same ResourceEntry + // instance + ResourceEntry entry2 = (ResourceEntry) resourceEntries.get(name); + if (entry2 == null) { + resourceEntries.put(name, entry); + } else { + entry = entry2; + } + } + + return entry; + + } + + + /** + * Returns true if the specified package name is sealed according to the + * given manifest. + */ + protected boolean isPackageSealed(String name, Manifest man) { + + String path = name + "/"; + Attributes attr = man.getAttributes(path); + String sealed = null; + if (attr != null) { + sealed = attr.getValue(Name.SEALED); + } + if (sealed == null) { + if ((attr = man.getMainAttributes()) != null) { + sealed = attr.getValue(Name.SEALED); + } + } + return "true".equalsIgnoreCase(sealed); + + } + + + /** + * Finds the resource with the given name if it has previously been + * loaded and cached by this class loader, and return an input stream + * to the resource data. If this resource has not been cached, return + * <code>null</code>. + * + * @param name Name of the resource to return + */ + protected InputStream findLoadedResource(String name) { + + ResourceEntry entry = (ResourceEntry) resourceEntries.get(name); + if (entry != null) { + if (entry.binaryContent != null) + return new ByteArrayInputStream(entry.binaryContent); + } + return (null); + + } + + + /** + * Finds the class with the given name if it has previously been + * loaded and cached by this class loader, and return the Class object. + * If this class has not been cached, return <code>null</code>. + * + * @param name Name of the resource to return + */ + protected Class findLoadedClass0(String name) { + + ResourceEntry entry = (ResourceEntry) resourceEntries.get(name); + if (entry != null) { + return entry.loadedClass; + } + return (null); // FIXME - findLoadedResource() + + } + + + /** + * Refresh the system policy file, to pick up eventual changes. + */ + protected void refreshPolicy() { + + try { + // The policy file may have been modified to adjust + // permissions, so we're reloading it when loading or + // reloading a Context + Policy policy = Policy.getPolicy(); + policy.refresh(); + } catch (AccessControlException e) { + // Some policy files may restrict this, even for the core, + // so this exception is ignored + } + + } + + + /** + * Filter classes. + * + * @param name class name + * @return true if the class should be filtered + */ + protected boolean filter(String name) { + + if (name == null) + return false; + + // Looking up the package + String packageName = null; + int pos = name.lastIndexOf('.'); + if (pos != -1) + packageName = name.substring(0, pos); + else + return false; + + for (int i = 0; i < packageTriggers.length; i++) { + if (packageName.startsWith(packageTriggers[i])) + return true; + } + + return false; + + } + + + /** + * Validate a classname. As per SRV.9.7.2, we must restict loading of + * classes from J2SE (java.*) and classes of the servlet API + * (javax.servlet.*). That should enhance robustness and prevent a number + * of user error (where an older version of servlet.jar would be present + * in /WEB-INF/lib). + * + * @param name class name + * @return true if the name is valid + */ + protected boolean validate(String name) { + + if (name == null) + return false; + if (name.startsWith("java.")) + return false; + + return true; + + } + + + /** + * Check the specified JAR file, and return <code>true</code> if it does + * not contain any of the trigger classes. + * + * @param jarfile The JAR file to be checked + * + * @exception IOException if an input/output error occurs + */ + private boolean validateJarFile(File jarfile) + throws IOException { + + if (triggers == null) + return (true); + JarFile jarFile = new JarFile(jarfile); + for (int i = 0; i < triggers.length; i++) { + Class clazz = null; + try { + if (parent != null) { + clazz = parent.loadClass(triggers[i]); + } else { + clazz = Class.forName(triggers[i]); + } + } catch (Throwable t) { + clazz = null; + } + if (clazz == null) + continue; + String name = triggers[i].replace('.', '/') + ".class"; + if (log.isDebugEnabled()) + log.debug(" Checking for " + name); + JarEntry jarEntry = jarFile.getJarEntry(name); + if (jarEntry != null) { + log.info("validateJarFile(" + jarfile + + ") - jar not loaded. See Servlet Spec 2.3, " + + "section 9.7.2. Offending class: " + name); + jarFile.close(); + return (false); + } + } + jarFile.close(); + return (true); + + } + + + /** + * Get URL. + */ + protected URL getURL(File file) + throws MalformedURLException { + + File realFile = file; + try { + realFile = realFile.getCanonicalFile(); + } catch (IOException e) { + // Ignore + } + return realFile.toURL(); + + } + + + /** + * Get URL. + */ + protected URL getURI(File file) + throws MalformedURLException { + + return jdkCompat.getURI(file); + + } + + + /** + * Delete the specified directory, including all of its contents and + * subdirectories recursively. + * + * @param dir File object representing the directory to be deleted + */ + protected static void deleteDir(File dir) { + + String files[] = dir.list(); + if (files == null) { + files = new String[0]; + } + for (int i = 0; i < files.length; i++) { + File file = new File(dir, files[i]); + if (file.isDirectory()) { + deleteDir(file); + } else { + file.delete(); + } + } + dir.delete(); + + } + + + /** + * Log a debugging output message. + * + * @param message Message to be logged + */ + private void log(String message) { + + System.out.println("WebappClassLoader: " + message); + + } + + + /** + * Log a debugging output message with an exception. + * + * @param message Message to be logged + * @param throwable Exception to be logged + */ + private void log(String message, Throwable throwable) { + + System.out.println("WebappClassLoader: " + message); + throwable.printStackTrace(System.out); + + } + +} +