//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.test.support;
//
//========================================================================
//------------------------------------------------------------------------
//All rights reserved. This program and the accompanying materials
//are made available under the terms of the Eclipse Public License v1.0
//and Apache License v2.0 which accompanies this distribution.
//
//  The Eclipse Public License is available at
//  http://www.eclipse.org/legal/epl-v10.html
//
//  The Apache License v2.0 is available at
//  http://www.opensource.org/licenses/apache2.0.php
//
//You may elect to redistribute this code under either of these licenses.
//========================================================================
//

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.IO;
import org.eclipse.jetty.toolchain.test.JAR;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.OS;
import org.eclipse.jetty.toolchain.test.PathAssert;
import org.eclipse.jetty.toolchain.test.TestingDir;
import org.junit.Assert;

/**
 * Basic process based executor for using the Jetty Distribution along with custom configurations to perform basic
 * <p>
 * Allows for a test specific directory, that is a copied jetty-distribution, and then modified for the test specific testing required.
 * <p>
 * Requires that you setup the maven-dependency-plugin appropriately for the base distribution you want to use, along with any other dependencies (wars, libs,
 * etc..) that you may need from other maven projects.
 * <p>
 * Maven Dependency Plugin Setup:
 * 
 * <pre>
 *  &lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 *    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"&gt;
 * 
 *   &lt;!-- Common Destination Directories --&gt;
 * 
 *   &lt;properties&gt;
 *     &lt;test-wars-dir&gt;${project.build.directory}/test-wars&lt;/test-wars-dir&gt;
 *     &lt;test-libs-dir&gt;${project.build.directory}/test-libs&lt;/test-libs-dir&gt;
 *     &lt;test-distro-dir&gt;${project.build.directory}/test-dist&lt;/test-distro-dir&gt;
 *   &lt;/properties&gt;
 * 
 *   &lt;build&gt;
 *     &lt;plugins&gt;
 *       &lt;plugin&gt;
 *         &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
 *         &lt;artifactId&gt;maven-dependency-plugin&lt;/artifactId&gt;
 *         &lt;version&gt;2.1&lt;/version&gt;
 *         &lt;executions&gt;
 * 
 *           &lt;!-- Copy LIB and WAR dependencies into place that JettyDistro can use them --&gt;
 * 
 *           &lt;execution&gt;
 *             &lt;id&gt;test-lib-war-copy&lt;/id&gt;
 *             &lt;phase&gt;process-test-resources&lt;/phase&gt;
 *             &lt;goals&gt;
 *               &lt;goal&gt;copy&lt;/goal&gt;
 *             &lt;/goals&gt;
 *             &lt;configuration&gt;
 *               &lt;artifactItems&gt;
 *                 &lt;artifactItem&gt;
 *                   &lt;groupId&gt;org.mortbay.jetty.testwars&lt;/groupId&gt;
 *                   &lt;artifactId&gt;test-war-java_util_logging&lt;/artifactId&gt;
 *                   &lt;version&gt;7.3.0&lt;/version&gt;
 *                   &lt;type&gt;war&lt;/type&gt;
 *                   &lt;outputDirectory&gt;${test-wars-dir}&lt;/outputDirectory&gt;
 *                 &lt;/artifactItem&gt;
 *                 &lt;artifactItem&gt;
 *                   &lt;groupId&gt;org.mortbay.jetty&lt;/groupId&gt;
 *                   &lt;artifactId&gt;jetty-aspect-servlet-api-2.5&lt;/artifactId&gt;
 *                   &lt;version&gt;7.3.0&lt;/version&gt;
 *                   &lt;type&gt;jar&lt;/type&gt;
 *                   &lt;outputDirectory&gt;${test-libs-dir}&lt;/outputDirectory&gt;
 *                 &lt;/artifactItem&gt;
 *               &lt;/artifactItems&gt;
 *               &lt;overWriteIfNewer&gt;true&lt;/overWriteIfNewer&gt;
 *               &lt;overWrite&gt;true&lt;/overWrite&gt;
 *               &lt;stripVersion&gt;true&lt;/stripVersion&gt;
 *             &lt;/configuration&gt;
 *           &lt;/execution&gt;
 * 
 *           &lt;!-- Extract Jetty DISTRIBUTION into place that JettyDistro can use it --&gt;
 * 
 *           &lt;execution&gt;
 *             &lt;id&gt;unpack-test-dist&lt;/id&gt;
 *             &lt;phase&gt;process-test-resources&lt;/phase&gt;
 *             &lt;goals&gt;
 *               &lt;goal&gt;unpack&lt;/goal&gt;
 *             &lt;/goals&gt;
 *             &lt;configuration&gt;
 *               &lt;artifactItems&gt;
 *                 &lt;artifactItem&gt;
 *                   &lt;groupId&gt;org.eclipse.jetty&lt;/groupId&gt;
 *                   &lt;artifactId&gt;jetty-distribution&lt;/artifactId&gt;
 *                   &lt;version&gt;7.3.0&lt;/version&gt;
 *                   &lt;type&gt;zip&lt;/type&gt;
 *                   &lt;overWrite&gt;true&lt;/overWrite&gt;
 *                 &lt;/artifactItem&gt;
 *               &lt;/artifactItems&gt;
 *               &lt;outputAbsoluteArtifactFilename&gt;true&lt;/outputAbsoluteArtifactFilename&gt;
 *               &lt;outputDirectory&gt;${test-distro-dir}&lt;/outputDirectory&gt;
 *               &lt;overWriteSnapshots&gt;true&lt;/overWriteSnapshots&gt;
 *               &lt;overWriteIfNewer&gt;true&lt;/overWriteIfNewer&gt;
 *             &lt;/configuration&gt;
 *           &lt;/execution&gt;
 *         &lt;/executions&gt;
 *       &lt;/plugin&gt;
 *     &lt;/plugins&gt;
 *   &lt;/build&gt;
 * 
 * &lt;/project&gt;
 * </pre>
 * <p>
 * If you have a specific configuration you want to setup, you'll want to prepare this configuration in an overlay directory underneath the
 * <code>src/test/resources/</code> directory. <br>
 * Notes:
 * <ol>
 * <li>The {@link JettyDistro} sets up a unique test directory (based on the constructor {@link #JettyDistro(Class)} or {@link #JettyDistro(TestingDir)}), by
 * ensuring the directory is empty, then copying the <code>target/test-dist</code> directory into this new testing directory prior to the test specific changes
 * to the configuration.<br>
 * Note: this testing directory is a complete jetty distribution, suitable for executing via the command line for additional testing needs.</li>
 * <li>The directory name you choose in <code>src/test/resources</code> will be the name you use in the {@link #overlayConfig(String)} method to provide
 * replacement configurations for the Jetty Distribution.</li>
 * <li>You'll want to {@link #delete(String)} any files and/or directories from the standard distribution prior to using the {@link #overlayConfig(String)}
 * method.</li>
 * <li>Use the {@link #copyLib(String, String)} method to copy JAR files from the <code>target/test-libs</code> directory (created and managed above using the
 * <code>maven-dependency-plugin</code>) to copy the lib into the test specific.</li>
 * <li>Use the {@link #copyTestWar(String)} method to copy WAR files from the <code>target/test-wars</code> directory (created and managed above using the
 * <code>maven-dependency-plugin</code>) to copy the WAR into the test specific directory.</li>
 * </ol>
 * <p>
 * Next you'll want to use Junit 4.8+ and the <code>&#064;BeforeClass</code> and <code>&#064;AfterClass</code> annotations to setup the <code>JettyDistro</code>
 * class for setting up your testing configuration.
 * <p>
 * Example Test Case using {@link JettyDistro} class
 * 
 * <pre>
 * public class MySampleTest
 * {
 *     private static JettyDistro jetty;
 * 
 *     &#064;BeforeClass
 *     public static void initJetty() throws Exception
 *     {
 *         jetty = new JettyDistro(MySampleTest.class);
 * 
 *         jetty.copyTestWar(&quot;test-war-java_util_logging.war&quot;);
 *         jetty.copyTestWar(&quot;test-war-policy.war&quot;);
 * 
 *         jetty.delete(&quot;webapps/test.war&quot;);
 *         jetty.delete(&quot;contexts/test.d&quot;);
 *         jetty.delete(&quot;contexts/javadoc.xml&quot;);
 *         jetty.delete(&quot;contexts/test.xml&quot;);
 * 
 *         jetty.overlayConfig(&quot;no_security&quot;);
 * 
 *         jetty.setDebug(true);
 * 
 *         jetty.start();
 *     }
 * 
 *     &#064;AfterClass
 *     public static void shutdownJetty() throws Exception
 *     {
 *         if (jetty != null)
 *         {
 *             jetty.stop();
 *         }
 *     }
 * 
 *     &#064;Test
 *     public void testRequest() throws Exception
 *     {
 *         SimpleRequest request = new SimpleRequest(jetty.getBaseUri());
 *         String path = &quot;/test-war-policy/security/PRACTICAL/testFilsystem&quot;);
 *         String response = request.getString(path);
 *         Assert.assertEquals(&quot;Success&quot;, response);
 *     }
 * }
 * </pre>
 */
public class JettyDistro
{
    private String artifactName = "jetty-distribution";
    private long startTime = 60;
    private TimeUnit timeUnit = TimeUnit.SECONDS;

    private File jettyHomeDir;
    private Process pid;
    private URI baseUri;

    private String jmxUrl;

    private boolean _debug = false;

    /**
     * Setup the JettyHome as belonging in a testing directory associated with a testing clazz.
     * 
     * @param clazz
     *            the testing class using this JettyDistro
     * @throws IOException
     *             if unable to copy unpacked distribution into place for the provided testing directory
     */
    public JettyDistro(Class<?> clazz) throws IOException
    {
        this(clazz,null);
    }

    /**
     * Setup the JettyHome as belonging in a testing directory associated with a testing clazz.
     * 
     * @param clazz
     *            the testing class using this JettyDistro
     * @param artifact
     *            name of jetty distribution artifact
     * @throws IOException
     *             if unable to copy unpacked distribution into place for the provided testing directory
     */
    public JettyDistro(Class<?> clazz, String artifact) throws IOException
    {
        this.jettyHomeDir = MavenTestingUtils.getTargetTestingDir(clazz,"jettyHome");
        if (artifact != null)
        {
            this.artifactName = artifact;
        }

        copyBaseDistro();
    }

    /**
     * Setup the JettyHome as belonging to a specific testing method directory
     * 
     * @param testdir
     *            the testing directory to use as the JettyHome for this JettyDistro
     * @throws IOException
     *             if unable to copy unpacked distribution into place for the provided testing directory
     */
    public JettyDistro(TestingDir testdir) throws IOException
    {
        this.jettyHomeDir = testdir.getDir();
        copyBaseDistro();
    }

    /**
     * Setup the JettyHome as belonging to a specific testing method directory
     * 
     * @param testdir
     *            the testing directory to use as the JettyHome for this JettyDistro
     * @param artifact
     *            name of jetty distribution artifact
     * @throws IOException
     *             if unable to copy unpacked distribution into place for the provided testing directory
     */
    public JettyDistro(TestingDir testdir, String artifact) throws IOException
    {
        this.jettyHomeDir = testdir.getDir();
        if (artifact != null)
        {
            this.artifactName = artifact;
        }

        copyBaseDistro();
    }

    /**
     * 
     * @throws IOException
     *             if unable to copy unpacked distribution into place for the provided testing directory
     */
    private void copyBaseDistro() throws IOException
    {
        // The outputDirectory for the maven side dependency:unpack goal.
        File distroUnpackDir = MavenTestingUtils.getTargetFile("test-dist");
        PathAssert.assertDirExists(artifactName + " dependency:unpack",distroUnpackDir);

        // The actual jetty-distribution-${version} directory is under this directory.
        // Lets find it.
        File subdirs[] = distroUnpackDir.listFiles(new FileFilter()
        {
            public boolean accept(File path)
            {
                if (!path.isDirectory())
                {
                    return false;
                }

                return path.getName().startsWith(artifactName + "-");
            }
        });

        if (subdirs.length == 0)
        {
            // No jetty-distribution found.
            StringBuilder err = new StringBuilder();
            err.append("No target/test-dist/");
            err.append(artifactName);
            err.append("-${version} directory found.");
            err.append("\n  To fix this, run 'mvn process-test-resources' to create the directory.");
            throw new IOException(err.toString());
        }

        if (subdirs.length != 1)
        {
            // Too many jetty-distributions found.
            StringBuilder err = new StringBuilder();
            err.append("Too many target/test-dist/");
            err.append(artifactName);
            err.append("-${version} directories found.");
            for (File dir : subdirs)
            {
                err.append("\n  ").append(dir.getAbsolutePath());
            }
            err.append("\n  To fix this, run 'mvn clean process-test-resources' to recreate the target/test-dist directory.");
            throw new IOException(err.toString());
        }

        File distroSrcDir = subdirs[0];
        FS.ensureEmpty(jettyHomeDir);
        System.out.printf("Copying Jetty Distribution: %s%n",distroSrcDir.getAbsolutePath());
        System.out.printf("            To Testing Dir: %s%n",jettyHomeDir.getAbsolutePath());
        IO.copyDir(distroSrcDir,jettyHomeDir);
    }

    /**
     * Return the $(jetty.home) directory being used for this JettyDistro
     * 
     * @return the jetty.home directory being used
     */
    public File getJettyHomeDir()
    {
        return this.jettyHomeDir;
    }

    /**
     * Copy a war file from ${project.basedir}/target/test-wars/${testWarFilename} into the ${jetty.home}/webapps/ directory
     * 
     * @param testWarFilename
     *            the war file to copy (must exist)
     * @throws IOException
     *             if unable to copy the war file.
     */
    public void copyTestWar(String testWarFilename) throws IOException
    {
        File srcWar = MavenTestingUtils.getTargetFile("test-wars/" + testWarFilename);
        File destWar = new File(jettyHomeDir,OS.separators("webapps/" + testWarFilename));
        FS.ensureDirExists(destWar.getParentFile());
        IO.copyFile(srcWar,destWar);
    }

    /**
     * Copy an arbitrary file from <code>src/test/resources/${resourcePath}</code> to the testing directory.
     * 
     * @param resourcePath
     *            the relative path for file content within the <code>src/test/resources</code> directory.
     * @param outputPath
     *            the testing directory relative output path for the file output (will result in a file with the outputPath name being created)
     * @throws IOException
     *             if unable to copy resource file
     */
    public void copyResource(String resourcePath, String outputPath) throws IOException
    {
        File srcFile = MavenTestingUtils.getTestResourceFile(resourcePath);
        File destFile = new File(jettyHomeDir,OS.separators(outputPath));
        FS.ensureDirExists(destFile.getParentFile());
        IO.copyFile(srcFile,destFile);
    }

    /**
     * Copy an arbitrary file from <code>target/test-libs/${libFilename}</code> to the testing directory.
     * 
     * @param libFilename
     *            the <code>target/test-libs/${libFilename}</code> to copy
     * @param outputPath
     *            the destination testing directory relative output path for the lib. (will result in a file with the outputPath name being created)
     * @throws IOException
     *             if unable to copy lib
     */
    public void copyLib(String libFilename, String outputPath) throws IOException
    {
        File srcLib = MavenTestingUtils.getTargetFile("test-libs/" + libFilename);
        File destLib = new File(jettyHomeDir,OS.separators(outputPath));
        FS.ensureDirExists(destLib.getParentFile());
        IO.copyFile(srcLib,destLib);
    }

    /**
     * Copy the <code>${project.basedir}/src/main/config/</code> tree into the testing directory.
     * 
     * @throws IOException
     *             if unable to copy the directory tree
     */
    public void copyProjectMainConfig() throws IOException
    {
        File srcDir = MavenTestingUtils.getProjectDir("src/main/config");
        IO.copyDir(srcDir,jettyHomeDir);
    }

    /**
     * Create a <code>${jetty.home}/lib/self/${jarFilename}</code> jar file from the content in the <code>${project.basedir}/target/classes/</code> directory.
     * 
     * @throws IOException
     *             if unable to copy the directory tree
     */
    public void createProjectLib(String jarFilename) throws IOException
    {
        File srcDir = MavenTestingUtils.getTargetFile("classes");
        File libSelfDir = new File(jettyHomeDir,OS.separators("lib/self"));
        FS.ensureDirExists(libSelfDir);
        File jarFile = new File(libSelfDir,jarFilename);
        JAR.create(srcDir,jarFile);
    }

    /**
     * Unpack an arbitrary config from <code>target/test-configs/${configFilename}</code> to the testing directory.
     * 
     * @param configFilename
     *            the <code>target/test-configs/${configFilename}</code> to copy
     * @throws IOException
     *             if unable to unpack config file
     */
    public void unpackConfig(String configFilename) throws IOException
    {
        File srcConfig = MavenTestingUtils.getTargetFile("test-configs/" + configFilename);
        JAR.unpack(srcConfig,jettyHomeDir);
    }

    /**
     * Delete a File or Directory found in the ${jetty.home} directory.
     * 
     * @param path
     *            the path to delete. (can be a file or directory)
     */
    public void delete(String path)
    {
        File jettyPath = new File(jettyHomeDir,OS.separators(path));
        FS.delete(jettyPath);
    }

    /**
     * Return the baseUri being used for this Jetty Process Instance.
     * 
     * @return the base URI for this Jetty Process Instance.
     */
    public URI getBaseUri()
    {
        return this.baseUri;
    }

    /**
     * Return the JMX URL being used for this Jetty Process Instance.
     * 
     * @return the JMX URL for this Jetty Process Instance.
     */
    public String getJmxUrl()
    {
        return this.jmxUrl;
    }

    /**
     * Take the directory contents from ${project.basedir}/src/test/resources/${testConfigName}/ and copy it over whatever happens to be at ${jetty.home}
     * 
     * @param testConfigName
     *            the src/test/resources/ directory name to use as the source diretory for the configuration we are interested in.
     * @throws IOException
     *             if unable to copy directory.
     */
    public void overlayConfig(String testConfigName) throws IOException
    {
        File srcDir = MavenTestingUtils.getTestResourceDir(testConfigName);
        IO.copyDir(srcDir,jettyHomeDir);
    }

    /**
     * Start the jetty server
     * 
     * @throws IOException
     *             if unable to start the server.
     */
    public void start() throws IOException
    {
        List<String> commands = new ArrayList<String>();
        commands.add(getJavaBin());

        commands.add("-Djetty.home=" + jettyHomeDir.getAbsolutePath());

        // Do a dry run first to get the exact command line for Jetty process
        commands.add("-jar");
        commands.add("start.jar");
        commands.add("jetty.port=0");
        if (_debug)
        {
            commands.add("-D.DEBUG=true");
        }
        commands.add("--dry-run");

        ProcessBuilder pbCmd = new ProcessBuilder(commands);
        pbCmd.directory(jettyHomeDir);

        String cmdLine = null;
        Process pidCmd = pbCmd.start();
        try
        {
            cmdLine = readOutputLine(pidCmd);
        }
        finally
        {
            pidCmd.destroy();
        }

        if (cmdLine == null || !cmdLine.contains("XmlConfiguration"))
        {
            Assert.fail("Unable to get Jetty command line");
        }

        // Need to breakdown commandline into parts, as spaces in command line will cause failures.
        List<String> execCommands = splitAndUnescapeCommandLine(cmdLine);

        System.out.printf("Executing: %s%n",cmdLine);
        System.out.printf("Working Dir: %s%n",jettyHomeDir.getAbsolutePath());

        pbCmd = new ProcessBuilder(execCommands);
        pid = pbCmd.start();

        ConsoleParser parser = new ConsoleParser();
        List<String[]> jmxList = parser.newPattern("JMX Remote URL: (.*)",0);
        List<String[]> connList = parser.newPattern("Started [A-Za-z]*Connector@([0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*):([0-9]*)",1);
        // DISABLED: This is what exists in Jetty 9+
        // List<String[]> connList = parser.newPattern("Started [A-Za-z]*Connector@.*[\\({]([0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*):([0-9]*)[\\)}].*",1);

        startPump("STDOUT",parser,this.pid.getInputStream());
        startPump("STDERR",parser,this.pid.getErrorStream());

        try
        {
            parser.waitForDone(this.startTime,this.timeUnit);

            if (!jmxList.isEmpty())
            {
                this.jmxUrl = jmxList.get(0)[0];
                System.out.printf("## Found JMX connector at %s%n",this.jmxUrl);
            }

            if (!connList.isEmpty())
            {
                String[] params = connList.get(0);
                if (params.length == 2)
                {
                    this.baseUri = URI.create("http://localhost:" + params[1] + "/");
                }
                System.out.printf("## Found Jetty connector at host: %s port: %s%n",(Object[])params);
            }

        }
        catch (InterruptedException e)
        {
            pid.destroy();
            Assert.fail("Unable to get required information within time limit");
        }
    }

    public static List<String> splitAndUnescapeCommandLine(CharSequence rawCmdLine)
    {
        List<String> cmds = new ArrayList<String>();

        int len = rawCmdLine.length();
        StringBuilder arg = new StringBuilder();
        boolean escaped = false;
        boolean inQuote = false;
        char c;
        for (int i = 0; i < len; i++)
        {
            c = rawCmdLine.charAt(i);
            if (escaped)
            {
                switch (c)
                {
                    case 'r':
                        arg.append('\r');
                        break;
                    case 'f':
                        arg.append('\f');
                        break;
                    case 't':
                        arg.append('\t');
                        break;
                    case 'n':
                        arg.append('\n');
                        break;
                    case 'b':
                        arg.append('\b');
                        break;
                    default:
                        arg.append(c);
                        break;
                }
                escaped = false;
                continue;
            }

            if (c == '\\')
            {
                escaped = true;
            }
            else
            {
                if ((c == ' ') && (!inQuote))
                {
                    // the delim!
                    cmds.add(String.valueOf(arg.toString()));
                    arg.setLength(0);
                }
                else if (c == '"')
                {
                    inQuote = !inQuote;
                }
                else
                {
                    arg.append(c);
                }
            }
        }
        cmds.add(String.valueOf(arg.toString()));

        return cmds;
    }

    private String readOutputLine(Process pidCmd) throws IOException
    {
        InputStream in = null;
        InputStreamReader reader = null;
        BufferedReader buf = null;
        try
        {
            in = pidCmd.getInputStream();
            reader = new InputStreamReader(in);
            buf = new BufferedReader(reader);
            return buf.readLine();
        }
        finally
        {
            IO.close(buf);
            IO.close(reader);
            IO.close(in);
        }
    }

    private static class ConsoleParser
    {
        private List<ConsolePattern> patterns = new ArrayList<ConsolePattern>();
        private CountDownLatch latch;
        private int count;

        public List<String[]> newPattern(String exp, int cnt)
        {
            ConsolePattern pat = new ConsolePattern(exp,cnt);
            patterns.add(pat);
            count += cnt;

            return pat.getMatches();
        }

        public void parse(String line)
        {
            for (ConsolePattern pat : patterns)
            {
                Matcher mat = pat.getMatcher(line);
                if (mat.find())
                {
                    int num = 0, count = mat.groupCount();
                    String[] match = new String[count];
                    while (num++ < count)
                    {
                        match[num - 1] = mat.group(num);
                    }
                    pat.getMatches().add(match);

                    if (pat.getCount() > 0)
                    {
                        getLatch().countDown();
                    }
                }
            }
        }

        public void waitForDone(long timeout, TimeUnit unit) throws InterruptedException
        {
            getLatch().await(timeout,unit);
        }

        private CountDownLatch getLatch()
        {
            synchronized (this)
            {
                if (latch == null)
                {
                    latch = new CountDownLatch(count);
                }
            }

            return latch;
        }
    }

    private static class ConsolePattern
    {
        private Pattern pattern;
        private List<String[]> matches;
        private int count;

        ConsolePattern(String exp, int cnt)
        {
            pattern = Pattern.compile(exp);
            matches = new ArrayList<String[]>();
            count = cnt;
        }

        public Matcher getMatcher(String line)
        {
            return pattern.matcher(line);
        }

        public List<String[]> getMatches()
        {
            return matches;
        }

        public int getCount()
        {
            return count;
        }
    }

    private void startPump(String mode, ConsoleParser parser, InputStream inputStream)
    {
        ConsoleStreamer pump = new ConsoleStreamer(mode,inputStream);
        pump.setParser(parser);
        Thread thread = new Thread(pump,"ConsoleStreamer/" + mode);
        thread.start();
    }

    /**
     * enable debug on the jetty process
     * 
     * @param debug
     */
    public void setDebug(boolean debug)
    {
        _debug = debug;
    }

    private String getJavaBin()
    {
        String javaexes[] = new String[]
        { "java", "java.exe" };

        File javaHomeDir = new File(System.getProperty("java.home"));
        for (String javaexe : javaexes)
        {
            File javabin = new File(javaHomeDir,OS.separators("bin/" + javaexe));
            if (javabin.exists() && javabin.isFile())
            {
                return javabin.getAbsolutePath();
            }
        }

        Assert.fail("Unable to find java bin");
        return "java";
    }

    /**
     * Stop the jetty server
     */
    public void stop()
    {
        System.out.println("Stopping JettyDistro ...");
        if (pid != null)
        {
            // TODO: maybe issue a STOP instead?
            pid.destroy();
        }
    }

    /**
     * Simple streamer for the console output from a Process
     */
    private static class ConsoleStreamer implements Runnable
    {
        private String mode;
        private BufferedReader reader;
        private ConsoleParser parser;

        public ConsoleStreamer(String mode, InputStream is)
        {
            this.mode = mode;
            this.reader = new BufferedReader(new InputStreamReader(is));
        }

        public void setParser(ConsoleParser connector)
        {
            this.parser = connector;
        }

        public void run()
        {
            String line;
            // System.out.printf("ConsoleStreamer/%s initiated%n",mode);
            try
            {
                while ((line = reader.readLine()) != (null))
                {
                    if (parser != null)
                    {
                        parser.parse(line);
                    }
                    System.out.println("[" + mode + "] " + line);
                }
            }
            catch (IOException ignore)
            {
                /* ignore */
            }
            finally
            {
                IO.close(reader);
            }
            // System.out.printf("ConsoleStreamer/%s finished%n",mode);
        }
    }

    public void setStartTime(long startTime, TimeUnit timeUnit)
    {
        this.startTime = startTime;
        this.timeUnit = timeUnit;
    }
}
