// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // 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> * <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"> * * <!-- Common Destination Directories --> * * <properties> * <test-wars-dir>${project.build.directory}/test-wars</test-wars-dir> * <test-libs-dir>${project.build.directory}/test-libs</test-libs-dir> * <test-distro-dir>${project.build.directory}/test-dist</test-distro-dir> * </properties> * * <build> * <plugins> * <plugin> * <groupId>org.apache.maven.plugins</groupId> * <artifactId>maven-dependency-plugin</artifactId> * <version>2.1</version> * <executions> * * <!-- Copy LIB and WAR dependencies into place that JettyDistro can use them --> * * <execution> * <id>test-lib-war-copy</id> * <phase>process-test-resources</phase> * <goals> * <goal>copy</goal> * </goals> * <configuration> * <artifactItems> * <artifactItem> * <groupId>org.mortbay.jetty.testwars</groupId> * <artifactId>test-war-java_util_logging</artifactId> * <version>7.3.0</version> * <type>war</type> * <outputDirectory>${test-wars-dir}</outputDirectory> * </artifactItem> * <artifactItem> * <groupId>org.mortbay.jetty</groupId> * <artifactId>jetty-aspect-servlet-api-2.5</artifactId> * <version>7.3.0</version> * <type>jar</type> * <outputDirectory>${test-libs-dir}</outputDirectory> * </artifactItem> * </artifactItems> * <overWriteIfNewer>true</overWriteIfNewer> * <overWrite>true</overWrite> * <stripVersion>true</stripVersion> * </configuration> * </execution> * * <!-- Extract Jetty DISTRIBUTION into place that JettyDistro can use it --> * * <execution> * <id>unpack-test-dist</id> * <phase>process-test-resources</phase> * <goals> * <goal>unpack</goal> * </goals> * <configuration> * <artifactItems> * <artifactItem> * <groupId>org.eclipse.jetty</groupId> * <artifactId>jetty-distribution</artifactId> * <version>7.3.0</version> * <type>zip</type> * <overWrite>true</overWrite> * </artifactItem> * </artifactItems> * <outputAbsoluteArtifactFilename>true</outputAbsoluteArtifactFilename> * <outputDirectory>${test-distro-dir}</outputDirectory> * <overWriteSnapshots>true</overWriteSnapshots> * <overWriteIfNewer>true</overWriteIfNewer> * </configuration> * </execution> * </executions> * </plugin> * </plugins> * </build> * * </project> * </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>@BeforeClass</code> and <code>@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; * * @BeforeClass * public static void initJetty() throws Exception * { * jetty = new JettyDistro(MySampleTest.class); * * jetty.copyTestWar("test-war-java_util_logging.war"); * jetty.copyTestWar("test-war-policy.war"); * * jetty.delete("webapps/test.war"); * jetty.delete("contexts/test.d"); * jetty.delete("contexts/javadoc.xml"); * jetty.delete("contexts/test.xml"); * * jetty.overlayConfig("no_security"); * * jetty.setDebug(true); * * jetty.start(); * } * * @AfterClass * public static void shutdownJetty() throws Exception * { * if (jetty != null) * { * jetty.stop(); * } * } * * @Test * public void testRequest() throws Exception * { * SimpleRequest request = new SimpleRequest(jetty.getBaseUri()); * String path = "/test-war-policy/security/PRACTICAL/testFilsystem"); * String response = request.getString(path); * Assert.assertEquals("Success", 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.getPath().toFile(); 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.getPath().toFile(); 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.http.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; } }