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

import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.management.ListenerNotFoundException;
import javax.management.MBeanNotificationInfo;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.annotation.MultipartConfig;

import org.apache.catalina.Container;
import org.apache.catalina.ContainerServlet;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Wrapper;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.PeriodicEventListener;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.log.SystemLogHandler;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.modeler.Util;

/**
 * Standard implementation of the <b>Wrapper</b> interface that represents an individual servlet definition. No child
 * Containers are allowed, and the parent Container must be a Context.
 */
public class StandardWrapper extends ContainerBase implements ServletConfig, Wrapper, NotificationEmitter {

    private final Log log = LogFactory.getLog(StandardWrapper.class); // must not be static

    /**
     * Default servlet methods supported by this wrapper.
     */
    protected static final String[] DEFAULT_SERVLET_METHODS = new String[] { "GET", "HEAD", "POST" };

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


    /**
     * Create a new StandardWrapper component with the default basic Valve.
     */
    public StandardWrapper() {

        super();
        swValve = new StandardWrapperValve();
        pipeline.setBasic(swValve);
        broadcaster = new NotificationBroadcasterSupport();

    }


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


    /**
     * The date and time at which this servlet will become available (in milliseconds since the epoch), or zero if the
     * servlet is available. If this value equals Long.MAX_VALUE, the unavailability of this servlet is considered
     * permanent.
     */
    protected long available = 0L;

    /**
     * The broadcaster that sends EE notifications.
     */
    protected final NotificationBroadcasterSupport broadcaster;

    /**
     * The count of allocations that are currently active.
     */
    protected final AtomicInteger countAllocated = new AtomicInteger(0);


    /**
     * The facade associated with this wrapper.
     */
    protected final StandardWrapperFacade facade = new StandardWrapperFacade(this);


    /**
     * The (single) possibly uninitialized instance of this servlet.
     */
    protected volatile Servlet instance = null;


    /**
     * Flag that indicates if this instance has been initialized
     */
    protected volatile boolean instanceInitialized = false;


    /**
     * The load-on-startup order value (negative value means load on first call) for this servlet.
     */
    protected int loadOnStartup = -1;


    /**
     * Mappings associated with the wrapper.
     */
    protected final ArrayList<String> mappings = new ArrayList<>();


    /**
     * The initialization parameters for this servlet, keyed by parameter name.
     */
    protected HashMap<String,String> parameters = new HashMap<>();


    /**
     * The security role references for this servlet, keyed by role name used in the servlet. The corresponding value is
     * the role name of the web application itself.
     */
    protected HashMap<String,String> references = new HashMap<>();


    /**
     * The run-as identity for this servlet.
     */
    protected String runAs = null;

    /**
     * The notification sequence number.
     */
    protected long sequenceNumber = 0;

    /**
     * The fully qualified servlet class name for this servlet.
     */
    protected String servletClass = null;


    /**
     * Are we unloading our servlet instance at the moment?
     */
    protected volatile boolean unloading = false;


    /**
     * Wait time for servlet unload in ms.
     */
    protected long unloadDelay = 2000;


    /**
     * True if this StandardWrapper is for the JspServlet
     */
    protected boolean isJspServlet;


    /**
     * The ObjectName of the JSP monitoring mbean
     */
    protected ObjectName jspMonitorON;


    /**
     * Should we swallow System.out
     */
    protected boolean swallowOutput = false;

    // To support jmx attributes
    StandardWrapperValve swValve;
    /**
     * Time taken to load the servlet.
     */
    protected long loadTime = 0;
    /**
     * Time taken to load the servlet class.
     */
    protected int classLoadTime = 0;

    /**
     * Multipart config
     */
    protected MultipartConfigElement multipartConfigElement = null;

    /**
     * Async support
     */
    protected boolean asyncSupported = false;

    /**
     * Enabled
     */
    protected boolean enabled = true;

    private boolean overridable = false;

    private final ReentrantReadWriteLock parametersLock = new ReentrantReadWriteLock();

    private final ReentrantReadWriteLock mappingsLock = new ReentrantReadWriteLock();

    private final ReentrantReadWriteLock referencesLock = new ReentrantReadWriteLock();


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

    @Override
    public boolean isOverridable() {
        return overridable;
    }

    @Override
    public void setOverridable(boolean overridable) {
        this.overridable = overridable;
    }

    @Override
    public long getAvailable() {
        return this.available;
    }


    @Override
    public void setAvailable(long available) {
        long oldAvailable = this.available;
        if (available > System.currentTimeMillis()) {
            this.available = available;
        } else {
            this.available = 0L;
        }
        support.firePropertyChange("available", Long.valueOf(oldAvailable), Long.valueOf(this.available));
    }


    /**
     * Returns the number of active allocations of this servlet.
     *
     * @return the number of active allocations of this servlet
     */
    public int getCountAllocated() {
        return this.countAllocated.get();
    }


    @Override
    public int getLoadOnStartup() {

        if (isJspServlet && loadOnStartup == -1) {
            /*
             * JspServlet must always be preloaded, because its instance is used during registerJMX (when registering
             * the JSP monitoring mbean)
             */
            return Integer.MAX_VALUE;
        } else {
            return this.loadOnStartup;
        }
    }


    @Override
    public void setLoadOnStartup(int value) {

        int oldLoadOnStartup = this.loadOnStartup;
        this.loadOnStartup = value;
        support.firePropertyChange("loadOnStartup", Integer.valueOf(oldLoadOnStartup),
                Integer.valueOf(this.loadOnStartup));

    }


    /**
     * Set the load-on-startup order value from a (possibly null) string. Per the specification, any missing or
     * non-numeric value is converted to a zero, so that this servlet will still be loaded at startup time, but in an
     * arbitrary order.
     *
     * @param value New load-on-startup value
     */
    public void setLoadOnStartupString(String value) {

        try {
            setLoadOnStartup(Integer.parseInt(value));
        } catch (NumberFormatException e) {
            setLoadOnStartup(0);
        }
    }

    /**
     * Returns the load-on-startup value that was parsed.
     *
     * @return the load-on-startup value that was parsed
     */
    public String getLoadOnStartupString() {
        return Integer.toString(getLoadOnStartup());
    }


    /**
     * Set the parent Container of this Wrapper, but only if it is a Context.
     *
     * @param container Proposed parent Container
     */
    @Override
    public void setParent(Container container) {

        if ((container != null) && !(container instanceof Context)) {
            throw new IllegalArgumentException(sm.getString("standardWrapper.notContext"));
        }
        if (container instanceof StandardContext) {
            swallowOutput = ((StandardContext) container).getSwallowOutput();
            unloadDelay = ((StandardContext) container).getUnloadDelay();
        }
        super.setParent(container);

    }


    @Override
    public String getRunAs() {
        return this.runAs;
    }


    @Override
    public void setRunAs(String runAs) {

        String oldRunAs = this.runAs;
        this.runAs = runAs;
        support.firePropertyChange("runAs", oldRunAs, this.runAs);

    }


    @Override
    public String getServletClass() {
        return this.servletClass;
    }


    @Override
    public void setServletClass(String servletClass) {

        String oldServletClass = this.servletClass;
        this.servletClass = servletClass;
        support.firePropertyChange("servletClass", oldServletClass, this.servletClass);
        if (Constants.JSP_SERVLET_CLASS.equals(servletClass)) {
            isJspServlet = true;
        }
    }


    /**
     * Set the name of this servlet. This is an alias for the normal <code>Container.setName()</code> method, and
     * complements the <code>getServletName()</code> method required by the <code>ServletConfig</code> interface.
     *
     * @param name The new name of this servlet
     */
    public void setServletName(String name) {

        setName(name);

    }


    @Override
    public boolean isUnavailable() {

        if (!isEnabled()) {
            return true;
        } else if (available == 0L) {
            return false;
        } else if (available <= System.currentTimeMillis()) {
            available = 0L;
            return false;
        } else {
            return true;
        }

    }


    @Override
    public String[] getServletMethods() throws ServletException {

        instance = loadServlet();

        Class<? extends Servlet> servletClazz = instance.getClass();
        if (!jakarta.servlet.http.HttpServlet.class.isAssignableFrom(servletClazz)) {
            return DEFAULT_SERVLET_METHODS;
        }

        Set<String> allow = new HashSet<>();
        allow.add("OPTIONS");

        if (isJspServlet) {
            allow.add("GET");
            allow.add("HEAD");
            allow.add("POST");
        } else {
            allow.add("TRACE");

            Method[] methods = getAllDeclaredMethods(servletClazz);
            for (int i = 0; methods != null && i < methods.length; i++) {
                Method m = methods[i];

                switch (m.getName()) {
                    case "doGet":
                        allow.add("GET");
                        allow.add("HEAD");
                        break;
                    case "doPost":
                        allow.add("POST");
                        break;
                    case "doPut":
                        allow.add("PUT");
                        break;
                    case "doDelete":
                        allow.add("DELETE");
                        break;
                }
            }
        }

        return allow.toArray(new String[0]);
    }


    @Override
    public Servlet getServlet() {
        return instance;
    }


    @Override
    public void setServlet(Servlet servlet) {
        instance = servlet;
    }


    // --------------------------------------------------------- Public Methods

    @Override
    public synchronized void backgroundProcess() {
        super.backgroundProcess();

        if (!getState().isAvailable()) {
            return;
        }

        if (getServlet() instanceof PeriodicEventListener) {
            ((PeriodicEventListener) getServlet()).periodicEvent();
        }
    }


    /**
     * Extract the root cause from a servlet exception.
     *
     * @param e The servlet exception
     *
     * @return the root cause of the Servlet exception
     */
    public static Throwable getRootCause(ServletException e) {
        Throwable rootCause = e;
        Throwable rootCauseCheck;
        // Extra aggressive rootCause finding
        int loops = 0;
        do {
            loops++;
            rootCauseCheck = rootCause.getCause();
            if (rootCauseCheck != null) {
                rootCause = rootCauseCheck;
            }
        } while (rootCauseCheck != null && (loops < 20));
        return rootCause;
    }


    /**
     * Refuse to add a child Container, because Wrappers are the lowest level of the Container hierarchy.
     *
     * @param child Child container to be added
     */
    @Override
    public void addChild(Container child) {

        throw new IllegalStateException(sm.getString("standardWrapper.notChild"));

    }


    @Override
    public void addInitParameter(String name, String value) {

        parametersLock.writeLock().lock();
        try {
            parameters.put(name, value);
        } finally {
            parametersLock.writeLock().unlock();
        }
        fireContainerEvent("addInitParameter", name);

    }


    @Override
    public void addMapping(String mapping) {

        mappingsLock.writeLock().lock();
        try {
            mappings.add(mapping);
        } finally {
            mappingsLock.writeLock().unlock();
        }
        if (parent.getState().equals(LifecycleState.STARTED)) {
            fireContainerEvent(ADD_MAPPING_EVENT, mapping);
        }

    }


    @Override
    public void addSecurityReference(String name, String link) {

        referencesLock.writeLock().lock();
        try {
            references.put(name, link);
        } finally {
            referencesLock.writeLock().unlock();
        }
        fireContainerEvent("addSecurityReference", name);

    }


    @Override
    public Servlet allocate() throws ServletException {

        // If we are currently unloading this servlet, throw an exception
        if (unloading) {
            throw new ServletException(sm.getString("standardWrapper.unloading", getName()));
        }

        boolean newInstance = false;

        // Load and initialize our instance if necessary
        if (instance == null || !instanceInitialized) {
            synchronized (this) {
                if (instance == null) {
                    try {
                        if (log.isTraceEnabled()) {
                            log.trace("Allocating instance");
                        }
                        instance = loadServlet();
                        newInstance = true;
                        // Increment here to prevent a race condition
                        // with unload. Bug 43683, test case #3
                        countAllocated.incrementAndGet();
                    } catch (ServletException e) {
                        throw e;
                    } catch (Throwable t) {
                        ExceptionUtils.handleThrowable(t);
                        throw new ServletException(sm.getString("standardWrapper.allocate"), t);
                    }
                }
                if (!instanceInitialized) {
                    initServlet(instance);
                }
            }
        }

        if (log.isTraceEnabled()) {
            log.trace("  Returning instance");
        }
        // For new instances, count will have been incremented at the
        // time of creation
        if (!newInstance) {
            countAllocated.incrementAndGet();
        }
        return instance;
    }


    @Override
    public void deallocate(Servlet servlet) throws ServletException {
        countAllocated.decrementAndGet();
    }


    @Override
    public String findInitParameter(String name) {

        parametersLock.readLock().lock();
        try {
            return parameters.get(name);
        } finally {
            parametersLock.readLock().unlock();
        }

    }


    @Override
    public String[] findInitParameters() {

        parametersLock.readLock().lock();
        try {
            return parameters.keySet().toArray(new String[0]);
        } finally {
            parametersLock.readLock().unlock();
        }

    }


    @Override
    public String[] findMappings() {

        mappingsLock.readLock().lock();
        try {
            return mappings.toArray(new String[0]);
        } finally {
            mappingsLock.readLock().unlock();
        }

    }


    @Override
    public String findSecurityReference(String name) {
        String reference;

        referencesLock.readLock().lock();
        try {
            reference = references.get(name);
        } finally {
            referencesLock.readLock().unlock();
        }

        return reference;
    }


    @Override
    public String[] findSecurityReferences() {

        referencesLock.readLock().lock();
        try {
            return references.keySet().toArray(new String[0]);
        } finally {
            referencesLock.readLock().unlock();
        }

    }


    /**
     * {@inheritDoc}
     * <p>
     * <b>IMPLEMENTATION NOTE</b>: Servlets whose classnames begin with <code>org.apache.catalina.</code> (so-called
     * "container" servlets) are loaded by the same classloader that loaded this class, rather than the classloader for
     * the current web application. This gives such classes access to Catalina internals, which are prevented for
     * classes loaded for web applications.
     */
    @Override
    public synchronized void load() throws ServletException {
        instance = loadServlet();

        if (!instanceInitialized) {
            initServlet(instance);
        }

        if (isJspServlet) {
            StringBuilder oname = new StringBuilder(getDomain());

            oname.append(":type=JspMonitor");

            oname.append(getWebModuleKeyProperties());

            oname.append(",name=");
            oname.append(getName());

            oname.append(getJ2EEKeyProperties());

            try {
                jspMonitorON = new ObjectName(oname.toString());
                Registry.getRegistry(null).registerComponent(instance, jspMonitorON, null);
            } catch (Exception e) {
                log.warn(sm.getString("standardWrapper.jspMonitorError", instance), e);
            }
        }
    }


    /**
     * Load and initialize an instance of this servlet, if there is not already an initialized instance. This can be
     * used, for example, to load servlets that are marked in the deployment descriptor to be loaded at server startup
     * time.
     *
     * @return the loaded Servlet instance
     *
     * @throws ServletException for a Servlet load error
     */
    public synchronized Servlet loadServlet() throws ServletException {

        // Nothing to do if we already have an instance or an instance pool
        if (instance != null) {
            return instance;
        }

        PrintStream out = System.out;
        if (swallowOutput) {
            SystemLogHandler.startCapture();
        }

        Servlet servlet;
        try {
            long t1 = System.currentTimeMillis();
            // Complain if no servlet class has been specified
            if (servletClass == null) {
                unavailable(null);
                throw new ServletException(sm.getString("standardWrapper.notClass", getName()));
            }

            InstanceManager instanceManager = ((StandardContext) getParent()).getInstanceManager();
            try {
                servlet = (Servlet) instanceManager.newInstance(servletClass);
            } catch (ClassCastException e) {
                unavailable(null);
                // Restore the context ClassLoader
                throw new ServletException(sm.getString("standardWrapper.notServlet", servletClass), e);
            } catch (Throwable t) {
                Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(throwable);
                unavailable(null);

                // Added extra log statement for Bugzilla 36630:
                // https://bz.apache.org/bugzilla/show_bug.cgi?id=36630
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("standardWrapper.instantiate", servletClass), throwable);
                }

                // Restore the context ClassLoader
                throw new ServletException(sm.getString("standardWrapper.instantiate", servletClass), throwable);
            }

            if (multipartConfigElement == null) {
                MultipartConfig annotation = servlet.getClass().getAnnotation(MultipartConfig.class);
                if (annotation != null) {
                    multipartConfigElement = new MultipartConfigElement(annotation);
                }
            }

            // Special handling for ContainerServlet instances
            // Note: The InstanceManager checks if the application is permitted
            // to load ContainerServlets
            if (servlet instanceof ContainerServlet) {
                ((ContainerServlet) servlet).setWrapper(this);
            }

            classLoadTime = (int) (System.currentTimeMillis() - t1);

            initServlet(servlet);

            fireContainerEvent("load", this);

            loadTime = System.currentTimeMillis() - t1;
        } finally {
            if (swallowOutput) {
                String log = SystemLogHandler.stopCapture();
                if (log != null && !log.isEmpty()) {
                    if (getServletContext() != null) {
                        getServletContext().log(log);
                    } else {
                        out.println(log);
                    }
                }
            }
        }
        return servlet;

    }


    private synchronized void initServlet(Servlet servlet) throws ServletException {

        if (instanceInitialized) {
            return;
        }

        // Call the initialization method of this servlet
        try {
            servlet.init(facade);
            instanceInitialized = true;
        } catch (UnavailableException f) {
            unavailable(f);
            throw f;
        } catch (ServletException f) {
            // If the servlet wanted to be unavailable it would have
            // said so, so do not call unavailable(null).
            throw f;
        } catch (Throwable f) {
            ExceptionUtils.handleThrowable(f);
            getServletContext().log(sm.getString("standardWrapper.initException", getName()), f);
            // If the servlet wanted to be unavailable it would have
            // said so, so do not call unavailable(null).
            throw new ServletException(sm.getString("standardWrapper.initException", getName()), f);
        }
    }

    @Override
    public void removeInitParameter(String name) {

        parametersLock.writeLock().lock();
        try {
            parameters.remove(name);
        } finally {
            parametersLock.writeLock().unlock();
        }
        fireContainerEvent("removeInitParameter", name);

    }


    @Override
    public void removeMapping(String mapping) {

        mappingsLock.writeLock().lock();
        try {
            mappings.remove(mapping);
        } finally {
            mappingsLock.writeLock().unlock();
        }
        if (parent.getState().equals(LifecycleState.STARTED)) {
            fireContainerEvent(REMOVE_MAPPING_EVENT, mapping);
        }

    }


    @Override
    public void removeSecurityReference(String name) {

        referencesLock.writeLock().lock();
        try {
            references.remove(name);
        } finally {
            referencesLock.writeLock().unlock();
        }
        fireContainerEvent("removeSecurityReference", name);

    }


    @Override
    public void unavailable(UnavailableException unavailable) {
        getServletContext().log(sm.getString("standardWrapper.unavailable", getName()));
        if (unavailable == null) {
            setAvailable(Long.MAX_VALUE);
        } else if (unavailable.isPermanent()) {
            setAvailable(Long.MAX_VALUE);
        } else {
            int unavailableSeconds = unavailable.getUnavailableSeconds();
            if (unavailableSeconds <= 0) {
                unavailableSeconds = 60; // Arbitrary default
            }
            setAvailable(System.currentTimeMillis() + (unavailableSeconds * 1000L));
        }

    }


    @Override
    public synchronized void unload() throws ServletException {

        // Nothing to do if we have never loaded the instance
        if (instance == null) {
            return;
        }
        unloading = true;

        // Loaf a while if the current instance is allocated. Use wait() to
        // release the lock while waiting to avoid blocking other threads.
        if (countAllocated.get() > 0) {
            int nRetries = 0;
            long delay = unloadDelay / 20;
            while ((nRetries < 21) && (countAllocated.get() > 0)) {
                if ((nRetries % 10) == 0) {
                    log.info(sm.getString("standardWrapper.waiting", countAllocated.toString(), getName()));
                }
                try {
                    wait(delay);
                } catch (InterruptedException e) {
                    // Ignore
                }
                nRetries++;
            }
        }

        ServletException servletException = null;

        if (instanceInitialized) {
            PrintStream out = System.out;
            if (swallowOutput) {
                SystemLogHandler.startCapture();
            }

            // Call the servlet destroy() method
            try {
                instance.destroy();
            } catch (Throwable t) {
                Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(throwable);
                servletException = new ServletException(sm.getString("standardWrapper.destroyException", getName()), throwable);
            } finally {
                // Annotation processing
                if (!((Context) getParent()).getIgnoreAnnotations()) {
                    try {
                        ((Context) getParent()).getInstanceManager().destroyInstance(instance);
                    } catch (Throwable t) {
                        ExceptionUtils.handleThrowable(t);
                        log.error(sm.getString("standardWrapper.destroyInstance", getName()), t);
                    }
                }
                // Write captured output
                if (swallowOutput) {
                    String log = SystemLogHandler.stopCapture();
                    if (log != null && !log.isEmpty()) {
                        if (getServletContext() != null) {
                            getServletContext().log(log);
                        } else {
                            out.println(log);
                        }
                    }
                }
                instanceInitialized = false;
            }
        }

        // Deregister the destroyed instance
        instance = null;

        if (isJspServlet && jspMonitorON != null) {
            Registry.getRegistry(null).unregisterComponent(jspMonitorON);
        }

        unloading = false;
        fireContainerEvent("unload", this);

        if (servletException != null) {
            throw servletException;
        }

    }


    // -------------------------------------------------- ServletConfig Methods


    @Override
    public String getInitParameter(String name) {
        return findInitParameter(name);
    }


    @Override
    public Enumeration<String> getInitParameterNames() {

        parametersLock.readLock().lock();
        try {
            return Collections.enumeration(parameters.keySet());
        } finally {
            parametersLock.readLock().unlock();
        }

    }


    @Override
    public ServletContext getServletContext() {
        if (parent == null) {
            return null;
        } else if (!(parent instanceof Context)) {
            return null;
        } else {
            return ((Context) parent).getServletContext();
        }
    }


    @Override
    public String getServletName() {
        return getName();
    }

    /**
     * Returns the total processing time for all requests.
     *
     * @return the total processing time in milliseconds
     */
    public long getProcessingTime() {
        return swValve.getProcessingTime();
    }

    /**
     * Returns the maximum processing time for a single request.
     *
     * @return the maximum processing time in milliseconds
     */
    public long getMaxTime() {
        return swValve.getMaxTime();
    }

    /**
     * Returns the minimum processing time for a single request.
     *
     * @return the minimum processing time in milliseconds
     */
    public long getMinTime() {
        return swValve.getMinTime();
    }

    /**
     * Returns the number of requests processed by the wrapper.
     *
     * @return the number of requests processed by the wrapper.
     */
    public long getRequestCount() {
        return swValve.getRequestCount();
    }

    /**
     * Returns the number of requests processed by the wrapper that resulted in an error.
     *
     * @return the number of requests processed by the wrapper that resulted in an error.
     */
    public long getErrorCount() {
        return swValve.getErrorCount();
    }

    /**
     * Increment the error count used for monitoring.
     */
    @Override
    public void incrementErrorCount() {
        swValve.incrementErrorCount();
    }

    /**
     * Returns the time taken to load the servlet.
     *
     * @return the load time in milliseconds
     */
    public long getLoadTime() {
        return loadTime;
    }

    /**
     * Returns the time taken to load the servlet class.
     *
     * @return the class load time in milliseconds
     */
    public int getClassLoadTime() {
        return classLoadTime;
    }

    @Override
    public MultipartConfigElement getMultipartConfigElement() {
        return multipartConfigElement;
    }

    @Override
    public void setMultipartConfigElement(MultipartConfigElement multipartConfigElement) {
        this.multipartConfigElement = multipartConfigElement;
    }

    @Override
    public boolean isAsyncSupported() {
        return asyncSupported;
    }

    @Override
    public void setAsyncSupported(boolean asyncSupported) {
        this.asyncSupported = asyncSupported;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    // -------------------------------------------------------- Package Methods


    // -------------------------------------------------------- protected Methods


    /**
     * Recursively collects all declared methods from the given class and its superclasses,
     * stopping at HttpServlet.
     *
     * @param c the class to inspect
     * @return array of all declared methods, or null if c is HttpServlet
     */
    protected Method[] getAllDeclaredMethods(Class<?> c) {

        if (c.equals(jakarta.servlet.http.HttpServlet.class)) {
            return null;
        }

        Method[] parentMethods = getAllDeclaredMethods(c.getSuperclass());

        Method[] thisMethods = c.getDeclaredMethods();
        if (thisMethods.length == 0) {
            return parentMethods;
        }

        if ((parentMethods != null) && (parentMethods.length > 0)) {
            Method[] allMethods = new Method[parentMethods.length + thisMethods.length];
            System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
            System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);

            thisMethods = allMethods;
        }

        return thisMethods;
    }


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


    /**
     * Start this component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error that prevents this component from being
     *                                   used
     */
    @Override
    protected void startInternal() throws LifecycleException {

        // Send j2ee.state.starting notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);
        }

        // Start up this component
        super.startInternal();

        setAvailable(0L);

        // Send j2ee.state.running notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);
        }

    }


    /**
     * Stop this component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error that prevents this component from being
     *                                   used
     */
    @Override
    protected void stopInternal() throws LifecycleException {

        setAvailable(Long.MAX_VALUE);

        // Send j2ee.state.stopping notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.stopping", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);
        }

        // Shut down our servlet instance (if it has been initialized)
        try {
            unload();
        } catch (ServletException e) {
            getServletContext().log(sm.getString("standardWrapper.unloadException", getName()), e);
        }

        // Shut down this component
        super.stopInternal();

        // Send j2ee.state.stopped notification
        if (this.getObjectName() != null) {
            Notification notification = new Notification("j2ee.state.stopped", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);

            // Send j2ee.object.deleted notification
            notification = new Notification("j2ee.object.deleted", this.getObjectName(), sequenceNumber++);
            broadcaster.sendNotification(notification);
        }

    }


    @Override
    protected String getObjectNameKeyProperties() {

        StringBuilder keyProperties = new StringBuilder("j2eeType=Servlet");

        keyProperties.append(getWebModuleKeyProperties());

        keyProperties.append(",name=");

        String name = getName();
        if (Util.objectNameValueNeedsQuote(name)) {
            name = ObjectName.quote(name);
        }
        keyProperties.append(name);

        keyProperties.append(getJ2EEKeyProperties());

        return keyProperties.toString();
    }


    private String getWebModuleKeyProperties() {

        StringBuilder keyProperties = new StringBuilder(",WebModule=//");
        String hostName = getParent().getParent().getName();
        keyProperties.append(Objects.requireNonNullElse(hostName, "DEFAULT"));

        String contextName = getParent().getName();
        if (!contextName.startsWith("/")) {
            keyProperties.append('/');
        }
        keyProperties.append(contextName);

        return keyProperties.toString();
    }

    private String getJ2EEKeyProperties() {

        StringBuilder keyProperties = new StringBuilder(",J2EEApplication=");

        StandardContext ctx = null;
        if (parent instanceof StandardContext) {
            ctx = (StandardContext) getParent();
        }

        if (ctx == null) {
            keyProperties.append("none");
        } else {
            keyProperties.append(ctx.getJ2EEApplication());
        }
        keyProperties.append(",J2EEServer=");
        if (ctx == null) {
            keyProperties.append("none");
        } else {
            keyProperties.append(ctx.getJ2EEServer());
        }

        return keyProperties.toString();
    }


    @Override
    public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object object)
            throws ListenerNotFoundException {
        broadcaster.removeNotificationListener(listener, filter, object);
    }

    /**
     * Cache for notification info.
     */
    protected MBeanNotificationInfo[] notificationInfo;

    @Override
    public MBeanNotificationInfo[] getNotificationInfo() {
        if (notificationInfo == null) {
            notificationInfo = new MBeanNotificationInfo[] {
                    new MBeanNotificationInfo(new String[] { "j2ee.object.created" }, Notification.class.getName(),
                            "servlet is created"),
                    new MBeanNotificationInfo(new String[] { "j2ee.state.starting" }, Notification.class.getName(),
                            "servlet is starting"),
                    new MBeanNotificationInfo(new String[] { "j2ee.state.running" }, Notification.class.getName(),
                            "servlet is running"),
                    new MBeanNotificationInfo(new String[] { "j2ee.state.stopped" }, Notification.class.getName(),
                            "servlet is stopping"),
                    new MBeanNotificationInfo(new String[] { "j2ee.object.stopped" }, Notification.class.getName(),
                            "servlet is stopped"),
                    new MBeanNotificationInfo(new String[] { "j2ee.object.deleted" }, Notification.class.getName(),
                            "servlet is deleted") };
        }
        return notificationInfo;
    }


    @Override
    public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object object)
            throws IllegalArgumentException {
        broadcaster.addNotificationListener(listener, filter, object);
    }


    @Override
    public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
        broadcaster.removeNotificationListener(listener);
    }
}
