/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.loader;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
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.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import javax.naming.Binding;
import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;

import org.apache.catalina.Globals;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.naming.JndiPermission;
import org.apache.naming.resources.ProxyDirContext;
import org.apache.naming.resources.Resource;
import org.apache.naming.resources.ResourceAttributes;
import org.apache.tomcat.InstrumentableClassLoader;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.buf.UriUtil;
import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.PermissionCheck;

/**
 * Specialized web application class loader.
 * <p>
 * This class loader is a full reimplementation of the
 * <code>URLClassLoader</code> from the JDK. It is designed to be fully
 * compatible with a normal <code>URLClassLoader</code>, although its internal
 * behavior may be completely different.
 * <p>
 * <strong>IMPLEMENTATION NOTE</strong> - By default, this class loader follows
 * the delegation model required by 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 repositories. The <code>delegate</code> property
 * allows an application to modify this behavior to move the parent class loader
 * ahead of the local repositories.
 * <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.
 * <p>
 * TODO: Is there any requirement to provide a proper Lifecycle implementation
 *       rather than the current stubbed implementation?
 * <strong>IMPLEMENTATION NOTE</strong> - As of 7.0.64/8.0, this class
 * loader implements {@link InstrumentableClassLoader}, permitting web
 * application classes to instrument other classes in the same web
 * application. It does not permit instrumentation of system or container
 * classes or classes in other web apps.
 *
 * @author Remy Maucherat
 * @author Craig R. McClanahan
 */
public abstract class WebappClassLoaderBase extends URLClassLoader
        implements Lifecycle, InstrumentableClassLoader, PermissionCheck {

    private static final org.apache.juli.logging.Log log =
            org.apache.juli.logging.LogFactory.getLog(WebappClassLoaderBase.class);

    private static final Charset CHARSET_UTF8 = Charset.forName("UTF-8");
    /**
     * List of ThreadGroup names to ignore when scanning for web application
     * started threads that need to be shut down.
     */
    private static final List<String> JVM_THREAD_GROUP_NAMES =
        new ArrayList<String>();

    private static final String JVM_THREAD_GROUP_SYSTEM = "system";

    private static final String SERVICES_PREFIX = "/META-INF/services/";

    private static final String CLASS_FILE_SUFFIX = ".class";

    private static final Manifest MANIFEST_UNKNOWN = new Manifest();

    private static final Method GET_CLASSLOADING_LOCK_METHOD;

    protected static final StringManager sm = StringManager.getManager(Constants.Package);

    static {
        // Register this base class loader as parallel capable on Java 7+ JREs
        Method getClassLoadingLockMethod = null;
        try {
            if (JreCompat.isJre7Available()) {
                final Method registerParallel =
                        ClassLoader.class.getDeclaredMethod("registerAsParallelCapable");
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    @Override
                    public Void run() {
                        registerParallel.setAccessible(true);
                        return null;
                    }
                });
                registerParallel.invoke(null);
                getClassLoadingLockMethod =
                        ClassLoader.class.getDeclaredMethod("getClassLoadingLock", String.class);
            }
        } catch (Exception e) {
            // ignore
        }
        GET_CLASSLOADING_LOCK_METHOD = getClassLoadingLockMethod;
        JVM_THREAD_GROUP_NAMES.add(JVM_THREAD_GROUP_SYSTEM);
        JVM_THREAD_GROUP_NAMES.add("RMI Runtime");
    }


    protected class PrivilegedFindResourceByName implements PrivilegedAction<ResourceEntry> {

        private final String name;
        private final String path;
        private final boolean manifestRequired;

        PrivilegedFindResourceByName(String name, String path, boolean manifestRequired) {
            this.name = name;
            this.path = path;
            this.manifestRequired = manifestRequired;
        }

        @Override
        public ResourceEntry run() {
            return findResourceInternal(name, path, manifestRequired);
        }

    }


    protected static final class PrivilegedGetClassLoader implements PrivilegedAction<ClassLoader> {

        private final Class<?> clazz;

        public PrivilegedGetClassLoader(Class<?> clazz){
            this.clazz = clazz;
        }

        @Override
        public ClassLoader run() {
            return clazz.getClassLoader();
        }
    }


    // ------------------------------------------------------- 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.
     */
    protected static final String[] triggers = {
        "javax.servlet.Servlet", "javax.el.Expression"       // Servlet API
    };


    /**
     * Set of package names which are not allowed to be loaded from a webapp
     * class loader without delegating first.
     */
    protected static final String[] packageTriggers = {
    };


    /**
     * Use anti JAR locking code, which does URL rerouting when accessing
     * resources.
     */
    boolean antiJARLocking = false;


    // ----------------------------------------------------------- Constructors

    /**
     * Construct a new ClassLoader with no defined repositories and no
     * parent ClassLoader.
     */
    public WebappClassLoaderBase() {

        super(new URL[0]);

        ClassLoader p = getParent();
        if (p == null) {
            p = getSystemClassLoader();
        }
        this.parent = p;

        ClassLoader j = String.class.getClassLoader();
        if (j == null) {
            j = getSystemClassLoader();
            while (j.getParent() != null) {
                j = j.getParent();
            }
        }
        this.j2seClassLoader = j;

        securityManager = System.getSecurityManager();
        if (securityManager != null) {
            refreshPolicy();
        }
    }


    /**
     * Construct a new ClassLoader with no defined repositories and the given
     * parent ClassLoader.
     * <p>
     * Method is used via reflection -
     * see {@link WebappLoader#createClassLoader()}
     *
     * @param parent Our parent class loader
     */
    public WebappClassLoaderBase(ClassLoader parent) {

        super(new URL[0], parent);

        ClassLoader p = getParent();
        if (p == null) {
            p = getSystemClassLoader();
        }
        this.parent = p;

        ClassLoader j = String.class.getClassLoader();
        if (j == null) {
            j = getSystemClassLoader();
            while (j.getParent() != null) {
                j = j.getParent();
            }
        }
        this.j2seClassLoader = j;

        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 path, not binary name. Path is used as the key since
     * resources may be requested by binary name (classes) or path (other
     * resources such as property files) and the mapping from binary name to
     * path is unambiguous but the reverse mapping is ambiguous.
     */
    protected Map<String, ResourceEntry> resourceEntries =
            new ConcurrentHashMap<String, ResourceEntry>();


    /**
     * The list of not found resources.
     */
    protected HashMap<String, String> notFoundResources =
        new LinkedHashMap<String, String>() {
        private static final long serialVersionUID = 1L;
        @Override
        protected boolean removeEldestEntry(
                Map.Entry<String, String> eldest) {
            return size() > 1000;
        }
    };


    /**
     * 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. Note that the default, <code>false</code>, is
     * the behavior called for by the servlet specification.
     */
    protected boolean delegate = false;


    /**
     * Last time a JAR was accessed.
     */
    protected long lastJarAccessed = 0L;

    /**
     * The interval in milliseconds that we can keep jar files open.
     */
    protected int jarOpenInterval = 90000;

    /**
     * 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.
     */
    protected ArrayList<Permission> permissionList =
        new ArrayList<Permission>();


    /**
     * Path where resources loaded from JARs will be extracted.
     */
    protected File loaderDir = null;
    protected String canonicalLoaderDir = null;

    /**
     * The PermissionCollection for each CodeSource for a web
     * application context.
     */
    protected HashMap<String, PermissionCollection> loaderPC = new HashMap<String, PermissionCollection>();


    /**
     * Instance of the SecurityManager installed.
     */
    protected SecurityManager securityManager = null;


    /**
     * The parent class loader.
     */
    protected ClassLoader parent = null;


    /**
     * The system class loader.
     * @deprecated Unused. Always null. Will be removed in 8.0.x.
     */
    @Deprecated
    protected ClassLoader system = null;


    /**
     * The bootstrap class loader used to load the JavaSE classes. In some
     * implementations this class loader is always <code>null</code> and in
     * those cases {@link ClassLoader#getParent()} will be called recursively on
     * the system class loader and the last non-null result used.
     */
    protected ClassLoader j2seClassLoader;


    /**
     * Has this component been started?
     */
    protected boolean started = false;


    /**
     * Has external repositories.
     */
    protected boolean hasExternalRepositories = false;

    /**
     * Search external repositories first
     */
    protected boolean searchExternalFirst = false;

    /**
     * need conversion for properties files
     */
    protected boolean needConvert = false;


    /**
     * All permission.
     */
    protected Permission allPermission = new java.security.AllPermission();


    /**
     * Enables the RMI Target memory leak detection to be controlled. This is
     * necessary since the detection can only work on Java 9 if some of the
     * modularity checks are disabled.
     */
    private boolean clearReferencesRmiTargets = true;

    /**
     * Should Tomcat attempt to null out any static or final fields from loaded
     * classes when a web application is stopped as a work around for apparent
     * garbage collection bugs and application coding errors? There have been
     * some issues reported with log4j when this option is true. Applications
     * without memory leaks using recent JVMs should operate correctly with this
     * option set to <code>false</code>. If not specified, the default value of
     * <code>false</code> will be used.
     *
     * @deprecated This option will be removed in Tomcat 8.5
     */
    @Deprecated
    private boolean clearReferencesStatic = false;

    /**
     * Should Tomcat attempt to terminate threads that have been started by the
     * web application? Stopping threads is performed via the deprecated (for
     * good reason) <code>Thread.stop()</code> method and is likely to result in
     * instability. As such, enabling this should be viewed as an option of last
     * resort in a development environment and is not recommended in a
     * production environment. If not specified, the default value of
     * <code>false</code> will be used.
     */
    private boolean clearReferencesStopThreads = false;

    /**
     * Should Tomcat attempt to terminate any {@link java.util.TimerThread}s
     * that have been started by the web application? If not specified, the
     * default value of <code>false</code> will be used.
     */
    private boolean clearReferencesStopTimerThreads = false;

    /**
     * Should Tomcat call {@link org.apache.juli.logging.LogFactory#release()}
     * when the class loader is stopped? If not specified, the default value
     * of <code>true</code> is used. Changing the default setting is likely to
     * lead to memory leaks and other issues.
     */
    private boolean clearReferencesLogFactoryRelease = true;

    /**
     * If an HttpClient keep-alive timer thread has been started by this web
     * application and is still running, should Tomcat change the context class
     * loader from the current {@link WebappClassLoaderBase} to
     * {@link WebappClassLoaderBase#parent} to prevent a memory leak? Note that
     * the keep-alive timer thread will stop on its own once the keep-alives all
     * expire however, on a busy system that might not happen for some time.
     */
    private boolean clearReferencesHttpClientKeepAliveThread = true;

    /**
     * Name of associated context used with logging and JMX to associate with
     * the right web application. Particularly useful for the clear references
     * messages. Defaults to unknown but if standard Tomcat components are used
     * it will be updated during initialisation from the resources.
     */
    private String contextName = "unknown";

    /**
     * Should Tomcat attempt to clear references to classes loaded by this class
     * loader from the ObjectStreamClass caches?
     */
    private boolean clearReferencesObjectStreamClassCaches = true;

    /**
     * Should Tomcat attempt to clear references to classes loaded by this class
     * loader from ThreadLocals?
     */
    private boolean clearReferencesThreadLocals = true;

    /**
     * Holds the class file transformers decorating this class loader. The
     * CopyOnWriteArrayList is thread safe. It is expensive on writes, but
     * those should be rare. It is very fast on reads, since synchronization
     * is not actually used. Importantly, the ClassLoader will never block
     * iterating over the transformers while loading a class.
     */
    private final List<ClassFileTransformer> transformers = new CopyOnWriteArrayList<ClassFileTransformer>();

    /**
     * Code base to use for classes loaded from WEB-INF/classes.
     */
    private URL webInfClassesCodeBase = null;

    // ------------------------------------------------------------- Properties


    /**
     * Get associated resources.
     */
    public DirContext getResources() {

        return this.resources;

    }


    /**
     * Set associated resources.
     */
    public void setResources(DirContext resources) {

        this.resources = resources;

        if (resources instanceof ProxyDirContext) {
            contextName = ((ProxyDirContext) resources).getContextName();
        }
    }


    /**
     * Return the context name for this class loader.
     */
    public String getContextName() {

        return (this.contextName);

    }


    /**
     * Return the "delegate first" flag for this class loader.
     */
    public boolean getDelegate() {

        return (this.delegate);

    }


    /**
     * Set the "delegate first" flag for this class loader.
     * If this flag is true, this class loader delegates
     * to the parent class loader
     * <strong>before</strong> searching its own repositories, as
     * in an ordinary (non-servlet) chain of Java class loaders.
     * If set to <code>false</code> (the default),
     * this class loader will search its own repositories first, and
     * delegate to the parent only if the class or resource is not
     * found locally, as per the servlet specification.
     *
     * @param delegate The new "delegate first" flag
     */
    public void setDelegate(boolean delegate) {

        this.delegate = delegate;

    }

    /**
     * Set the interval to keep all jar files open
     * @param jarOpenInterval the interval in milliseconds to keep jar files open
     */
    public void setJarOpenInterval(int jarOpenInterval) {
        this.jarOpenInterval = jarOpenInterval;
    }

    /**
     * Return the interval to keep all jar files open if no jar is accessed
     */
    public int getJarOpenInterval() {
        return jarOpenInterval;
    }

    /**
     * @return Returns the antiJARLocking.
     */
    public boolean getAntiJARLocking() {
        return antiJARLocking;
    }


    /**
     * @param antiJARLocking The antiJARLocking to set.
     */
    public void setAntiJARLocking(boolean antiJARLocking) {
        this.antiJARLocking = antiJARLocking;
    }

    /**
     * @return Returns the searchExternalFirst.
     */
    public boolean getSearchExternalFirst() {
        return searchExternalFirst;
    }

    public boolean getClearReferencesRmiTargets() {
        return this.clearReferencesRmiTargets;
    }


    public void setClearReferencesRmiTargets(boolean clearReferencesRmiTargets) {
        this.clearReferencesRmiTargets = clearReferencesRmiTargets;
    }


    /**
     * @param searchExternalFirst Whether external repositories should be searched first
     */
    public void setSearchExternalFirst(boolean searchExternalFirst) {
        this.searchExternalFirst = searchExternalFirst;
    }


    /**
     * If there is a Java SecurityManager create a read FilePermission
     * or JndiPermission for the file directory path.
     *
     * @param filepath file directory path
     */
    public void addPermission(String filepath) {
        if (filepath == null) {
            return;
        }

        String path = filepath;

        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 permission The permission
     */
    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");
        try {
            canonicalLoaderDir = loaderDir.getCanonicalPath();
            if (!canonicalLoaderDir.endsWith(File.separator)) {
                canonicalLoaderDir += File.separator;
            }
        } catch (IOException ioe) {
            canonicalLoaderDir = null;
        }
    }

     /**
      * Utility method for use in subclasses.
      * Must be called before Lifecycle methods to have any effect.
      *
      * @deprecated Will be removed in 8.0.x onwards.
      */
    @Deprecated
     protected void setParentClassLoader(ClassLoader pcl) {
         parent = pcl;
     }

     /**
      * Return the clearReferencesStatic flag for this Context.
      *
      * @deprecated Will be removed in 8.5
      */
     @Deprecated
     public boolean getClearReferencesStatic() {
         return (this.clearReferencesStatic);
     }


     /**
      * Set the clearReferencesStatic feature for this Context.
      *
      * @param clearReferencesStatic The new flag value
      *
      * @deprecated Will be removed in 8.5
      */
     @Deprecated
     public void setClearReferencesStatic(boolean clearReferencesStatic) {
         this.clearReferencesStatic = clearReferencesStatic;
     }


     /**
      * Return the clearReferencesStopThreads flag for this Context.
      */
     public boolean getClearReferencesStopThreads() {
         return (this.clearReferencesStopThreads);
     }


     /**
      * Set the clearReferencesStopThreads feature for this Context.
      *
      * @param clearReferencesStopThreads The new flag value
      */
     public void setClearReferencesStopThreads(
             boolean clearReferencesStopThreads) {
         this.clearReferencesStopThreads = clearReferencesStopThreads;
     }


     /**
      * Return the clearReferencesStopTimerThreads flag for this Context.
      */
     public boolean getClearReferencesStopTimerThreads() {
         return (this.clearReferencesStopTimerThreads);
     }


     /**
      * Set the clearReferencesStopTimerThreads feature for this Context.
      *
      * @param clearReferencesStopTimerThreads The new flag value
      */
     public void setClearReferencesStopTimerThreads(
             boolean clearReferencesStopTimerThreads) {
         this.clearReferencesStopTimerThreads = clearReferencesStopTimerThreads;
     }


     /**
      * Return the clearReferencesLogFactoryRelease flag for this Context.
      */
     public boolean getClearReferencesLogFactoryRelease() {
         return (this.clearReferencesLogFactoryRelease);
     }


     /**
      * Set the clearReferencesLogFactoryRelease feature for this Context.
      *
      * @param clearReferencesLogFactoryRelease The new flag value
      */
     public void setClearReferencesLogFactoryRelease(
             boolean clearReferencesLogFactoryRelease) {
         this.clearReferencesLogFactoryRelease =
             clearReferencesLogFactoryRelease;
     }


     /**
      * Return the clearReferencesHttpClientKeepAliveThread flag for this
      * Context.
      */
     public boolean getClearReferencesHttpClientKeepAliveThread() {
         return (this.clearReferencesHttpClientKeepAliveThread);
     }


     /**
      * Set the clearReferencesHttpClientKeepAliveThread feature for this
      * Context.
      *
      * @param clearReferencesHttpClientKeepAliveThread The new flag value
      */
     public void setClearReferencesHttpClientKeepAliveThread(
             boolean clearReferencesHttpClientKeepAliveThread) {
         this.clearReferencesHttpClientKeepAliveThread =
             clearReferencesHttpClientKeepAliveThread;
     }


    public boolean getClearReferencesObjectStreamClassCaches() {
        return clearReferencesObjectStreamClassCaches;
    }


    public void setClearReferencesObjectStreamClassCaches(
            boolean clearReferencesObjectStreamClassCaches) {
        this.clearReferencesObjectStreamClassCaches = clearReferencesObjectStreamClassCaches;
    }


    public boolean getClearReferencesThreadLocals() {
        return clearReferencesThreadLocals;
    }


    public void setClearReferencesThreadLocals(boolean clearReferencesThreadLocals) {
        this.clearReferencesThreadLocals = clearReferencesThreadLocals;
    }


    // ------------------------------------------------------- Reloader Methods

    /**
     * Adds the specified class file transformer to this class loader. The
     * transformer will then be able to modify the bytecode of any classes
     * loaded by this class loader after the invocation of this method.
     *
     * @param transformer The transformer to add to the class loader
     */
    @Override
    public void addTransformer(ClassFileTransformer transformer) {

        if (transformer == null) {
            throw new IllegalArgumentException(sm.getString(
                    "webappClassLoader.addTransformer.illegalArgument", getContextName()));
        }

        if (this.transformers.contains(transformer)) {
            // if the same instance of this transformer was already added, bail out
            log.warn(sm.getString("webappClassLoader.addTransformer.duplicate",
                    transformer, getContextName()));
            return;
        }
        this.transformers.add(transformer);

        log.info(sm.getString("webappClassLoader.addTransformer", transformer, getContextName()));

    }

    /**
     * Removes the specified class file transformer from this class loader.
     * It will no longer be able to modify the byte code of any classes
     * loaded by the class loader after the invocation of this method.
     * However, any classes already modified by this transformer will
     * remain transformed.
     *
     * @param transformer The transformer to remove
     */
    @Override
    public void removeTransformer(ClassFileTransformer transformer) {

        if (transformer == null) {
            return;
        }

        if (this.transformers.remove(transformer)) {
            log.info(sm.getString("webappClassLoader.removeTransformer",
                    transformer, getContextName()));
            return;
        }

    }


    protected void copyStateWithoutTransformers(WebappClassLoaderBase base) {
        base.antiJARLocking = this.antiJARLocking;
        base.resources = this.resources;
        base.files = this.files;
        base.delegate = this.delegate;
        base.lastJarAccessed = this.lastJarAccessed;
        base.repositories = this.repositories;
        base.jarPath = this.jarPath;
        base.loaderDir = this.loaderDir;
        base.canonicalLoaderDir = this.canonicalLoaderDir;
        base.clearReferencesStatic = this.clearReferencesStatic;
        base.clearReferencesStopThreads = this.clearReferencesStopThreads;
        base.clearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads;
        base.clearReferencesLogFactoryRelease = this.clearReferencesLogFactoryRelease;
        base.clearReferencesHttpClientKeepAliveThread = this.clearReferencesHttpClientKeepAliveThread;
        base.repositoryURLs = this.repositoryURLs.clone();
        base.jarFiles = this.jarFiles.clone();
        base.jarRealFiles = this.jarRealFiles.clone();
        base.jarNames = this.jarNames.clone();
        base.lastModifiedDates = this.lastModifiedDates.clone();
        base.paths = this.paths.clone();
        base.notFoundResources.putAll(this.notFoundResources);
        base.permissionList.addAll(this.permissionList);
        base.loaderPC.putAll(this.loaderPC);
        base.contextName = this.contextName;
        base.hasExternalRepositories = this.hasExternalRepositories;
        base.searchExternalFirst = this.searchExternalFirst;
        base.jarOpenInterval = this.jarOpenInterval;
    }


   /**
     * 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);
            iae.initCause(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 (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<Binding> enumeration =
                    resources.listBindings(getJarPath());
                int i = 0;
                while (enumeration.hasMoreElements() && (i < length)) {
                    NameClassPair ncPair = enumeration.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 (enumeration.hasMoreElements()) {
                    while (enumeration.hasMoreElements()) {
                        NameClassPair ncPair = enumeration.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.
     */
    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder(this.getClass().getSimpleName());
        sb.append("\r\n  context: ");
        sb.append(contextName);
        sb.append("\r\n  delegate: ");
        sb.append(delegate);
        sb.append("\r\n  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");
        }
        if (this.transformers.size() > 0) {
            sb.append("----------> Class file transformers:\r\n");
            for (ClassFileTransformer transformer : this.transformers) {
                sb.append(transformer).append("\r\n");
            }
        }
        return (sb.toString());

    }


    // ---------------------------------------------------- ClassLoader Methods


    /**
     * Add the specified URL to the classloader.
     */
    @Override
    protected void addURL(URL url) {
        super.addURL(url);
        hasExternalRepositories = true;
        repositoryURLs = null;
    }


    /**
     * Expose this method for use by the unit tests.
     */
    protected final Class<?> doDefineClass(String name, byte[] b, int off, int len,
            ProtectionDomain protectionDomain) {
        return super.defineClass(name, b, off, len, protectionDomain);
    }

    /**
     * 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
     */
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {

        if (log.isDebugEnabled())
            log.debug("    findClass(" + name + ")");

        // Cannot load anything from local repositories if class loader is stopped
        if (!started) {
            throw new ClassNotFoundException(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 + ")");
            if (hasExternalRepositories && searchExternalFirst) {
                try {
                    clazz = super.findClass(name);
                } catch(ClassNotFoundException cnfe) {
                    // Ignore - will search internal repositories next
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            if ((clazz == null)) {
                try {
                    clazz = findClassInternal(name);
                } catch(ClassNotFoundException cnfe) {
                    if (!hasExternalRepositories || searchExternalFirst) {
                        throw cnfe;
                    }
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), ace);
                    throw new ClassNotFoundException(name, ace);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled())
                        log.trace("      -->RuntimeException Rethrown", e);
                    throw e;
                }
            }
            if ((clazz == null) && hasExternalRepositories && !searchExternalFirst) {
                try {
                    clazz = super.findClass(name);
                } catch(AccessControlException ace) {
                    log.warn("WebappClassLoaderBase.findClassInternal(" + name
                            + ") security exception: " + ace.getMessage(), 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()) {
            ClassLoader cl;
            if (Globals.IS_SECURITY_ENABLED){
                cl = AccessController.doPrivileged(
                    new PrivilegedGetClassLoader(clazz));
            } else {
                cl = clazz.getClassLoader();
            }
            log.debug("      Loaded by " + cl.toString());
        }
        return (clazz);

    }


    /**
     * Find the specified resource in our local repository, and return a
     * <code>URL</code> referring to it, or <code>null</code> if this resource
     * cannot be found.
     *
     * @param name Name of the resource to be found
     */
    @Override
    public URL findResource(final String name) {

        if (log.isDebugEnabled())
            log.debug("    findResource(" + name + ")");

        URL url = null;
        String path = nameToPath(name);

        if (hasExternalRepositories && searchExternalFirst)
            url = super.findResource(name);

        if (url == null) {
            ResourceEntry entry = resourceEntries.get(path);
            if (entry == null) {
                if (securityManager != null) {
                    PrivilegedAction<ResourceEntry> dp =
                        new PrivilegedFindResourceByName(name, path, false);
                    entry = AccessController.doPrivileged(dp);
                } else {
                    entry = findResourceInternal(name, path, false);
                }
            }
            if (entry != null) {
                url = entry.source;
            }
        }

        if ((url == null) && hasExternalRepositories && !searchExternalFirst)
            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
     */
    @Override
    public Enumeration<URL> findResources(String name) throws IOException {

        if (log.isDebugEnabled())
            log.debug("    findResources(" + name + ")");

        //we use a LinkedHashSet instead of a Vector to avoid duplicates with virtualmappings
        LinkedHashSet<URL> result = new LinkedHashSet<URL>();

        int jarFilesLength = jarFiles.length;
        int repositoriesLength = repositories.length;

        int i;

        // Adding the results of a call to the superclass
        if (hasExternalRepositories && searchExternalFirst) {

            Enumeration<URL> otherResourcePaths = super.findResources(name);

            while (otherResourcePaths.hasMoreElements()) {
                result.add(otherResourcePaths.nextElement());
            }

        }
        // 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.add(getURI(new File(files[i], name)));
                } catch (MalformedURLException e) {
                    // Ignore
                }
            } catch (NamingException e) {
                // Ignore
            }
        }

        // Looking at the JAR files
        String jarPath = (name.startsWith("/")) ? name.substring(1) : name;
        synchronized (jarFiles) {
            if (openJARs()) {
                for (i = 0; i < jarFilesLength; i++) {
                    JarEntry jarEntry = jarFiles[i].getJarEntry(jarPath);
                    if (jarEntry != null) {
                        try {
                            String jarFakeUrl = getURI(jarRealFiles[i]).toString();
                            result.add(UriUtil.buildJarUrl(jarFakeUrl, jarPath));
                        } catch (MalformedURLException e) {
                            // Ignore
                        }
                    }
                }
            }
        }

        // Adding the results of a call to the superclass
        if (hasExternalRepositories && !searchExternalFirst) {

            Enumeration<URL> otherResourcePaths = super.findResources(name);

            while (otherResourcePaths.hasMoreElements()) {
                result.add(otherResourcePaths.nextElement());
            }

        }

        return Collections.enumeration(result);
    }


    /**
     * 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
     */
    @Override
    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);
            url = parent.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
            if (antiJARLocking) {
                String path = nameToPath(name);
                ResourceEntry entry = resourceEntries.get(path);
                try {
                    String repository = entry.codeBase.toString();
                    if ((repository.endsWith(".jar"))
                            && (!(name.endsWith(CLASS_FILE_SUFFIX)))) {
                        // Copy binary content to the work directory if not present
                        File resourceFile = new File(loaderDir, name);
                        url = getURI(resourceFile);
                    }
                } 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 ) {
            url = parent.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);

    }


    @Override
    public Enumeration<URL> getResources(String name) throws IOException {

        Enumeration<URL> parentResources = getParent().getResources(name);
        Enumeration<URL> localResources = findResources(name);

        // Need to combine these enumerations. The order in which the
        // Enumerations are combined depends on how delegation is configured
        if (delegate) {
            return new CombinedEnumeration(parentResources, localResources);
        } else {
            return new CombinedEnumeration(localResources, parentResources);
        }
    }


    /**
     * 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
     */
    @Override
    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);
            stream = parent.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);
            stream = parent.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
     */
    @Override
    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
     */
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLockInternal(name)) {
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;

            // Log access to stopped classloader
            if (!started) {
                try {
                    throw new IllegalStateException();
                } catch (IllegalStateException e) {
                    log.info(sm.getString("webappClassLoader.stopped", name), e);
                }
            }

            // (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 = j2seClassLoader.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;
                        if (name.endsWith("BeanInfo")) {
                            // BZ 57906: suppress logging for calls from
                            // java.beans.Introspector.findExplicitBeanInfo()
                            log.debug(error, se);
                        } else {
                            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);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (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) {
                // Ignore
            }

            // (3) Delegate to parent unconditionally
            if (!delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader at end: " + parent);
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled())
                            log.debug("  Loading class from parent");
                        if (resolve)
                            resolveClass(clazz);
                        return (clazz);
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }

        throw new ClassNotFoundException(name);
    }


    private Object getClassLoadingLockInternal(String className) {
        if (JreCompat.isJre7Available() && GET_CLASSLOADING_LOCK_METHOD != null) {
            try {
                return GET_CLASSLOADING_LOCK_METHOD.invoke(this, className);
            } catch (Exception e) {
                // ignore
            }
        }
        return this;
    }


    /**
     * Get the Permissions for a CodeSource.  If this instance
     * of WebappClassLoaderBase 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
     */
    @Override
    protected PermissionCollection getPermissions(CodeSource codeSource) {

        String codeUrl = codeSource.getLocation().toString();
        PermissionCollection pc;
        if ((pc = loaderPC.get(codeUrl)) == null) {
            pc = super.getPermissions(codeSource);
            if (pc != null) {
                Iterator<Permission> perms = permissionList.iterator();
                while (perms.hasNext()) {
                    Permission p = perms.next();
                    pc.add(p);
                }
                loaderPC.put(codeUrl,pc);
            }
        }
        return (pc);

    }


    @Override
    public boolean check(Permission permission) {
        if (!Globals.IS_SECURITY_ENABLED) {
            return true;
        }
        Policy currentPolicy = Policy.getPolicy();
        if (currentPolicy != null) {
            ResourceEntry entry = findResourceInternal("/", "/", false);
            if (entry != null) {
                CodeSource cs = new CodeSource(
                        entry.codeBase, (java.security.cert.Certificate[]) null);
                PermissionCollection pc = currentPolicy.getPermissions(cs);
                if (pc.implies(permission)) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * 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.
     */
    @Override
    public URL[] getURLs() {

        if (repositoryURLs != null) {
            return repositoryURLs.clone();
        }

        URL[] external = super.getURLs();

        int filesLength = files.length;
        int jarFilesLength = jarRealFiles.length;
        int externalsLength = external.length;
        int off = 0;
        int i;

        try {

            URL[] urls = new URL[filesLength + jarFilesLength + externalsLength];
            if (searchExternalFirst) {
                for (i = 0; i < externalsLength; i++) {
                    urls[i] = external[i];
                }
                off = externalsLength;
            }
            for (i = 0; i < filesLength; i++) {
                urls[off + i] = getURI(files[i]);
            }
            off += filesLength;
            for (i = 0; i < jarFilesLength; i++) {
                urls[off + i] = getURI(jarRealFiles[i]);
            }
            off += jarFilesLength;
            if (!searchExternalFirst) {
                for (i = 0; i < externalsLength; i++) {
                    urls[off + i] = external[i];
                }
            }

            repositoryURLs = urls;

        } catch (MalformedURLException e) {
            repositoryURLs = new URL[0];
        }

        return repositoryURLs.clone();

    }


    // ------------------------------------------------------ Lifecycle Methods


    /**
     * Add a lifecycle event listener to this component.
     *
     * @param listener The listener to add
     */
    @Override
    public void addLifecycleListener(LifecycleListener listener) {
        // NOOP
    }


    /**
     * Get the lifecycle listeners associated with this lifecycle. If this
     * Lifecycle has no listeners registered, a zero-length array is returned.
     */
    @Override
    public LifecycleListener[] findLifecycleListeners() {
        return new LifecycleListener[0];
    }


    /**
     * Remove a lifecycle event listener from this component.
     *
     * @param listener The listener to remove
     */
    @Override
    public void removeLifecycleListener(LifecycleListener listener) {
        // NOOP
    }


    /**
     * Obtain the current state of the source component.
     *
     * @return The current state of the source component.
     */
    @Override
    public LifecycleState getState() {
        return LifecycleState.NEW;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public String getStateName() {
        return getState().toString();
    }


    @Override
    public void init() {
        // NOOP
    }


    /**
     * Start the class loader.
     *
     * @exception LifecycleException if a lifecycle error occurs
     */
    @Override
    public void start() throws LifecycleException {

        started = true;
        String encoding = null;
        try {
            encoding = System.getProperty("file.encoding");
        } catch (SecurityException e) {
            return;
        }
        if (encoding.indexOf("EBCDIC")!=-1) {
            needConvert = true;
        }

        for (int i = 0; i < repositories.length; i++) {
            if (repositories[i].equals("/WEB-INF/classes/")) {
                try {
                    webInfClassesCodeBase = files[i].toURI().toURL();
                } catch (MalformedURLException e) {
                    // Ignore - leave it as null
                }
                break;
            }
        }

    }


    public boolean isStarted() {
        return started;
    }

    /**
     * Stop the class loader.
     *
     * @exception LifecycleException if a lifecycle error occurs
     */
    @Override
    public void stop() throws LifecycleException {

        // Clearing references should be done before setting started to
        // false, due to possible side effects
        clearReferences();

        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;
        webInfClassesCodeBase = null;

        permissionList.clear();
        loaderPC.clear();

        if (loaderDir != null) {
            deleteDir(loaderDir);
        }

    }


    @Override
    public void destroy() {
        // NOOP
    }


    /**
     * Used to periodically signal to the classloader to release
     * JAR resources.
     */
    public void closeJARs(boolean force) {
        if (jarFiles.length > 0 && (force || jarOpenInterval > 0)) {
                synchronized (jarFiles) {
                    if (force || (jarOpenInterval > 0 && System.currentTimeMillis()
                            > (lastJarAccessed + jarOpenInterval))) {
                        for (int i = 0; i < jarFiles.length; i++) {
                            try {
                                if (jarFiles[i] != null) {
                                    jarFiles[i].close();
                                    jarFiles[i] = null;
                                }
                            } catch (IOException e) {
                                if (log.isDebugEnabled()) {
                                    log.debug("Failed to close JAR", e);
                                }
                            }
                        }
                    }
                }
        }
    }


    // ------------------------------------------------------ Protected Methods

    protected ClassLoader getJavaseClassLoader() {
        return j2seClassLoader;
    }

    protected void setJavaseClassLoader(ClassLoader classLoader) {
        if (classLoader == null) {
            throw new IllegalArgumentException(
                    sm.getString("webappClassLoader.javaseClassLoaderNull"));
        }
        j2seClassLoader = classLoader;
    }

    /**
     * Clear references.
     */
    protected void clearReferences() {

        // De-register any remaining JDBC drivers
        clearReferencesJdbc();

        // Stop any threads the web application started
        clearReferencesThreads();

        // Clear any references retained in the serialization caches
        if (clearReferencesObjectStreamClassCaches) {
            clearReferencesObjectStreamClassCaches();
        }

        // Check for leaks triggered by ThreadLocals loaded by this class loader
        if (clearReferencesThreadLocals) {
            checkThreadLocalsForLeaks();
        }

        // Clear RMI Targets loaded by this class loader
        if (clearReferencesRmiTargets) {
            clearReferencesRmiTargets();
        }

        // Null out any static or final fields from loaded classes,
        // as a workaround for apparent garbage collection bugs
        if (clearReferencesStatic) {
            clearReferencesStaticFinal();
        }

         // Clear the IntrospectionUtils cache.
        IntrospectionUtils.clear();

        // Clear the classloader reference in common-logging
        if (clearReferencesLogFactoryRelease) {
            org.apache.juli.logging.LogFactory.release(this);
        }

        // Clear the classloader reference in the VM's bean introspector
        java.beans.Introspector.flushCaches();

    }


    /**
     * Deregister any JDBC drivers registered by the webapp that the webapp
     * forgot. This is made unnecessary complex because a) DriverManager
     * checks the class loader of the calling class (it would be much easier
     * if it checked the context class loader) b) using reflection would
     * create a dependency on the DriverManager implementation which can,
     * and has, changed.
     *
     * We can't just create an instance of JdbcLeakPrevention as it will be
     * loaded by the common class loader (since it's .class file is in the
     * $CATALINA_HOME/lib directory). This would fail DriverManager's check
     * on the class loader of the calling class. So, we load the bytes via
     * our parent class loader but define the class with this class loader
     * so the JdbcLeakPrevention looks like a webapp class to the
     * DriverManager.
     *
     * If only apps cleaned up after themselves...
     */
    private final void clearReferencesJdbc() {
        InputStream is = getResourceAsStream(
                "org/apache/catalina/loader/JdbcLeakPrevention.class");
        // We know roughly how big the class will be (~ 1K) so allow 2k as a
        // starting point
        byte[] classBytes = new byte[2048];
        int offset = 0;
        try {
            int read = is.read(classBytes, offset, classBytes.length-offset);
            while (read > -1) {
                offset += read;
                if (offset == classBytes.length) {
                    // Buffer full - double size
                    byte[] tmp = new byte[classBytes.length * 2];
                    System.arraycopy(classBytes, 0, tmp, 0, classBytes.length);
                    classBytes = tmp;
                }
                read = is.read(classBytes, offset, classBytes.length-offset);
            }
            Class<?> lpClass =
                defineClass("org.apache.catalina.loader.JdbcLeakPrevention",
                    classBytes, 0, offset, this.getClass().getProtectionDomain());
            Object obj = lpClass.newInstance();
            @SuppressWarnings("unchecked") // clearJdbcDriverRegistrations() returns List<String>
            List<String> driverNames = (List<String>) obj.getClass().getMethod(
                    "clearJdbcDriverRegistrations").invoke(obj);
            for (String name : driverNames) {
                log.error(sm.getString("webappClassLoader.clearJdbc",
                        contextName, name));
            }
        } catch (Exception e) {
            // So many things to go wrong above...
            Throwable t = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(t);
            log.warn(sm.getString(
                    "webappClassLoader.jdbcRemoveFailed", contextName), t);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException ioe) {
                    log.warn(sm.getString(
                            "webappClassLoader.jdbcRemoveStreamError",
                            contextName), ioe);
                }
            }
        }
    }


    private final void clearReferencesStaticFinal() {

        List<ResourceEntry> values = new ArrayList<ResourceEntry>();
        values.addAll(resourceEntries.values());
        Iterator<ResourceEntry> loadedClasses = values.iterator();
        //
        // walk through all loaded class to trigger initialization for
        //    any uninitialized classes, otherwise initialization of
        //    one class may call a previously cleared class.
        while(loadedClasses.hasNext()) {
            ResourceEntry entry = loadedClasses.next();
            if (entry.loadedClass != null) {
                Class<?> clazz = entry.loadedClass;
                try {
                    Field[] fields = clazz.getDeclaredFields();
                    for (int i = 0; i < fields.length; i++) {
                        if(Modifier.isStatic(fields[i].getModifiers())) {
                            fields[i].get(null);
                            break;
                        }
                    }
                } catch(Throwable t) {
                    // Ignore
                }
            }
        }
        loadedClasses = values.iterator();
        while (loadedClasses.hasNext()) {
            ResourceEntry entry = loadedClasses.next();
            if (entry.loadedClass != null) {
                Class<?> clazz = entry.loadedClass;
                try {
                    Field[] fields = clazz.getDeclaredFields();
                    for (int i = 0; i < fields.length; i++) {
                        Field field = fields[i];
                        int mods = field.getModifiers();
                        if (field.getType().isPrimitive()
                                || (field.getName().indexOf('$') != -1)) {
                            continue;
                        }
                        if (Modifier.isStatic(mods)) {
                            try {
                                field.setAccessible(true);
                                if (Modifier.isFinal(mods)) {
                                    if (!((field.getType().getName().startsWith("java."))
                                            || (field.getType().getName().startsWith("javax.")))) {
                                        nullInstance(field.get(null));
                                    }
                                } else {
                                    field.set(null, null);
                                    if (log.isDebugEnabled()) {
                                        log.debug("Set field " + field.getName()
                                                + " to null in class " + clazz.getName());
                                    }
                                }
                            } catch (Throwable t) {
                                ExceptionUtils.handleThrowable(t);
                                if (log.isDebugEnabled()) {
                                    log.debug("Could not set field " + field.getName()
                                            + " to null in class " + clazz.getName(), t);
                                }
                            }
                        }
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    if (log.isDebugEnabled()) {
                        log.debug("Could not clean fields for class " + clazz.getName(), t);
                    }
                }
            }
        }

    }


    private void nullInstance(Object instance) {
        if (instance == null) {
            return;
        }
        Field[] fields = instance.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            int mods = field.getModifiers();
            if (field.getType().isPrimitive()
                    || (field.getName().indexOf('$') != -1)) {
                continue;
            }
            try {
                field.setAccessible(true);
                if (Modifier.isStatic(mods) && Modifier.isFinal(mods)) {
                    // Doing something recursively is too risky
                    continue;
                }
                Object value = field.get(instance);
                if (null != value) {
                    Class<? extends Object> valueClass = value.getClass();
                    if (!loadedByThisOrChild(valueClass)) {
                        if (log.isDebugEnabled()) {
                            log.debug("Not setting field " + field.getName() +
                                    " to null in object of class " +
                                    instance.getClass().getName() +
                                    " because the referenced object was of type " +
                                    valueClass.getName() +
                                    " which was not loaded by this web application class loader.");
                        }
                    } else {
                        field.set(instance, null);
                        if (log.isDebugEnabled()) {
                            log.debug("Set field " + field.getName()
                                    + " to null in class " + instance.getClass().getName());
                        }
                    }
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                if (log.isDebugEnabled()) {
                    log.debug("Could not set field " + field.getName()
                            + " to null in object instance of class "
                            + instance.getClass().getName(), t);
                }
            }
        }
    }


    @SuppressWarnings("deprecation") // thread.stop()
    private void clearReferencesThreads() {
        Thread[] threads = getThreads();
        List<Thread> threadsToStop = new ArrayList<Thread>();

        // Iterate over the set of threads
        for (Thread thread : threads) {
            if (thread != null) {
                ClassLoader ccl = thread.getContextClassLoader();
                if (ccl == this) {
                    // Don't warn about this thread
                    if (thread == Thread.currentThread()) {
                        continue;
                    }

                    // JVM controlled threads
                    ThreadGroup tg = thread.getThreadGroup();
                    if (tg != null &&
                            JVM_THREAD_GROUP_NAMES.contains(tg.getName())) {

                        // HttpClient keep-alive threads
                        if (clearReferencesHttpClientKeepAliveThread &&
                                thread.getName().equals("Keep-Alive-Timer")) {
                            thread.setContextClassLoader(parent);
                            log.debug(sm.getString(
                                    "webappClassLoader.checkThreadsHttpClient"));
                        }

                        // Don't warn about remaining JVM controlled threads
                        continue;
                    }

                    // Skip threads that have already died
                    if (!thread.isAlive()) {
                        continue;
                    }

                    // TimerThread can be stopped safely so treat separately
                    // "java.util.TimerThread" in Sun/Oracle JDK
                    // "java.util.Timer$TimerImpl" in Apache Harmony and in IBM JDK
                    if (thread.getClass().getName().startsWith("java.util.Timer") &&
                            clearReferencesStopTimerThreads) {
                        clearReferencesStopTimerThread(thread);
                        continue;
                    }

                    if (isRequestThread(thread)) {
                        log.error(sm.getString("webappClassLoader.warnRequestThread",
                                contextName, thread.getName()));
                    } else {
                        log.error(sm.getString("webappClassLoader.warnThread",
                                contextName, thread.getName()));
                    }

                    // Don't try and stop the threads unless explicitly
                    // configured to do so
                    if (!clearReferencesStopThreads) {
                        continue;
                    }

                    // If the thread has been started via an executor, try
                    // shutting down the executor
                    boolean usingExecutor = false;
                    try {

                        // Runnable wrapped by Thread
                        // "target" in Sun/Oracle JDK
                        // "runnable" in IBM JDK
                        // "action" in Apache Harmony
                        Object target = null;
                        for (String fieldName : new String[] { "target", "runnable", "action" }) {
                            try {
                                Field targetField = thread.getClass().getDeclaredField(fieldName);
                                targetField.setAccessible(true);
                                target = targetField.get(thread);
                                break;
                            } catch (NoSuchFieldException nfe) {
                                continue;
                            }
                        }

                        // "java.util.concurrent" code is in public domain,
                        // so all implementations are similar
                        if (target != null && target.getClass().getCanonicalName() != null &&
                                target.getClass().getCanonicalName().equals(
                                        "java.util.concurrent.ThreadPoolExecutor.Worker")) {
                            Field executorField = target.getClass().getDeclaredField("this$0");
                            executorField.setAccessible(true);
                            Object executor = executorField.get(target);
                            if (executor instanceof ThreadPoolExecutor) {
                                ((ThreadPoolExecutor) executor).shutdownNow();
                                usingExecutor = true;
                            }
                        }
                    } catch (SecurityException e) {
                        log.warn(sm.getString(
                                "webappClassLoader.stopThreadFail",
                                thread.getName(), contextName), e);
                    } catch (NoSuchFieldException e) {
                        log.warn(sm.getString(
                                "webappClassLoader.stopThreadFail",
                                thread.getName(), contextName), e);
                    } catch (IllegalArgumentException e) {
                        log.warn(sm.getString(
                                "webappClassLoader.stopThreadFail",
                                thread.getName(), contextName), e);
                    } catch (IllegalAccessException e) {
                        log.warn(sm.getString(
                                "webappClassLoader.stopThreadFail",
                                thread.getName(), contextName), e);
                    } catch (RuntimeException e) {
                        // InaccessibleObjectException is only available in Java 9+,
                        // swapped for RuntimeException
                        log.warn(sm.getString(
                                "webappClassLoader.stopThreadFail",
                                thread.getName(), contextName), e);
                    }

                    // Stopping an executor automatically interrupts the
                    // associated threads. For non-executor threads, interrupt
                    // them here.
                    if (!usingExecutor && !thread.isInterrupted()) {
                        thread.interrupt();
                    }

                    // Threads are expected to take a short time to stop after
                    // being interrupted. Make a note of all threads that are
                    // expected to stop to enable them to be checked at the end
                    // of this method.
                    threadsToStop.add(thread);
                }
            }
        }

        // If thread stopping is enabled, threads should have been stopped above
        // when the executor was shut down or the thread was interrupted but
        // that depends on the thread correctly handling the interrupt. Check
        // each thread and if any are still running give all threads up to a
        // total of 2 seconds to shutdown.
        int count = 0;
        for (Thread t : threadsToStop) {
            while (t.isAlive() && count < 100) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    // Quit the while loop
                    break;
                }
                count++;
            }
            if (t.isAlive()) {
                // This method is deprecated and for good reason. This is
                // very risky code but is the only option at this point.
                // A *very* good reason for apps to do this clean-up
                // themselves.
                t.stop();
            }
        }
    }


    /*
     * Look at a threads stack trace to see if it is a request thread or not. It
     * isn't perfect, but it should be good-enough for most cases.
     */
    private boolean isRequestThread(Thread thread) {

        StackTraceElement[] elements = thread.getStackTrace();

        if (elements == null || elements.length == 0) {
            // Must have stopped already. Too late to ignore it. Assume not a
            // request processing thread.
            return false;
        }

        // Step through the methods in reverse order looking for calls to any
        // CoyoteAdapter method. All request threads will have this unless
        // Tomcat has been heavily modified - in which case there isn't much we
        // can do.
        for (int i = 0; i < elements.length; i++) {
            StackTraceElement element = elements[elements.length - (i+1)];
            if ("org.apache.catalina.connector.CoyoteAdapter".equals(
                    element.getClassName())) {
                return true;
            }
        }
        return false;
    }


    private void clearReferencesStopTimerThread(Thread thread) {

        // Need to get references to:
        // in Sun/Oracle JDK:
        // - newTasksMayBeScheduled field (in java.util.TimerThread)
        // - queue field
        // - queue.clear()
        // in IBM JDK, Apache Harmony:
        // - cancel() method (in java.util.Timer$TimerImpl)

        try {

            try {
                Field newTasksMayBeScheduledField =
                    thread.getClass().getDeclaredField("newTasksMayBeScheduled");
                newTasksMayBeScheduledField.setAccessible(true);
                Field queueField = thread.getClass().getDeclaredField("queue");
                queueField.setAccessible(true);

                Object queue = queueField.get(thread);

                Method clearMethod = queue.getClass().getDeclaredMethod("clear");
                clearMethod.setAccessible(true);

                synchronized(queue) {
                    newTasksMayBeScheduledField.setBoolean(thread, false);
                    clearMethod.invoke(queue);
                    // In case queue was already empty. Should only be one
                    // thread waiting but use notifyAll() to be safe.
                    queue.notifyAll();
                }

            }catch (NoSuchFieldException nfe){
                Method cancelMethod = thread.getClass().getDeclaredMethod("cancel");
                synchronized(thread) {
                    cancelMethod.setAccessible(true);
                    cancelMethod.invoke(thread);
                }
            }

            log.error(sm.getString("webappClassLoader.warnTimerThread",
                    contextName, thread.getName()));

        } catch (Exception e) {
            // So many things to go wrong above...
            Throwable t = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(t);
            log.warn(sm.getString(
                    "webappClassLoader.stopTimerThreadFail",
                    thread.getName(), contextName), t);
        }
    }

    private void checkThreadLocalsForLeaks() {
        Thread[] threads = getThreads();

        try {
            // Make the fields in the Thread class that store ThreadLocals
            // accessible
            Field threadLocalsField =
                Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true);
            Field inheritableThreadLocalsField =
                Thread.class.getDeclaredField("inheritableThreadLocals");
            inheritableThreadLocalsField.setAccessible(true);
            // Make the underlying array of ThreadLoad.ThreadLocalMap.Entry objects
            // accessible
            Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries");
            expungeStaleEntriesMethod.setAccessible(true);

            for (Thread thread : threads) {
                Object threadLocalMap;
                if (thread != null) {

                    // Clear the first map
                    threadLocalMap = threadLocalsField.get(thread);
                    if (null != threadLocalMap) {
                        expungeStaleEntriesMethod.invoke(threadLocalMap);
                        checkThreadLocalMapForLeaks(threadLocalMap, tableField);
                    }

                    // Clear the second map
                    threadLocalMap = inheritableThreadLocalsField.get(thread);
                    if (null != threadLocalMap) {
                        expungeStaleEntriesMethod.invoke(threadLocalMap);
                        checkThreadLocalMapForLeaks(threadLocalMap, tableField);
                    }
                }
            }
        } catch (Throwable t) {
            JreCompat jreCompat = JreCompat.getInstance();
            if (jreCompat.isInstanceOfInaccessibleObjectException(t)) {
                // Must be running on Java 9 without the necessary command line
                // options.
                String currentModule = JreCompat.getInstance().getModuleName(this.getClass());
                log.warn(sm.getString("webappClassLoader.addExportsThreadLocal", currentModule));
            } else {
                ExceptionUtils.handleThrowable(t);
                log.warn(sm.getString(
                        "webappClassLoader.checkThreadLocalsForLeaksFail",
                        getContextName()), t);
            }
        }
    }


    /**
     * Analyzes the given thread local map object. Also pass in the field that
     * points to the internal table to save re-calculating it on every
     * call to this method.
     */
    private void checkThreadLocalMapForLeaks(Object map,
            Field internalTableField) throws IllegalAccessException,
            NoSuchFieldException {
        if (map != null) {
            Object[] table = (Object[]) internalTableField.get(map);
            if (table != null) {
                for (Object obj : table) {
                    if (obj != null) {
                        boolean keyLoadedByWebapp = false;
                        boolean valueLoadedByWebapp = false;
                        // Check the key
                        Object key = ((Reference<?>) obj).get();
                        if (this.equals(key) || loadedByThisOrChild(key)) {
                            keyLoadedByWebapp = true;
                        }
                        // Check the value
                        Field valueField =
                                obj.getClass().getDeclaredField("value");
                        valueField.setAccessible(true);
                        Object value = valueField.get(obj);
                        if (this.equals(value) || loadedByThisOrChild(value)) {
                            valueLoadedByWebapp = true;
                        }
                        if (keyLoadedByWebapp || valueLoadedByWebapp) {
                            Object[] args = new Object[5];
                            args[0] = contextName;
                            if (key != null) {
                                args[1] = getPrettyClassName(key.getClass());
                                try {
                                    args[2] = key.toString();
                                } catch (Exception e) {
                                    log.error(sm.getString(
                                            "webappClassLoader.checkThreadLocalsForLeaks.badKey",
                                            args[1]), e);
                                    args[2] = sm.getString(
                                            "webappClassLoader.checkThreadLocalsForLeaks.unknown");
                                }
                            }
                            if (value != null) {
                                args[3] = getPrettyClassName(value.getClass());
                                try {
                                    args[4] = value.toString();
                                } catch (Exception e) {
                                    log.error(sm.getString(
                                            "webappClassLoader.checkThreadLocalsForLeaks.badValue",
                                            args[3]), e);
                                    args[4] = sm.getString(
                                            "webappClassLoader.checkThreadLocalsForLeaks.unknown");
                                }
                            }
                            if (valueLoadedByWebapp) {
                                log.error(sm.getString(
                                        "webappClassLoader.checkThreadLocalsForLeaks",
                                        args));
                            } else if (value == null) {
                                if (log.isDebugEnabled()) {
                                    log.debug(sm.getString(
                                            "webappClassLoader.checkThreadLocalsForLeaksNull",
                                            args));
                                }
                            } else {
                                if (log.isDebugEnabled()) {
                                    log.debug(sm.getString(
                                            "webappClassLoader.checkThreadLocalsForLeaksNone",
                                            args));
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    private String getPrettyClassName(Class<?> clazz) {
        String name = clazz.getCanonicalName();
        if (name==null){
            name = clazz.getName();
        }
        return name;
    }

    /**
     * @param o object to test, may be null
     * @return <code>true</code> if o has been loaded by the current classloader
     * or one of its descendants.
     */
    private boolean loadedByThisOrChild(Object o) {
        if (o == null) {
            return false;
        }

        Class<?> clazz;
        if (o instanceof Class) {
            clazz = (Class<?>) o;
        } else {
            clazz = o.getClass();
        }

        ClassLoader cl = clazz.getClassLoader();
        while (cl != null) {
            if (cl == this) {
                return true;
            }
            cl = cl.getParent();
        }

        if (o instanceof Collection<?>) {
            Iterator<?> iter = ((Collection<?>) o).iterator();
            try {
                while (iter.hasNext()) {
                    Object entry = iter.next();
                    if (loadedByThisOrChild(entry)) {
                        return true;
                    }
                }
            } catch (ConcurrentModificationException e) {
                log.warn(sm.getString(
                        "webappClassLoader", clazz.getName(), getContextName()),
                        e);
            }
        }
        return false;
    }

    /*
     * Get the set of current threads as an array.
     */
    private Thread[] getThreads() {
        // Get the current thread group
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        // Find the root thread group
        try {
            while (tg.getParent() != null) {
                tg = tg.getParent();
            }
        } catch (SecurityException se) {
            String msg = sm.getString(
                    "webappClassLoader.getThreadGroupError", tg.getName());
            if (log.isDebugEnabled()) {
                log.debug(msg, se);
            } else {
                log.warn(msg);
            }
        }

        int threadCountGuess = tg.activeCount() + 50;
        Thread[] threads = new Thread[threadCountGuess];
        int threadCountActual = tg.enumerate(threads);
        // Make sure we don't miss any threads
        while (threadCountActual == threadCountGuess) {
            threadCountGuess *=2;
            threads = new Thread[threadCountGuess];
            // Note tg.enumerate(Thread[]) silently ignores any threads that
            // can't fit into the array
            threadCountActual = tg.enumerate(threads);
        }

        return threads;
    }


    /**
     * This depends on the internals of the Sun JVM so it does everything by
     * reflection.
     */
    private void clearReferencesRmiTargets() {
        try {
            // Need access to the ccl field of sun.rmi.transport.Target to find
            // the leaks
            Class<?> objectTargetClass =
                Class.forName("sun.rmi.transport.Target");
            Field cclField = objectTargetClass.getDeclaredField("ccl");
            cclField.setAccessible(true);
            // Need access to the stub field to report the leaks
            Field stubField = objectTargetClass.getDeclaredField("stub");
            stubField.setAccessible(true);

            // Clear the objTable map
            Class<?> objectTableClass = Class.forName("sun.rmi.transport.ObjectTable");
            Field objTableField = objectTableClass.getDeclaredField("objTable");
            objTableField.setAccessible(true);
            Object objTable = objTableField.get(null);
            if (objTable == null) {
                return;
            }
            Field tableLockField = objectTableClass.getDeclaredField("tableLock");
            tableLockField.setAccessible(true);
            Object tableLock = tableLockField.get(null);

            synchronized (tableLock) {
                // Iterate over the values in the table
                if (objTable instanceof Map<?,?>) {
                    Iterator<?> iter = ((Map<?,?>) objTable).values().iterator();
                    while (iter.hasNext()) {
                        Object obj = iter.next();
                        Object cclObject = cclField.get(obj);
                        if (this == cclObject) {
                            iter.remove();
                            Object stubObject = stubField.get(obj);
                            log.error(sm.getString("webappClassLoader.clearRmi",
                                    stubObject.getClass().getName(), stubObject));
                        }
                    }
                }

                // Clear the implTable map
                Field implTableField = objectTableClass.getDeclaredField("implTable");
                implTableField.setAccessible(true);
                Object implTable = implTableField.get(null);
                if (implTable == null) {
                    return;
                }

                // Iterate over the values in the table
                if (implTable instanceof Map<?,?>) {
                    Iterator<?> iter = ((Map<?,?>) implTable).values().iterator();
                    while (iter.hasNext()) {
                        Object obj = iter.next();
                        Object cclObject = cclField.get(obj);
                        if (this == cclObject) {
                            iter.remove();
                        }
                    }
                }
            }
        } catch (ClassNotFoundException e) {
            log.info(sm.getString("webappClassLoader.clearRmiInfo",
                    contextName), e);
        } catch (SecurityException e) {
            log.warn(sm.getString("webappClassLoader.clearRmiFail",
                    contextName), e);
        } catch (NoSuchFieldException e) {
            log.warn(sm.getString("webappClassLoader.clearRmiFail",
                    contextName), e);
        } catch (IllegalArgumentException e) {
            log.warn(sm.getString("webappClassLoader.clearRmiFail",
                    contextName), e);
        } catch (IllegalAccessException e) {
            log.warn(sm.getString("webappClassLoader.clearRmiFail",
                    contextName), e);
        } catch (Exception e) {
            JreCompat jreCompat = JreCompat.getInstance();
            if (jreCompat.isInstanceOfInaccessibleObjectException(e)) {
                // Must be running on Java 9 without the necessary command line
                // options.
                String currentModule = JreCompat.getInstance().getModuleName(this.getClass());
                log.warn(sm.getString("webappClassLoader.addExportsRmi", currentModule));
            } else {
                // Re-throw all other exceptions
                // Have to wrap this below Java 7
                throw new RuntimeException(e);
            }
        }
    }


    private void clearReferencesObjectStreamClassCaches() {
        try {
            Class<?> clazz = Class.forName("java.io.ObjectStreamClass$Caches");
            clearCache(clazz, "localDescs");
            clearCache(clazz, "reflectors");
        } catch (SecurityException e) {
            log.warn(sm.getString(
                    "webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e);
        } catch (ClassCastException e) {
            log.warn(sm.getString(
                    "webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e);
        } catch (IllegalArgumentException e) {
            log.warn(sm.getString(
                    "webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e);
        } catch (NoSuchFieldException e) {
            log.warn(sm.getString(
                    "webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e);
        } catch (IllegalAccessException e) {
            log.warn(sm.getString(
                    "webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e);
        } catch (ClassNotFoundException e) {
            log.warn(sm.getString(
                    "webappClassLoader.clearObjectStreamClassCachesFail", getContextName()), e);
        } catch (RuntimeException e) {
            JreCompat jreCompat = JreCompat.getInstance();
            if (jreCompat.isInstanceOfInaccessibleObjectException(e)) {
                // Must be running on Java 9 without the necessary command line
                // options.
                String currentModule = JreCompat.getInstance().getModuleName(this.getClass());
                log.warn(sm.getString("webappClassLoader.addExportsJavaIo", currentModule));
                return;
            } else {
                // Re-throw all other exceptions
                throw e;
            }
        }
    }


    private void clearCache(Class<?> target, String mapName) throws SecurityException,
            ClassCastException, NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field f = target.getDeclaredField(mapName);
        f.setAccessible(true);
        Map<?,?> map = (Map<?,?>) f.get(null);
        Iterator<?> keys = map.keySet().iterator();
        while (keys.hasNext()) {
            Object key = keys.next();
            if (key instanceof Reference) {
                Object clazz = ((Reference<?>) key).get();
                if (loadedByThisOrChild(clazz)) {
                    keys.remove();
                }
            }
        }
    }


    /**
     * Used to periodically signal to the classloader to release JAR resources.
     */
    protected boolean openJARs() {
        if (started && (jarFiles.length > 0)) {
            lastJarAccessed = System.currentTimeMillis();
            if (jarFiles[0] == null) {
                for (int i = 0; i < jarFiles.length; i++) {
                    try {
                        jarFiles[i] = JreCompat.getInstance().jarFileNewInstance(jarRealFiles[i]);
                    } catch (IOException e) {
                        log.warn(sm.getString("webappClassLoader.jarOpenFail", jarFiles[i]), e);
                        closeJARs(true);
                        return false;
                    }
                }
            }
        }
        return true;
    }


    /**
     * 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);

        ResourceEntry entry = null;
        String path = binaryNameToPath(name, true);

        if (securityManager != null) {
            PrivilegedAction<ResourceEntry> dp =
                new PrivilegedFindResourceByName(name, path, true);
            entry = AccessController.doPrivileged(dp);
        } else {
            entry = findResourceInternal(name, path, true);
        }

        if (entry == null)
            throw new ClassNotFoundException(name);

        Class<?> clazz = entry.loadedClass;
        if (clazz != null)
            return clazz;

        synchronized (getClassLoadingLockInternal(name)) {
            clazz = entry.loadedClass;
            if (clazz != null)
                return clazz;

            if (entry.binaryContent == null)
                throw new ClassNotFoundException(name);

            if (this.transformers.size() > 0) {
                // If the resource is a class just being loaded, decorate it
                // with any attached transformers

                // Ignore leading '/' and trailing CLASS_FILE_SUFFIX
                // Should be cheaper than replacing '.' by '/' in class name.
                String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length());

                for (ClassFileTransformer transformer : this.transformers) {
                    try {
                        byte[] transformed = transformer.transform(
                                this, internalName, null, null, entry.binaryContent
                        );
                        if (transformed != null) {
                            entry.binaryContent = transformed;
                        }
                    } catch (IllegalClassFormatException e) {
                        log.error(sm.getString("webappClassLoader.transformError", name), e);
                        return null;
                    }
                }
            }

            // 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) {
                    try {
                        if (entry.manifest == null) {
                            definePackage(packageName, null, null, null, null,
                                    null, null, null);
                        } else {
                            definePackage(packageName, entry.manifest,
                                    entry.codeBase);
                        }
                    } catch (IllegalArgumentException e) {
                        // Ignore: normal error due to dual definition of package
                    }
                    pkg = getPackage(packageName);
                }
            }

            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.");
                }

            }

            try {
                clazz = defineClass(name, entry.binaryContent, 0,
                        entry.binaryContent.length,
                        new CodeSource(entry.codeBase, entry.certificates));
            } catch (UnsupportedClassVersionError ucve) {
                throw new UnsupportedClassVersionError(
                        ucve.getLocalizedMessage() + " " +
                        sm.getString("webappClassLoader.wrongVersion",
                                name));
            }
            // Now the class has been defined, clear the elements of the local
            // resource cache that are no longer required.
            entry.loadedClass = clazz;
            entry.binaryContent = null;
            entry.codeBase = null;
            entry.manifest = null;
            entry.certificates = null;
            // Retain entry.source in case of a getResourceAsStream() call on
            // the class file after the class has been defined.
        }

        return clazz;

    }

    /**
     * Find specified resource in local repositories.
     *
     * @return the loaded resource, or null if the resource isn't found
     */
    protected ResourceEntry findResourceInternal(File file, String path){
        ResourceEntry entry = new ResourceEntry();
        try {
            entry.source = getURI(new File(file, path));
            String sourceString = entry.source.toString();
            if (sourceString.startsWith(webInfClassesCodeBase.toString()) &&
                    sourceString.endsWith(CLASS_FILE_SUFFIX)) {
                entry.codeBase = webInfClassesCodeBase;
            } else {
                entry.codeBase = entry.source;
            }
        } 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(final String name, final String path,
            final boolean manifestRequired) {

        if (!started) {
            log.info(sm.getString("webappClassLoader.stopped", name));
            return null;
        }

        if ((name == null) || (path == null))
            return null;

        JarEntry jarEntry = null;
        // Need to skip the leading / to find resources in JARs
        String jarEntryPath = path.substring(1);

        ResourceEntry entry = resourceEntries.get(path);
        if (entry != null) {
            if (manifestRequired && entry.manifest == MANIFEST_UNKNOWN) {
                // This resource was added to the cache when a request was made
                // for the resource that did not need the manifest. Now the
                // manifest is required, the cache entry needs to be updated.
                synchronized (jarFiles) {
                    if (openJARs()) {
                        for (int i = 0; i < jarFiles.length; i++) {

                            jarEntry = jarFiles[i].getJarEntry(jarEntryPath);

                            if (jarEntry != null) {
                                try {
                                    entry.manifest = jarFiles[i].getManifest();
                                } catch (IOException ioe) {
                                    // Ignore
                                }
                                break;
                            }
                         }
                     }
                }
            }
            return entry;
        }

        int contentLength = -1;
        InputStream binaryStream = null;
        boolean isClassResource = path.endsWith(CLASS_FILE_SUFFIX);
        boolean isCacheable = isClassResource;
        if (!isCacheable) {
             isCacheable = path.startsWith(SERVICES_PREFIX);
        }

        int jarFilesLength = jarFiles.length;
        int repositoriesLength = repositories.length;

        int i;

        Resource resource = null;

        boolean fileNeedConvert = false;

        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

                ResourceAttributes attributes =
                    (ResourceAttributes) resources.getAttributes(fullPath);
                contentLength = (int) attributes.getContentLength();
                String canonicalPath = attributes.getCanonicalPath();
                if (canonicalPath != null) {
                    // we create the ResourceEntry based on the information returned
                    // by the DirContext rather than just using the path to the
                    // repository. This allows to have smart DirContext implementations
                    // that "virtualize" the docbase (e.g. Eclipse WTP)
                    entry = findResourceInternal(new File(canonicalPath), "");
                } else {
                    // probably a resource not in the filesystem (e.g. in a
                    // packaged war)
                    entry = findResourceInternal(files[i], path);
                }
                entry.lastModified = attributes.getLastModified();

                if (resource != null) {


                    try {
                        binaryStream = resource.streamContent();
                    } catch (IOException e) {
                        return null;
                    }

                    if (needConvert) {
                        if (path.endsWith(".properties")) {
                            fileNeedConvert = true;
                        }
                    }

                    // 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) {
                // Ignore
            }
        }

        if ((entry == null) && (notFoundResources.containsKey(name)))
            return null;

        synchronized (jarFiles) {

            try {
                if (!openJARs()) {
                    return null;
                }
                for (i = 0; (entry == null) && (i < jarFilesLength); i++) {

                    jarEntry = jarFiles[i].getJarEntry(jarEntryPath);

                    if (jarEntry != null) {

                        entry = new ResourceEntry();
                        try {
                            entry.codeBase = getURI(jarRealFiles[i]);
                            entry.source =
                                    UriUtil.buildJarUrl(entry.codeBase.toString(), jarEntryPath);
                            entry.lastModified = jarRealFiles[i].lastModified();
                        } catch (MalformedURLException e) {
                            return null;
                        }
                        contentLength = (int) jarEntry.getSize();
                        try {
                            if (manifestRequired) {
                                entry.manifest = jarFiles[i].getManifest();
                            } else {
                                entry.manifest = MANIFEST_UNKNOWN;
                            }
                            binaryStream = jarFiles[i].getInputStream(jarEntry);
                        } catch (IOException e) {
                            return null;
                        }

                        // Extract resources contained in JAR to the workdir
                        if (antiJARLocking && !(path.endsWith(CLASS_FILE_SUFFIX))) {
                            byte[] buf = new byte[1024];
                            File resourceFile = new File(loaderDir, jarEntry.getName());
                            if (!resourceFile.exists()) {
                                Enumeration<JarEntry> entries = jarFiles[i].entries();
                                while (entries.hasMoreElements()) {
                                    JarEntry jarEntry2 =  entries.nextElement();
                                    if (!(jarEntry2.isDirectory()) &&
                                            (!jarEntry2.getName().endsWith(CLASS_FILE_SUFFIX))) {
                                        resourceFile = new File(loaderDir, jarEntry2.getName());
                                        try {
                                            if (!resourceFile.getCanonicalPath().startsWith(
                                                    canonicalLoaderDir)) {
                                                throw new IllegalArgumentException(
                                                        sm.getString("webappClassLoader.illegalJarPath",
                                                                jarEntry2.getName()));
                                            }
                                        } catch (IOException ioe) {
                                            throw new IllegalArgumentException(
                                                    sm.getString("webappClassLoader.validationErrorJarPath",
                                                            jarEntry2.getName()), ioe);
                                        }
                                        File parentFile = resourceFile.getParentFile();
                                        if (!parentFile.mkdirs() && !parentFile.exists()) {
                                            // Ignore the error (like the IOExceptions below)
                                        }
                                        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);
                                            }
                                            resourceFile.setLastModified(jarEntry2.getTime());
                                        } catch (IOException e) {
                                            // Ignore
                                        } finally {
                                            try {
                                                if (is != null) {
                                                    is.close();
                                                }
                                            } catch (IOException e) {
                                                // Ignore
                                            }
                                            try {
                                                if (os != null) {
                                                    os.close();
                                                }
                                            } catch (IOException e) {
                                                // Ignore
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                if (entry == null) {
                    synchronized (notFoundResources) {
                        notFoundResources.put(name, name);
                    }
                    return null;
                }

                /* Only cache the binary content if there is some content
                 * available one of the following is true:
                 * a) It is a class file since the binary content is only cached
                 *    until the class has been loaded
                 *    or
                 * b) The file needs conversion to address encoding issues (see
                 *    below)
                 *    or
                 * c) The resource is a service provider configuration file located
                 *    under META=INF/services
                 *
                 * In all other cases do not cache the content to prevent
                 * excessive memory usage if large resources are present (see
                 * https://bz.apache.org/bugzilla/show_bug.cgi?id=53081).
                 */
                if (binaryStream != null && (isCacheable || fileNeedConvert)) {

                    byte[] binaryContent = new byte[contentLength];

                    int pos = 0;
                    try {
                        while (true) {
                            int n = binaryStream.read(binaryContent, pos,
                                                      binaryContent.length - pos);
                            if (n <= 0)
                                break;
                            pos += n;
                        }
                    } catch (IOException e) {
                        log.error(sm.getString("webappClassLoader.readError", name), e);
                        return null;
                    }
                    if (fileNeedConvert) {
                        // Workaround for certain files on platforms that use
                        // EBCDIC encoding, when they are read through FileInputStream.
                        // See commit message of rev.303915 for details
                        // http://svn.apache.org/viewvc?view=revision&revision=303915
                        String str = new String(binaryContent,0,pos);
                        try {
                            binaryContent = str.getBytes(CHARSET_UTF8);
                        } catch (Exception e) {
                            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();
                    }

                }
            } finally {
                if (binaryStream != null) {
                    try {
                        binaryStream.close();
                    } catch (IOException e) { /* Ignore */}
                }
            }
        }

        // 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 = resourceEntries.get(path);
            if (entry2 == null) {
                resourceEntries.put(path, entry);
            } else {
                entry = entry2;
            }
        }

        return entry;

    }


    private String binaryNameToPath(String binaryName, boolean withLeadingSlash) {
        // 1 for leading '/', 6 for ".class"
        StringBuilder path = new StringBuilder(7 + binaryName.length());
        if (withLeadingSlash) {
            path.append('/');
        }
        path.append(binaryName.replace('.', '/'));
        path.append(CLASS_FILE_SUFFIX);
        return path.toString();
    }


    private String nameToPath(String name) {
        if (name.startsWith("/")) {
            return name;
        }
        StringBuilder path = new StringBuilder(
                1 + name.length());
        path.append('/');
        path.append(name);
        return path.toString();
    }


    /**
     * Returns true if the specified package name is sealed according to the
     * given manifest.
     */
    protected boolean isPackageSealed(String name, Manifest man) {

        String path = name.replace('.', '/') + '/';
        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) {
        String path = nameToPath(name);
        ResourceEntry entry = resourceEntries.get(path);
        if (entry != null) {
            if (entry.binaryContent != null)
                return new ByteArrayInputStream(entry.binaryContent);
            else {
                try {
                    return entry.source.openStream();
                } catch (IOException ioe) {
                    // Ignore
                }
            }
        }
        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) {
        String path = binaryNameToPath(name, true);
        ResourceEntry entry = resourceEntries.get(path);
        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 restrict loading of
     * classes from J2SE (java.*) and most 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) {

        // Need to be careful with order here
        if (name == null) {
            // Can't load a class without a name
            return false;
        }
        if (name.startsWith("java.")) {
            // Must never load java.* classes
            return false;
        }
        if (name.startsWith("javax.servlet.jsp.jstl")) {
            // OK for web apps to package JSTL
            return true;
        }
        if (name.startsWith("javax.servlet.")) {
            // Web apps should never package any other Servlet or JSP classes
            return false;
        }
        if (name.startsWith("javax.el")) {
            // Must never load javax.el.* classes
            return false;
        }

        // Assume everything else is OK
        return true;

    }


    /**
     * Check the specified JAR file, and return <code>true</code> if it does
     * not contain any of the trigger classes.
     *
     * @param file  The JAR file to be checked
     *
     * @exception IOException if an input/output error occurs
     */
    protected boolean validateJarFile(File file)
        throws IOException {

        if (triggers == null)
            return (true);

        JarFile jarFile = null;
        try {
            jarFile = JreCompat.getInstance().jarFileNewInstance(file);
            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 (Exception e) {
                    clazz = null;
                }
                if (clazz == null)
                    continue;
                String name = triggers[i].replace('.', '/') + CLASS_FILE_SUFFIX;
                if (log.isDebugEnabled())
                    log.debug(" Checking for " + name);
                JarEntry jarEntry = jarFile.getJarEntry(name);
                if (jarEntry != null) {
                    log.info("validateJarFile(" + file +
                        ") - jar not loaded. See Servlet Spec 3.0, "
                        + "section 10.7.2. Offending class: " + name);
                    return false;
                }
            }
            return true;
        } finally {
            if (jarFile != null) {
                try {
                    jarFile.close();
                } catch (IOException ioe) {
                    // Ignore
                }
            }
        }
    }


    /**
     * Get URL.
     * @deprecated Use {@link #getURI(File)} instead
     */
    @Deprecated
    protected URL getURL(File file, boolean encoded)
        throws MalformedURLException {

        File realFile = file;
        try {
            realFile = realFile.getCanonicalFile();
        } catch (IOException e) {
            // Ignore
        }
        if(encoded) {
            return getURI(realFile);
        }

        return realFile.toURI().toURL();
    }


    /**
     * Get the URI for the given file.
     */
    protected URL getURI(File file)
        throws MalformedURLException {


        File realFile = file;
        try {
            realFile = realFile.getCanonicalFile();
        } catch (IOException e) {
            // Ignore
        }
        return realFile.toURI().toURL();

    }


    /**
     * 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();

    }


    private static class CombinedEnumeration implements Enumeration<URL> {

        private final Enumeration<URL>[] sources;
        private int index = 0;

        public CombinedEnumeration(Enumeration<URL> enum1, Enumeration<URL> enum2) {
            @SuppressWarnings("unchecked")
            Enumeration<URL>[] sources = new Enumeration[] { enum1, enum2 };
            this.sources = sources;
        }


        @Override
        public boolean hasMoreElements() {
            return inc();
        }


        @Override
        public URL nextElement() {
            if (inc()) {
                return sources[index].nextElement();
            }
            throw new NoSuchElementException();
        }


        private boolean inc() {
            while (index < sources.length) {
                if (sources[index].hasMoreElements()) {
                    return true;
                }
                index++;
            }
            return false;
        }
    }
}
