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

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Serial;
import java.io.Serializable;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;

import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.ServletResponseWrapper;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.util.ServerInfo;
import org.apache.catalina.util.URLEncoder;
import org.apache.catalina.webresources.CachedResource;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.http.FastHttpDateFormat;
import org.apache.tomcat.util.http.ResponseUtil;
import org.apache.tomcat.util.http.parser.ContentRange;
import org.apache.tomcat.util.http.parser.EntityTag;
import org.apache.tomcat.util.http.parser.Ranges;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.Escape;


/**
 * <p>
 * The default resource-serving servlet for most web applications, used to serve static resources such as HTML pages and
 * images.
 * </p>
 * <p>
 * This servlet is intended to be mapped to <em>/</em> e.g.:
 * </p>
 *
 * <pre>
 *   &lt;servlet-mapping&gt;
 *       &lt;servlet-name&gt;default&lt;/servlet-name&gt;
 *       &lt;url-pattern&gt;/&lt;/url-pattern&gt;
 *   &lt;/servlet-mapping&gt;
 * </pre>
 * <p>
 * It can be mapped to sub-paths, however in all cases resources are served from the web application resource root using
 * the full path from the root of the web application context. <br>
 * e.g. given a web application structure:
 * </p>
 *
 * <pre>
 * /context
 *   /images
 *     tomcat2.jpg
 *   /static
 *     /images
 *       tomcat.jpg
 * </pre>
 * <p>
 * ... and a servlet mapping that maps only <code>/static/*</code> to the default servlet:
 * </p>
 *
 * <pre>
 *   &lt;servlet-mapping&gt;
 *       &lt;servlet-name&gt;default&lt;/servlet-name&gt;
 *       &lt;url-pattern&gt;/static/*&lt;/url-pattern&gt;
 *   &lt;/servlet-mapping&gt;
 * </pre>
 * <p>
 * Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed while a request to
 * <code>/context/images/tomcat2.jpg</code> will fail.
 * </p>
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class DefaultServlet extends HttpServlet {

    @Serial
    private static final long serialVersionUID = 1L;

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

    /**
     * Full range marker.
     */
    protected static final Ranges FULL = new Ranges(null, new ArrayList<>());

    private static final ContentRange IGNORE = new ContentRange(null, 0, 0, 0);

    /**
     * MIME multipart separation string
     */
    protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY";

    /**
     * Size of file transfer buffer in bytes.
     */
    protected static final int BUFFER_SIZE = 4096;


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

    /**
     * The debugging detail level for this servlet.
     */
    protected int debug = 0;

    /**
     * The input buffer size to use when serving resources.
     */
    protected int input = 2048;

    /**
     * Should we generate directory listings?
     */
    protected boolean listings = false;

    /**
     * Status code to use for directory redirects.
     */
    protected int directoryRedirectStatusCode = HttpServletResponse.SC_FOUND;

    /**
     * Read only flag. By default, it's set to true.
     */
    protected boolean readOnly = true;

    /**
     * List of compression formats to serve and their preference order.
     */
    protected CompressionFormat[] compressionFormats;

    /**
     * The output buffer size to use when serving resources.
     */
    protected int output = 2048;

    /**
     * Allow customized directory listing per directory.
     */
    protected String localXsltFile = null;

    /**
     * Allow customized directory listing per context.
     */
    protected String contextXsltFile = null;

    /**
     * Allow customized directory listing per instance.
     */
    protected String globalXsltFile = null;

    /**
     * Allow a readme file to be included.
     */
    protected String readmeFile = null;

    /**
     * The complete set of web application resources
     */
    protected transient WebResourceRoot resources = null;

    /**
     * File encoding to be used when reading static files. If none is specified the platform default is used.
     */
    protected String fileEncoding = null;
    private transient Charset fileEncodingCharset = null;

    /**
     * If a file has a BOM, should that be used in preference to fileEncoding? Will default to {@link BomConfig#TRUE} in
     * {@link #init()}.
     */
    private BomConfig useBomIfPresent = null;

    /**
     * Minimum size for sendfile usage in bytes.
     */
    protected int sendfileSize = 48 * 1024;

    /**
     * Should the Accept-Ranges: bytes header be send with static resources?
     *
     * @deprecated This option will be removed without replacement in Tomcat 12 onwards where it will effectively be
     *                 hard coded to {@code true}.
     */
    @Deprecated
    protected boolean useAcceptRanges = true;

    /**
     * Flag to determine if server information is presented.
     */
    protected boolean showServerInfo = true;

    /**
     * Flag to determine if resources should be sorted.
     */
    protected boolean sortListings = false;

    /**
     * The sorting manager for sorting files and directories.
     */
    protected transient SortManager sortManager;

    /**
     * Flag that indicates whether partial PUTs are permitted.
     */
    private boolean allowPartialPut = true;

    /**
     * Use strong etags whenever possible.
     */
    private boolean useStrongETags = false;

    /**
     * Will direct ({@link DispatcherType#REQUEST} or {@link DispatcherType#ASYNC}) requests using the POST method be
     * processed as GET requests. If not allowed, direct requests using the POST method will be rejected with a 405
     * (method not allowed).
     */
    private boolean allowPostAsGet = true;


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

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


    @Override
    public void init() throws ServletException {

        if (getServletConfig().getInitParameter("debug") != null) {
            debug = Integer.parseInt(getServletConfig().getInitParameter("debug"));
        }

        if (getServletConfig().getInitParameter("input") != null) {
            input = Integer.parseInt(getServletConfig().getInitParameter("input"));
        }

        if (getServletConfig().getInitParameter("output") != null) {
            output = Integer.parseInt(getServletConfig().getInitParameter("output"));
        }

        listings = Boolean.parseBoolean(getServletConfig().getInitParameter("listings"));

        if (getServletConfig().getInitParameter("directoryRedirectStatusCode") != null) {
            String statusCodeString = getServletConfig().getInitParameter("directoryRedirectStatusCode");
            int statusCode = Integer.parseInt(statusCodeString);
            switch (statusCode) {
                case HttpServletResponse.SC_MOVED_PERMANENTLY:
                case HttpServletResponse.SC_FOUND:
                case HttpServletResponse.SC_TEMPORARY_REDIRECT:
                case HttpServletResponse.SC_PERMANENT_REDIRECT:
                    directoryRedirectStatusCode = statusCode;
                    break;
                default:
                    log(sm.getString("defaultServlet.invalidRedirectStatusCode", Integer.valueOf(statusCode)));
            }
        }

        if (getServletConfig().getInitParameter("readonly") != null) {
            readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly"));
        }

        compressionFormats = parseCompressionFormats(getServletConfig().getInitParameter("precompressed"),
                getServletConfig().getInitParameter("gzip"));

        if (getServletConfig().getInitParameter("sendfileSize") != null) {
            sendfileSize = Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024;
        }

        fileEncoding = getServletConfig().getInitParameter("fileEncoding");
        if (fileEncoding == null) {
            fileEncodingCharset = Charset.defaultCharset();
            fileEncoding = fileEncodingCharset.name();
        } else {
            try {
                fileEncodingCharset = B2CConverter.getCharset(fileEncoding);
            } catch (UnsupportedEncodingException e) {
                throw new ServletException(e);
            }
        }

        String useBomIfPresent = getServletConfig().getInitParameter("useBomIfPresent");
        if (useBomIfPresent == null) {
            // Use default
            this.useBomIfPresent = BomConfig.TRUE;
        } else {
            for (BomConfig bomConfig : BomConfig.values()) {
                if (bomConfig.configurationValue.equalsIgnoreCase(useBomIfPresent)) {
                    this.useBomIfPresent = bomConfig;
                    break;
                }
            }
            if (this.useBomIfPresent == null) {
                // Unrecognised configuration value
                IllegalArgumentException iae =
                        new IllegalArgumentException(sm.getString("defaultServlet.unknownBomConfig", useBomIfPresent));
                throw new ServletException(iae);
            }
        }

        globalXsltFile = getServletConfig().getInitParameter("globalXsltFile");
        contextXsltFile = getServletConfig().getInitParameter("contextXsltFile");
        localXsltFile = getServletConfig().getInitParameter("localXsltFile");
        readmeFile = getServletConfig().getInitParameter("readmeFile");

        if (getServletConfig().getInitParameter("useAcceptRanges") != null) {
            useAcceptRanges = Boolean.parseBoolean(getServletConfig().getInitParameter("useAcceptRanges"));
        }

        // Prevent the use of buffer sizes that are too small
        if (input < 256) {
            input = 256;
        }
        if (output < 256) {
            output = 256;
        }

        if (debug > 0) {
            log("DefaultServlet.init:  input buffer size=" + input + ", output buffer size=" + output);
        }

        // Load the web resources
        resources = (WebResourceRoot) getServletContext().getAttribute(Globals.RESOURCES_ATTR);

        if (resources == null) {
            throw new UnavailableException(sm.getString("defaultServlet.noResources"));
        }

        if (getServletConfig().getInitParameter("showServerInfo") != null) {
            showServerInfo = Boolean.parseBoolean(getServletConfig().getInitParameter("showServerInfo"));
        }

        if (getServletConfig().getInitParameter("sortListings") != null) {
            sortListings = Boolean.parseBoolean(getServletConfig().getInitParameter("sortListings"));

            if (sortListings) {
                boolean sortDirectoriesFirst;
                if (getServletConfig().getInitParameter("sortDirectoriesFirst") != null) {
                    sortDirectoriesFirst =
                            Boolean.parseBoolean(getServletConfig().getInitParameter("sortDirectoriesFirst"));
                } else {
                    sortDirectoriesFirst = false;
                }

                sortManager = new SortManager(sortDirectoriesFirst);
            }
        }

        if (getServletConfig().getInitParameter("allowPartialPut") != null) {
            allowPartialPut = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPartialPut"));
        }

        if (getServletConfig().getInitParameter("useStrongETags") != null) {
            useStrongETags = Boolean.parseBoolean(getServletConfig().getInitParameter("useStrongETags"));
        }

        if (getServletConfig().getInitParameter("allowPostAsGet") != null) {
            allowPostAsGet = Boolean.parseBoolean(getServletConfig().getInitParameter("allowPostAsGet"));
        }
    }

    private CompressionFormat[] parseCompressionFormats(String precompressed, String gzip) {
        List<CompressionFormat> ret = new ArrayList<>();
        if (precompressed != null && precompressed.indexOf('=') > 0) {
            for (String pair : precompressed.split(",")) {
                String[] setting = pair.split("=");
                String encoding = setting[0];
                String extension = setting[1];
                ret.add(new CompressionFormat(extension, encoding));
            }
        } else if (precompressed != null) {
            if (Boolean.parseBoolean(precompressed)) {
                ret.add(new CompressionFormat(".br", "br"));
                ret.add(new CompressionFormat(".gz", "gzip"));
            }
        } else if (Boolean.parseBoolean(gzip)) {
            // gzip handling is for backwards compatibility with Tomcat 8.x
            ret.add(new CompressionFormat(".gz", "gzip"));
        }
        return ret.toArray(new CompressionFormat[0]);
    }


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


    /**
     * Return the relative path associated with this servlet.
     *
     * @param request The servlet request we are processing
     *
     * @return the relative path
     */
    protected String getRelativePath(HttpServletRequest request) {
        return getRelativePath(request, false);
    }

    protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
        // IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always
        // serves resources from the web app root with context rooted paths.
        // i.e. it cannot be used to mount the web app root under a sub-path
        // This method must construct a complete context rooted path, although
        // subclasses can change this behaviour.

        String servletPath;
        String pathInfo;

        if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
            // For includes, get the info from the attributes
            pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
            servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
        } else {
            pathInfo = request.getPathInfo();
            servletPath = request.getServletPath();
        }

        StringBuilder result = new StringBuilder();
        if (!servletPath.isEmpty()) {
            result.append(servletPath);
        }
        if (pathInfo != null) {
            result.append(pathInfo);
        }
        if (result.isEmpty() && !allowEmptyPath) {
            result.append('/');
        }

        return result.toString();
    }


    /**
     * Determines the appropriate path to prepend resources with when generating directory listings. Depending on the
     * behaviour of {@link #getRelativePath(HttpServletRequest)} this will change.
     *
     * @param request the request to determine the path for
     *
     * @return the prefix to apply to all resources in the listing.
     */
    protected String getPathPrefix(final HttpServletRequest request) {
        return request.getContextPath();
    }


    protected boolean isListings() {
        return listings;
    }


    protected boolean isReadOnly() {
        return readOnly || resources.isReadOnly();
    }


    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        if (req.getDispatcherType() == DispatcherType.ERROR) {
            doGet(req, resp);
        } else {
            super.service(req, resp);
        }
    }


    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

        // Serve the requested resource, including the data content
        serveResource(request, response, true, fileEncoding);

    }


    @Override
    protected void doHead(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        // Serve the requested resource, without the data content unless we are
        // being included since in that case the content needs to be provided so
        // the correct content length is reported for the including resource
        boolean serveContent = DispatcherType.INCLUDE.equals(request.getDispatcherType());
        serveResource(request, response, serveContent, fileEncoding);
    }


    /**
     * Override default implementation to ensure that TRACE is correctly handled.
     *
     * @param req  the {@link HttpServletRequest} object that contains the request the client made of the servlet
     * @param resp the {@link HttpServletResponse} object that contains the response the servlet returns to the client
     *
     * @exception IOException      if an input or output error occurs while the servlet is handling the OPTIONS request
     * @exception ServletException if the request for the OPTIONS cannot be handled
     */
    @Override
    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        resp.setHeader("Allow", determineMethodsAllowed(req));
    }


    /**
     * Determines the methods normally allowed for the resource.
     *
     * @param req The Servlet request
     *
     * @return The allowed HTTP methods
     */
    protected String determineMethodsAllowed(HttpServletRequest req) {
        StringBuilder allow = new StringBuilder();

        // Start with methods that are always allowed
        allow.append("OPTIONS, GET, HEAD");

        if (allowPostAsGet) {
            allow.append(", POST");
        }

        // PUT and DELETE depend on readonly
        if (!isReadOnly()) {
            allow.append(", PUT, DELETE");
        }

        // Trace - assume disabled unless we can prove otherwise
        if (req instanceof RequestFacade && ((RequestFacade) req).getAllowTrace()) {
            allow.append(", TRACE");
        }

        return allow.toString();
    }


    protected void sendNotAllowed(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.addHeader("Allow", determineMethodsAllowed(req));
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
    }


    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        if (allowPostAsGet) {
            doGet(request, response);
        } else {
            // Use a switch without a default to ensure all possibilities are explicitly handled
            switch (request.getDispatcherType()) {
                case ASYNC:
                case REQUEST: {
                    // Direct POST requests may not be processed as GET
                    sendNotAllowed(request, response);
                    break;
                }
                case ERROR:
                case FORWARD:
                case INCLUDE: {
                    /*
                     * Forward and Include are processed as GET as it is possible that a POST to a servlet may use a
                     * forward or an include as part of generating the response.
                     *
                     * Error should have already been converted to GET but convert here anyway as that is better than
                     * failing the request.
                     */
                    doGet(request, response);
                    break;
                }
            }
        }
    }


    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        if (isReadOnly()) {
            sendNotAllowed(req, resp);
            return;
        }

        String path = getRelativePath(req);

        WebResource resource = resources.getResource(path);

        ContentRange range = parseContentRange(req, resp);

        if (range == null) {
            // Processing error. parseContentRange() set the error code
            return;
        }

        if (!checkIfHeaders(req, resp, resource)) {
            return;
        }

        InputStream resourceInputStream = null;
        File tempContentFile = null;
        try {
            // Append data specified in ranges to existing content for this
            // resource - create a temp. file on the local filesystem to
            // perform this operation
            // Assume just one range is specified for now
            if (range == IGNORE) {
                resourceInputStream = req.getInputStream();
            } else {
                tempContentFile = executePartialPut(req, range, path);
                if (tempContentFile != null) {
                    resourceInputStream = new FileInputStream(tempContentFile);
                }
            }

            if (resourceInputStream != null && resources.write(path, resourceInputStream, true)) {
                if (resource.exists()) {
                    resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
                } else {
                    resp.setStatus(HttpServletResponse.SC_CREATED);
                }
            } else {
                try {
                    resp.sendError(resourceInputStream != null ? HttpServletResponse.SC_CONFLICT :
                            HttpServletResponse.SC_BAD_REQUEST);
                } catch (IllegalStateException e) {
                    // Already committed, ignore
                }
            }
        } finally {
            if (resourceInputStream != null) {
                try {
                    resourceInputStream.close();
                } catch (IOException ignore) {
                    // Ignore
                }
            }
            if (tempContentFile != null) {
                if (!tempContentFile.delete()) {
                    log(sm.getString("defaultServlet.deleteTempFileFailed", tempContentFile.getAbsolutePath()));
                }
            }
        }
    }


    /**
     * Handle a partial PUT. New content specified in request is appended to existing content in oldRevisionContent (if
     * present). This code does not support simultaneous partial updates to the same resource.
     *
     * @param req   The Servlet request
     * @param range The range that will be written
     * @param path  The path
     *
     * @return the associated file object
     *
     * @throws IOException an IO error occurred
     */
    protected File executePartialPut(HttpServletRequest req, ContentRange range, String path) throws IOException {

        // Append data specified in ranges to existing content for this
        // resource - create a temp. file on the local filesystem to
        // perform this operation
        File tempDir = (File) getServletContext().getAttribute(ServletContext.TEMPDIR);
        File contentFile = File.createTempFile("put-part-", null, tempDir);

        try (RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw")) {

            WebResource oldResource = resources.getResource(path);

            // Copy data in oldRevisionContent to contentFile
            if (oldResource.isFile()) {
                try (BufferedInputStream bufOldRevStream =
                        new BufferedInputStream(oldResource.getInputStream(), BUFFER_SIZE)) {

                    int numBytesRead;
                    byte[] copyBuffer = new byte[BUFFER_SIZE];
                    while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) {
                        randAccessContentFile.write(copyBuffer, 0, numBytesRead);
                    }

                }
            }

            randAccessContentFile.setLength(range.getLength());

            // Append data in request input stream to contentFile
            randAccessContentFile.seek(range.getStart());
            long received = 0;
            int numBytesRead;
            byte[] transferBuffer = new byte[BUFFER_SIZE];
            try (BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE)) {
                long rangeBytes = range.getEnd() - range.getStart() + 1L;
                while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
                    received += numBytesRead;
                    if (received > rangeBytes) {
                        throw new IllegalStateException(sm.getString("defaultServlet.wrongByteCountForRange",
                                String.valueOf(received), String.valueOf(rangeBytes)));
                    }
                    randAccessContentFile.write(transferBuffer, 0, numBytesRead);
                }
                if (received < rangeBytes) {
                    throw new IllegalStateException(sm.getString("defaultServlet.wrongByteCountForRange",
                            String.valueOf(received), String.valueOf(rangeBytes)));
                }
            }

        } catch (IOException | RuntimeException | Error e) {
            // This has to be done this way to be able to close the file without changing the method signature
            if (!contentFile.delete()) {
                log(sm.getString("defaultServlet.deleteTempFileFailed", contentFile.getAbsolutePath()));
            }
            return null;
        }

        return contentFile;
    }


    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        if (isReadOnly()) {
            sendNotAllowed(req, resp);
            return;
        }

        String path = getRelativePath(req);

        WebResource resource = resources.getResource(path);

        if (!checkIfHeaders(req, resp, resource)) {
            return;
        }

        if (resource.exists()) {
            if (resource.delete()) {
                resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
            } else {
                sendNotAllowed(req, resp);
            }
        } else {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }


    /**
     * Check if the conditions specified in the optional If headers are satisfied.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return <code>true</code> if the resource meets all the specified conditions, and <code>false</code> if any of
     *             the conditions is not satisfied, in which case request processing is stopped
     *
     * @throws IOException an IO error occurred
     */
    protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, WebResource resource)
            throws IOException {
        if (request.getHeader("If-Match") != null) {
            if (!checkIfMatch(request, response, resource)) {
                return false;
            }
        } else if (request.getHeader("If-Unmodified-Since") != null) {
            if (!checkIfUnmodifiedSince(request, response, resource)) {
                return false;
            }
        }
        if (request.getHeader("If-None-Match") != null) {
            return checkIfNoneMatch(request, response, resource);
        } else if (request.getHeader("If-Modified-Since") != null) {
            return checkIfModifiedSince(request, response, resource);
        }
        return true;
    }


    /**
     * URL rewriter.
     *
     * @param path Path which has to be rewritten
     *
     * @return the rewritten path
     */
    protected String rewriteUrl(String path) {
        return URLEncoder.DEFAULT.encode(path, StandardCharsets.UTF_8);
    }


    /**
     * Serve the specified resource, optionally including the data content.
     *
     * @param request       The servlet request we are processing
     * @param response      The servlet response we are creating
     * @param content       Should the content be included?
     * @param inputEncoding The encoding to use if it is necessary to access the source as characters rather than as
     *                          bytes
     *
     * @exception IOException      if an input/output error occurs
     * @exception ServletException if a servlet-specified error occurs
     */
    protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content,
            String inputEncoding) throws IOException, ServletException {

        boolean serveContent = content;

        // Identify the requested resource path
        String path = getRelativePath(request, true);

        if (debug > 0) {
            if (serveContent) {
                log("DefaultServlet.serveResource:  Serving resource '" + path + "' headers and data");
            } else {
                log("DefaultServlet.serveResource:  Serving resource '" + path + "' headers only");
            }
        }

        if (path.isEmpty()) {
            // Context root redirect
            doDirectoryRedirect(request, response);
            return;
        }

        WebResource resource = resources.getResource(path);
        boolean isError = DispatcherType.ERROR == request.getDispatcherType();

        if (!resource.exists()) {
            // Check if we're included so we can return the appropriate
            // missing resource name in the error
            String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
            if (requestUri == null) {
                requestUri = request.getRequestURI();
            } else {
                // We're included
                // SRV.9.3 says we must throw a FNFE
                throw new FileNotFoundException(sm.getString("defaultServlet.missingResource", requestUri));
            }

            if (isError) {
                response.sendError(((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue());
            } else {
                // Need to check If headers before we return a 404
                if (!checkIfHeaders(request, response, resource)) {
                    return;
                }
                response.sendError(HttpServletResponse.SC_NOT_FOUND,
                        sm.getString("defaultServlet.missingResource", requestUri));
            }
            return;
        }

        if (!resource.canRead()) {
            // Check if we're included so we can return the appropriate
            // missing resource name in the error
            String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
            if (requestUri == null) {
                requestUri = request.getRequestURI();
            } else {
                // We're included
                // Spec doesn't say what to do in this case but a FNFE seems
                // reasonable
                throw new FileNotFoundException(sm.getString("defaultServlet.missingResource", requestUri));
            }

            if (isError) {
                response.sendError(((Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue());
            } else {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, requestUri);
            }
            return;
        }

        boolean included = false;

        // Find content type.
        String contentType = resource.getMimeType();
        if (contentType == null) {
            contentType = getServletContext().getMimeType(resource.getName());
            resource.setMimeType(contentType);
        }

        // These need to reflect the original resource, not the potentially
        // precompressed version of the resource so get them now if they are going to
        // be needed later
        String eTag = null;
        String lastModifiedHttp = null;

        if (resource.isFile() && !isError) {
            eTag = generateETag(resource);
            lastModifiedHttp = resource.getLastModifiedHttp();
        }

        // Check if the conditions specified in the optional If headers are
        // satisfied.
        if (resource.isFile()) {
            // Checking If headers
            included = (request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
            if (!included && !isError && !checkIfHeaders(request, response, resource)) {
                return;
            }
        }

        // Serve a precompressed version of the file if present
        boolean usingPrecompressedVersion = false;
        if (compressionFormats.length > 0 && !included && resource.isFile() && !pathEndsWithCompressedExtension(path)) {
            List<PrecompressedResource> precompressedResources = getAvailablePrecompressedResources(path);
            if (!precompressedResources.isEmpty()) {
                ResponseUtil.addVaryFieldName(response, "accept-encoding");
                PrecompressedResource bestResource = getBestPrecompressedResource(request, precompressedResources);
                if (bestResource != null) {
                    response.addHeader("Content-Encoding", bestResource.format.encoding);
                    resource = bestResource.resource;
                    usingPrecompressedVersion = true;
                }
            }
        }

        Ranges ranges = FULL;
        long contentLength = -1L;

        if (resource.isDirectory()) {
            if (!path.endsWith("/")) {
                doDirectoryRedirect(request, response);
                return;
            }

            // Skip directory listings if we have been configured to
            // suppress them
            if (!isListings()) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND,
                        sm.getString("defaultServlet.missingResource", request.getRequestURI()));
                return;
            }
            contentType = "text/html;charset=UTF-8";
        } else {
            if (!isError) {
                if (useAcceptRanges) {
                    // Accept ranges header
                    response.setHeader("Accept-Ranges", "bytes");
                }

                // Parse range specifier
                ranges = parseRange(request, response, resource);
                if (ranges == null) {
                    return;
                }

                // ETag header
                response.setHeader("ETag", eTag);

                // Last-Modified header
                response.setHeader("Last-Modified", lastModifiedHttp);
            }

            // Get content length
            contentLength = resource.getContentLength();
            // Special case for zero length files, which would cause a
            // (silent) ISE when setting the output buffer size
            if (contentLength == 0L) {
                serveContent = false;
            }
        }

        ServletOutputStream ostream = null;
        PrintWriter writer = null;

        if (serveContent) {
            // Trying to retrieve the servlet output stream
            try {
                ostream = response.getOutputStream();
            } catch (IllegalStateException e) {
                // If it fails, we try to get a Writer instead if we're
                // trying to serve a text file
                if (!usingPrecompressedVersion && isText(contentType)) {
                    writer = response.getWriter();
                    // Cannot reliably serve partial content with a Writer
                    ranges = FULL;
                } else {
                    throw e;
                }
            }
        }

        // Check to see if a Filter, Valve or wrapper has written some content.
        // If it has, disable range requests and setting of a content length
        // since neither can be done reliably.
        ServletResponse r = response;
        long contentWritten = 0;
        while (r instanceof ServletResponseWrapper) {
            r = ((ServletResponseWrapper) r).getResponse();
        }
        if (r instanceof ResponseFacade) {
            contentWritten = ((ResponseFacade) r).getContentWritten();
        }
        if (contentWritten > 0) {
            ranges = FULL;
        }

        String outputEncoding = response.getCharacterEncoding();
        Charset charset = B2CConverter.getCharset(outputEncoding);
        boolean conversionRequired;
        /*
         * The test below deliberately uses != to compare two Strings. This is because the code is looking to see if the
         * default character encoding has been returned because no explicit character encoding has been defined. There
         * is no clean way of doing this via the Servlet API. It would be possible to add a Tomcat specific API but that
         * would require quite a bit of code to get to the Tomcat specific request object that may have been wrapped.
         * The != test is a (slightly hacky) quick way of doing this.
         */
        boolean outputEncodingSpecified = outputEncoding != org.apache.coyote.Constants.DEFAULT_BODY_CHARSET.name() &&
                outputEncoding != resources.getContext().getResponseCharacterEncoding();
        if (!usingPrecompressedVersion && isText(contentType) && outputEncodingSpecified &&
                !charset.equals(fileEncodingCharset)) {
            conversionRequired = true;
            // Conversion often results fewer/more/different bytes.
            // That does not play nicely with range requests.
            ranges = FULL;
        } else {
            conversionRequired = false;
        }

        if (resource.isDirectory() || isError || ranges == FULL) {
            // Set the appropriate output headers
            if (contentType != null) {
                if (debug > 0) {
                    log("DefaultServlet.serveFile:  contentType='" + contentType + "'");
                }
                // Don't override a previously set content type
                if (response.getContentType() == null) {
                    response.setContentType(contentType);
                }
            }
            if (resource.isFile() && contentLength >= 0 && (!serveContent || ostream != null || writer != null)) {
                if (debug > 0) {
                    log("DefaultServlet.serveFile:  contentLength=" + contentLength);
                }
                // Don't set a content length if something else has already
                // written to the response or if conversion will be taking place
                if (contentWritten == 0 && !conversionRequired) {
                    response.setContentLengthLong(contentLength);
                }
            }

            if (serveContent) {
                try {
                    response.setBufferSize(output);
                } catch (IllegalStateException ignore) {
                    // Content has already been written - this must be an include. Ignore the error and continue.
                }
                InputStream renderResult = null;
                if (ostream == null) {
                    // Output via a writer so can't use sendfile or write
                    // content directly.
                    if (resource.isDirectory()) {
                        renderResult = render(request, getPathPrefix(request), resource, inputEncoding);
                    } else {
                        renderResult = resource.getInputStream();
                        if (included) {
                            // Need to make sure any BOM is removed
                            if (!renderResult.markSupported()) {
                                renderResult = new BufferedInputStream(renderResult);
                            }
                            Charset bomCharset = processBom(renderResult, useBomIfPresent.stripBom);
                            if (bomCharset != null && useBomIfPresent.useBomEncoding) {
                                inputEncoding = bomCharset.name();
                            }
                        }
                    }
                    copy(renderResult, writer, inputEncoding);
                } else {
                    // Output is via an OutputStream
                    if (resource.isDirectory()) {
                        renderResult = render(request, getPathPrefix(request), resource, inputEncoding);
                    } else {
                        // Output is content of resource
                        // Check to see if conversion is required
                        if (conversionRequired || included) {
                            // When including a file, we need to check for a BOM
                            // to determine if a conversion is required, so we
                            // might as well always convert
                            InputStream source = resource.getInputStream();
                            if (!source.markSupported()) {
                                source = new BufferedInputStream(source);
                            }
                            Charset bomCharset = processBom(source, useBomIfPresent.stripBom);
                            if (bomCharset != null && useBomIfPresent.useBomEncoding) {
                                inputEncoding = bomCharset.name();
                            }
                            // Following test also ensures included resources
                            // are converted if an explicit output encoding was
                            // specified
                            if (outputEncodingSpecified) {
                                OutputStreamWriter osw = new OutputStreamWriter(ostream, charset);
                                PrintWriter pw = new PrintWriter(osw);
                                copy(source, pw, inputEncoding);
                                pw.flush();
                            } else {
                                // Just included but no conversion
                                renderResult = source;
                            }
                        } else {
                            if (!checkSendfile(request, response, resource, contentLength, null)) {
                                // sendfile not possible so check if resource
                                // content is available directly via
                                // CachedResource. Do not want to call
                                // getContent() on other resource
                                // implementations as that could trigger loading
                                // the contents of a very large file into memory
                                byte[] resourceBody = null;
                                if (resource instanceof CachedResource) {
                                    resourceBody = resource.getContent();
                                }
                                if (resourceBody == null) {
                                    // Resource content not directly available,
                                    // use InputStream
                                    renderResult = resource.getInputStream();
                                } else {
                                    // Use the resource content directly
                                    ostream.write(resourceBody);
                                }
                            }
                        }
                    }
                    // If a stream was configured, it needs to be copied to
                    // the output (this method closes the stream)
                    if (renderResult != null) {
                        copy(renderResult, ostream);
                    }
                }
            }

        } else {

            if (ranges.getEntries().isEmpty()) {
                return;
            }

            // Partial content response.

            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

            if (ranges.getEntries().size() == 1) {

                Ranges.Entry range = ranges.getEntries().get(0);
                long start = getStart(range, contentLength);
                long end = getEnd(range, contentLength);
                response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + contentLength);
                long length = end - start + 1;
                response.setContentLengthLong(length);

                if (contentType != null) {
                    if (debug > 0) {
                        log("DefaultServlet.serveFile:  contentType='" + contentType + "'");
                    }
                    response.setContentType(contentType);
                }

                if (serveContent) {
                    try {
                        response.setBufferSize(output);
                    } catch (IllegalStateException ignore) {
                        // Content has already been written - this must be an include. Ignore the error and continue.
                    }
                    if (ostream != null) {
                        if (!checkSendfile(request, response, resource, contentLength, range)) {
                            copy(resource, contentLength, ostream, range);
                        }
                    } else {
                        // we should not get here
                        throw new IllegalStateException();
                    }
                }
            } else {
                response.setContentType("multipart/byteranges; boundary=" + mimeSeparation);
                if (serveContent) {
                    try {
                        response.setBufferSize(output);
                    } catch (IllegalStateException e) {
                        // Content has already been written - this must be an include. Ignore the error and continue.
                    }
                    if (ostream != null) {
                        copy(resource, contentLength, ostream, ranges, contentType);
                    } else {
                        // we should not get here
                        throw new IllegalStateException();
                    }
                }
            }
        }
    }


    /*
     * Code borrowed heavily from Jasper's EncodingDetector
     */
    private static Charset processBom(InputStream is, boolean stripBom) throws IOException {
        // Java supported character sets do not use BOMs longer than 4 bytes
        byte[] bom = new byte[4];
        is.mark(bom.length);

        int count = is.read(bom);

        // BOMs are at least 2 bytes
        if (count < 2) {
            skip(is, 0, stripBom);
            return null;
        }

        // Look for two byte BOMs
        int b0 = bom[0] & 0xFF;
        int b1 = bom[1] & 0xFF;
        if (b0 == 0xFE && b1 == 0xFF) {
            skip(is, 2, stripBom);
            return StandardCharsets.UTF_16BE;
        }
        // Delay the UTF_16LE check if there are more than 2 bytes since it
        // overlaps with UTF-32LE.
        if (count == 2 && b0 == 0xFF && b1 == 0xFE) {
            skip(is, 2, stripBom);
            return StandardCharsets.UTF_16LE;
        }

        // Remaining BOMs are at least 3 bytes
        if (count < 3) {
            skip(is, 0, stripBom);
            return null;
        }

        // UTF-8 is only 3-byte BOM
        int b2 = bom[2] & 0xFF;
        if (b0 == 0xEF && b1 == 0xBB && b2 == 0xBF) {
            skip(is, 3, stripBom);
            return StandardCharsets.UTF_8;
        }

        if (count < 4) {
            skip(is, 0, stripBom);
            return null;
        }

        // Look for 4-byte BOMs
        int b3 = bom[3] & 0xFF;
        if (b0 == 0x00 && b1 == 0x00 && b2 == 0xFE && b3 == 0xFF) {
            return Charset.forName("UTF-32BE");
        }
        if (b0 == 0xFF && b1 == 0xFE && b2 == 0x00 && b3 == 0x00) {
            return Charset.forName("UTF-32LE");
        }

        // Now we can check for UTF16-LE. There is an assumption here that we
        // won't see a UTF16-LE file with a BOM where the first real data is
        // 0x00 0x00
        if (b0 == 0xFF && b1 == 0xFE) {
            skip(is, 2, stripBom);
            return StandardCharsets.UTF_16LE;
        }

        skip(is, 0, stripBom);
        return null;
    }


    private static void skip(InputStream is, int skip, boolean stripBom) throws IOException {
        is.reset();
        if (stripBom) {
            while (skip-- > 0) {
                if (is.read() < 0) {
                    // Ignore since included
                    break;
                }
            }
        }
    }


    private static boolean isText(String contentType) {
        return contentType == null || contentType.startsWith("text") || contentType.endsWith("xml") ||
                contentType.contains("/javascript");
    }

    private static boolean validate(Ranges ranges, long length) {
        List<long[]> rangeContext = new ArrayList<>();
        int overlapCount = 0;
        for (Ranges.Entry range : ranges.getEntries()) {
            long start = getStart(range, length);
            long end = getEnd(range, length);
            if (start < 0 || start > end) {
                // Invalid range
                return false;
            }
            /*
             * See https://www.rfc-editor.org/rfc/rfc9110.html#name-range and
             * https://www.rfc-editor.org/rfc/rfc9110.html#status.416
             *
             * The server MAY ignore or reject Range headers with:
             *
             * - "Many" (undefined) small ranges not in ascending order - not currently enforced.
             *
             * - More than two overlapping ranges (enforced)
             */
            for (long[] r : rangeContext) {
                long s2 = r[0];
                long e2 = r[1];
                // Given valid [s1,e1] and [s2,e2]
                // If { s1>e2 || s2>e1 } then no overlap
                // equivalent to
                // If not { s1>e2 || s2>e1 } then overlap
                // De Morgan's law
                if (start <= e2 && s2 <= end) {
                    overlapCount++;
                    // Off by one is deliberate. There is 1 more overlapping range than there are overlaps.
                    if (overlapCount > 1) {
                        return false;
                    }
                }
            }
            rangeContext.add(new long[] { start, end });
        }
        return true;
    }

    private static long getStart(Ranges.Entry range, long length) {
        long start = range.getStart();
        if (start == -1) {
            long end = range.getEnd();
            // If there is no start, then the start is based on the end
            if (end >= length) {
                return 0;
            } else {
                return length - end;
            }
        } else {
            return start;
        }
    }

    private static long getEnd(Ranges.Entry range, long length) {
        long end = range.getEnd();
        if (range.getStart() == -1 || end == -1 || end >= length) {
            return length - 1;
        } else {
            return end;
        }
    }

    private boolean pathEndsWithCompressedExtension(String path) {
        for (CompressionFormat format : compressionFormats) {
            if (path.endsWith(format.extension)) {
                return true;
            }
        }
        return false;
    }

    private List<PrecompressedResource> getAvailablePrecompressedResources(String path) {
        List<PrecompressedResource> ret = new ArrayList<>(compressionFormats.length);
        for (CompressionFormat format : compressionFormats) {
            WebResource precompressedResource = resources.getResource(path + format.extension);
            if (precompressedResource.exists() && precompressedResource.isFile()) {
                ret.add(new PrecompressedResource(precompressedResource, format));
            }
        }
        return ret;
    }

    /**
     * Match the client preferred encoding formats to the available precompressed resources.
     *
     * @param request                The servlet request we are processing
     * @param precompressedResources List of available precompressed resources.
     *
     * @return The best matching precompressed resource or null if no match was found.
     */
    private PrecompressedResource getBestPrecompressedResource(HttpServletRequest request,
            List<PrecompressedResource> precompressedResources) {
        Enumeration<String> headers = request.getHeaders("Accept-Encoding");
        PrecompressedResource bestResource = null;
        double bestResourceQuality = 0;
        int bestResourcePreference = Integer.MAX_VALUE;
        while (headers.hasMoreElements()) {
            String header = headers.nextElement();
            for (String preference : header.split(",")) {
                double quality = 1;
                int qualityIdx = preference.indexOf(';');
                if (qualityIdx > 0) {
                    int equalsIdx = preference.indexOf('=', qualityIdx + 1);
                    if (equalsIdx == -1) {
                        continue;
                    }
                    quality = Double.parseDouble(preference.substring(equalsIdx + 1).trim());
                }
                if (quality >= bestResourceQuality) {
                    String encoding = preference;
                    if (qualityIdx > 0) {
                        encoding = encoding.substring(0, qualityIdx);
                    }
                    encoding = encoding.trim();
                    if ("identity".equals(encoding)) {
                        bestResource = null;
                        bestResourceQuality = quality;
                        bestResourcePreference = Integer.MAX_VALUE;
                        continue;
                    }
                    if ("*".equals(encoding)) {
                        bestResource = precompressedResources.get(0);
                        bestResourceQuality = quality;
                        bestResourcePreference = 0;
                        continue;
                    }
                    for (int i = 0; i < precompressedResources.size(); ++i) {
                        PrecompressedResource resource = precompressedResources.get(i);
                        if (encoding.equals(resource.format.encoding)) {
                            if (quality > bestResourceQuality || i < bestResourcePreference) {
                                bestResource = resource;
                                bestResourceQuality = quality;
                                bestResourcePreference = i;
                            }
                            break;
                        }
                    }
                }
            }
        }
        return bestResource;
    }

    private void doDirectoryRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
        StringBuilder location = new StringBuilder(request.getRequestURI());
        location.append('/');
        if (request.getQueryString() != null) {
            location.append('?');
            location.append(request.getQueryString());
        }
        // Avoid protocol relative redirects
        while (location.length() > 1 && location.charAt(1) == '/') {
            location.deleteCharAt(0);
        }
        response.sendRedirect(response.encodeRedirectURL(location.toString()), directoryRedirectStatusCode);
    }

    /**
     * Parse the content-range header.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     *
     * @return the partial content-range, {@code null} if the content-range header was invalid or {@code #IGNORE} if
     *             there is no header to process
     *
     * @throws IOException an IO error occurred
     */
    protected ContentRange parseContentRange(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        // Retrieving the content-range header (if any is specified
        String contentRangeHeader = request.getHeader("Content-Range");

        if (contentRangeHeader == null) {
            return IGNORE;
        }

        if (!allowPartialPut) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }

        ContentRange contentRange = ContentRange.parse(new StringReader(contentRangeHeader));

        if (contentRange == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
        // bytes is the only range unit supported
        if (!"bytes".equals(contentRange.getUnits())) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }

        return contentRange;
    }


    /**
     * Parse the range header.
     * <p>
     * The caller is required to have confirmed that the requested resource exists and is a file before calling this
     * method.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return a list of ranges, {@code null} if the range header was invalid or {@code #FULL} if the Range header
     *             should be ignored.
     *
     * @throws IOException an IO error occurred
     */
    protected Ranges parseRange(HttpServletRequest request, HttpServletResponse response, WebResource resource)
            throws IOException {

        // Retrieving the range header (if any is specified)
        String rangeHeader = request.getHeader("Range");

        if (rangeHeader == null) {
            // No Range header is the same as ignoring any Range header
            return FULL;
        }

        if (!"GET".equals(request.getMethod()) || !isRangeRequestsSupported()) {
            // RFC 9110 - Section 14.2: GET is the only method for which range handling is defined.
            // Otherwise MUST ignore a Range header field
            return FULL;
        }

        // Evaluate If-Range
        if (!checkIfRange(request, response, resource)) {
            if (response.isCommitted()) {
                /*
                 * Ideally, checkIfRange() would be changed to return Boolean so the three states (satisfied,
                 * unsatisfied and error) could each be communicated via the return value. There isn't a backwards
                 * compatible way to do that that doesn't involve changing the method name and there are benefits to
                 * retaining the consistency of the existing method name pattern. Hence, this 'trick'. For the error
                 * state, checkIfRange() will call response.sendError() which will commit the response which this method
                 * can then detect.
                 */
                return null;
            }
            // No error but If-Range not satisfied
            return FULL;
        }

        long fileLength = resource.getContentLength();

        if (fileLength == 0) {
            // Range header makes no sense for a zero length resource. Tomcat
            // therefore opts to ignore it.
            return FULL;
        }


        Ranges ranges = Ranges.parse(new StringReader(rangeHeader));

        if (ranges == null) {
            // The Range header is present but not formatted correctly.
            // Could argue for a 400 response but 416 is more specific.
            // There is also the option to ignore the (invalid) Range header.
            // RFC7233#4.4 notes that many servers do ignore the Range header in
            // these circumstances but Tomcat has always returned a 416.
            response.addHeader("Content-Range", "bytes */" + fileLength);
            response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return null;
        }

        // bytes is the only range unit supported (and I don't see the point
        // of adding new ones).
        if (!ranges.getUnits().equals("bytes")) {
            // RFC7233#3.1 Servers must ignore range units they don't understand
            return FULL;
        }

        if (!validate(ranges, fileLength)) {
            response.addHeader("Content-Range", "bytes */" + fileLength);
            response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            return null;
        }

        return ranges;
    }


    /**
     * Decide which way to render. HTML or XML.
     *
     * @param request     The HttpServletRequest being served
     * @param contextPath The path
     * @param resource    The resource
     * @param encoding    The encoding to use to process the readme (if any)
     *
     * @return the input stream with the rendered output
     *
     * @throws IOException      an IO error occurred
     * @throws ServletException rendering error
     */
    protected InputStream render(HttpServletRequest request, String contextPath, WebResource resource, String encoding)
            throws IOException, ServletException {

        Source xsltSource = findXsltSource(resource);

        if (xsltSource == null) {
            return renderHtml(request, contextPath, resource, encoding);
        }
        return renderXml(request, contextPath, resource, xsltSource, encoding);
    }


    /**
     * Return an InputStream to an XML representation of the contents this directory.
     *
     * @param request     The HttpServletRequest being served
     * @param contextPath Context path to which our internal paths are relative
     * @param resource    The associated resource
     * @param xsltSource  The XSL stylesheet
     * @param encoding    The encoding to use to process the readme (if any)
     *
     * @return the XML data
     *
     * @throws IOException      an IO error occurred
     * @throws ServletException rendering error
     */
    protected InputStream renderXml(HttpServletRequest request, String contextPath, WebResource resource,
            Source xsltSource, String encoding) throws IOException, ServletException {

        StringBuilder sb = new StringBuilder();

        sb.append("<?xml version=\"1.0\"?>");
        sb.append("<listing ");
        sb.append(" contextPath='");
        sb.append(contextPath);
        sb.append('\'');
        sb.append(" directory='");
        sb.append(resource.getName());
        sb.append("' ");
        sb.append(" hasParent='").append(!resource.getName().equals("/"));
        sb.append("'>");

        sb.append("<entries>");

        String[] entries = resources.list(resource.getWebappPath());

        // rewriteUrl(contextPath) is expensive. cache result for later reuse
        String rewrittenContextPath = rewriteUrl(contextPath);
        String directoryWebappPath = resource.getWebappPath();

        for (String entry : entries) {

            if (entry.equalsIgnoreCase("WEB-INF") || entry.equalsIgnoreCase("META-INF") ||
                    entry.equalsIgnoreCase(localXsltFile)) {
                continue;
            }

            if ((directoryWebappPath + entry).equals(contextXsltFile)) {
                continue;
            }

            WebResource childResource = resources.getResource(directoryWebappPath + entry);
            if (!childResource.exists()) {
                continue;
            }

            sb.append("<entry");
            sb.append(" type='").append(childResource.isDirectory() ? "dir" : "file").append('\'');
            sb.append(" urlPath='").append(rewrittenContextPath)
                    .append(Escape.xml(rewriteUrl(directoryWebappPath + entry)))
                    .append(childResource.isDirectory() ? "/" : "").append('\'');
            if (childResource.isFile()) {
                sb.append(" size='").append(renderSize(childResource.getContentLength())).append('\'');
            }
            sb.append(" date='").append(childResource.getLastModifiedHttp()).append('\'');
            sb.append(" longDate='").append(childResource.getLastModified()).append('\'');

            sb.append('>');
            sb.append(Escape.htmlElementContent(entry));
            if (childResource.isDirectory()) {
                sb.append('/');
            }
            sb.append("</entry>");
        }
        sb.append("</entries>");

        String readme = getReadme(resource, encoding);

        if (readme != null) {
            sb.append("<readme><![CDATA[");
            sb.append(readme);
            sb.append("]]></readme>");
        }

        sb.append("</listing>");

        // Prevent possible memory leak. Ensure Transformer and
        // TransformerFactory are not loaded from the web application.
        Thread currentThread = Thread.currentThread();
        ClassLoader original = currentThread.getContextClassLoader();
        try {
            currentThread.setContextClassLoader(DefaultServlet.class.getClassLoader());

            TransformerFactory tFactory = TransformerFactory.newInstance();
            Source xmlSource = new StreamSource(new StringReader(sb.toString()));
            Transformer transformer = tFactory.newTransformer(xsltSource);

            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
            StreamResult out = new StreamResult(osWriter);
            transformer.transform(xmlSource, out);
            osWriter.flush();
            return new ByteArrayInputStream(stream.toByteArray());
        } catch (TransformerException e) {
            throw new ServletException(sm.getString("defaultServlet.xslError"), e);
        } finally {
            currentThread.setContextClassLoader(original);
        }
    }

    /**
     * Return an InputStream to an HTML representation of the contents of this directory.
     *
     * @param request     The HttpServletRequest being served
     * @param contextPath Context path to which our internal paths are relative
     * @param resource    The associated resource
     * @param encoding    The encoding to use to process the readme (if any)
     *
     * @return the HTML data
     *
     * @throws IOException an IO error occurred
     */
    protected InputStream renderHtml(HttpServletRequest request, String contextPath, WebResource resource,
            String encoding) throws IOException {

        // Prepare a writer to a buffered area
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        OutputStreamWriter osWriter = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
        PrintWriter writer = new PrintWriter(osWriter);

        StringBuilder sb = new StringBuilder();

        // Get the right strings
        StringManager sm = StringManager.getManager(DefaultServlet.class.getPackageName(), request.getLocales());

        String directoryWebappPath = resource.getWebappPath();
        WebResource[] entries = resources.listResources(directoryWebappPath);

        // rewriteUrl(contextPath) is expensive. cache result for later reuse
        String rewrittenContextPath = rewriteUrl(contextPath);

        // Render the page header
        sb.append("<!doctype html>\r\n");
        sb.append("<html lang=\"").append(sm.getLocale().getLanguage()).append("\">\r\n");
        sb.append("<head>\r\n");
        sb.append("<title>");
        sb.append(sm.getString("defaultServlet.directory.title", directoryWebappPath));
        sb.append("</title>\r\n");
        sb.append("<style>");
        sb.append(org.apache.catalina.util.TomcatCSS.TOMCAT_CSS);
        sb.append("</style>\r\n");
        sb.append("</head>\r\n");
        sb.append("<body>\r\n");
        sb.append("<h1>");
        sb.append(sm.getString("defaultServlet.directory.title", directoryWebappPath));

        // Render the link to our parent (if required)
        String parentDirectory = directoryWebappPath;
        if (parentDirectory.endsWith("/")) {
            parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1);
        }
        int slash = parentDirectory.lastIndexOf('/');
        if (slash >= 0) {
            String parent = directoryWebappPath.substring(0, slash);
            sb.append(" \u2013 <a href=\"");
            sb.append(rewrittenContextPath);
            if (parent.isEmpty()) {
                parent = "/";
            }
            sb.append(rewriteUrl(parent));
            if (!parent.endsWith("/")) {
                sb.append('/');
            }
            sb.append("\">");
            sb.append("<b>");
            sb.append(sm.getString("defaultServlet.directory.parent", parent));
            sb.append("</b>");
            sb.append("</a>");
        }

        sb.append("</h1>\r\n");
        sb.append("<hr class=\"line\">\r\n");

        sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n");

        SortManager.Order order;
        if (sortListings) {
            order = sortManager.getOrder(request.getQueryString());
        } else {
            order = null;
        }
        // Render the column headings
        sb.append("<thead>\r\n");
        sb.append("<tr>\r\n");
        sb.append("<th align=\"left\"><font size=\"+1\"><strong>");
        if (order != null) {
            sb.append("<a href=\"?C=N;O=");
            sb.append(getOrderChar(order, 'N'));
            sb.append("\">");
            sb.append(sm.getString("defaultServlet.resource.name"));
            sb.append("</a>");
        } else {
            sb.append(sm.getString("defaultServlet.resource.name"));
        }
        sb.append("</strong></font></th>\r\n");
        sb.append("<th align=\"center\"><font size=\"+1\"><strong>");
        if (order != null) {
            sb.append("<a href=\"?C=S;O=");
            sb.append(getOrderChar(order, 'S'));
            sb.append("\">");
            sb.append(sm.getString("defaultServlet.resource.size"));
            sb.append("</a>");
        } else {
            sb.append(sm.getString("defaultServlet.resource.size"));
        }
        sb.append("</strong></font></th>\r\n");
        sb.append("<th align=\"right\"><font size=\"+1\"><strong>");
        if (order != null) {
            sb.append("<a href=\"?C=M;O=");
            sb.append(getOrderChar(order, 'M'));
            sb.append("\">");
            sb.append(sm.getString("defaultServlet.resource.lastModified"));
            sb.append("</a>");
        } else {
            sb.append(sm.getString("defaultServlet.resource.lastModified"));
        }
        sb.append("</strong></font></th>\r\n");
        sb.append("</tr>\r\n");
        sb.append("</thead>\r\n");

        if (null != sortManager) {
            sortManager.sort(entries, request.getQueryString());
        }

        boolean shade = false;
        sb.append("<tbody>\r\n");
        for (WebResource childResource : entries) {
            String filename = childResource.getName();
            if (filename.equalsIgnoreCase("WEB-INF") || filename.equalsIgnoreCase("META-INF")) {
                continue;
            }

            if (!childResource.exists()) {
                continue;
            }

            sb.append("<tr");
            if (shade) {
                sb.append(" bgcolor=\"#eeeeee\"");
            }
            sb.append(">\r\n");
            shade = !shade;

            sb.append("<td align=\"left\">&nbsp;&nbsp;\r\n");
            sb.append("<a href=\"");
            sb.append(rewrittenContextPath);
            sb.append(rewriteUrl(childResource.getWebappPath()));
            if (childResource.isDirectory()) {
                sb.append('/');
            }
            sb.append("\"><tt>");
            sb.append(Escape.htmlElementContent(filename));
            if (childResource.isDirectory()) {
                sb.append('/');
            }
            sb.append("</tt></a></td>\r\n");

            sb.append("<td align=\"right\"><tt>");
            if (childResource.isDirectory()) {
                sb.append("&nbsp;");
            } else {
                sb.append(renderSize(childResource.getContentLength()));
            }
            sb.append("</tt></td>\r\n");

            sb.append("<td align=\"right\"><tt>");
            sb.append(renderTimestamp(childResource.getLastModified()));
            sb.append("</tt></td>\r\n");

            sb.append("</tr>\r\n");
        }
        sb.append("</tbody>\r\n");

        // Render the page footer
        sb.append("</table>\r\n");

        sb.append("<hr class=\"line\">\r\n");

        String readme = getReadme(resource, encoding);
        if (readme != null) {
            sb.append(readme);
            sb.append("<hr class=\"line\">\r\n");
        }

        if (showServerInfo) {
            sb.append("<h3>").append(ServerInfo.getServerInfo()).append("</h3>\r\n");
        }
        sb.append("</body>\r\n");
        sb.append("</html>\r\n");

        // Return an input stream to the underlying bytes
        writer.write(sb.toString());
        writer.flush();
        return new ByteArrayInputStream(stream.toByteArray());

    }


    /**
     * Render the specified file size (in bytes).
     *
     * @param size File size (in bytes)
     *
     * @return the formatted size
     */
    protected String renderSize(long size) {

        long leftSide = size / 1024;
        long rightSide = (size % 1024) / 103; // Makes 1 digit
        if ((leftSide == 0) && (rightSide == 0) && (size > 0)) {
            rightSide = 1;
        }

        return (String.valueOf(leftSide) + "." + String.valueOf(rightSide) + " KiB");

    }


    /**
     * Render the specified file timestamp.
     *
     * @param timestamp File timestamp
     *
     * @return the formatted timestamp
     */
    protected String renderTimestamp(long timestamp) {

        return FastHttpDateFormat.formatDate(timestamp);

    }


    /**
     * Get the readme file as a string.
     *
     * @param directory The directory to search
     * @param encoding  The readme encoding
     *
     * @return the readme for the specified directory
     */
    protected String getReadme(WebResource directory, String encoding) {

        if (readmeFile != null) {
            WebResource resource = resources.getResource(directory.getWebappPath() + readmeFile);
            if (resource.isFile()) {
                StringWriter buffer = new StringWriter();
                InputStreamReader reader = null;
                try (InputStream is = resource.getInputStream()) {
                    if (encoding != null) {
                        reader = new InputStreamReader(is, encoding);
                    } else {
                        reader = new InputStreamReader(is);
                    }
                    IOException e = copyRange(reader, new PrintWriter(buffer));
                    if (debug > 10) {
                        log("readme '" + readmeFile + "' output error: " + ((e != null) ? e.getMessage() : ""));
                    }
                } catch (IOException ioe) {
                    log(sm.getString("defaultServlet.readerCloseFailed"), ioe);
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException ignore) {
                            // Ignore
                        }
                    }
                }
                return buffer.toString();
            } else {
                if (debug > 10) {
                    log("readme '" + readmeFile + "' not found");
                }

                return null;
            }
        }

        return null;
    }


    /**
     * Return a Source for the xsl template (if possible).
     *
     * @param directory The directory to search
     *
     * @return the source for the specified directory
     *
     * @throws IOException an IO error occurred
     */
    protected Source findXsltSource(WebResource directory) throws IOException {

        if (localXsltFile != null) {
            WebResource resource = resources.getResource(directory.getWebappPath() + localXsltFile);
            if (resource.isFile()) {
                InputStream is = resource.getInputStream();
                if (is != null) {
                    return new StreamSource(is);
                }
            }
            if (debug > 10) {
                log("localXsltFile '" + localXsltFile + "' not found");
            }
        }

        if (contextXsltFile != null) {
            InputStream is = getServletContext().getResourceAsStream(contextXsltFile);
            if (is != null) {
                return new StreamSource(is);
            }

            if (debug > 10) {
                log("contextXsltFile '" + contextXsltFile + "' not found");
            }
        }

        /*
         * Open and read in file in one fell swoop to reduce the chance of leaving handle open.
         */
        if (globalXsltFile != null) {
            File f = validateGlobalXsltFile();
            if (f != null) {
                long globalXsltFileSize = f.length();
                if (globalXsltFileSize > Integer.MAX_VALUE) {
                    log(sm.getString("defaultServlet.globalXSLTTooBig", f.getAbsolutePath()));
                } else {
                    try (FileInputStream fis = new FileInputStream(f)) {
                        byte[] b = new byte[(int) f.length()];
                        IOTools.readFully(fis, b);
                        return new StreamSource(new ByteArrayInputStream(b));
                    }
                }
            }
        }

        return null;
    }


    private File validateGlobalXsltFile() {
        Context context = resources.getContext();

        File baseConf = new File(context.getCatalinaBase(), "conf");
        File result = validateGlobalXsltFile(baseConf);
        if (result == null) {
            File homeConf = new File(context.getCatalinaHome(), "conf");
            if (!baseConf.equals(homeConf)) {
                result = validateGlobalXsltFile(homeConf);
            }
        }

        return result;
    }


    private File validateGlobalXsltFile(File base) {
        File candidate = new File(globalXsltFile);
        if (!candidate.isAbsolute()) {
            candidate = new File(base, globalXsltFile);
        }

        if (!candidate.isFile()) {
            return null;
        }

        // First check that the resulting path is under the provided base
        try {
            if (!candidate.getCanonicalFile().toPath().startsWith(base.getCanonicalFile().toPath())) {
                return null;
            }
        } catch (IOException ioe) {
            return null;
        }

        // Next check that an .xsl or .xslt file has been specified
        String nameLower = candidate.getName().toLowerCase(Locale.ENGLISH);
        if (!nameLower.endsWith(".xslt") && !nameLower.endsWith(".xsl")) {
            return null;
        }

        return candidate;
    }


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

    /**
     * Check if sendfile can be used.
     *
     * @param request  The Servlet request
     * @param response The Servlet response
     * @param resource The resource
     * @param length   The length which will be written (will be used only if range is null)
     * @param range    The range that will be written
     *
     * @return <code>true</code> if sendfile should be used (writing is then delegated to the endpoint)
     */
    protected boolean checkSendfile(HttpServletRequest request, HttpServletResponse response, WebResource resource,
            long length, Ranges.Entry range) {
        String canonicalPath;
        if (sendfileSize > 0 && length > sendfileSize &&
                (Boolean.TRUE.equals(request.getAttribute(Globals.SENDFILE_SUPPORTED_ATTR))) &&
                (request.getClass().getName().equals("org.apache.catalina.connector.RequestFacade")) &&
                (response.getClass().getName().equals("org.apache.catalina.connector.ResponseFacade")) &&
                resource.isFile() && ((canonicalPath = resource.getCanonicalPath()) != null)) {
            request.setAttribute(Globals.SENDFILE_FILENAME_ATTR, canonicalPath);
            if (range == null) {
                request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(0L));
                request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(length));
            } else {
                request.setAttribute(Globals.SENDFILE_FILE_START_ATTR, Long.valueOf(getStart(range, length)));
                request.setAttribute(Globals.SENDFILE_FILE_END_ATTR, Long.valueOf(getEnd(range, length) + 1));
            }
            return true;
        }
        return false;
    }


    /**
     * Check if the if-match condition is satisfied.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition
     *             is not satisfied, in which case request processing is stopped
     *
     * @throws IOException an IO error occurred
     */
    protected boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response, WebResource resource)
            throws IOException {

        boolean conditionSatisfied = false;
        Enumeration<String> headerValues = request.getHeaders("If-Match");
        String resourceETag = generateETag(resource);

        boolean hasAsteriskValue = false;// check existence of special header value '*'
        int headerCount = 0;
        while (headerValues.hasMoreElements() && !conditionSatisfied) {
            headerCount++;
            String headerValue = headerValues.nextElement();
            if ("*".equals(headerValue)) {
                hasAsteriskValue = true;
                if (resourceETag != null) {
                    conditionSatisfied = true;
                }
            } else {
                // RFC 7232 requires strong comparison for If-Match headers
                Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), false, resourceETag);
                if (matched == null) {
                    if (debug > 10) {
                        log("DefaultServlet.checkIfMatch:  Invalid header value [" + headerValue + "]");
                    }
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                    return false;
                } else {
                    conditionSatisfied = matched.booleanValue();
                }
            }
        }
        if (headerValues.hasMoreElements()) {
            headerCount++;
        }

        if (hasAsteriskValue && headerCount > 1) {
            // Note that an If-Match header field with a list value containing "*" and other values (including other
            // instances of "*") is syntactically invalid (therefore not allowed to be generated) and furthermore is
            // unlikely to be interoperable.
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }
        if (!conditionSatisfied) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return false;
        }
        return true;
    }


    /**
     * Check if the if-modified-since condition is satisfied.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition
     *             is not satisfied, in which case request processing is stopped
     */
    protected boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response,
            WebResource resource) {

        String method = request.getMethod();
        if (!"GET".equals(method) && !"HEAD".equals(method)) {
            return true;
        }

        long resourceLastModified = resource.getLastModified();
        if (resourceLastModified <= 0) {
            // MUST ignore if the resource does not have a modification date available.
            return true;
        }

        // Must be at least one header for this method to be called
        Enumeration<String> headerEnum = request.getHeaders("If-Modified-Since");
        headerEnum.nextElement();
        if (headerEnum.hasMoreElements()) {
            // If-Modified-Since is a list of dates
            return true;
        }

        try {
            // Header is present so -1 will be not returned. Only a valid date or an IAE are possible.
            long headerValue = request.getDateHeader("If-Modified-Since");
            if (resourceLastModified < (headerValue + 1000)) {
                // The entity has not been modified since the date
                // specified by the client. This is not an error case.
                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                response.setHeader("ETag", generateETag(resource));
                return false;
            }
        } catch (IllegalArgumentException illegalArgument) {
            return true;
        }
        return true;
    }


    /**
     * Check if the if-none-match condition is satisfied.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition
     *             is not satisfied, in which case request processing is stopped
     *
     * @throws IOException an IO error occurred
     */
    protected boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response, WebResource resource)
            throws IOException {

        String resourceETag = generateETag(resource);

        Enumeration<String> headerValues = request.getHeaders("If-None-Match");
        boolean hasAsteriskValue = false;// check existence of special header value '*'
        boolean conditionSatisfied = true;
        int headerCount = 0;
        while (headerValues.hasMoreElements()) {
            headerCount++;
            String headerValue = headerValues.nextElement();

            if (headerValue.equals("*")) {
                hasAsteriskValue = true;
                if (headerCount > 1 || headerValues.hasMoreElements()) {
                    conditionSatisfied = false;
                } else {
                    // asterisk '*' is the only field value.
                    // RFC9110: If the field value is "*", the condition is false if the origin server has a current
                    // representation for the target resource.
                    if (resourceETag != null) {
                        conditionSatisfied = false;
                    }
                }
                break;
            } else {
                // RFC 7232 requires weak comparison for If-None-Match headers
                Boolean matched = EntityTag.compareEntityTag(new StringReader(headerValue), true, resourceETag);
                if (matched == null) {
                    if (debug > 10) {
                        log("DefaultServlet.checkIfNoneMatch:  Invalid header value [" + headerValue + "]");
                    }
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                    return false;
                }
                if (matched.booleanValue()) {
                    // RFC9110: If the field value is a list of entity tags, the condition is false if one of the
                    // listed tags
                    // matches the entity tag of the selected representation.
                    conditionSatisfied = false;
                    break;
                }
            }
        }
        if (headerValues.hasMoreElements()) {
            headerCount++;
        }

        if (hasAsteriskValue && headerCount > 1) {
            // Note that an If-None-Match header field with a list value containing "*" and other values (including
            // other instances of "*") is syntactically invalid (therefore not allowed to be generated) and furthermore
            // is unlikely to be interoperable.
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }
        if (!conditionSatisfied) {
            // For GET and HEAD, we should respond with
            // 304 Not Modified.
            // For every other method, 412 Precondition Failed is sent
            // back.
            if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) {
                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                response.setHeader("ETag", resourceETag);
            } else {
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            }
            return false;
        }
        return true;
    }


    /**
     * Check if the if-unmodified-since condition is satisfied.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return <code>true</code> if the resource meets the specified condition, and <code>false</code> if the condition
     *             is not satisfied, in which case request processing is stopped
     *
     * @throws IOException an IO error occurred
     */
    protected boolean checkIfUnmodifiedSince(HttpServletRequest request, HttpServletResponse response,
            WebResource resource) throws IOException {

        long resourceLastModified = resource.getLastModified();
        if (resourceLastModified <= 0) {
            // MUST ignore if the resource does not have a modification date available.
            return true;
        }
        // Must be at least one header for this method to be called
        Enumeration<String> headerEnum = request.getHeaders("If-Unmodified-Since");
        headerEnum.nextElement();
        if (headerEnum.hasMoreElements()) {
            // If-Unmodified-Since is a list of dates
            return true;
        }

        try {
            // Header is present so -1 will be not returned. Only a valid date or an IAE are possible.
            long headerValue = request.getDateHeader("If-Unmodified-Since");
            if (resourceLastModified >= (headerValue + 1000)) {
                // The entity has not been modified since the date
                // specified by the client. This is not an error case.
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return false;
            }
        } catch (IllegalArgumentException illegalArgument) {
            return true;
        }
        return true;
    }


    /**
     * Check if the if-range condition is satisfied. The calling method is required to ensure a Range header is present
     * and that Range requests are supported for the current resource.
     *
     * @param request  The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resource The resource
     *
     * @return {@code true} if the resource meets the specified condition, and {@code false} if the condition is not
     *             satisfied, resulting in transfer of the new selected representation instead of a 412 (Precondition
     *             Failed) response. If the if-range condition is not valid then an appropriate status code will be set,
     *             the response will be committed and this method will return {@code false}
     *
     * @throws IOException an IO error occurred
     */
    protected boolean checkIfRange(HttpServletRequest request, HttpServletResponse response, WebResource resource)
            throws IOException {
        String resourceETag = generateETag(resource);
        long resourceLastModified = resource.getLastModified();

        Enumeration<String> headerEnum = request.getHeaders("If-Range");
        if (!headerEnum.hasMoreElements()) {
            // If-Range is not present
            return true;
        }
        String headerValue = headerEnum.nextElement().trim();
        if (headerEnum.hasMoreElements()) {
            // Multiple If-Range headers
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return false;
        }

        if (headerValue.length() > 2 && (headerValue.charAt(0) == '"' || headerValue.charAt(2) == '"')) {
            boolean weakETag = headerValue.startsWith("W/\"");
            if ((!weakETag && headerValue.charAt(0) != '"') || headerValue.charAt(headerValue.length() - 1) != '"' ||
                    headerValue.indexOf('"', weakETag ? 3 : 1) != headerValue.length() - 1) {
                // Not a single entity tag
                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return false;
            }
            // If the ETag the client gave does not match the entity
            // etag, then the entire entity is returned.
            return !weakETag && resourceETag != null && resourceETag.equals(headerValue);
        } else {
            long headerValueTime = -1L;
            try {
                headerValueTime = request.getDateHeader("If-Range");
            } catch (IllegalArgumentException ignore) {
                // Ignore
            }
            if (headerValueTime >= 0) {
                // unit of HTTP date is second, ignore millisecond part.
                return resourceLastModified >= headerValueTime && resourceLastModified < headerValueTime + 1000;
            } else {
                // Not a single entity tag and not a valid date either
                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
                return false;
            }
        }

    }

    /**
     * Checks if range request is supported by server
     *
     * @return <code>true</code> server supports range requests feature.
     */
    protected boolean isRangeRequestsSupported() {
        // Range-Requests optional feature is enabled implicitly.
        return true;
    }

    /**
     * Provides the entity tag (the ETag header) for the given resource. Intended to be over-ridden by custom
     * DefaultServlet implementations that wish to use an alternative format for the entity tag.
     *
     * @param resource The resource for which an entity tag is required.
     *
     * @return The result of calling {@link WebResource#getETag()} on the given resource
     */
    protected String generateETag(WebResource resource) {
        if (useStrongETags) {
            return resource.getStrongETag();
        } else {
            return resource.getETag();
        }
    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param is      The input stream to read the source resource from
     * @param ostream The output stream to write to
     *
     * @exception IOException if an input/output error occurs
     */
    protected void copy(InputStream is, ServletOutputStream ostream) throws IOException {

        InputStream istream = new BufferedInputStream(is, input);

        // Copy the input stream to the output stream
        IOException exception = copyRange(istream, ostream);

        // Clean up the input stream
        istream.close();

        // Rethrow any exception that has occurred
        if (exception != null) {
            throw exception;
        }
    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param is       The input stream to read the source resource from
     * @param writer   The writer to write to
     * @param encoding The encoding to use when reading the source input stream
     *
     * @exception IOException if an input/output error occurs
     */
    protected void copy(InputStream is, PrintWriter writer, String encoding) throws IOException {

        Reader reader;
        if (encoding == null) {
            reader = new InputStreamReader(is);
        } else {
            reader = new InputStreamReader(is, encoding);
        }

        // Copy the input stream to the output stream
        IOException exception = copyRange(reader, writer);

        // Clean up the reader
        reader.close();

        // Rethrow any exception that has occurred
        if (exception != null) {
            throw exception;
        }
    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param resource The source resource
     * @param length   the resource length
     * @param ostream  The output stream to write to
     * @param range    Range the client wanted to retrieve
     *
     * @exception IOException if an input/output error occurs
     */
    protected void copy(WebResource resource, long length, ServletOutputStream ostream, Ranges.Entry range)
            throws IOException {

        InputStream resourceInputStream = resource.getInputStream();
        InputStream istream = new BufferedInputStream(resourceInputStream, input);
        IOException exception = copyRange(istream, ostream, getStart(range, length), getEnd(range, length));

        // Clean up the input stream
        istream.close();

        // Rethrow any exception that has occurred
        if (exception != null) {
            throw exception;
        }

    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param resource    The source resource
     * @param length      the resource length
     * @param ostream     The output stream to write to
     * @param ranges      Enumeration of the ranges the client wanted to retrieve
     * @param contentType Content type of the resource
     *
     * @exception IOException if an input/output error occurs
     */
    protected void copy(WebResource resource, long length, ServletOutputStream ostream, Ranges ranges,
            String contentType) throws IOException {

        IOException exception = null;

        for (Ranges.Entry range : ranges.getEntries()) {
            if (exception != null) {
                break;
            }
            InputStream resourceInputStream = resource.getInputStream();
            try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) {

                // Writing MIME header.
                ostream.println();
                ostream.println("--" + mimeSeparation);
                if (contentType != null) {
                    ostream.println("Content-Type: " + contentType);
                }
                long start = getStart(range, length);
                long end = getEnd(range, length);
                ostream.println("Content-Range: bytes " + start + "-" + end + "/" + length);
                ostream.println();

                // Printing content
                exception = copyRange(istream, ostream, start, end);
            }
        }

        ostream.println();
        ostream.print("--" + mimeSeparation + "--");

        // Rethrow any exception that has occurred
        if (exception != null) {
            throw exception;
        }

    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param istream The input stream to read from
     * @param ostream The output stream to write to
     *
     * @return Exception which occurred during processing
     */
    protected IOException copyRange(InputStream istream, ServletOutputStream ostream) {

        // Copy the input stream to the output stream
        IOException exception = null;
        byte[] buffer = new byte[input];
        while (true) {
            try {
                int len = istream.read(buffer);
                if (len == -1) {
                    break;
                }
                ostream.write(buffer, 0, len);
            } catch (IOException ioe) {
                exception = ioe;
                break;
            }
        }
        return exception;

    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param reader The reader to read from
     * @param writer The writer to write to
     *
     * @return Exception which occurred during processing
     */
    protected IOException copyRange(Reader reader, PrintWriter writer) {

        // Copy the input stream to the output stream
        IOException exception = null;
        char[] buffer = new char[input];
        while (true) {
            try {
                int len = reader.read(buffer);
                if (len == -1) {
                    break;
                }
                writer.write(buffer, 0, len);
            } catch (IOException ioe) {
                exception = ioe;
                break;
            }
        }
        return exception;

    }


    /**
     * Copy the contents of the specified input stream to the specified output stream, and ensure that both streams are
     * closed before returning (even in the face of an exception).
     *
     * @param istream The input stream to read from
     * @param ostream The output stream to write to
     * @param start   Start of the range which will be copied
     * @param end     End of the range which will be copied
     *
     * @return Exception which occurred during processing
     */
    protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) {

        if (debug > 10) {
            log("Serving bytes: " + start + "-" + end);
        }

        long skipped;
        try {
            skipped = istream.skip(start);
        } catch (IOException ioe) {
            return ioe;
        }
        if (skipped < start) {
            return new IOException(sm.getString("defaultServlet.skipfail", Long.valueOf(skipped), Long.valueOf(start)));
        }

        IOException exception = null;
        long bytesToRead = end - start + 1;

        byte[] buffer = new byte[input];
        int len = buffer.length;
        while ((bytesToRead > 0) && (len >= buffer.length)) {
            try {
                len = istream.read(buffer);
                if (bytesToRead >= len) {
                    ostream.write(buffer, 0, len);
                    bytesToRead -= len;
                } else {
                    ostream.write(buffer, 0, (int) bytesToRead);
                    bytesToRead = 0;
                }
            } catch (IOException ioe) {
                exception = ioe;
                len = -1;
            }
        }

        return exception;

    }


    protected record CompressionFormat(String extension, String encoding) implements Serializable {
        @Serial
        private static final long serialVersionUID = 1L;
    }


    private record PrecompressedResource(WebResource resource, CompressionFormat format) {
    }


    /**
     * Gets the ordering character to be used for a particular column.
     *
     * @param order  The order that is currently being applied
     * @param column The column that will be rendered.
     *
     * @return Either 'A' or 'D', to indicate "ascending" or "descending" sort order.
     */
    private char getOrderChar(SortManager.Order order, char column) {
        if (column == order.column) {
            if (order.ascending) {
                return 'D';
            } else {
                return 'A';
            }
        } else {
            return 'D';
        }
    }


    /**
     * A class encapsulating the sorting of resources.
     */
    protected static class SortManager {
        /**
         * The default sort.
         */
        protected Comparator<WebResource> defaultResourceComparator;

        /**
         * Comparator to use when sorting resources by name.
         */
        protected Comparator<WebResource> resourceNameComparator;

        /**
         * Comparator to use when sorting files by name, ascending (reverse).
         */
        protected Comparator<WebResource> resourceNameComparatorAsc;

        /**
         * Comparator to use when sorting resources by size.
         */
        protected Comparator<WebResource> resourceSizeComparator;

        /**
         * Comparator to use when sorting files by size, ascending (reverse).
         */
        protected Comparator<WebResource> resourceSizeComparatorAsc;

        /**
         * Comparator to use when sorting resources by last-modified date.
         */
        protected Comparator<WebResource> resourceLastModifiedComparator;

        /**
         * Comparator to use when sorting files by last-modified date, ascending (reverse).
         */
        protected Comparator<WebResource> resourceLastModifiedComparatorAsc;

        SortManager(boolean directoriesFirst) {
            resourceNameComparator = Comparator.comparing(WebResource::getName);
            resourceNameComparatorAsc = resourceNameComparator.reversed();
            resourceSizeComparator =
                    Comparator.comparing(WebResource::getContentLength).thenComparing(resourceNameComparator);
            resourceSizeComparatorAsc = resourceSizeComparator.reversed();
            resourceLastModifiedComparator =
                    Comparator.comparing(WebResource::getLastModified).thenComparing(resourceNameComparator);
            resourceLastModifiedComparatorAsc = resourceLastModifiedComparator.reversed();

            if (directoriesFirst) {
                Comparator<WebResource> dirsFirst = comparingTrueFirst(WebResource::isDirectory);
                resourceNameComparator = dirsFirst.thenComparing(resourceNameComparator);
                resourceNameComparatorAsc = dirsFirst.thenComparing(resourceNameComparatorAsc);
                resourceSizeComparator = dirsFirst.thenComparing(resourceSizeComparator);
                resourceSizeComparatorAsc = dirsFirst.thenComparing(resourceSizeComparatorAsc);
                resourceLastModifiedComparator = dirsFirst.thenComparing(resourceLastModifiedComparator);
                resourceLastModifiedComparatorAsc = dirsFirst.thenComparing(resourceLastModifiedComparatorAsc);
            }

            defaultResourceComparator = resourceNameComparator;
        }

        /**
         * Sorts an array of resources according to an ordering string.
         *
         * @param resources The array to sort.
         * @param order     The ordering string.
         *
         * @see #getOrder(String)
         */
        public void sort(WebResource[] resources, String order) {
            Comparator<WebResource> comparator = getComparator(order);

            if (null != comparator) {
                Arrays.sort(resources, comparator);
            }
        }

        public Comparator<WebResource> getComparator(String order) {
            return getComparator(getOrder(order));
        }

        public Comparator<WebResource> getComparator(Order order) {
            if (null == order) {
                return defaultResourceComparator;
            }

            if ('N' == order.column) {
                if (order.ascending) {
                    return resourceNameComparatorAsc;
                } else {
                    return resourceNameComparator;
                }
            }

            if ('S' == order.column) {
                if (order.ascending) {
                    return resourceSizeComparatorAsc;
                } else {
                    return resourceSizeComparator;
                }
            }

            if ('M' == order.column) {
                if (order.ascending) {
                    return resourceLastModifiedComparatorAsc;
                } else {
                    return resourceLastModifiedComparator;
                }
            }

            return defaultResourceComparator;
        }

        /**
         * Gets the Order to apply given an ordering-string. This ordering-string matches a subset of the
         * ordering-strings supported by <a href="https://httpd.apache.org/docs/2.4/mod/mod_autoindex.html#query">Apache
         * httpd</a>.
         *
         * @param order The ordering-string provided by the client.
         *
         * @return An Order specifying the column and ascending/descending to be applied to resources.
         */
        public Order getOrder(String order) {
            if (null == order || order.trim().isEmpty()) {
                return Order.DEFAULT;
            }

            String[] options = order.split(";");

            if (0 == options.length) {
                return Order.DEFAULT;
            }

            char column = '\0';
            boolean ascending = false;

            for (String option : options) {
                option = option.trim();

                if (2 < option.length()) {
                    char opt = option.charAt(0);
                    if ('C' == opt) {
                        column = option.charAt(2);
                    } else if ('O' == opt) {
                        ascending = ('A' == option.charAt(2));
                    }
                }
            }

            if ('N' == column) {
                if (ascending) {
                    return Order.NAME_ASC;
                } else {
                    return Order.NAME;
                }
            }

            if ('S' == column) {
                if (ascending) {
                    return Order.SIZE_ASC;
                } else {
                    return Order.SIZE;
                }
            }

            if ('M' == column) {
                if (ascending) {
                    return Order.LAST_MODIFIED_ASC;
                } else {
                    return Order.LAST_MODIFIED;
                }
            }

            return Order.DEFAULT;
        }

        public static class Order {
            final char column;
            final boolean ascending;

            Order(char column, boolean ascending) {
                this.column = column;
                this.ascending = ascending;
            }

            public static final Order NAME = new Order('N', false);
            public static final Order NAME_ASC = new Order('N', true);
            public static final Order SIZE = new Order('S', false);
            public static final Order SIZE_ASC = new Order('S', true);
            public static final Order LAST_MODIFIED = new Order('M', false);
            public static final Order LAST_MODIFIED_ASC = new Order('M', true);

            public static final Order DEFAULT = NAME;
        }
    }


    private static Comparator<WebResource> comparingTrueFirst(Function<WebResource,Boolean> keyExtractor) {
        return (s1, s2) -> {
            Boolean r1 = keyExtractor.apply(s1);
            Boolean r2 = keyExtractor.apply(s2);
            if (r1.booleanValue()) {
                if (r2.booleanValue()) {
                    return 0;
                } else {
                    return -1; // r1 (property is true) first
                }
            } else if (r2.booleanValue()) {
                return 1; // r2 (property is true) first
            } else {
                return 0;
            }
        };
    }


    enum BomConfig {
        /**
         * BoM is stripped if present and any BoM found used to determine the encoding used to read the resource.
         */
        TRUE("true", true, true),
        /**
         * BoM is stripped if present but the configured file encoding is used to read the resource.
         */
        FALSE("false", true, false),
        /**
         * BoM is not stripped and the configured file encoding is used to read the resource.
         */
        PASS_THROUGH("pass-through", false, false);

        final String configurationValue;
        final boolean stripBom;
        final boolean useBomEncoding;

        BomConfig(String configurationValue, boolean stripBom, boolean useBomEncoding) {
            this.configurationValue = configurationValue;
            this.stripBom = stripBom;
            this.useBomEncoding = useBomEncoding;
        }
    }
}
