diff --git a/spring-boot-tools/spring-boot-loader/README.md b/spring-boot-tools/spring-boot-loader/README.md index f245eb5d6f9..e141bf2e572 100644 --- a/spring-boot-tools/spring-boot-loader/README.md +++ b/spring-boot-tools/spring-boot-loader/README.md @@ -11,7 +11,7 @@ opposed to explicitly on the classpath). In the case of the those locations if you want more. The `PropertiesLauncher` looks in `lib/` by default, but you can add additional locations by setting an environment variable `LOADER_PATH`or `loader.path` in -`application.properties` (colon-separated list of directories). +`application.properties` (colon-separated list of directories or archives). > **Note:** The quickest way to build a compatible archive is to use the > [spring-boot-maven-plugin](../spring-boot-maven-plugin) or diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java index accead37edd..d4a3639d72e 100644 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -343,6 +343,12 @@ public class PropertiesLauncher extends Launcher { Archive archive = new ExplodedArchive(file); lib.add(archive); } + Archive archive = getArchive(file); + if (archive != null) { + this.logger.info("Adding classpath entries from nested " + archive.getUrl() + + root); + lib.add(archive); + } Archive nested = getNestedArchive(root); if (nested != null) { this.logger.info("Adding classpath entries from nested " + nested.getUrl() @@ -352,6 +358,14 @@ public class PropertiesLauncher extends Launcher { return lib; } + private Archive getArchive(File file) throws IOException { + String name = file.getName().toLowerCase(); + if (name.endsWith(".jar") || name.endsWith(".zip")) { + return new JarFileArchive(file); + } + return null; + } + private Archive getNestedArchive(final String root) throws Exception { Archive parent = createArchive(); if (root.startsWith("/") || parent.getUrl().equals(this.home.toURI().toURL())) { @@ -401,9 +415,17 @@ public class PropertiesLauncher extends Launcher { private String cleanupPath(String path) { path = path.trim(); - // Always a directory - if (!path.endsWith("/")) { - path = path + "/"; + if (path.toLowerCase().endsWith(".jar") || path.toLowerCase().endsWith(".zip")) { + return path; + } + if (path.endsWith("/*")) { + path = path.substring(0, path.length() - 1); + } + else { + // It's a directory + if (!path.endsWith("/")) { + path = path + "/"; + } } // No need for current dir path if (path.startsWith("./")) { diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/OutputCapture.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/OutputCapture.java new file mode 100644 index 00000000000..8e39c0ce65e --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/OutputCapture.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Capture output from System.out and System.err. + * + * @author Phillip Webb + */ +public class OutputCapture implements TestRule { + + private CaptureOutputStream captureOut; + + private CaptureOutputStream captureErr; + + private ByteArrayOutputStream copy; + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + captureOutput(); + try { + base.evaluate(); + } + finally { + releaseOutput(); + } + } + }; + } + + protected void captureOutput() { + this.copy = new ByteArrayOutputStream(); + this.captureOut = new CaptureOutputStream(System.out, this.copy); + this.captureErr = new CaptureOutputStream(System.err, this.copy); + System.setOut(new PrintStream(this.captureOut)); + System.setErr(new PrintStream(this.captureErr)); + } + + protected void releaseOutput() { + System.setOut(this.captureOut.getOriginal()); + System.setErr(this.captureErr.getOriginal()); + this.copy = null; + } + + public void flush() { + try { + this.captureOut.flush(); + this.captureErr.flush(); + } + catch (IOException ex) { + // ignore + } + } + + @Override + public String toString() { + flush(); + return this.copy.toString(); + } + + private static class CaptureOutputStream extends OutputStream { + + private final PrintStream original; + + private final OutputStream copy; + + public CaptureOutputStream(PrintStream original, OutputStream copy) { + this.original = original; + this.copy = copy; + } + + @Override + public void write(int b) throws IOException { + this.copy.write(b); + this.original.write(b); + this.original.flush(); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.copy.write(b, off, len); + this.original.write(b, off, len); + } + + public PrintStream getOriginal() { + return this.original; + } + + @Override + public void flush() throws IOException { + this.copy.flush(); + this.original.flush(); + } + } + +} diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java index d3f68360f17..11228f0f8f3 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java @@ -21,10 +21,12 @@ import java.io.IOException; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * Tests for {@link PropertiesLauncher}. @@ -33,6 +35,9 @@ import static org.junit.Assert.assertEquals; */ public class PropertiesLauncherTests { + @Rule + public OutputCapture output = new OutputCapture(); + @Before public void setup() throws IOException { System.setProperty("loader.home", @@ -65,13 +70,34 @@ public class PropertiesLauncherTests { @Test public void testUserSpecifiedConfigName() throws Exception { - System.setProperty("loader.config.name", "foo"); PropertiesLauncher launcher = new PropertiesLauncher(); assertEquals("my.Application", launcher.getMainClass()); assertEquals("[etc/]", ReflectionTestUtils.getField(launcher, "paths").toString()); } + @Test + public void testUserSpecifiedPath() throws Exception { + System.setProperty("loader.path", "jars/*"); + System.setProperty("loader.main", "demo.Application"); + PropertiesLauncher launcher = new PropertiesLauncher(); + assertEquals("[jars/]", ReflectionTestUtils.getField(launcher, "paths") + .toString()); + launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + public void testUserSpecifiedJarPath() throws Exception { + System.setProperty("loader.path", "jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + PropertiesLauncher launcher = new PropertiesLauncher(); + assertEquals("[jars/app.jar]", ReflectionTestUtils.getField(launcher, "paths") + .toString()); + launcher.launch(new String[0]); + waitFor("Hello World"); + } + @Test public void testUserSpecifiedConfigPathWins() throws Exception { @@ -95,4 +121,15 @@ public class PropertiesLauncherTests { assertEquals("demo.Application", System.getProperty("loader.main")); } + private void waitFor(String value) throws Exception { + int count = 0; + boolean timeout = false; + while (!timeout && count < 100) { + count++; + Thread.sleep(50L); + timeout = this.output.toString().contains(value); + } + assertTrue("Timed out waiting for (" + value + ")", timeout); + } + } diff --git a/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar b/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar new file mode 100644 index 00000000000..c7c485ae5da Binary files /dev/null and b/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar differ