diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractApplicationLauncher.java index bd16fdf01b..a7a16aaedf 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractApplicationLauncher.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractApplicationLauncher.java @@ -34,8 +34,6 @@ import org.springframework.util.FileCopyUtils; */ abstract class AbstractApplicationLauncher extends ExternalResource { - private final File serverPortFile = new File("target/server.port"); - private final ApplicationBuilder applicationBuilder; private Process process; @@ -62,8 +60,15 @@ abstract class AbstractApplicationLauncher extends ExternalResource { protected abstract List getArguments(File archive); + protected abstract File getWorkingDirectory(); + + protected abstract String getDescription(String packaging); + private Process startApplication() throws Exception { - this.serverPortFile.delete(); + File workingDirectory = getWorkingDirectory(); + File serverPortFile = workingDirectory == null ? new File("target/server.port") + : new File(workingDirectory, "target/server.port"); + serverPortFile.delete(); File archive = this.applicationBuilder.buildApplication(); List arguments = new ArrayList(); arguments.add(System.getProperty("java.home") + "/bin/java"); @@ -72,14 +77,17 @@ abstract class AbstractApplicationLauncher extends ExternalResource { arguments.toArray(new String[arguments.size()])); processBuilder.redirectOutput(Redirect.INHERIT); processBuilder.redirectError(Redirect.INHERIT); + if (workingDirectory != null) { + processBuilder.directory(workingDirectory); + } Process process = processBuilder.start(); - this.httpPort = awaitServerPort(process); + this.httpPort = awaitServerPort(process, serverPortFile); return process; } - private int awaitServerPort(Process process) throws Exception { + private int awaitServerPort(Process process, File serverPortFile) throws Exception { long end = System.currentTimeMillis() + 30000; - while (this.serverPortFile.length() == 0) { + while (serverPortFile.length() == 0) { if (System.currentTimeMillis() > end) { throw new IllegalStateException( "server.port file was not written within 30 seconds"); @@ -89,8 +97,8 @@ abstract class AbstractApplicationLauncher extends ExternalResource { } Thread.sleep(100); } - return Integer.parseInt( - FileCopyUtils.copyToString(new FileReader(this.serverPortFile))); + return Integer + .parseInt(FileCopyUtils.copyToString(new FileReader(serverPortFile))); } } diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerIntegrationTests.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerIntegrationTests.java index 02822f5cc4..ca4fd715c2 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerIntegrationTests.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerIntegrationTests.java @@ -19,6 +19,8 @@ package org.springframework.boot.context.embedded; import java.io.IOException; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -47,29 +49,38 @@ public abstract class AbstractEmbeddedServletContainerIntegrationTests { protected final RestTemplate rest = new RestTemplate(); - public static Object[] parameters(String packaging) { + public static Object[] parameters(String packaging, + List> applicationLaunchers) { List parameters = new ArrayList(); - parameters.addAll(createParameters(packaging, "jetty", "current")); - parameters.addAll( - createParameters(packaging, "tomcat", "current", "8.0.41", "7.0.75")); - parameters.addAll(createParameters(packaging, "undertow", "current")); + parameters.addAll(createParameters(packaging, "jetty", + Collections.singletonList("current"), applicationLaunchers)); + parameters.addAll(createParameters(packaging, "tomcat", + Arrays.asList("current", "8.0.41", "7.0.75"), applicationLaunchers)); + parameters.addAll(createParameters(packaging, "undertow", + Collections.singletonList("current"), applicationLaunchers)); return parameters.toArray(new Object[parameters.size()]); } private static List createParameters(String packaging, String container, - String... versions) { + List versions, + List> applicationLaunchers) { List parameters = new ArrayList(); for (String version : versions) { ApplicationBuilder applicationBuilder = new ApplicationBuilder( temporaryFolder, packaging, container, version); - parameters.add(new Object[] { - StringUtils.capitalise(container) + " " + version + " packaged " - + packaging, - new PackagedApplicationLauncher(applicationBuilder) }); - parameters.add(new Object[] { - StringUtils.capitalise(container) + " " + version + " exploded " - + packaging, - new ExplodedApplicationLauncher(applicationBuilder) }); + for (Class launcherClass : applicationLaunchers) { + try { + AbstractApplicationLauncher launcher = launcherClass + .getDeclaredConstructor(ApplicationBuilder.class) + .newInstance(applicationBuilder); + String name = StringUtils.capitalise(container) + " " + version + ": " + + launcher.getDescription(packaging); + parameters.add(new Object[] { name, launcher }); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } } return parameters; } diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/BootRunApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/BootRunApplicationLauncher.java new file mode 100644 index 0000000000..1488614a6a --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/BootRunApplicationLauncher.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2017 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.context.embedded; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.springframework.util.FileSystemUtils; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * {@link AbstractApplicationLauncher} that launches a Spring Boot application with a + * classpath similar to that used when run with Maven or Gradle. + * + * @author Andy Wilkinson + */ +class BootRunApplicationLauncher extends AbstractApplicationLauncher { + + private final File exploded = new File("target/run"); + + BootRunApplicationLauncher(ApplicationBuilder applicationBuilder) { + super(applicationBuilder); + } + + @Override + protected List getArguments(File archive) { + try { + explodeArchive(archive); + deleteLauncherClasses(); + File targetClasses = populateTargetClasses(archive); + File dependencies = populateDependencies(archive); + populateSrcMainWebapp(); + List classpath = new ArrayList(); + classpath.add(targetClasses.getAbsolutePath()); + for (File dependency : dependencies.listFiles()) { + classpath.add(dependency.getAbsolutePath()); + } + return Arrays.asList("-cp", + StringUtils.collectionToDelimitedString(classpath, + File.pathSeparator), + "com.example.ResourceHandlingApplication"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void deleteLauncherClasses() { + FileSystemUtils.deleteRecursively(new File(this.exploded, "org")); + } + + private File populateTargetClasses(File archive) { + File targetClasses = new File(this.exploded, "target/classes"); + targetClasses.mkdirs(); + new File(this.exploded, getClassesPath(archive)).renameTo(targetClasses); + return targetClasses; + } + + private File populateDependencies(File archive) { + File dependencies = new File(this.exploded, "dependencies"); + dependencies.mkdirs(); + List libPaths = getLibPaths(archive); + for (String libPath : libPaths) { + for (File jar : new File(this.exploded, libPath).listFiles()) { + jar.renameTo(new File(dependencies, jar.getName())); + } + } + return dependencies; + } + + private void populateSrcMainWebapp() { + File srcMainWebapp = new File(this.exploded, "src/main/webapp"); + srcMainWebapp.mkdirs(); + new File(this.exploded, "webapp-resource.txt") + .renameTo(new File(srcMainWebapp, "webapp-resource.txt")); + } + + private String getClassesPath(File archive) { + return archive.getName().endsWith(".jar") ? "BOOT-INF/classes" + : "WEB-INF/classes"; + } + + private List getLibPaths(File archive) { + return archive.getName().endsWith(".jar") + ? Collections.singletonList("BOOT-INF/lib") + : Arrays.asList("WEB-INF/lib", "WEB-INF/lib-provided"); + } + + private void explodeArchive(File archive) throws IOException { + FileSystemUtils.deleteRecursively(this.exploded); + JarFile jarFile = new JarFile(archive); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + File extracted = new File(this.exploded, jarEntry.getName()); + if (jarEntry.isDirectory()) { + extracted.mkdirs(); + } + else { + FileOutputStream extractedOutputStream = new FileOutputStream(extracted); + StreamUtils.copy(jarFile.getInputStream(jarEntry), extractedOutputStream); + extractedOutputStream.close(); + } + } + jarFile.close(); + } + + @Override + protected File getWorkingDirectory() { + return this.exploded; + } + + @Override + protected String getDescription(String packaging) { + return "build system run " + packaging + " project"; + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java new file mode 100644 index 0000000000..ce70ec15fa --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2017 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.context.embedded; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for Spring Boot's embedded servlet container support when developing + * a jar application. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class EmbeddedServletContainerJarDevelopmentIntegrationTests + extends AbstractEmbeddedServletContainerIntegrationTests { + + @Parameters(name = "{0}") + public static Object[] parameters() { + return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar", Arrays + .asList(BootRunApplicationLauncher.class, IdeApplicationLauncher.class)); + } + + public EmbeddedServletContainerJarDevelopmentIntegrationTests(String name, + AbstractApplicationLauncher launcher) { + super(name, launcher); + } + + @Test + public void metaInfResourceFromDependencyIsAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void metaInfResourceFromDependencyIsAvailableViaServletContext() + throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java index fd1a7cc466..98e81854cd 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.embedded; +import java.util.Arrays; + import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -38,7 +40,9 @@ public class EmbeddedServletContainerJarPackagingIntegrationTests @Parameters(name = "{0}") public static Object[] parameters() { - return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar"); + return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar", + Arrays.asList(PackagedApplicationLauncher.class, + ExplodedApplicationLauncher.class)); } public EmbeddedServletContainerJarPackagingIntegrationTests(String name, diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java new file mode 100644 index 0000000000..7713ee1a4d --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2017 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.context.embedded; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for Spring Boot's embedded servlet container support when developing + * a war application. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class EmbeddedServletContainerWarDevelopmentIntegrationTests + extends AbstractEmbeddedServletContainerIntegrationTests { + + @Parameters(name = "{0}") + public static Object[] parameters() { + return AbstractEmbeddedServletContainerIntegrationTests.parameters("war", Arrays + .asList(BootRunApplicationLauncher.class, IdeApplicationLauncher.class)); + } + + public EmbeddedServletContainerWarDevelopmentIntegrationTests(String name, + AbstractApplicationLauncher launcher) { + super(name, launcher); + } + + @Test + public void metaInfResourceFromDependencyIsAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void metaInfResourceFromDependencyIsAvailableViaServletContext() + throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void webappResourcesAreAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest.getForEntity("/webapp-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java index 131319bf08..c8d2309558 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.embedded; +import java.util.Arrays; + import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -38,7 +40,9 @@ public class EmbeddedServletContainerWarPackagingIntegrationTests @Parameters(name = "{0}") public static Object[] parameters() { - return AbstractEmbeddedServletContainerIntegrationTests.parameters("war"); + return AbstractEmbeddedServletContainerIntegrationTests.parameters("war", + Arrays.asList(PackagedApplicationLauncher.class, + ExplodedApplicationLauncher.class)); } public EmbeddedServletContainerWarPackagingIntegrationTests(String name, diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java index 021c219291..52bb10c664 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java @@ -29,8 +29,8 @@ import org.springframework.util.FileSystemUtils; import org.springframework.util.StreamUtils; /** - * {@link AbstractApplicationLauncher} that launches an exploded Spring Boot application - * using Spring Boot's Jar or War launcher. + * {@link AbstractApplicationLauncher} that launches a Spring Boot application using + * {@code JarLauncher} or {@code WarLauncher} and an exploded archive. * * @author Andy Wilkinson */ @@ -42,6 +42,16 @@ class ExplodedApplicationLauncher extends AbstractApplicationLauncher { super(applicationBuilder); } + @Override + protected File getWorkingDirectory() { + return this.exploded; + } + + @Override + protected String getDescription(String packaging) { + return "exploded " + packaging; + } + @Override protected List getArguments(File archive) { String mainClass = archive.getName().endsWith(".war") diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java new file mode 100644 index 0000000000..1dc5ac3033 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2017 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.context.embedded; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.springframework.util.FileSystemUtils; +import org.springframework.util.StreamUtils; +import org.springframework.util.StringUtils; + +/** + * {@link AbstractApplicationLauncher} that launches a Spring Boot application with a + * classpath similar to that used when run in an IDE. + * + * @author Andy Wilkinson + */ +class IdeApplicationLauncher extends AbstractApplicationLauncher { + + private final File exploded = new File("target/ide"); + + IdeApplicationLauncher(ApplicationBuilder applicationBuilder) { + super(applicationBuilder); + } + + @Override + protected File getWorkingDirectory() { + return this.exploded; + } + + @Override + protected String getDescription(String packaging) { + return "IDE run " + packaging + " project"; + } + + @Override + protected List getArguments(File archive) { + try { + explodeArchive(archive, this.exploded); + deleteLauncherClasses(); + File targetClasses = populateTargetClasses(archive); + File dependencies = populateDependencies(archive); + File resourcesProject = explodedResourcesProject(dependencies); + populateSrcMainWebapp(); + List classpath = new ArrayList(); + classpath.add(targetClasses.getAbsolutePath()); + for (File dependency : dependencies.listFiles()) { + classpath.add(dependency.getAbsolutePath()); + } + classpath.add(resourcesProject.getAbsolutePath()); + return Arrays.asList("-cp", + StringUtils.collectionToDelimitedString(classpath, + File.pathSeparator), + "com.example.ResourceHandlingApplication"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private File populateTargetClasses(File archive) { + File targetClasses = new File(this.exploded, "target/classes"); + targetClasses.mkdirs(); + new File(this.exploded, getClassesPath(archive)).renameTo(targetClasses); + return targetClasses; + } + + private File populateDependencies(File archive) { + File dependencies = new File(this.exploded, "dependencies"); + dependencies.mkdirs(); + List libPaths = getLibPaths(archive); + for (String libPath : libPaths) { + for (File jar : new File(this.exploded, libPath).listFiles()) { + jar.renameTo(new File(dependencies, jar.getName())); + } + } + return dependencies; + } + + private File explodedResourcesProject(File dependencies) throws IOException { + File resourcesProject = new File(this.exploded, + "resources-project/target/classes"); + File resourcesJar = new File(dependencies, "resources-1.0.jar"); + explodeArchive(resourcesJar, resourcesProject); + resourcesJar.delete(); + return resourcesProject; + } + + private void populateSrcMainWebapp() { + File srcMainWebapp = new File(this.exploded, "src/main/webapp"); + srcMainWebapp.mkdirs(); + new File(this.exploded, "webapp-resource.txt") + .renameTo(new File(srcMainWebapp, "webapp-resource.txt")); + } + + private void deleteLauncherClasses() { + FileSystemUtils.deleteRecursively(new File(this.exploded, "org")); + } + + private String getClassesPath(File archive) { + return archive.getName().endsWith(".jar") ? "BOOT-INF/classes" + : "WEB-INF/classes"; + } + + private List getLibPaths(File archive) { + return archive.getName().endsWith(".jar") + ? Collections.singletonList("BOOT-INF/lib") + : Arrays.asList("WEB-INF/lib", "WEB-INF/lib-provided"); + } + + private void explodeArchive(File archive, File destination) throws IOException { + FileSystemUtils.deleteRecursively(destination); + JarFile jarFile = new JarFile(archive); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry jarEntry = entries.nextElement(); + File extracted = new File(destination, jarEntry.getName()); + if (jarEntry.isDirectory()) { + extracted.mkdirs(); + } + else { + FileOutputStream extractedOutputStream = new FileOutputStream(extracted); + StreamUtils.copy(jarFile.getInputStream(jarEntry), extractedOutputStream); + extractedOutputStream.close(); + } + } + jarFile.close(); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/PackagedApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/PackagedApplicationLauncher.java index 0c352ec733..3c176a1040 100644 --- a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/PackagedApplicationLauncher.java +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/PackagedApplicationLauncher.java @@ -32,6 +32,16 @@ class PackagedApplicationLauncher extends AbstractApplicationLauncher { super(applicationBuilder); } + @Override + protected File getWorkingDirectory() { + return null; + } + + @Override + protected String getDescription(String packaging) { + return "packaged " + packaging; + } + @Override protected List getArguments(File archive) { return Arrays.asList("-jar", archive.getAbsolutePath()); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java index 9a668b7491..0d43cc0bc4 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerFactory.java @@ -96,15 +96,23 @@ public abstract class AbstractEmbeddedServletContainerFactory if (classLoader instanceof URLClassLoader) { for (URL url : ((URLClassLoader) classLoader).getURLs()) { try { - URLConnection connection = url.openConnection(); - if (connection instanceof JarURLConnection) { - JarURLConnection jarConnection = (JarURLConnection) connection; - JarFile jar = jarConnection.getJarFile(); - if (jar.getName().endsWith(".jar") - && jar.getJarEntry("META-INF/resources") != null) { + if ("file".equals(url.getProtocol())) { + File file = new File(url.getFile()); + if (file.isDirectory() + && new File(file, "META-INF/resources").isDirectory()) { staticResourceUrls.add(url); } - jar.close(); + else if (isResourcesJar(file)) { + staticResourceUrls.add(url); + } + } + else { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + if (isResourcesJar((JarURLConnection) connection)) { + staticResourceUrls.add(url); + } + } } } catch (IOException ex) { @@ -115,6 +123,34 @@ public abstract class AbstractEmbeddedServletContainerFactory return staticResourceUrls; } + private boolean isResourcesJar(JarURLConnection connection) { + try { + return isResourcesJar(connection.getJarFile()); + } + catch (IOException ex) { + return false; + } + } + + private boolean isResourcesJar(File file) { + try { + return isResourcesJar(new JarFile(file)); + } + catch (IOException ex) { + return false; + } + } + + private boolean isResourcesJar(JarFile jar) throws IOException { + try { + return jar.getName().endsWith(".jar") + && (jar.getJarEntry("META-INF/resources") != null); + } + finally { + jar.close(); + } + } + File getExplodedWarFileDocumentRoot(File codeSourceFile) { if (this.logger.isDebugEnabled()) { this.logger.debug("Code archive: " + codeSourceFile); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java index 243132fb84..f11ab3a5fa 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/jetty/JettyEmbeddedServletContainerFactory.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.jetty; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; @@ -410,8 +411,7 @@ public class JettyEmbeddedServletContainerFactory root.isDirectory() ? Resource.newResource(root.getCanonicalFile()) : JarResource.newJarResource(Resource.newResource(root))); for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) { - Resource resource = Resource - .newResource(resourceJarUrl + "META-INF/resources"); + Resource resource = createResource(resourceJarUrl); // Jetty 9.2 and earlier do not support nested jars. See // https://github.com/eclipse/jetty.project/issues/518 if (resource.exists() && resource.isDirectory()) { @@ -426,6 +426,16 @@ public class JettyEmbeddedServletContainerFactory } } + private Resource createResource(URL url) throws MalformedURLException { + if ("file".equals(url.getProtocol())) { + File file = new File(url.getFile()); + if (file.isFile()) { + return Resource.newResource("jar:" + url + "!/META-INF/resources"); + } + } + return Resource.newResource(url + "META-INF/resources"); + } + /** * Add Jetty's {@code DefaultServlet} to the given {@link WebAppContext}. * @param context the jetty {@link WebAppContext} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatResources.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatResources.java index 84b9ccbac5..716ff9f03c 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatResources.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatResources.java @@ -22,7 +22,6 @@ import java.net.URL; import java.util.List; import javax.naming.directory.DirContext; -import javax.servlet.ServletContext; import org.apache.catalina.Context; import org.apache.catalina.WebResourceRoot.ResourceSetType; @@ -57,6 +56,9 @@ abstract class TomcatResources { } addJar(jar); } + else { + addDir(file, url); + } } } @@ -127,7 +129,7 @@ abstract class TomcatResources { @Override protected void addDir(String dir, URL url) { - if (getContext() instanceof ServletContext) { + if (getContext() instanceof StandardContext) { try { Class fileDirContextClass = Class .forName("org.apache.naming.resources.FileDirContext"); diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java index 75a5f3219d..53c1f108c1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/UndertowEmbeddedServletContainerFactory.java @@ -18,6 +18,7 @@ package org.springframework.boot.context.embedded.undertow; import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; @@ -468,11 +469,35 @@ public class UndertowEmbeddedServletContainerFactory private ResourceManager getDocumentRootResourceManager() { File root = getCanonicalDocumentRoot(); - List metaInfResourceJarUrls = getUrlsOfJarsWithMetaInfResources(); + List metaInfResourceUrls = getUrlsOfJarsWithMetaInfResources(); + List resourceJarUrls = new ArrayList(); + List resourceManagers = new ArrayList(); ResourceManager rootResourceManager = root.isDirectory() ? new FileResourceManager(root, 0) : new JarResourceManager(root); - return new CompositeResourceManager(rootResourceManager, - new MetaInfResourcesResourceManager(metaInfResourceJarUrls)); + resourceManagers.add(rootResourceManager); + for (URL url : metaInfResourceUrls) { + if ("file".equals(url.getProtocol())) { + File file = new File(url.getFile()); + if (file.isFile()) { + try { + resourceJarUrls.add(new URL("jar:" + url + "!/")); + } + catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + } + else { + resourceManagers.add(new FileResourceManager( + new File(file, "META-INF/resources"), 0)); + } + } + else { + resourceJarUrls.add(url); + } + } + resourceManagers.add(new MetaInfResourcesResourceManager(resourceJarUrls)); + return new CompositeResourceManager( + resourceManagers.toArray(new ResourceManager[resourceManagers.size()])); } /** @@ -617,12 +642,17 @@ public class UndertowEmbeddedServletContainerFactory } @Override - public Resource getResource(String path) throws IOException { + public Resource getResource(String path) { for (URL url : this.metaInfResourceJarUrls) { - URL resourceUrl = new URL(url + "META-INF/resources" + path); - URLConnection connection = resourceUrl.openConnection(); - if (connection.getContentLength() >= 0) { - return new URLResource(resourceUrl, connection, path); + try { + URL resourceUrl = new URL(url + "META-INF/resources" + path); + URLConnection connection = resourceUrl.openConnection(); + if (connection.getContentLength() >= 0) { + return new URLResource(resourceUrl, connection, path); + } + } + catch (IOException ex) { + // Continue } } return null;