diff options
8 files changed, 1051 insertions, 41 deletions
diff --git a/VERSION.txt b/VERSION.txt index d53dba269d..8a973b743f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -108,7 +108,6 @@ jetty-9.0.4.v20130625 - 25 June 2013 + 410405 Avoid NPE for requestDispatcher(../) + 410469 UpgradeRequest is sent twice when using SSL, one fails warning about WritePendingException - + 410498 ignore type of exception in GoAwayTest.testDataNotProcessedAfterGoAway + 410522 jetty start broken for command line options + 410537 Exceptions during @OnWebSocketConnect not reported to @OnWebSocketError @@ -301,6 +300,75 @@ jetty-9.0.0.v20130308 - 08 March 2013 + 402757 WebSocket client module can't be used with WebSocket server module in the same WAR +jetty-8.1.12.v20130726 - 26 July 2013 + + 396706 CGI support parameters + + 397193 MongoSessionManager refresh updates last access time + + 407342 ReloadedSessionMissingClassTest uses class compiled with jdk7 + + 408529 Etags set in 304 response + + 408600 set correct jetty.url in all pom files + + 408642 setContentType from addHeader + + 408662 In pax-web servlet services requests even if init() has not finished + running + + 408806 getParameter returns null on Multipart request if called before + request.getPart()/getParts() + + 408909 GzipFilter setting of headers when reset and/or not compressed + + 409028 Jetty HttpClient does not work with proxy CONNECT method. + + 409133 Empty <welcome-file> causes StackOverflowError + + 409436 NPE on context restart using dynamic servlet registration + + 409449 Ensure servlets, filters and listeners added via dynamic + registration, annotations or descriptors are cleaned on context restarts + + 409556 FileInputStream not closed in DirectNIOBuffer + + 410405 Avoid NPE for requestDispatcher(../) + + 410630 MongoSessionManager conflicting session update op + + 410750 NoSQLSessions: implement session context data persistence across + server restarts + + 410893 async support defaults to false for spec created servlets and filters + + 411135 HttpClient may send proxied https requests to the proxy instead of + the target server. + + 411216 RequestLogHandler handles async completion + + 411458 MultiPartFilter getParameterMap doesn't preserve multivalued + parameters 411459 MultiPartFilter.Wrapper getParameter should use charset + encoding of part + + 411755 MultiPartInputStreamParser fails on base64 encoded content + + 411909 GzipFilter flushbuffer() results in erroneous finish() call + + 412712 HttpClient does not send the terminal chunk after partial writes. + + 412750 HttpClient close expired connections fix + + 413371 Default JSON.Converters for List and Set. + + 413372 JSON Enum uses name rather than toString() + + 413684 Trailing slash shows JSP source + + 413812 Make RateTracker serializable + +jetty-7.6.12.v20130726 - 26 July 2013 + + 396706 CGI support parameters + + 397193 MongoSessionManager refresh updates last access time + + 407342 ReloadedSessionMissingClassTest uses class compiled with jdk7 + + 408529 Etags set in 304 response + + 408600 set correct jetty.url in all pom files + + 408642 setContentType from addHeader + + 408662 In pax-web servlet services requests even if init() has not finished + running + + 408909 GzipFilter setting of headers when reset and/or not compressed + + 409028 Jetty HttpClient does not work with proxy CONNECT method. + + 409133 Empty <welcome-file> causes StackOverflowError + + 409556 FileInputStream not closed in DirectNIOBuffer + + 410630 MongoSessionManager conflicting session update op + + 410750 NoSQLSessions: implement session context data persistence across + server restarts + + 411135 HttpClient may send proxied https requests to the proxy instead of + the target server. + + 411216 RequestLogHandler handles async completion + + 411458 MultiPartFilter getParameterMap doesn't preserve multivalued + parameters 411459 MultiPartFilter.Wrapper getParameter should use charset + encoding of part + + 411755 MultiPartInputStreamParser fails on base64 encoded content + + 411909 GzipFilter flushbuffer() results in erroneous finish() call + + 412712 HttpClient does not send the terminal chunk after partial writes. + + 412750 HttpClient close expired connections fix + + 413371 Default JSON.Converters for List and Set. + + 413372 JSON Enum uses name rather than toString() + + 413684 Trailing slash shows JSP source + + 413812 Make RateTracker serializable + jetty-8.1.11.v20130520 - 20 May 2013 + 402844 STOP.PORT & STOP.KEY behaviour has changed + 403281 jetty.sh waits for started or failure before returning diff --git a/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java b/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java index 45561d4f42..028e4f269d 100644 --- a/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java +++ b/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java @@ -20,10 +20,10 @@ package org.eclipse.jetty.jaas.spi; import java.security.Principal; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; @@ -45,7 +45,7 @@ public class PropertyFileLoginModule extends AbstractLoginModule private static final Logger LOG = Log.getLogger(PropertyFileLoginModule.class); - private static Map<String, PropertyUserStore> _propertyUserStores = new HashMap<String, PropertyUserStore>(); + private static ConcurrentHashMap<String, PropertyUserStore> _propertyUserStores = new ConcurrentHashMap<String, PropertyUserStore>(); private int _refreshInterval = 0; private String _filename = DEFAULT_FILENAME; @@ -68,33 +68,37 @@ public class PropertyFileLoginModule extends AbstractLoginModule private void setupPropertyUserStore(Map<String, ?> options) { + parseConfig(options); + if (_propertyUserStores.get(_filename) == null) { - parseConfig(options); - - PropertyUserStore _propertyUserStore = new PropertyUserStore(); - _propertyUserStore.setConfig(_filename); - _propertyUserStore.setRefreshInterval(_refreshInterval); - LOG.debug("setupPropertyUserStore: Starting new PropertyUserStore. PropertiesFile: " + _filename + " refreshInterval: " + _refreshInterval); + PropertyUserStore propertyUserStore = new PropertyUserStore(); + propertyUserStore.setConfig(_filename); + propertyUserStore.setRefreshInterval(_refreshInterval); - try + PropertyUserStore prev = _propertyUserStores.putIfAbsent(_filename, propertyUserStore); + if (prev == null) { - _propertyUserStore.start(); + LOG.debug("setupPropertyUserStore: Starting new PropertyUserStore. PropertiesFile: " + _filename + " refreshInterval: " + _refreshInterval); + + try + { + propertyUserStore.start(); + } + catch (Exception e) + { + LOG.warn("Exception while starting propertyUserStore: ",e); + } } - catch (Exception e) - { - LOG.warn("Exception while starting propertyUserStore: ",e); - } - - _propertyUserStores.put(_filename,_propertyUserStore); } } private void parseConfig(Map<String, ?> options) { - _filename = (String)options.get("file") != null?(String)options.get("file"):DEFAULT_FILENAME; - String refreshIntervalString = (String)options.get("refreshInterval"); - _refreshInterval = refreshIntervalString == null?_refreshInterval:Integer.parseInt(refreshIntervalString); + String tmp = (String)options.get("file"); + _filename = (tmp == null? DEFAULT_FILENAME : tmp); + tmp = (String)options.get("refreshInterval"); + _refreshInterval = (tmp == null?_refreshInterval:Integer.parseInt(tmp)); } /** diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java index 7d70908618..9098ea3bcf 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java @@ -703,7 +703,6 @@ public class MultipartFilterTest assertTrue(response.getContent().contains("aaaa,bbbbb")); } - @Test public void testContentTypeWithCharSet() throws Exception { // generated and parsed test @@ -733,6 +732,39 @@ public class MultipartFilterTest assertTrue(response.getContent().indexOf("brown cow")>=0); } + + @Test + public void testBufferOverflowNoCRLF () throws Exception + { + String boundary="XyXyXy"; + // generated and parsed test + HttpTester.Request request = HttpTester.newRequest(); + HttpTester.Response response; + tester.addServlet(BoundaryServlet.class,"/testb"); + tester.setAttribute("fileName", "abc"); + tester.setAttribute("desc", "123"); + tester.setAttribute("title", "ttt"); + request.setMethod("POST"); + request.setVersion("HTTP/1.0"); + request.setHeader("Host","tester"); + request.setURI("/context/testb"); + request.setHeader("Content-Type","multipart/form-data; boundary="+boundary); + + String content = "--XyXyXy"; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(content.getBytes()); + + for (int i=0; i< 8500; i++) //create content that will overrun default buffer size of BufferedInputStream + { + baos.write('a'); + } + request.setContent(baos.toString()); + + response = HttpTester.parseResponse(tester.getResponses(request.generate())); + assertTrue(response.getContent().contains("Buffer size exceeded")); + assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + } /* * see the testParameterMap test diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java index 5b9717d311..ee29487d40 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java @@ -488,7 +488,16 @@ public class MultiPartInputStreamParser byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1); // Get first boundary - String line=((ReadLineInputStream)_in).readLine(); + String line = null; + try + { + line=((ReadLineInputStream)_in).readLine(); + } + catch (IOException e) + { + LOG.warn("Badly formatted multipart request"); + throw e; + } if (line == null) throw new IOException("Missing content for multipart request"); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java index bb4c9fbc87..1aeec5fbb6 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java @@ -49,6 +49,10 @@ public class ReadLineInputStream extends BufferedInputStream while (true) { int b=super.read(); + + if (markpos < 0) + throw new IOException("Buffer size exceeded: no line terminator"); + if (b==-1) { int m=markpos; diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java index b3f50cee0c..e096b24e23 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java @@ -18,17 +18,8 @@ package org.eclipse.jetty.util; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -530,6 +521,34 @@ public class MultiPartInputStreamTest } @Test + public void testBufferOverflowNoCRLF () throws Exception + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write("--AaB03x".getBytes()); + for (int i=0; i< 8500; i++) //create content that will overrun default buffer size of BufferedInputStream + { + baos.write('a'); + } + + MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50); + MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(baos.toByteArray()), + _contentType, + config, + _tmpDir); + mpis.setDeleteOnExit(true); + try + { + mpis.getParts(); + fail ("Multipart buffer overrun"); + } + catch (IOException e) + { + assertTrue(e.getMessage().startsWith("Buffer size exceeded")); + } + + } + + public void testCharsetEncoding () throws Exception { String contentType = "multipart/form-data; boundary=TheBoundary; charset=ISO-8859-1"; @@ -137,7 +137,7 @@ </configuration> </execution> <execution> - <id>ban-junit.jar</id> + <id>ban-junit-dep.jar</id> <goals> <goal>enforce</goal> </goals> @@ -145,10 +145,10 @@ <rules> <bannedDependencies> <excludes> - <exclude>junit:junit:*:jar</exclude> + <exclude>junit:junit-dep:*:jar</exclude> </excludes> <searchTransitive>true</searchTransitive> - <message>We use junit-dep.jar, not junit.jar (as the standard junit.jar aggregates too many 3rd party libs inside of it)</message> + <message>We use junit.jar, not junit-dep.jar (as of junit 4.11, hamcrest is no longer embedded)</message> </bannedDependencies> </rules> </configuration> @@ -553,7 +553,7 @@ <dependency> <groupId>org.eclipse.jetty.toolchain</groupId> <artifactId>jetty-test-helper</artifactId> - <version>2.3</version> + <version>2.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> @@ -575,7 +575,7 @@ <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> - <version>4.8.1</version> + <version>4.11</version> </dependency> --> <dependency> @@ -586,17 +586,17 @@ <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> - <version>1.2.1</version> + <version>1.3</version> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> - <version>1.2.1</version> + <version>1.3</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> - <version>1.8.5</version> + <version>1.9.5</version> <exclusions> <exclusion> <groupId>junit</groupId> diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/JettyDistro.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/JettyDistro.java new file mode 100644 index 0000000000..99ce5ffdc2 --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/JettyDistro.java @@ -0,0 +1,874 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.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; + } +}
\ No newline at end of file |