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

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Serial;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;

import org.apache.catalina.Container;
import org.apache.catalina.ContainerEvent;
import org.apache.catalina.ContainerListener;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Manager;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Session;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.session.ManagerBase;
import org.apache.catalina.session.StandardManager;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.valves.AccessLogValve;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.coyote.http11.Http11NioProtocol;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap;
import org.apache.tomcat.util.http.Method;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.scan.StandardJarScanFilter;
import org.apache.tomcat.util.scan.StandardJarScanner;

/**
 * Base test case that provides a Tomcat instance for each test - mainly so we
 * don't have to keep writing the cleanup code.
 */
public abstract class TomcatBaseTest extends LoggingBaseTest {

    // Used by parameterized tests. Defined here to reduce duplication.
    protected static final Boolean[] booleans = new Boolean[] { Boolean.FALSE, Boolean.TRUE };

    protected static final int DEFAULT_CLIENT_TIMEOUT_MS = 300_000;

    public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");

    private Tomcat tomcat;
    private boolean accessLogEnabled = false;

    /**
     * Make the Tomcat instance available to sub-classes.
     *
     * @return A Tomcat instance without any pre-configured web applications
     */
    public Tomcat getTomcatInstance() {
        return tomcat;
    }

    /**
     * Make the Tomcat instance preconfigured with test/webapp available to
     * sub-classes.
     * @param addJstl Should JSTL support be added to the test webapp
     * @param start   Should the Tomcat instance be started
     *
     * @return A Tomcat instance pre-configured with the web application located
     *         at test/webapp
     *
     * @throws LifecycleException If a problem occurs while starting the
     *                            instance
     */
    public Tomcat getTomcatInstanceTestWebapp(boolean addJstl, boolean start)
            throws LifecycleException {
        File appDir = new File("test/webapp");
        Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath());

        StandardJarScanner scanner = (StandardJarScanner) ctx.getJarScanner();
        StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter();
        filter.setTldSkip(filter.getTldSkip() + ",testclasses");
        filter.setPluggabilitySkip(filter.getPluggabilitySkip() + ",testclasses");

        if (addJstl) {
            File lib = new File("webapps/examples/WEB-INF/lib");
            ctx.setResources(new StandardRoot(ctx));
            ctx.getResources().createWebResourceSet(
                    WebResourceRoot.ResourceSetType.POST, "/WEB-INF/lib",
                    lib.getAbsolutePath(), null, "/");
        }

        if (start) {
            tomcat.start();
        }
        return tomcat;
    }


    public Context getProgrammaticRootContext() {
        // No file system docBase required
        Context ctx = tomcat.addContext("", null);
        // Disable class path scanning - it slows the tests down by almost an order of magnitude
        ((StandardJarScanner) ctx.getJarScanner()).setScanClassPath(false);
        return ctx;
    }
    public Context getProgrammaticRootContextWithManager() {
        Context ctx = getProgrammaticRootContext();
        if (ctx.getManager() == null) {
            ctx.setManager(new StandardManager());
        }
        return ctx;
    }
    /*
     * Sub-classes need to know port so they can connect
     */
    public int getPort() {
        return tomcat.getConnector().getLocalPort();
    }

    /*
     * Sub-classes may want to check, whether an AccessLogValve is active
     */
    public boolean isAccessLogEnabled() {
        return accessLogEnabled;
    }

    @Before
    @Override
    public void setUp() throws Exception {
        super.setUp();

        // Trigger loading of catalina.properties
        CatalinaProperties.getProperty("foo");

        File appBase = new File(getTemporaryDirectory(), "webapps");
        if (!appBase.exists() && !appBase.mkdir()) {
            Assert.fail("Unable to create appBase for test");
        }

        tomcat = new TomcatWithFastSessionIDs();

        String protocol = getProtocol();
        Connector connector = new Connector(protocol);
        // Listen only on localhost
        Assert.assertTrue(connector.setProperty("address", InetAddress.getByName("localhost").getHostAddress()));
        // Use random free port
        connector.setPort(0);
        // By default, a connector failure means a failed test
        connector.setThrowOnFailure(true);
        // Mainly set to reduce timeouts during async tests
        Assert.assertTrue(connector.setProperty("connectionTimeout", "3000"));
        tomcat.getService().addConnector(connector);
        tomcat.setConnector(connector);

        File catalinaBase = getTemporaryDirectory();
        tomcat.setBaseDir(catalinaBase.getAbsolutePath());
        tomcat.getHost().setAppBase(appBase.getAbsolutePath());

        accessLogEnabled = Boolean.getBoolean("tomcat.test.accesslog");
        if (accessLogEnabled) {
            String accessLogDirectory = System
                    .getProperty("tomcat.test.reports");
            if (accessLogDirectory == null) {
                accessLogDirectory = new File(getBuildDirectory(), "logs")
                        .toString();
            }
            AccessLogValve alv = new AccessLogValve();
            alv.setDirectory(accessLogDirectory);
            alv.setPattern("%h %l %u %t \"%r\" %s %b %I %D");
            tomcat.getHost().getPipeline().addValve(alv);
        }

        // Cannot delete the whole tempDir, because logs are there,
        // but delete known subdirectories of it.
        addDeleteOnTearDown(new File(catalinaBase, "webapps"));
        addDeleteOnTearDown(new File(catalinaBase, "work"));
    }

    protected String getProtocol() {
        // Has a protocol been specified
        String protocol = System.getProperty("tomcat.test.protocol");

        // Use NIO by default starting with Tomcat 8
        if (protocol == null) {
            protocol = Http11NioProtocol.class.getName();
        }

        return protocol;
    }

    @After
    @Override
    public void tearDown() throws Exception {
        try {
            // Some tests may call tomcat.destroy(), some tests may just call
            // tomcat.stop(), some not call either method. Make sure that stop()
            // & destroy() are called as necessary.
            if (tomcat.server != null
                    && tomcat.server.getState() != LifecycleState.DESTROYED) {
                if (tomcat.server.getState() != LifecycleState.STOPPED) {
                    tomcat.stop();
                }
                tomcat.destroy();
            }
        } finally {
            super.tearDown();
        }
    }

    /**
     * Simple Hello World servlet for use by test cases
     */
    public static final class HelloWorldServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        public static final String RESPONSE_TEXT =
            "<html><body><p>Hello World</p></body></html>";

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            PrintWriter out = resp.getWriter();
            out.print(RESPONSE_TEXT);
        }
    }


    public static final class RequestDescriptor {

        private final Map<String, String> requestInfo = new HashMap<>();
        private final Map<String, String> contextInitParameters = new HashMap<>();
        private final Map<String, String> contextAttributes = new HashMap<>();
        private final Map<String, String> headers = new CaseInsensitiveKeyMap<>();
        private final Map<String, String> attributes = new HashMap<>();
        private final Map<String, String> params = new HashMap<>();
        private final Map<String, String> sessionAttributes = new HashMap<>();

        public Map<String, String> getRequestInfo() {
            return requestInfo;
        }

        public Map<String, String> getContextInitParameters() {
            return contextInitParameters;
        }

        public Map<String, String> getContextAttributes() {
            return contextAttributes;
        }

        public Map<String, String> getHeaders() {
            return headers;
        }

        public Map<String, String> getAttributes() {
            return attributes;
        }

        public Map<String, String> getParams() {
            return params;
        }

        public Map<String, String> getSessionAttributes() {
            return sessionAttributes;
        }

        public String getRequestInfo(String name) {
            return requestInfo.get(name);
        }

        public void putRequestInfo(String name, String value) {
            requestInfo.put(name, value);
        }

        public String getContextInitParameter(String name) {
            return contextInitParameters.get(name);
        }

        public void putContextInitParameter(String name, String value) {
            contextInitParameters.put(name, value);
        }

        public String getContextAttribute(String name) {
            return contextAttributes.get(name);
        }

        public void putContextAttribute(String name, String value) {
            contextAttributes.put(name, value);
        }

        public String getHeader(String name) {
            return headers.get(name);
        }

        public void putHeader(String name, String value) {
            headers.put(name, value);
        }

        public String getAttribute(String name) {
            return attributes.get(name);
        }

        public void putAttribute(String name, String value) {
            attributes.put(name, value);
        }

        public String getParam(String name) {
            return params.get(name);
        }

        public void putParam(String name, String value) {
            params.put(name, value);
        }

        public String getSessionAttribute(String name) {
            return sessionAttributes.get(name);
        }

        public void putSessionAttribute(String name, String value) {
            sessionAttributes.put(name, value);
        }

        public void compare (RequestDescriptor request) {
            Map<String, String> base;
            Map<String, String> cmp;
            base = request.getRequestInfo();
            cmp = this.getRequestInfo();
            for (String name: base.keySet()) {
                Assert.assertEquals("Request info " + name, base.get(name), cmp.get(name));
            }
            base = request.getContextInitParameters();
            cmp = this.getContextInitParameters();
            for (String name: base.keySet()) {
                Assert.assertEquals("Context parameter " + name, base.get(name), cmp.get(name));
            }
            base = request.getContextAttributes();
            cmp = this.getContextAttributes();
            for (String name: base.keySet()) {
                Assert.assertEquals("Context attribute " + name, base.get(name), cmp.get(name));
            }
            base = request.getHeaders();
            cmp = this.getHeaders();
            for (String name: base.keySet()) {
                Assert.assertEquals("Header " + name, base.get(name), cmp.get(name));
            }
            base = request.getAttributes();
            cmp = this.getAttributes();
            for (String name: base.keySet()) {
                Assert.assertEquals("Attribute " + name, base.get(name), cmp.get(name));
            }
            base = request.getParams();
            cmp = this.getParams();
            for (String name: base.keySet()) {
                Assert.assertEquals("Param " + name, base.get(name), cmp.get(name));
            }
            base = request.getSessionAttributes();
            cmp = this.getSessionAttributes();
            for (String name: base.keySet()) {
                Assert.assertEquals("Session attribute " + name, base.get(name), cmp.get(name));
            }
        }
    }


    public static final class SnoopResult {

        public static RequestDescriptor parse(String body) {

            int n;
            int m;
            String key;
            String value;
            String name;

            RequestDescriptor request = new RequestDescriptor();

            for (String line: body.split(System.lineSeparator())) {
                n = line.indexOf(": ");
                if (n > 0) {
                    key = line.substring(0, n);
                    value = line.substring(n + 2);
                    m = key.indexOf(':');
                    if (m > 0) {
                        name = key.substring(m + 1);
                        key = key.substring(0, m);
                        if (key.equals("CONTEXT-PARAM")) {
                            request.putContextInitParameter(name, value);
                        } else if (key.equals("CONTEXT-ATTRIBUTE")) {
                            request.putContextAttribute(name, value);
                        } else if (key.equals("HEADER")) {
                            request.putHeader(name, value);
                        } else if (key.equals("ATTRIBUTE")) {
                            request.putAttribute(name, value);
                        } else if (key.equals("PARAM")) {
                            request.putParam(name, value);
                        } else if (key.equals("SESSION-ATTRIBUTE")) {
                            request.putSessionAttribute(name, value);
                        } else {
                            request.putRequestInfo(key + ":" + name, value);
                        }
                    } else {
                        request.putRequestInfo(key, value);
                    }
                }
            }

            return request;
        }
    }

    /**
     * Simple servlet that dumps request information. Tests using this should
     * note that additional information may be added to in the future and should
     * therefore test return values using SnoopResult.
     */
    public static final class SnoopServlet extends HttpServlet {

        @Serial
        private static final long serialVersionUID = 1L;

        @Override
        public void service(HttpServletRequest request,
                            HttpServletResponse response)
                throws ServletException, IOException {

            String name;
            StringBuilder value;
            Object attribute;

            response.setContentType("text/plain");
            response.setCharacterEncoding("UTF-8");

            ServletContext ctx = this.getServletContext();
            HttpSession session = request.getSession("true".equals(request.getParameter("createSession")));
            PrintWriter out = response.getWriter();

            out.println("CONTEXT-NAME: " + ctx.getServletContextName());
            out.println("CONTEXT-PATH: " + ctx.getContextPath());
            out.println("CONTEXT-MAJOR-VERSION: " + ctx.getMajorVersion());
            out.println("CONTEXT-MINOR-VERSION: " + ctx.getMinorVersion());
            out.println("CONTEXT-SERVER-INFO: " + ctx.getServerInfo());
            for (Enumeration<String> e = ctx.getInitParameterNames();
                 e.hasMoreElements();) {
                name = e.nextElement();
                out.println("CONTEXT-INIT-PARAM:" + name + ": " +
                            ctx.getInitParameter(name));
            }
            for (Enumeration<String> e = ctx.getAttributeNames();
                 e.hasMoreElements();) {
                name = e.nextElement();
                out.println("CONTEXT-ATTRIBUTE:" + name + ": " +
                            ctx.getAttribute(name));
            }
            out.println("REQUEST-CONTEXT-PATH: " + request.getContextPath());
            out.println("REQUEST-SERVER-NAME: " + request.getServerName());
            out.println("REQUEST-SERVER-PORT: " + request.getServerPort());
            out.println("REQUEST-LOCAL-NAME: " + request.getLocalName());
            out.println("REQUEST-LOCAL-ADDR: " + request.getLocalAddr());
            out.println("REQUEST-LOCAL-PORT: " + request.getLocalPort());
            out.println("REQUEST-REMOTE-HOST: " + request.getRemoteHost());
            out.println("REQUEST-REMOTE-ADDR: " + request.getRemoteAddr());
            out.println("REQUEST-REMOTE-PORT: " + request.getRemotePort());
            out.println("REQUEST-PROTOCOL: " + request.getProtocol());
            out.println("REQUEST-SCHEME: " + request.getScheme());
            out.println("REQUEST-IS-SECURE: " + request.isSecure());
            out.println("REQUEST-URI: " + request.getRequestURI());
            out.println("REQUEST-URL: " + request.getRequestURL());
            out.println("REQUEST-SERVLET-PATH: " + request.getServletPath());
            out.println("REQUEST-METHOD: " + request.getMethod());
            out.println("REQUEST-PATH-INFO: " + request.getPathInfo());
            out.println("REQUEST-PATH-TRANSLATED: " +
                        request.getPathTranslated());
            out.println("REQUEST-QUERY-STRING: " + request.getQueryString());
            out.println("REQUEST-REMOTE-USER: " + request.getRemoteUser());
            out.println("REQUEST-AUTH-TYPE: " + request.getAuthType());
            out.println("REQUEST-USER-PRINCIPAL: " +
                        request.getUserPrincipal());
            out.println("REQUEST-CHARACTER-ENCODING: " +
                        request.getCharacterEncoding());
            out.println("REQUEST-CONTENT-LENGTH: " +
                        request.getContentLengthLong());
            out.println("REQUEST-CONTENT-TYPE: " + request.getContentType());
            out.println("REQUEST-LOCALE: " + request.getLocale());

            for (Enumeration<String> e = request.getHeaderNames();
                 e.hasMoreElements();) {
                name = e.nextElement();
                value = new StringBuilder();
                for (Enumeration<String> h = request.getHeaders(name);
                     h.hasMoreElements();) {
                    value.append(h.nextElement());
                    if (h.hasMoreElements()) {
                        value.append(';');
                    }
                }
                out.println("HEADER:" + name + ": " + value);
            }

            for (Enumeration<String> e = request.getAttributeNames();
                 e.hasMoreElements();) {
                name = e.nextElement();
                attribute = request.getAttribute(name);
                out.println("ATTRIBUTE:" + name + ": " +
                            (attribute != null ? attribute : "(null)"));
            }

            for (Enumeration<String> e = request.getParameterNames();
                 e.hasMoreElements();) {
                name = e.nextElement();
                value = new StringBuilder();
                String[] values = request.getParameterValues(name);
                int m = values.length;
                for (int j = 0; j < m; j++) {
                    value.append(values[j]);
                    if (j < m - 1) {
                        value.append(';');
                    }
                }
                out.println("PARAM:" + name + ": " + value);
            }

            out.println("SESSION-REQUESTED-ID: " +
                        request.getRequestedSessionId());
            out.println("SESSION-REQUESTED-ID-COOKIE: " +
                        request.isRequestedSessionIdFromCookie());
            out.println("SESSION-REQUESTED-ID-URL: " +
                        request.isRequestedSessionIdFromURL());
            out.println("SESSION-REQUESTED-ID-VALID: " +
                        request.isRequestedSessionIdValid());

            if (session != null) {
                out.println("SESSION-ID: " + session.getId());
                out.println("SESSION-CREATION-TIME: " +
                        session.getCreationTime());
                out.println("SESSION-LAST-ACCESSED-TIME: " +
                        session.getLastAccessedTime());
                out.println("SESSION-MAX-INACTIVE-INTERVAL: " +
                        session.getMaxInactiveInterval());
                out.println("SESSION-IS-NEW: " + session.isNew());
                for (Enumeration<String> e = session.getAttributeNames();
                     e.hasMoreElements();) {
                    name = e.nextElement();
                    attribute = session.getAttribute(name);
                    out.println("SESSION-ATTRIBUTE:" + name + ": " +
                                (attribute != null ? attribute : "(null)"));
                }
            }

            int bodySize = 0;
            if (Method.PUT.equals(request.getMethod()) || Method.POST.equals(request.getMethod())) {
                InputStream is = request.getInputStream();
                int read = 0;
                byte[] buffer = new byte[8192];
                while (read != -1) {
                    read = is.read(buffer);
                    if (read > -1) {
                        bodySize += read;
                    }
                }
            }
            out.println("REQUEST-BODY-SIZE: " + bodySize);
        }
    }


    /**
     * Servlet that simply echos the request body back as the response body.
     */
    public static class EchoBodyServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            // NO-OP - No body to echo
        }

        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            // Beware of clients that try to send the whole request body before
            // reading any of the response. They may cause this test to lock up.
            try (InputStream is = req.getInputStream();
                    OutputStream os = resp.getOutputStream()) {
                IOTools.flow(is, os);
            }
        }
    }


    /*
     *  Wrapper for getting the response.
     */
    public static ByteChunk getUrl(String path) throws IOException {
        ByteChunk out = new ByteChunk();
        getUrl(path, out, null);
        return out;
    }

    public static int getUrl(String path, ByteChunk out, Map<String, List<String>> resHead)
            throws IOException {
        return getUrl(path, out, null, resHead);
    }

    public static int getUrl(String path, ByteChunk out, boolean followRedirects)
            throws IOException {
        return methodUrl(path, out, DEFAULT_CLIENT_TIMEOUT_MS, null, null, Method.GET, followRedirects);
    }

    public static int headUrl(String path, ByteChunk out, Map<String, List<String>> resHead)
            throws IOException {
        return methodUrl(path, out, DEFAULT_CLIENT_TIMEOUT_MS, null, resHead, Method.HEAD);
    }

    public static int getUrl(String path, ByteChunk out, Map<String, List<String>> reqHead,
            Map<String, List<String>> resHead) throws IOException {
        return getUrl(path, out, DEFAULT_CLIENT_TIMEOUT_MS, reqHead, resHead);
    }

    public static int getUrl(String path, ByteChunk out, int readTimeout,
            Map<String, List<String>> reqHead, Map<String, List<String>> resHead)
            throws IOException {
        return methodUrl(path, out, readTimeout, reqHead, resHead, Method.GET);
    }

    public static int methodUrl(String path, ByteChunk out, int readTimeout,
            Map<String, List<String>> reqHead, Map<String, List<String>> resHead, String method)
            throws IOException {
        return methodUrl(path, out, readTimeout, reqHead, resHead, method, true);
    }

    public static int methodUrl(String path, ByteChunk out, int readTimeout,
                Map<String, List<String>> reqHead, Map<String, List<String>> resHead, String method,
                boolean followRedirects) throws IOException {

        URL url = URI.create(path).toURL();
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setUseCaches(false);
        connection.setReadTimeout(readTimeout);
        connection.setRequestMethod(method);
        connection.setInstanceFollowRedirects(followRedirects);
        if (reqHead != null) {
            for (Map.Entry<String, List<String>> entry : reqHead.entrySet()) {
                StringBuilder valueList = new StringBuilder();
                for (String value : entry.getValue()) {
                    if (valueList.length() > 0) {
                        valueList.append(',');
                    }
                    valueList.append(value);
                }
                connection.setRequestProperty(entry.getKey(),
                        valueList.toString());
            }
        }
        connection.connect();
        int rc = connection.getResponseCode();
        if (resHead != null) {
            // Skip the entry with null key that is used for the response line
            // that some Map implementations may not accept.
            for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
                if (entry.getKey() != null) {
                    resHead.put(entry.getKey(), entry.getValue());
                }
            }
        }
        InputStream is;
        if (rc < 400) {
            is = connection.getInputStream();
        } else {
            is = connection.getErrorStream();
        }
        if (is != null) {
            try (BufferedInputStream bis = new BufferedInputStream(is)) {
                byte[] buf = new byte[2048];
                int rd = 0;
                while((rd = bis.read(buf)) > 0) {
                    out.append(buf, 0, rd);
                }
            }
        }
        return rc;
    }

    public static ByteChunk postUrl(byte[] body, String path)
            throws IOException {
        ByteChunk out = new ByteChunk();
        postUrl(body, path, out, null);
        return out;
    }

    public static int postUrl(byte[] body, String path, ByteChunk out,
            Map<String, List<String>> resHead) throws IOException {
        return postUrl(body, path, out, null, resHead);
    }

    public static int postUrl(final byte[] body, String path, ByteChunk out,
            Map<String, List<String>> reqHead,
            Map<String, List<String>> resHead) throws IOException {
            BytesStreamer s = new BytesStreamer() {
            boolean done = false;
            @Override
            public byte[] next() {
                done = true;
                return body;

            }

            @Override
            public int getLength() {
                return body!=null?body.length:0;
            }

            @Override
            public int available() {
                if (done) {
                  return 0;
                } else {
                  return getLength();
                }
            }
        };
        return postUrl(false,s,path,out,reqHead,resHead);
    }


    public static int postUrl(boolean stream, BytesStreamer streamer, String path, ByteChunk out,
                Map<String, List<String>> reqHead,
                Map<String, List<String>> resHead) throws IOException {

        URL url = URI.create(path).toURL();
        HttpURLConnection connection =
            (HttpURLConnection) url.openConnection();
        connection.setDoOutput(true);
        connection.setReadTimeout(1000000);
        if (reqHead != null) {
            for (Map.Entry<String, List<String>> entry : reqHead.entrySet()) {
                StringBuilder valueList = new StringBuilder();
                for (String value : entry.getValue()) {
                    if (valueList.length() > 0) {
                        valueList.append(',');
                    }
                    valueList.append(value);
                }
                connection.setRequestProperty(entry.getKey(),
                        valueList.toString());
            }
        }
        if (streamer != null && stream) {
            if (streamer.getLength()>0) {
                connection.setFixedLengthStreamingMode(streamer.getLength());
            } else {
                connection.setChunkedStreamingMode(1024);
            }
        }

        connection.connect();

        // Write the request body
        try (OutputStream os = connection.getOutputStream()) {
            while (streamer != null && streamer.available() > 0) {
                byte[] next = streamer.next();
                os.write(next);
                os.flush();
            }
        }

        int rc = connection.getResponseCode();
        if (resHead != null) {
            Map<String, List<String>> head = connection.getHeaderFields();
            resHead.putAll(head);
        }
        InputStream is;
        if (rc < 400) {
            is = connection.getInputStream();
        } else {
            is = connection.getErrorStream();
        }

        try (BufferedInputStream bis = new BufferedInputStream(is)) {
            byte[] buf = new byte[2048];
            int rd = 0;
            while((rd = bis.read(buf)) > 0) {
                out.append(buf, 0, rd);
            }
        }
        return rc;
    }

    protected static String getStatusCode(String statusLine) {
        if (statusLine == null || statusLine.length() < 12) {
            return statusLine;
        } else {
            return statusLine.substring(9, 12);
        }
    }

    protected static String getSingleHeader(String header, Map<String,List<String>> headers) {
        // Assume headers is never null

        // Assume that either:
        // a) is correct since HTTP headers are case insensitive but most Map
        //    implementations are case-sensitive; or
        // b) CaseInsensitiveKeyMap or similar is used
        List<String> headerValues = headers.get(header);

        // Looking for a single header. No matches are OK
        if (headerValues == null) {
            return null;
        }

        // Found a single header - return the header value
        if (headerValues.size() == 1) {
            return headerValues.get(0);
        }

        // More than one header value is an error
        throw new IllegalStateException("Found multiple headers for [" + header + "]");
    }

    private static class TomcatWithFastSessionIDs extends Tomcat {

        @Override
        public void start() throws LifecycleException {
            // Use fast, insecure session ID generation for all tests
            Server server = getServer();
            for (Service service : server.findServices()) {
                Container e = service.getContainer();
                for (Container h : e.findChildren()) {
                    for (Container c : h.findChildren()) {
                        Manager m = ((Context) c).getManager();
                        if (m == null) {
                            m = new StandardManager();
                            ((Context) c).setManager(m);
                        }
                        if (m instanceof ManagerBase) {
                            ((ManagerBase) m).setSecureRandomClass(
                                    "org.apache.catalina.startup.FastNonSecureRandom");
                        }
                    }
                }
            }
            super.start();
        }
    }


    public static void recursiveCopy(final Path src, final Path dest)
            throws IOException {

        Files.walkFileTree(src, new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir,
                    BasicFileAttributes attrs) throws IOException {
                Files.copy(dir, dest.resolve(src.relativize(dir)));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file,
                    BasicFileAttributes attrs) throws IOException {
                Path destPath = dest.resolve(src.relativize(file));
                Files.copy(file, destPath);
                // Make sure that HostConfig thinks all newly copied files have
                // been modified.
                Assert.assertTrue("Failed to set last modified for [" + destPath + "]",
                        destPath.toFile().setLastModified(
                        System.currentTimeMillis() - 2 * HostConfig.FILE_MODIFICATION_RESOLUTION_MS));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException ioe)
                    throws IOException {
                throw ioe;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException ioe)
                    throws IOException {
                // NO-OP
                return FileVisitResult.CONTINUE;
            }});
    }


    public static void skipTldsForResourceJars(Context context) {
        StandardJarScanner scanner = (StandardJarScanner) context.getJarScanner();
        StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter();
        filter.setTldSkip(filter.getTldSkip() + ",resources*.jar");
    }


    public static void forceSessionMaxInactiveInterval(Context context, int newIntervalSecs) {
        Session[] sessions = context.getManager().findSessions();
        for (Session session : sessions) {
            session.setMaxInactiveInterval(newIntervalSecs);
        }
    }

    /**
     * Captures logs for the given logger names in the current ClassLoader.
     */
    public static class LogCapture implements AutoCloseable {
        protected final Level level;
        protected final String[] loggerNames;
        protected final List<LogRecord> logRecords = Collections.synchronizedList(new ArrayList<>());
        protected final Map<Logger, Level> previousLevelsOfLoggersMap = new IdentityHashMap<>();
        private volatile boolean installed = false;
        protected final Handler handler = new Handler() {
            @Override
            public void publish(LogRecord record) {
                logRecords.add(record);
            }

            @Override
            public void flush() {
            }

            @Override
            public void close() throws SecurityException {
                logRecords.clear();
            }
        };
        public LogCapture(Level level, String... loggerNames) {
            this.level = level;
            this.loggerNames = loggerNames;
        }

        public void attach() {
            if (!installed) {
                for (String name : loggerNames) {
                    Logger logger = Logger.getLogger(name);
                    logger.addHandler(handler);
                    if (level != null) {
                        previousLevelsOfLoggersMap.put(logger, logger.getLevel());
                        logger.setLevel(level);
                    }
                }
                installed = true;
            }
        }

        public boolean containsText(CharSequence s) {
            for (LogRecord record : logRecords) {
                if (record.getMessage().contains(s)) {
                    return true;
                }
            }
            return false;
        }
        public boolean hasException(Class<? extends Throwable> type) {
            for (LogRecord record : logRecords) {
                Throwable t = record.getThrown();
                while (t != null) {
                    if (type.isInstance(t)) {return true;}
                    t = t.getCause();
                }
            }
            return false;
        }

        @Override
        public void close() throws Exception {
            for (Logger l : previousLevelsOfLoggersMap.keySet()) {
                try {
                    l.removeHandler(handler);
                } catch (Throwable ignore) {
                }
                try {
                    l.setLevel(previousLevelsOfLoggersMap.get(l));
                } catch (Throwable ignore) {
                }
            }
            previousLevelsOfLoggersMap.clear();
        }
    }

    public static LogCapture attachLogCapture(Level level, String... loggerNames) {
        LogCapture logCapture = new LogCapture(level, loggerNames);
        logCapture.attach();
        return logCapture;
    }

    /**
     * Captures webapp-scoped logs (e.g. ContextConfig/Digester) during the
     * CONFIGURE_START phase of a {@link Context}.
     */
    public static class WebappLogCapture extends LogCapture implements LifecycleListener {
        private String lifecycleEvent = Lifecycle.CONFIGURE_START_EVENT;
        public WebappLogCapture(String lifecycleEvent, Level level, String... loggerNames) {
            this(level, loggerNames);
            this.lifecycleEvent = lifecycleEvent;
        }
        public WebappLogCapture(Level level, String... loggerNames) {
            super(level, loggerNames);
        }

        @Override
        public void lifecycleEvent(LifecycleEvent event) {
            if (this.lifecycleEvent.equals(event.getType())) {
                this.attach();
            }
        }
    }

    /**
     * Installs a {@link WebappLogCapture} on the given {@link Context} so it runs
     * before {@link ContextConfig} during CONFIGURE_START.
     * @param ctx         the webapp context
     * @param level       level for loggers (e.g. {@code Level.ALL})
     * @param loggerNames fully-qualified logger names
     * @return the active capture
     */
    public static WebappLogCapture attachWebappLogCapture(Context ctx, Level level, String... loggerNames) {
        List<LifecycleListener> lifecycleListenersToReAdd = new ArrayList<>();
        for (LifecycleListener l : ctx.findLifecycleListeners()) {
            if (l instanceof ContextConfig) {
                lifecycleListenersToReAdd.add(l);
            }
        }
        for (LifecycleListener l : lifecycleListenersToReAdd) {
            ctx.removeLifecycleListener(l);
        }
        WebappLogCapture webappLogCapture = new WebappLogCapture(level, loggerNames);
        ctx.addLifecycleListener(webappLogCapture);
        for (LifecycleListener l : lifecycleListenersToReAdd) {
            ctx.addLifecycleListener(l);
        }
        return webappLogCapture;
    }

    /**
     * Returns the localized key in a LocalStrings.properties file.
     *
     * @param packagePath The package that contains LocalStrings.properties, e.g. 'org.apache.catalina.startup'
     * @param key           The key to find, e.g. 'versionLoggerListener.serverInfo.server.built'
     * @param locale        The locale to use, e.g. Locale.ENGLISH
     * @return The prefix before the first argument placeholder and if no placeholder, returns the whole formatted string.
     */
    public static String getKeyFromPropertiesFile(String packagePath, String key, Locale locale) {
        StringManager sm;
        if (locale != null) {
            sm = StringManager.getManager(packagePath, locale);
        } else {
            sm = StringManager.getManager(packagePath);
        }

        String formatted = sm.getString(key, "XXX");
        int insertIndex = formatted.indexOf("XXX");
        return (insertIndex == -1) ? formatted : formatted.substring(0, insertIndex);
    }
    public static String getKeyFromPropertiesFile(String packagePath, String key) {
        return getKeyFromPropertiesFile(packagePath, key, Locale.getDefault());
    }
    public static String getKeyFromPropertiesFile(StringManager sm, String key) {
        String formatted = sm.getString(key, "XXX");
        int insertIndex = formatted.indexOf("XXX");
        return (insertIndex == -1) ? formatted : formatted.substring(0, insertIndex);
    }

    /**
     * Injects a {@link LifecycleListener} to a {@link Context} of a {@link Container} that sends {@code ADD_CHILD_EVENT}.
     * Useful when deploying with the Manager / HostConfig.
     */
    public static class ContainerInjector implements ContainerListener, AutoCloseable {

        private final Container container;
        private final Predicate<Context> filter;
        private final Consumer<Context> action;
        private volatile boolean installed = false;
        private String containerEvent = Container.ADD_CHILD_EVENT;

        private ContainerInjector(Container container, Predicate<Context> filter, Consumer<Context> action, String containerEvent) {
            this.container = container;
            this.filter = filter;
            this.action = action;
            if (containerEvent != null) {
                this.containerEvent = containerEvent;
            }
            container.addContainerListener(this);
        }

        public static ContainerInjector inject(Container container, Predicate<Context> filter, Consumer<Context> action) {
            return new ContainerInjector(container, filter, action, null);
        }
        public static ContainerInjector inject(Container container, Predicate<Context> filter, Consumer<Context> action, String containerEvent) {
            return new ContainerInjector(container, filter, action, containerEvent);
        }

        @Override
        public void containerEvent(ContainerEvent event) {
            if (this.containerEvent.equals(event.getType()) && !installed) {
                Object data = event.getData();
                if (data instanceof Context ctx) {
                    if (filter != null && filter.test(ctx)) {
                        action.accept(ctx);
                        installed = true;
                    }
                }
            }
        }

        @Override
        public void close() {
            container.removeContainerListener(this);
        }
    }
}
