WebappClassLoaderBase.java

/*
 * 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.ByteArrayOutputStream;
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.InaccessibleObjectException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;

import org.apache.catalina.Container;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory;
import org.apache.juli.WebappProperties;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.InstrumentableClassLoader;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.buf.ToStringUtil;
import org.apache.tomcat.util.collections.ConcurrentLruCache;
import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;

/**
 * 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 bootstrap 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.
 * <p>
 * <strong>IMPLEMENTATION NOTE</strong> - As of 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, WebappProperties {

    private static final Log log = LogFactory.getLog(WebappClassLoaderBase.class);

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

    private static final String JVM_THREAD_GROUP_SYSTEM = "system";

    private static final String CLASS_FILE_SUFFIX = ".class";

    static {
        if (!JreCompat.isGraalAvailable()) {
            registerAsParallelCapable();
        }
        JVM_THREAD_GROUP_NAMES.add(JVM_THREAD_GROUP_SYSTEM);
        JVM_THREAD_GROUP_NAMES.add("RMI Runtime");
    }


    // ------------------------------------------------------- Static Variables

    /**
     * The string manager for this package.
     */
    protected static final StringManager sm = StringManager.getManager(WebappClassLoaderBase.class);


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

    /**
     * Construct a new ClassLoader with no defined repositories and no parent ClassLoader.
     */
    protected 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.javaseClassLoader = j;
    }


    /**
     * 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
     */
    protected 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.javaseClassLoader = j;
    }


    // ----------------------------------------------------- Instance Variables

    /**
     * Associated web resources for this webapp.
     */
    protected WebResourceRoot 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 final Map<String,ResourceEntry> resourceEntries = new ConcurrentHashMap<>();


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


    private final Map<String,Long> jarModificationTimes = new HashMap<>();


    /**
     * A list of read File Permission's required if this loader is for a web application context.
     */
    protected final ArrayList<Permission> permissionList = new ArrayList<>();


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


    /**
     * The parent class loader.
     */
    protected final ClassLoader parent;


    /**
     * 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.
     */
    private ClassLoader javaseClassLoader;


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

    /**
     * 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(ClassLoader)} 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 ClassLoader} to {@link ClassLoader#getParent()} 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;

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

    /**
     * Should Tomcat skip the memory leak checks when the web application is stopped as part of the process of shutting
     * down the JVM?
     */
    private boolean skipMemoryLeakChecksOnJvmShutdown = false;

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


    /**
     * Flag that indicates that {@link #addURL(URL)} has been called which creates a requirement to check the super
     * class when searching for resources.
     */
    private boolean hasExternalRepositories = false;


    /**
     * Repositories managed by this class rather than the super class.
     */
    private List<URL> localRepositories = new ArrayList<>();


    private volatile LifecycleState state = LifecycleState.NEW;

    /*
     * Class resources are not cached since they are loaded on first use and the resource is then no longer required. It
     * does help, however, to cache classes that are not found as in some scenarios the same class will be searched for
     * many times and the greater the number of JARs/classes, the longer that lookup will take.
     */
    private final ConcurrentLruCache<String> notFoundClassResources = new ConcurrentLruCache<>(1000);


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

    public void setNotFoundClassResourceCacheSize(int notFoundClassResourceCacheSize) {
        notFoundClassResources.setLimit(notFoundClassResourceCacheSize);
    }


    public int getNotFoundClassResourceCacheSize() {
        return notFoundClassResources.getLimit();
    }


    /**
     * Set associated resources.
     *
     * @param resources the resources from which the classloader will load the classes
     */
    public void setResources(WebResourceRoot resources) {
        this.resources = resources;
    }


    /**
     * @return the context name for this class loader.
     */
    public String getContextName() {
        if (resources == null) {
            return "Unknown";
        } else {
            return resources.getContext().getBaseName();
        }
    }


    /**
     * Return the "delegate first" flag for this class loader.
     *
     * @return <code>true</code> if the class lookup will delegate to the parent first. The default in Tomcat is
     *             <code>false</code>.
     */
    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;
    }


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


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


    /**
     * @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 getClearReferencesThreadLocals() {
        return clearReferencesThreadLocals;
    }


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


    public boolean getSkipMemoryLeakChecksOnJvmShutdown() {
        return skipMemoryLeakChecksOnJvmShutdown;
    }


    public void setSkipMemoryLeakChecksOnJvmShutdown(boolean skipMemoryLeakChecksOnJvmShutdown) {
        this.skipMemoryLeakChecksOnJvmShutdown = skipMemoryLeakChecksOnJvmShutdown;
    }


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

    @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()));
    }

    @Override
    public void removeTransformer(ClassFileTransformer transformer) {

        if (transformer == null) {
            return;
        }

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

    protected void copyStateWithoutTransformers(WebappClassLoaderBase base) {
        base.resources = this.resources;
        base.delegate = this.delegate;
        base.state = LifecycleState.NEW;
        base.clearReferencesStopThreads = this.clearReferencesStopThreads;
        base.clearReferencesStopTimerThreads = this.clearReferencesStopTimerThreads;
        base.clearReferencesLogFactoryRelease = this.clearReferencesLogFactoryRelease;
        base.clearReferencesHttpClientKeepAliveThread = this.clearReferencesHttpClientKeepAliveThread;
        base.jarModificationTimes.putAll(this.jarModificationTimes);
        base.permissionList.addAll(this.permissionList);
        base.loaderPC.putAll(this.loaderPC);
    }

    /**
     * Have one or more classes or resources been modified so that a reload is appropriate?
     *
     * @return <code>true</code> if there's been a modification
     */
    public boolean modified() {

        if (log.isTraceEnabled()) {
            log.trace("modified()");
        }

        for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) {
            long cachedLastModified = entry.getValue().lastModified;
            long lastModified = resources.getClassLoaderResource(entry.getKey()).getLastModified();
            if (lastModified != cachedLastModified) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("webappClassLoader.resourceModified", entry.getKey(),
                            new Date(cachedLastModified), new Date(lastModified)));
                }
                return true;
            }
        }

        // Check if JARs have been added or removed
        WebResource[] jars = resources.listResources("/WEB-INF/lib");
        // Filter out non-JAR resources

        int jarCount = 0;
        for (WebResource jar : jars) {
            if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
                jarCount++;
                Long recordedLastModified = jarModificationTimes.get(jar.getName());
                if (recordedLastModified == null) {
                    // Jar has been added
                    log.info(sm.getString("webappClassLoader.jarsAdded", resources.getContext().getName()));
                    return true;
                }
                if (recordedLastModified.longValue() != jar.getLastModified()) {
                    // Jar has been changed
                    log.info(sm.getString("webappClassLoader.jarsModified", resources.getContext().getName()));
                    return true;
                }
            }
        }

        if (jarCount < jarModificationTimes.size()) {
            log.info(sm.getString("webappClassLoader.jarsRemoved", resources.getContext().getName()));
            return true;
        }


        // No classes have been modified
        return false;
    }


    @Override
    public String toString() {

        StringBuilder sb = new StringBuilder(this.getClass().getSimpleName());
        sb.append("\r\n  context: ");
        sb.append(getContextName());
        sb.append("\r\n  delegate: ");
        sb.append(delegate);
        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


    // Note: exposed for use by 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 The binary 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.isTraceEnabled()) {
            log.trace("    findClass(" + name + ")");
        }

        checkStateForClassLoading(name);

        if (name == null) {
            throw new ClassNotFoundException("null");
        }

        String path = binaryNameToPath(name, true);

        // 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 (!notFoundClassResources.contains(path)) {
                try {
                    clazz = findClassInternal(name, path);
                } catch (RuntimeException e) {
                    if (log.isTraceEnabled()) {
                        log.trace("      -->RuntimeException Rethrown", e);
                    }
                    throw e;
                }
                if (clazz == null && hasExternalRepositories) {
                    try {
                        clazz = super.findClass(name);
                    } catch (RuntimeException e) {
                        if (log.isTraceEnabled()) {
                            log.trace("      -->RuntimeException Rethrown", e);
                        }
                        throw e;
                    }
                }
            }
        } catch (ClassNotFoundException e) {
            if (log.isTraceEnabled()) {
                log.trace("    --> Passing on ClassNotFoundException");
            }
            notFoundClassResources.add(path);
            throw e;
        }
        if (clazz == null) {
            if (log.isTraceEnabled()) {
                log.trace("    --> Returning ClassNotFoundException");
            }
            notFoundClassResources.add(path);
            throw new ClassNotFoundException(name);
        }

        // Return the class we have located
        if (log.isTraceEnabled()) {
            log.trace("      Returning class " + clazz);
        }

        if (log.isTraceEnabled()) {
            ClassLoader cl = clazz.getClassLoader();
            log.trace("      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.isTraceEnabled()) {
            log.trace("    findResource(" + name + ")");
        }

        checkStateForResourceLoading(name);

        URL url = null;

        String path = nameToPath(name);

        if (!notFoundClassResources.contains(path)) {
            WebResource resource = resources.getClassLoaderResource(path);
            if (resource.exists()) {
                url = resource.getURL();
                trackLastModified(path, resource);
            }

            if (url == null && hasExternalRepositories) {
                url = super.findResource(name);
            }

            if (url == null) {
                notFoundClassResources.add(path);
            }
        }

        if (log.isTraceEnabled()) {
            if (url != null) {
                log.trace("    --> Returning '" + url.toString() + "'");
            } else {
                log.trace("    --> Resource not found, returning null");
            }
        }
        return url;
    }


    private void trackLastModified(String path, WebResource resource) {
        if (resourceEntries.containsKey(path)) {
            return;
        }
        ResourceEntry entry = new ResourceEntry();
        entry.lastModified = resource.getLastModified();
        synchronized (resourceEntries) {
            resourceEntries.putIfAbsent(path, entry);
        }
    }


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

        if (log.isTraceEnabled()) {
            log.trace("    findResources(" + name + ")");
        }

        checkStateForResourceLoading(name);

        LinkedHashSet<URL> result = new LinkedHashSet<>();

        String path = nameToPath(name);

        WebResource[] webResources = resources.getClassLoaderResources(path);
        for (WebResource webResource : webResources) {
            if (webResource.exists()) {
                result.add(webResource.getURL());
            }
        }

        // Adding the results of a call to the superclass
        if (hasExternalRepositories) {
            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.isTraceEnabled()) {
            log.trace("getResource(" + name + ")");
        }

        checkStateForResourceLoading(name);

        URL url = null;

        boolean delegateFirst = delegate || filter(name, false);

        // (1) Delegate to parent if requested
        if (delegateFirst) {
            if (log.isTraceEnabled()) {
                log.trace("  Delegating to parent classloader " + parent);
            }
            url = parent.getResource(name);
            if (url != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  --> Returning '" + url.toString() + "'");
                }
                return url;
            }
        }

        // (2) Search local repositories
        url = findResource(name);
        if (url != null) {
            if (log.isTraceEnabled()) {
                log.trace("  --> Returning '" + url.toString() + "'");
            }
            return url;
        }

        // (3) Delegate to parent unconditionally if not already attempted
        if (!delegateFirst) {
            url = parent.getResource(name);
            if (url != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  --> Returning '" + url.toString() + "'");
                }
                return url;
            }
        }

        // (4) Resource was not found
        if (log.isTraceEnabled()) {
            log.trace("  --> Resource not found, returning null");
        }
        return null;

    }


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

        Enumeration<URL> parentResources = parent.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
        boolean delegateFirst = delegate || filter(name, false);

        if (delegateFirst) {
            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.isTraceEnabled()) {
            log.trace("getResourceAsStream(" + name + ")");
        }

        checkStateForResourceLoading(name);

        InputStream stream = null;

        boolean delegateFirst = delegate || filter(name, false);

        // (1) Delegate to parent if requested
        if (delegateFirst) {
            if (log.isTraceEnabled()) {
                log.trace("  Delegating to parent classloader " + parent);
            }
            stream = parent.getResourceAsStream(name);
            if (stream != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  --> Returning stream from parent");
                }
                return stream;
            }
        }

        // (2) Search local repositories
        if (log.isTraceEnabled()) {
            log.trace("  Searching local repositories");
        }
        String path = nameToPath(name);
        if (!notFoundClassResources.contains(path)) {
            WebResource resource = resources.getClassLoaderResource(path);
            if (resource.exists()) {
                stream = resource.getInputStream();
                // Filter out .class resources through the ClassFileTranformer
                if (name.endsWith(CLASS_FILE_SUFFIX) && transformers.size() > 0) {
                    // If the resource is a class, decorate it with any attached transformers
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buf = new byte[8192];
                    int numRead;
                    try {
                        while ((numRead = stream.read(buf)) >= 0) {
                            baos.write(buf, 0, numRead);
                        }
                    } catch (IOException e) {
                        log.error(sm.getString("webappClassLoader.transformError", name), e);
                        return null;
                    } finally {
                        try {
                            stream.close();
                        } catch (IOException e) {
                        }
                    }
                    byte[] binaryContent = baos.toByteArray();
                    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, binaryContent);
                            if (transformed != null) {
                                binaryContent = transformed;
                            }
                        } catch (IllegalClassFormatException e) {
                            log.error(sm.getString("webappClassLoader.transformError", name), e);
                            return null;
                        }
                    }
                    stream = new ByteArrayInputStream(binaryContent);
                }
                trackLastModified(path, resource);
            }
            try {
                if (hasExternalRepositories && stream == null) {
                    URL url = super.findResource(name);
                    if (url != null) {
                        stream = url.openStream();
                    }
                }
            } catch (IOException e) {
                // Ignore
            }
            if (stream != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  --> Returning stream from local");
                }
                return stream;
            }

            notFoundClassResources.add(path);
        }

        // (3) Delegate to parent unconditionally
        if (!delegateFirst) {
            if (log.isTraceEnabled()) {
                log.trace("  Delegating to parent classloader unconditionally " + parent);
            }
            stream = parent.getResourceAsStream(name);
            if (stream != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  --> Returning stream from parent");
                }
                return stream;
            }
        }

        // (4) Resource was not found
        if (log.isTraceEnabled()) {
            log.trace("  --> 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 The binary 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    The binary 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 (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
            if (log.isTraceEnabled()) {
                log.trace("loadClass(" + name + ", " + resolve + ")");
            }
            Class<?> clazz = null;

            // Log access to stopped class loader
            checkStateForClassLoading(name);

            // (0) Check our previously loaded local class cache
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  Returning class from cache");
                }
                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            }

            // (0.1) Check our previously loaded class cache
            clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
            if (clazz != null) {
                if (log.isTraceEnabled()) {
                    log.trace("  Returning class from cache");
                }
                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            }

            /*
             * (0.2) Try loading the class with the bootstrap class loader, to prevent the webapp from overriding Java
             * SE classes. This implements SRV.10.7.2
             */
            String resourceName = binaryNameToPath(name, false);

            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                /*
                 * Use getResource as it won't trigger an expensive ClassNotFoundException if the resource is not
                 * available from the Java SE class loader.
                 *
                 * See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for details of how this may trigger a
                 * StackOverflowError.
                 *
                 * Given these reported errors, catch Throwable to ensure all edge cases are caught.
                 */
                URL url = javaseLoader.getResource(resourceName);
                tryLoadingFromJavaseLoader = url != null;
            } catch (Throwable t) {
                // Swallow all exceptions apart from those that must be re-thrown
                ExceptionUtils.handleThrowable(t);
                // The getResource() trick won't work for this class. We have to
                // try loading it directly and accept that we might get a
                // ClassNotFoundException.
                tryLoadingFromJavaseLoader = true;
            }

            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            boolean delegateLoad = delegate || filter(name, true);

            // (1) Delegate to our parent if requested
            if (delegateLoad) {
                if (log.isTraceEnabled()) {
                    log.trace("  Delegating to parent classloader1 " + parent);
                }
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isTraceEnabled()) {
                            log.trace("  Loading class from parent");
                        }
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (2) Search local repositories
            if (log.isTraceEnabled()) {
                log.trace("  Searching local repositories");
            }
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isTraceEnabled()) {
                        log.trace("  Loading class from local repository");
                    }
                    if (resolve) {
                        resolveClass(clazz);
                    }
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

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

        if (log.isDebugEnabled()) {
            log.debug(ToStringUtil.classPathForCNFE(this));
        }
        throw new ClassNotFoundException(name);
    }


    protected void checkStateForClassLoading(String className) throws ClassNotFoundException {
        // It is not permitted to load new classes once the web application has
        // been stopped.
        try {
            checkStateForResourceLoading(className);
        } catch (IllegalStateException ise) {
            throw new ClassNotFoundException(ise.getMessage(), ise);
        }
    }


    protected void checkStateForResourceLoading(String resource) throws IllegalStateException {
        // It is not permitted to load resources once the web application has
        // been stopped.
        if (!state.isAvailable()) {
            String msg = sm.getString("webappClassLoader.stopped", resource);
            IllegalStateException ise = new IllegalStateException(msg);
            log.info(msg, ise);
            throw ise;
        }
    }

    /**
     * Get the Permissions for a CodeSource. If this instance of WebappClassLoaderBase is for a web application context,
     * add read FilePermission for the appropriate resources.
     *
     * @param codeSource where the code was loaded from
     *
     * @return PermissionCollection for CodeSource
     */
    @Override
    protected PermissionCollection getPermissions(CodeSource codeSource) {
        return null;
    }


    /**
     * {@inheritDoc}
     * <p>
     * Note that list of URLs returned by this method may not be complete. The web application class loader accesses
     * class loader resources via the {@link WebResourceRoot} which supports the arbitrary mapping of additional files,
     * directories and contents of JAR files under WEB-INF/classes. Any such resources will not be included in the URLs
     * returned here.
     */
    @Override
    public URL[] getURLs() {
        ArrayList<URL> result = new ArrayList<>();
        result.addAll(localRepositories);
        result.addAll(Arrays.asList(super.getURLs()));
        return result.toArray(new URL[0]);
    }


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


    @Override
    public void addLifecycleListener(LifecycleListener listener) {
        // NOOP
    }


    @Override
    public LifecycleListener[] findLifecycleListeners() {
        return new LifecycleListener[0];
    }


    @Override
    public void removeLifecycleListener(LifecycleListener listener) {
        // NOOP
    }


    @Override
    public LifecycleState getState() {
        return state;
    }


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


    @Override
    public void init() {
        state = LifecycleState.INITIALIZED;
    }


    @Override
    public void start() throws LifecycleException {

        state = LifecycleState.STARTING_PREP;

        WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
        for (WebResource classes : classesResources) {
            if (classes.isDirectory() && classes.canRead()) {
                localRepositories.add(classes.getURL());
            }
        }
        WebResource[] jars = resources.listResources("/WEB-INF/lib");
        for (WebResource jar : jars) {
            if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
                localRepositories.add(jar.getURL());
                jarModificationTimes.put(jar.getName(), Long.valueOf(jar.getLastModified()));
            }
        }

        state = LifecycleState.STARTED;
    }


    @Override
    public void stop() throws LifecycleException {

        state = LifecycleState.STOPPING_PREP;

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

        state = LifecycleState.STOPPING;

        resourceEntries.clear();
        notFoundClassResources.clear();
        jarModificationTimes.clear();
        resources = null;

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

        state = LifecycleState.STOPPED;
    }


    @Override
    public void destroy() {
        state = LifecycleState.DESTROYING;

        try {
            super.close();
        } catch (IOException ioe) {
            log.warn(sm.getString("webappClassLoader.superCloseFail"), ioe);
        }
        state = LifecycleState.DESTROYED;
    }


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

    protected ClassLoader getJavaseClassLoader() {
        return javaseClassLoader;
    }

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

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

        // If the JVM is shutting down, skip the memory leak checks
        if (skipMemoryLeakChecksOnJvmShutdown && !resources.getContext().getParent().getState().isAvailable()) {
            // During reloading / redeployment the parent is expected to be
            // available. Parent is not available so this might be a JVM
            // shutdown.
            try {
                Thread dummyHook = new Thread();
                Runtime.getRuntime().addShutdownHook(dummyHook);
                Runtime.getRuntime().removeShutdownHook(dummyHook);
            } catch (IllegalStateException ise) {
                return;
            }
        }

        if (!JreCompat.isGraalAvailable()) {
            // De-register any remaining JDBC drivers
            clearReferencesJdbc();
        }

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

        // Check for leaks triggered by ThreadLocals loaded by this class loader
        if (clearReferencesThreadLocals && !JreCompat.isGraalAvailable()) {
            checkThreadLocalsForLeaks();
        }

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

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

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

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

        // Clear any custom URLStreamHandlers
        TomcatURLStreamHandlerFactory.release(this);
    }


    /**
     * 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.
     * <p>
     * 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.
     * <p>
     * If only apps cleaned up after themselves...
     */
    private void clearReferencesJdbc() {
        // 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 (InputStream is = getResourceAsStream("org/apache/catalina/loader/JdbcLeakPrevention.class")) {
            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.getConstructor().newInstance();
            @SuppressWarnings("unchecked")
            List<String> driverNames =
                    (List<String>) obj.getClass().getMethod("clearJdbcDriverRegistrations").invoke(obj);
            for (String name : driverNames) {
                log.warn(sm.getString("webappClassLoader.clearJdbc", getContextName(), 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", getContextName()), t);
        }
    }


    private void clearReferencesThreads() {
        Thread[] threads = getThreads();
        List<Thread> threadsToStop = new ArrayList<>();

        // 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;
                    }

                    final String threadName = thread.getName();

                    // JVM controlled threads
                    ThreadGroup tg = thread.getThreadGroup();
                    if (tg != null && JVM_THREAD_GROUP_NAMES.contains(tg.getName())) {
                        // HttpClient keep-alive threads
                        if (clearReferencesHttpClientKeepAliveThread && threadName.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.warn(sm.getString("webappClassLoader.stackTraceRequestThread", getContextName(), threadName,
                                getStackTrace(thread)));
                    } else {
                        log.warn(sm.getString("webappClassLoader.stackTrace", getContextName(), threadName,
                                getStackTrace(thread)));
                    }

                    // 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 {
                        Object executor = getExecutor(thread);
                        if (executor instanceof ThreadPoolExecutor) {
                            ((ThreadPoolExecutor) executor).shutdownNow();
                            usingExecutor = true;
                        } else if (executor instanceof java.util.concurrent.ThreadPoolExecutor) {
                            ((java.util.concurrent.ThreadPoolExecutor) executor).shutdownNow();
                            usingExecutor = true;
                        }
                    } catch (SecurityException | NoSuchFieldException | IllegalArgumentException |
                            IllegalAccessException | InaccessibleObjectException e) {
                        log.warn(sm.getString("webappClassLoader.stopThreadFail", thread.getName(), getContextName()),
                                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()) {
                // Unable to stop the thread. Log an error.
                log.error(sm.getString("webappClassLoader.stopThreadFail", t.getName(), getContextName()));
            }
        }
    }


    private Object getExecutor(Thread thread)
            throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {

        Object result = null;

        // 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 including our
        // internal fork.
        if (target != null && target.getClass().getCanonicalName() != null && (target.getClass().getCanonicalName()
                .equals("org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") ||
                target.getClass().getCanonicalName().equals("java.util.concurrent.ThreadPoolExecutor.Worker"))) {
            Field executorField = target.getClass().getDeclaredField("this$0");
            executorField.setAccessible(true);
            result = executorField.get(target);
        }

        if (result == null) {
            Object holder = null;
            Object task = null;
            try {
                Field holderField = thread.getClass().getDeclaredField("holder");
                holderField.setAccessible(true);
                holder = holderField.get(thread);

                Field taskField = holder.getClass().getDeclaredField("task");
                taskField.setAccessible(true);
                task = taskField.get(holder);
            } catch (NoSuchFieldException nfe) {
                return null;
            }

            if (task != null && task.getClass().getCanonicalName() != null && (task.getClass().getCanonicalName()
                    .equals("org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") ||
                    task.getClass().getCanonicalName().equals("java.util.concurrent.ThreadPoolExecutor.Worker"))) {
                Field executorField = task.getClass().getDeclaredField("this$0");
                executorField.setAccessible(true);
                result = executorField.get(task);
            }
        }

        return result;
    }


    /*
     * 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.warn(sm.getString("webappClassLoader.warnTimerThread", getContextName(), 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(), getContextName()), 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 (InaccessibleObjectException e) {
            // Must be running on without the necessary command line options.
            log.warn(sm.getString("webappClassLoader.addExportsThreadLocal", getCurrentModuleName()));
        } catch (Throwable t) {
            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] = getContextName();
                            if (key != null) {
                                args[1] = getPrettyClassName(key.getClass());
                                try {
                                    args[2] = key.toString();
                                } catch (Exception e) {
                                    log.warn(
                                            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.warn(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;
    }

    private String getStackTrace(Thread thread) {
        StringBuilder builder = new StringBuilder();
        for (StackTraceElement ste : thread.getStackTrace()) {
            builder.append("\n ").append(ste);
        }
        return builder.toString();
    }

    /**
     * @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<?>) {
            try {
                for (Object entry : (Collection<?>) o) {
                    if (loadedByThisOrChild(entry)) {
                        return true;
                    }
                }
            } catch (ConcurrentModificationException e) {
                log.warn(sm.getString("webappClassLoader.loadedByThisOrChildFail", clazz.getName(), getContextName()),
                        e);
            }
        }
        return false;
    }

    /**
     * @return 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", getContextName()), e);
        } catch (SecurityException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
            log.warn(sm.getString("webappClassLoader.clearRmiFail", getContextName()), e);
        } catch (InaccessibleObjectException e) {
            // Must be running on without the necessary command line options.
            log.warn(sm.getString("webappClassLoader.addExportsRmi", getCurrentModuleName()));
        }
    }


    private String getCurrentModuleName() {
        String moduleName = this.getClass().getModule().getName();
        if (moduleName == null) {
            moduleName = "ALL-UNNAMED";
        }
        return moduleName;
    }


    /**
     * Find specified class in local repositories.
     *
     * @param name The binary name of the class to be loaded
     *
     * @return the loaded class, or null if the class isn't found
     */
    protected Class<?> findClassInternal(String name) {

        checkStateForResourceLoading(name);

        if (name == null) {
            return null;
        }
        String path = binaryNameToPath(name, true);

        return findClassInternal(name, path);
    }


    /*
     * The use of getPackage() is appropriate given that the code is checking if the package is sealed. Therefore,
     * parent class loaders need to be checked.
     */
    @SuppressWarnings("deprecation")
    private Class<?> findClassInternal(String name, String path) {
        ResourceEntry entry = resourceEntries.get(path);
        WebResource resource = null;

        if (entry == null) {
            resource = resources.getClassLoaderResource(path);

            if (!resource.exists()) {
                return null;
            }

            entry = new ResourceEntry();
            entry.lastModified = resource.getLastModified();

            // 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;
                }
            }
        }

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

        synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
            clazz = entry.loadedClass;
            if (clazz != null) {
                return clazz;
            }

            if (resource == null) {
                resource = resources.getClassLoaderResource(path);
            }

            if (!resource.exists()) {
                return null;
            }

            byte[] binaryContent = resource.getContent();
            if (binaryContent == null) {
                // Something went wrong reading the class bytes (and will have
                // been logged at debug level).
                return null;
            }
            Manifest manifest = resource.getManifest();
            URL codeBase = resource.getCodeBase();
            Certificate[] certificates = resource.getCertificates();

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

            try {
                clazz = defineClass(name, binaryContent, 0, binaryContent.length,
                        new CodeSource(codeBase, certificates));
            } catch (UnsupportedClassVersionError ucve) {
                throw new UnsupportedClassVersionError(
                        ucve.getLocalizedMessage() + " " + sm.getString("webappClassLoader.wrongVersion", name));
            } catch (LinkageError e) {
                // May be caused by the transformation also triggering loading of the class - BZ 68721
                try {
                    // Try and load the already defined class
                    clazz = findLoadedClass0(name);
                } catch (Throwable t) {
                    // Not BZ 68721
                    ExceptionUtils.handleThrowable(t);
                    // Re-throw the original exception
                    throw e;
                }
                if (clazz == null) {
                    // Not BZ 68721
                    throw e;
                }
            }
            entry.loadedClass = clazz;
        }

        return clazz;
    }


    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.
     *
     * @param name Path name to check
     * @param man  Associated manifest
     *
     * @return <code>true</code> if the manifest associated says it is sealed
     */
    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 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 The binary name of the resource to return
     *
     * @return a loaded class
     */
    protected Class<?> findLoadedClass0(String name) {

        String path = binaryNameToPath(name, true);

        ResourceEntry entry = resourceEntries.get(path);
        if (entry != null) {
            return entry.loadedClass;
        }
        return null;
    }


    /**
     * Filter classes.
     *
     * @param name        class name
     * @param isClassName <code>true</code> if name is a class name, <code>false</code> if name is a resource name
     *
     * @return <code>true</code> if the class should be filtered
     */
    protected boolean filter(String name, boolean isClassName) {

        if (name == null) {
            return false;
        }

        char ch;
        if (name.startsWith("jakarta")) {
            /* 7 == length("jakarta") */
            if (name.length() == 7) {
                return false;
            }
            ch = name.charAt(7);
            if (isClassName && ch == '.') {
                /* 8 == length("jakarta.") */
                if (name.startsWith("servlet.jsp.jstl.", 8)) {
                    return false;
                }
                if (name.startsWith("annotation.", 8) || name.startsWith("el.", 8) || name.startsWith("servlet.", 8) ||
                        name.startsWith("websocket.", 8) || name.startsWith("security.auth.message.", 8)) {
                    return true;
                }
            } else if (!isClassName && ch == '/') {
                /* 8 == length("jakarta/") */
                if (name.startsWith("servlet/jsp/jstl/", 8)) {
                    return false;
                }
                if (name.startsWith("annotation/", 8) || name.startsWith("el/", 8) || name.startsWith("servlet/", 8) ||
                        name.startsWith("websocket/", 8) || name.startsWith("security/auth/message/", 8)) {
                    return true;
                }
            }
        } else if (name.startsWith("javax")) {
            /* 5 == length("javax") */
            if (name.length() == 5) {
                return false;
            }
            ch = name.charAt(5);
            if (isClassName && ch == '.') {
                /* 6 == length("javax.") */
                if (name.startsWith("websocket.", 6)) {
                    return true;
                }
            } else if (!isClassName && ch == '/') {
                /* 6 == length("javax/") */
                if (name.startsWith("websocket/", 6)) {
                    return true;
                }
            }
        } else if (name.startsWith("org")) {
            /* 3 == length("org") */
            if (name.length() == 3) {
                return false;
            }
            ch = name.charAt(3);
            if (isClassName && ch == '.') {
                /* 4 == length("org.") */
                if (name.startsWith("apache.", 4)) {
                    /* 11 == length("org.apache.") */
                    if (name.startsWith("tomcat.jdbc.", 11)) {
                        return false;
                    }
                    if (name.startsWith("el.", 11) || name.startsWith("catalina.", 11) ||
                            name.startsWith("jasper.", 11) || name.startsWith("juli.", 11) ||
                            name.startsWith("tomcat.", 11) || name.startsWith("naming.", 11) ||
                            name.startsWith("coyote.", 11)) {
                        return true;
                    }
                }
            } else if (!isClassName && ch == '/') {
                /* 4 == length("org/") */
                if (name.startsWith("apache/", 4)) {
                    /* 11 == length("org/apache/") */
                    if (name.startsWith("tomcat/jdbc/", 11)) {
                        return false;
                    }
                    if (name.startsWith("el/", 11) || name.startsWith("catalina/", 11) ||
                            name.startsWith("jasper/", 11) || name.startsWith("juli/", 11) ||
                            name.startsWith("tomcat/", 11) || name.startsWith("naming/", 11) ||
                            name.startsWith("coyote/", 11)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }


    @Override
    protected void addURL(URL url) {
        super.addURL(url);
        hasExternalRepositories = true;
    }


    @Override
    public String getWebappName() {
        return getContextName();
    }


    @Override
    public String getHostName() {
        if (resources != null) {
            Container host = resources.getContext().getParent();
            if (host != null) {
                return host.getName();
            }
        }
        return null;
    }


    @Override
    public String getServiceName() {
        if (resources != null) {
            Container host = resources.getContext().getParent();
            if (host != null) {
                Container engine = host.getParent();
                if (engine != null) {
                    return engine.getName();
                }
            }
        }
        return null;
    }


    @Override
    public boolean hasLoggingConfig() {
        return findResource("logging.properties") != null;
    }


    private static class CombinedEnumeration implements Enumeration<URL> {

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

        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;
        }
    }
}