/*
 * 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.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;

import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestWrapper;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.ServletResponseWrapper;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.AsyncDispatcher;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.util.URLEncoder;
import org.apache.coyote.BadRequestException;
import org.apache.coyote.CloseNowException;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.http.Method;
import org.apache.tomcat.util.res.StringManager;

/**
 * Standard implementation of <code>RequestDispatcher</code> that allows a request to be forwarded to a different
 * resource to create the ultimate response, or to include the output of another resource in the response from this
 * resource. This implementation allows application level servlets to wrap the request and/or response objects that are
 * passed on to the called resource, as long as the wrapping classes extend
 * <code>jakarta.servlet.ServletRequestWrapper</code> and <code>jakarta.servlet.ServletResponseWrapper</code>.
 */
final class ApplicationDispatcher implements AsyncDispatcher, RequestDispatcher {

    /**
     * Used to pass state when the request dispatcher is used. Using instance variables causes threading issues and
     * state is too complex to pass and return single ServletRequest or ServletResponse objects.
     */
    private static class State {
        State(ServletRequest request, ServletResponse response, boolean including) {
            this.outerRequest = request;
            this.outerResponse = response;
            this.including = including;
        }

        /**
         * The outermost request that will be passed on to the invoked servlet.
         */
        ServletRequest outerRequest;


        /**
         * The outermost response that will be passed on to the invoked servlet.
         */
        ServletResponse outerResponse;

        /**
         * The request wrapper we have created and installed (if any).
         */
        ServletRequest wrapRequest = null;


        /**
         * The response wrapper we have created and installed (if any).
         */
        ServletResponse wrapResponse = null;

        /**
         * Are we performing an include() instead of a forward()?
         */
        boolean including;

        /**
         * Outermost HttpServletRequest in the chain
         */
        HttpServletRequest hrequest = null;

        /**
         * Outermost HttpServletResponse in the chain
         */
        HttpServletResponse hresponse = null;
    }

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


    /**
     * Construct a new instance of this class, configured according to the specified parameters. If both servletPath and
     * pathInfo are <code>null</code>, it will be assumed that this RequestDispatcher was acquired by name, rather than
     * by path.
     *
     * @param wrapper     The Wrapper associated with the resource that will be forwarded to or included (required)
     * @param requestURI  The request URI to this resource (if any)
     * @param servletPath The revised servlet path to this resource (if any)
     * @param pathInfo    The revised extra path information to this resource (if any)
     * @param queryString Query string parameters included with this request (if any)
     * @param mapping     The mapping for this resource (if any)
     * @param name        Servlet name (if a named dispatcher was created) else <code>null</code>
     */
    ApplicationDispatcher(Wrapper wrapper, String requestURI, String servletPath, String pathInfo, String queryString,
            HttpServletMapping mapping, String name) {

        super();

        // Save all of our configuration parameters
        this.wrapper = wrapper;
        this.context = (Context) wrapper.getParent();
        this.requestURI = requestURI;
        this.servletPath = servletPath;
        this.pathInfo = pathInfo;
        this.queryString = queryString;
        this.mapping = mapping;
        this.name = name;
    }


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

    /**
     * The Context this RequestDispatcher is associated with.
     */
    private final Context context;


    /**
     * The servlet name for a named dispatcher.
     */
    private final String name;


    /**
     * The extra path information for this RequestDispatcher.
     */
    private final String pathInfo;


    /**
     * The query string parameters for this RequestDispatcher.
     */
    private final String queryString;


    /**
     * The request URI for this RequestDispatcher.
     */
    private final String requestURI;


    /**
     * The servlet path for this RequestDispatcher.
     */
    private final String servletPath;


    /**
     * The mapping for this RequestDispatcher.
     */
    private final HttpServletMapping mapping;


    /**
     * The StringManager for this package.
     */
    private static final StringManager sm = StringManager.getManager(ApplicationDispatcher.class);


    /**
     * The Wrapper associated with the resource that will be forwarded to or included.
     */
    private final Wrapper wrapper;


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


    @Override
    public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // Reset any output that has been buffered, but keep headers/cookies
        if (response.isCommitted()) {
            throw new IllegalStateException(sm.getString("applicationDispatcher.forward.ise"));
        }
        response.resetBuffer();

        // Set up to handle the specified request and response
        State state = new State(request, response, false);

        if (context.getDispatcherWrapsSameObject()) {
            // Check SRV.9.2 / RequestDispatcher Javadoc
            checkSameObjects(request, response);
        }

        wrapResponse(state);
        ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state);
        HttpServletRequest hrequest = state.hrequest;
        if (servletPath == null && pathInfo == null) {
            // Handle an HTTP named dispatcher forward

            // All ERROR dispatches must be GET requests. Use the presence of ERROR_METHOD to determine if this is an
            // error dispatch as not all components (JSP) set the dispatcher type.
            if (request.getAttribute(ERROR_METHOD) != null) {
                wrequest.setMethod(Method.GET);
            }
            wrequest.setRequestURI(hrequest.getRequestURI());
            wrequest.setContextPath(hrequest.getContextPath());
            wrequest.setServletPath(hrequest.getServletPath());
            wrequest.setPathInfo(hrequest.getPathInfo());
            wrequest.setQueryString(hrequest.getQueryString());

        } else {
            // Handle an HTTP path-based forward

            if (hrequest.getAttribute(FORWARD_REQUEST_URI) == null) {
                wrequest.setAttribute(FORWARD_REQUEST_URI, hrequest.getRequestURI());
                wrequest.setAttribute(FORWARD_CONTEXT_PATH, hrequest.getContextPath());
                wrequest.setAttribute(FORWARD_SERVLET_PATH, hrequest.getServletPath());
                wrequest.setAttribute(FORWARD_PATH_INFO, hrequest.getPathInfo());
                wrequest.setAttribute(FORWARD_QUERY_STRING, hrequest.getQueryString());
                wrequest.setAttribute(FORWARD_MAPPING, hrequest.getHttpServletMapping());
            }

            // All ERROR dispatches must be GET requests. Use the presence of ERROR_METHOD to determine if this is an
            // error dispatch as not all components (JSP) set the dispatcher type.
            if (request.getAttribute(ERROR_METHOD) != null) {
                wrequest.setMethod(Method.GET);
            }
            wrequest.setContextPath(context.getEncodedPath());
            wrequest.setRequestURI(requestURI);
            wrequest.setServletPath(servletPath);
            wrequest.setPathInfo(pathInfo);
            if (queryString != null) {
                wrequest.setQueryString(queryString);
                wrequest.setQueryParams(queryString);
            }
            wrequest.setMapping(mapping);

        }
        processRequest(request, response, state);

        if (request.isAsyncStarted()) {
            // An async request was started during the forward, don't close the
            // response as it may be written to during the async handling
            return;
        }

        // This is not a real close in order to support error processing
        if (wrapper.getLogger().isTraceEnabled()) {
            wrapper.getLogger().trace(" Disabling the response for further output");
        }

        boolean finished = false;
        if (response instanceof ResponseFacade) {
            finished = true;
            ((ResponseFacade) response).finish();
        } else if (context.getSuspendWrappedResponseAfterForward() && response instanceof ServletResponseWrapper) {
            ServletResponse baseResponse = response;
            do {
                baseResponse = ((ServletResponseWrapper) baseResponse).getResponse();
            } while (baseResponse instanceof ServletResponseWrapper);
            if (baseResponse instanceof ResponseFacade) {
                finished = true;
                ((ResponseFacade) baseResponse).finish();
            }
        }
        if (!finished) {
            // Servlet SRV.6.2.2. The Request/Response may have been wrapped
            // and may no longer be an instance of RequestFacade
            if (wrapper.getLogger().isDebugEnabled()) {
                wrapper.getLogger().debug(sm.getString("applicationDispatcher.customResponse", response.getClass()));
            }
            // Close anyway
            try {
                PrintWriter writer = response.getWriter();
                writer.close();
            } catch (IllegalStateException e) {
                try {
                    ServletOutputStream stream = response.getOutputStream();
                    stream.close();
                } catch (IllegalStateException | IOException f) {
                    // Ignore
                }
            } catch (IOException ignore) {
                // Ignore
            }
        }

    }


    /**
     * Prepare the request based on the filter configuration.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param state    The RD state
     *
     * @exception IOException      if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    private void processRequest(ServletRequest request, ServletResponse response, State state)
            throws IOException, ServletException {

        DispatcherType disInt = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);
        if (disInt != null) {
            if (!context.getFireRequestListenersOnForwards() || context.fireRequestInitEvent(request)) {
                if (disInt != DispatcherType.ERROR) {
                    state.outerRequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath());
                    state.outerRequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.FORWARD);
                    invoke(state.outerRequest, response, state);
                } else {
                    invoke(state.outerRequest, response, state);
                }

                if (context.getFireRequestListenersOnForwards()) {
                    context.fireRequestDestroyEvent(request);
                }
            }
        }
    }


    /**
     * Combine the servletPath and the pathInfo. If pathInfo is <code>null</code> it is ignored. If servletPath is
     * <code>null</code> then <code>null</code> is returned.
     *
     * @return The combined path with pathInfo appended to servletInfo
     */
    private String getCombinedPath() {
        if (servletPath == null) {
            return null;
        }
        if (pathInfo == null) {
            return servletPath;
        }
        return servletPath + pathInfo;
    }


    @Override
    public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // Set up to handle the specified request and response
        State state = new State(request, response, true);

        if (context.getDispatcherWrapsSameObject()) {
            // Check SRV.8.2 / SRV.14.2.5.1 compliance
            checkSameObjects(request, response);
        }

        // Create a wrapped response to use for this request
        wrapResponse(state);

        ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state);
        if (name != null) {
            // Handle an HTTP named dispatcher include

            wrequest.setAttribute(Globals.NAMED_DISPATCHER_ATTR, name);
            if (servletPath != null) {
                wrequest.setServletPath(servletPath);
            }
        } else {
            // Handle an HTTP path based include

            String contextPath = context.getEncodedPath();
            if (requestURI != null) {
                wrequest.setAttribute(INCLUDE_REQUEST_URI, requestURI);
            }
            if (contextPath != null) {
                wrequest.setAttribute(INCLUDE_CONTEXT_PATH, contextPath);
            }
            if (servletPath != null) {
                wrequest.setAttribute(INCLUDE_SERVLET_PATH, servletPath);
            }
            if (pathInfo != null) {
                wrequest.setAttribute(INCLUDE_PATH_INFO, pathInfo);
            }
            if (queryString != null) {
                wrequest.setAttribute(INCLUDE_QUERY_STRING, queryString);
                wrequest.setQueryParams(queryString);
            }
            if (mapping != null) {
                wrequest.setAttribute(INCLUDE_MAPPING, mapping);
            }

        }
        wrequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.INCLUDE);
        wrequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath());
        invoke(state.outerRequest, state.outerResponse, state);

    }


    @Override
    public void dispatch(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        // Set up to handle the specified request and response
        State state = new State(request, response, false);

        // Create a wrapped response to use for this request
        wrapResponse(state);

        ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state);
        HttpServletRequest hrequest = state.hrequest;

        wrequest.setAttribute(Globals.DISPATCHER_TYPE_ATTR, DispatcherType.ASYNC);
        wrequest.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, getCombinedPath());
        wrequest.setAttribute(AsyncContext.ASYNC_MAPPING, hrequest.getHttpServletMapping());

        wrequest.setContextPath(context.getEncodedPath());
        wrequest.setRequestURI(requestURI);
        wrequest.setServletPath(servletPath);
        wrequest.setPathInfo(pathInfo);
        if (queryString != null) {
            wrequest.setQueryString(queryString);
            wrequest.setQueryParams(queryString);
        }
        wrequest.setMapping(mapping);

        invoke(state.outerRequest, state.outerResponse, state);
    }


    // -------------------------------------------------------- Private Methods


    /**
     * Ask the resource represented by this RequestDispatcher to process the associated request, and create (or append
     * to) the associated response.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     *
     * @exception IOException      if an input/output error occurs
     * @exception ServletException if a servlet error occurs
     */
    private void invoke(ServletRequest request, ServletResponse response, State state)
            throws IOException, ServletException {

        // Checking to see if the context classloader is the current context
        // classloader. If it's not, we're saving it, and setting the context
        // classloader to the Context classloader
        ClassLoader oldCCL = context.bind(null);

        // Initialize local variables we may need
        HttpServletResponse hresponse = state.hresponse;
        Servlet servlet = null;
        IOException ioException = null;
        ServletException servletException = null;
        RuntimeException runtimeException = null;
        boolean unavailable = false;

        // Check for the servlet being marked unavailable
        if (wrapper.isUnavailable()) {
            wrapper.getLogger().warn(sm.getString("applicationDispatcher.isUnavailable", wrapper.getName()));
            long available = wrapper.getAvailable();
            if (available > 0L && available < Long.MAX_VALUE) {
                hresponse.setDateHeader("Retry-After", available);
            }
            hresponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                    sm.getString("applicationDispatcher.isUnavailable", wrapper.getName()));
            unavailable = true;
        }

        // Allocate a servlet instance to process this request
        try {
            if (!unavailable) {
                servlet = wrapper.allocate();
            }
        } catch (ServletException e) {
            wrapper.getLogger().error(sm.getString("applicationDispatcher.allocateException", wrapper.getName()),
                    StandardWrapper.getRootCause(e));
            servletException = e;
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            wrapper.getLogger().error(sm.getString("applicationDispatcher.allocateException", wrapper.getName()), t);
            servletException =
                    new ServletException(sm.getString("applicationDispatcher.allocateException", wrapper.getName()), t);
            // servlet = null; is already done so no need to do it explicitly
        }

        // Get the FilterChain Here
        ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

        // Call the service() method for the allocated servlet instance
        try {
            // for includes/forwards
            if (servlet != null && filterChain != null) {
                filterChain.doFilter(request, response);
            }
            // Servlet Service Method is called by the FilterChain
        } catch (BadRequestException | CloseNowException e) {
            ioException = e;
        } catch (IOException ioe) {
            wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), ioe);
            ioException = ioe;
        } catch (UnavailableException e) {
            wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), e);
            servletException = e;
            wrapper.unavailable(e);
        } catch (ServletException e) {
            Throwable rootCause = StandardWrapper.getRootCause(e);
            if (!(rootCause instanceof BadRequestException)) {
                wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()),
                        rootCause);
            }
            servletException = e;
        } catch (RuntimeException e) {
            wrapper.getLogger().error(sm.getString("applicationDispatcher.serviceException", wrapper.getName()), e);
            runtimeException = e;
        }

        // Release the filter chain (if any) for this request
        if (filterChain != null) {
            filterChain.release();
        }

        // Deallocate the allocated servlet instance
        try {
            if (servlet != null) {
                wrapper.deallocate(servlet);
            }
        } catch (ServletException e) {
            wrapper.getLogger().error(sm.getString("applicationDispatcher.deallocateException", wrapper.getName()), e);
            servletException = e;
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            wrapper.getLogger().error(sm.getString("applicationDispatcher.deallocateException", wrapper.getName()), t);
            servletException = new ServletException(
                    sm.getString("applicationDispatcher.deallocateException", wrapper.getName()), t);
        }

        // Reset the old context class loader
        context.unbind(oldCCL);

        // Unwrap request/response if needed
        // See Bugzilla 30949
        unwrapRequest(state);
        unwrapResponse(state);
        // Recycle request if necessary (also BZ 30949)
        recycleRequestWrapper(state);

        // Rethrow an exception if one was thrown by the invoked servlet
        if (ioException != null) {
            throw ioException;
        }
        if (servletException != null) {
            throw servletException;
        }
        if (runtimeException != null) {
            throw runtimeException;
        }
    }


    /**
     * Unwrap the request if we have wrapped it.
     */
    private void unwrapRequest(State state) {

        if (state.wrapRequest == null) {
            return;
        }

        if (state.outerRequest.isAsyncStarted()) {
            if (!state.outerRequest.getAsyncContext().hasOriginalRequestAndResponse()) {
                return;
            }
        }

        ServletRequest previous = null;
        ServletRequest current = state.outerRequest;
        while (current != null) {

            // If we run into the container request we are done
            if (current instanceof Request || current instanceof RequestFacade) {
                break;
            }

            // Remove the current request if it is our wrapper
            if (current == state.wrapRequest) {
                ServletRequest next = ((ServletRequestWrapper) current).getRequest();
                if (previous == null) {
                    state.outerRequest = next;
                } else {
                    ((ServletRequestWrapper) previous).setRequest(next);
                }
                break;
            }

            // Advance to the next request in the chain
            previous = current;
            current = ((ServletRequestWrapper) current).getRequest();

        }

    }

    /**
     * Unwrap the response if we have wrapped it.
     */
    private void unwrapResponse(State state) {

        if (state.wrapResponse == null) {
            return;
        }

        if (state.outerRequest.isAsyncStarted()) {
            if (!state.outerRequest.getAsyncContext().hasOriginalRequestAndResponse()) {
                return;
            }
        }

        ServletResponse previous = null;
        ServletResponse current = state.outerResponse;
        while (current != null) {

            // If we run into the container response we are done
            if (current instanceof Response || current instanceof ResponseFacade) {
                break;
            }

            // Remove the current response if it is our wrapper
            if (current == state.wrapResponse) {
                ServletResponse next = ((ServletResponseWrapper) current).getResponse();
                if (previous == null) {
                    state.outerResponse = next;
                } else {
                    ((ServletResponseWrapper) previous).setResponse(next);
                }
                break;
            }

            // Advance to the next response in the chain
            previous = current;
            current = ((ServletResponseWrapper) current).getResponse();

        }

    }


    /**
     * Create and return a request wrapper that has been inserted in the appropriate spot in the request chain.
     */
    private ServletRequest wrapRequest(State state) {

        // Locate the request we should insert in front of
        ServletRequest previous = null;
        ServletRequest current = state.outerRequest;
        while (current != null) {
            if (state.hrequest == null && current instanceof HttpServletRequest) {
                state.hrequest = (HttpServletRequest) current;
            }
            if (!(current instanceof ServletRequestWrapper)) {
                break;
            }
            if (current instanceof ApplicationHttpRequest) {
                break;
            }
            if (current instanceof ApplicationRequest) {
                break;
            }
            previous = current;
            current = ((ServletRequestWrapper) current).getRequest();
        }

        // Instantiate a new wrapper at this point and insert it in the chain
        ServletRequest wrapper;
        if (current instanceof HttpServletRequest hcurrent) {
            // Compute a crossContext flag
            boolean crossContext = false;
            if (state.outerRequest instanceof HttpServletRequest houterRequest) {
                Object contextPath = houterRequest.getAttribute(INCLUDE_CONTEXT_PATH);
                if (contextPath == null) {
                    // Forward
                    contextPath = URLEncoder.DEFAULT.encode(houterRequest.getServletContext().getContextPath(),
                            StandardCharsets.UTF_8);
                }
                crossContext = !context.getEncodedPath().equals(contextPath);
            }
            wrapper = new ApplicationHttpRequest(hcurrent, context, crossContext);
        } else {
            wrapper = new ApplicationRequest(current);
        }
        if (previous == null) {
            state.outerRequest = wrapper;
        } else {
            ((ServletRequestWrapper) previous).setRequest(wrapper);
        }
        state.wrapRequest = wrapper;
        return wrapper;

    }


    /**
     * Create and return a response wrapper that has been inserted in the appropriate spot in the response chain.
     */
    private ServletResponse wrapResponse(State state) {

        // Locate the response we should insert in front of
        ServletResponse previous = null;
        ServletResponse current = state.outerResponse;
        while (current != null) {
            if (state.hresponse == null && current instanceof HttpServletResponse) {
                state.hresponse = (HttpServletResponse) current;
                if (!state.including) { // Forward only needs hresponse
                    return null;
                }
            }
            if (!(current instanceof ServletResponseWrapper)) {
                break;
            }
            if (current instanceof ApplicationHttpResponse) {
                break;
            }
            if (current instanceof ApplicationResponse) {
                break;
            }
            previous = current;
            current = ((ServletResponseWrapper) current).getResponse();
        }

        // Instantiate a new wrapper at this point and insert it in the chain
        ServletResponse wrapper;
        if (current instanceof HttpServletResponse) {
            wrapper = new ApplicationHttpResponse((HttpServletResponse) current, state.including);
        } else {
            wrapper = new ApplicationResponse(current, state.including);
        }
        if (previous == null) {
            state.outerResponse = wrapper;
        } else {
            ((ServletResponseWrapper) previous).setResponse(wrapper);
        }
        state.wrapResponse = wrapper;
        return wrapper;

    }

    private void checkSameObjects(ServletRequest appRequest, ServletResponse appResponse) throws ServletException {
        ServletRequest originalRequest = ApplicationFilterChain.getLastServicedRequest();
        ServletResponse originalResponse = ApplicationFilterChain.getLastServicedResponse();

        // Some forwards, e.g. from valves will not set original values
        if (originalRequest == null || originalResponse == null) {
            return;
        }

        boolean same = false;
        ServletRequest dispatchedRequest = appRequest;

        // find the request that was passed into the service method
        while (originalRequest instanceof ServletRequestWrapper &&
                ((ServletRequestWrapper) originalRequest).getRequest() != null) {
            originalRequest = ((ServletRequestWrapper) originalRequest).getRequest();
        }
        // compare with the dispatched request
        while (true) {
            if (originalRequest.equals(dispatchedRequest)) {
                same = true;
            }
            if (!same && dispatchedRequest instanceof ServletRequestWrapper) {
                dispatchedRequest = ((ServletRequestWrapper) dispatchedRequest).getRequest();
            } else {
                break;
            }
        }
        if (!same) {
            throw new ServletException(sm.getString("applicationDispatcher.specViolation.request"));
        }

        same = false;
        ServletResponse dispatchedResponse = appResponse;

        // find the response that was passed into the service method
        while (originalResponse instanceof ServletResponseWrapper &&
                ((ServletResponseWrapper) originalResponse).getResponse() != null) {
            originalResponse = ((ServletResponseWrapper) originalResponse).getResponse();
        }
        // compare with the dispatched response
        while (true) {
            if (originalResponse.equals(dispatchedResponse)) {
                same = true;
            }

            if (!same && dispatchedResponse instanceof ServletResponseWrapper) {
                dispatchedResponse = ((ServletResponseWrapper) dispatchedResponse).getResponse();
            } else {
                break;
            }
        }

        if (!same) {
            throw new ServletException(sm.getString("applicationDispatcher.specViolation.response"));
        }
    }

    private void recycleRequestWrapper(State state) {
        if (state.wrapRequest instanceof ApplicationHttpRequest) {
            ((ApplicationHttpRequest) state.wrapRequest).recycle();
        }
    }
}
