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 3c192162ecf..4b803a7faff 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 fd6638fe231..70fb93d2bf9 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 @@ -47,25 +47,33 @@ 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")); - parameters.addAll(createParameters(packaging, "tomcat")); - parameters.addAll(createParameters(packaging, "undertow")); + parameters.addAll(createParameters(packaging, "jetty", applicationLaunchers)); + parameters.addAll(createParameters(packaging, "tomcat", applicationLaunchers)); + parameters.addAll(createParameters(packaging, "undertow", applicationLaunchers)); return parameters.toArray(new Object[parameters.size()]); } private static List createParameters(String packaging, String container, - String... versions) { - List parameters = new ArrayList<>(); + List> applicationLaunchers) { + List parameters = new ArrayList(); ApplicationBuilder applicationBuilder = new ApplicationBuilder(temporaryFolder, packaging, container); - parameters.add(new Object[] { - StringUtils.capitalise(container) + " packaged " + packaging, - new PackagedApplicationLauncher(applicationBuilder) }); - parameters.add(new Object[] { - StringUtils.capitalise(container) + " exploded " + packaging, - new ExplodedApplicationLauncher(applicationBuilder) }); + for (Class launcherClass : applicationLaunchers) { + try { + AbstractApplicationLauncher launcher = launcherClass + .getDeclaredConstructor(ApplicationBuilder.class) + .newInstance(applicationBuilder); + String name = StringUtils.capitalise(container) + ": " + + 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 00000000000..1488614a6af --- /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 00000000000..ce70ec15faa --- /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 fd1a7cc466f..98e81854cdd 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 00000000000..7713ee1a4da --- /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 131319bf080..c8d23095581 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 021c2192917..52bb10c6649 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 00000000000..1dc5ac3033d --- /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 0c352ec7334..3c176a1040e 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 87fa06faefb..7eb55655adb 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 143dd120314..deeed9b2438 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 @@ -392,8 +392,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()) { @@ -408,6 +407,16 @@ public class JettyEmbeddedServletContainerFactory } } + private Resource createResource(URL url) throws IOException { + 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/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index 4d84190327a..3147177262a 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -840,20 +840,23 @@ public class TomcatEmbeddedServletContainerFactory // A jar file in the file system. Convert to Jar URL. jar = "jar:" + jar + "!/"; } - addJar(jar); + addResourceSet(jar); + } + else { + addResourceSet(url.toString()); } } } - private void addJar(String jar) { + private void addResourceSet(String resource) { try { - if (isInsideNestedJar(jar)) { + if (isInsideNestedJar(resource)) { // It's a nested jar but we now don't want the suffix because Tomcat // is going to try and locate it as a root URL (not the resource // inside it) - jar = jar.substring(0, jar.length() - 2); + resource = resource.substring(0, resource.length() - 2); } - URL url = new URL(jar); + URL url = new URL(resource); String path = "/META-INF/resources"; this.context.getResources().createWebResourceSet( ResourceSetType.RESOURCE_JAR, "/", url, path); 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 new file mode 100644 index 00000000000..716ff9f03cf --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatResources.java @@ -0,0 +1,196 @@ +/* + * 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.tomcat; + +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import javax.naming.directory.DirContext; + +import org.apache.catalina.Context; +import org.apache.catalina.WebResourceRoot.ResourceSetType; +import org.apache.catalina.core.StandardContext; + +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Abstraction to add resources that works with both Tomcat 8 and 7. + * + * @author Dave Syer + * @author Phillip Webb + * @author Andy Wilkinson + */ +abstract class TomcatResources { + + private final Context context; + + TomcatResources(Context context) { + this.context = context; + } + + void addResourceJars(List resourceJarUrls) { + for (URL url : resourceJarUrls) { + String file = url.getFile(); + if (file.endsWith(".jar") || file.endsWith(".jar!/")) { + String jar = url.toString(); + if (!jar.startsWith("jar:")) { + // A jar file in the file system. Convert to Jar URL. + jar = "jar:" + jar + "!/"; + } + addJar(jar); + } + else { + addDir(file, url); + } + } + } + + protected final Context getContext() { + return this.context; + } + + /** + * Called to add a JAR to the resources. + * @param jar the URL spec for the jar + */ + protected abstract void addJar(String jar); + + /** + * Called to add a dir to the resource. + * @param dir the dir + * @param url the URL + */ + protected abstract void addDir(String dir, URL url); + + /** + * Return a {@link TomcatResources} instance for the currently running Tomcat version. + * @param context the tomcat context + * @return a {@link TomcatResources} instance. + */ + public static TomcatResources get(Context context) { + if (ClassUtils.isPresent("org.apache.catalina.deploy.ErrorPage", null)) { + return new Tomcat7Resources(context); + } + return new Tomcat8Resources(context); + } + + /** + * {@link TomcatResources} for Tomcat 7. + */ + private static class Tomcat7Resources extends TomcatResources { + + private final Method addResourceJarUrlMethod; + + Tomcat7Resources(Context context) { + super(context); + this.addResourceJarUrlMethod = ReflectionUtils.findMethod(context.getClass(), + "addResourceJarUrl", URL.class); + } + + @Override + protected void addJar(String jar) { + URL url = getJarUrl(jar); + if (url != null) { + try { + this.addResourceJarUrlMethod.invoke(getContext(), url); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + + private URL getJarUrl(String jar) { + try { + return new URL(jar); + } + catch (MalformedURLException ex) { + // Ignore + return null; + } + } + + @Override + protected void addDir(String dir, URL url) { + if (getContext() instanceof StandardContext) { + try { + Class fileDirContextClass = Class + .forName("org.apache.naming.resources.FileDirContext"); + Method setDocBaseMethod = ReflectionUtils + .findMethod(fileDirContextClass, "setDocBase", String.class); + Object fileDirContext = fileDirContextClass.newInstance(); + setDocBaseMethod.invoke(fileDirContext, dir); + Method addResourcesDirContextMethod = ReflectionUtils.findMethod( + StandardContext.class, "addResourcesDirContext", + DirContext.class); + addResourcesDirContextMethod.invoke(getContext(), fileDirContext); + } + catch (Exception ex) { + throw new IllegalStateException("Tomcat 7 reflection failed", ex); + } + } + } + + } + + /** + * {@link TomcatResources} for Tomcat 8. + */ + static class Tomcat8Resources extends TomcatResources { + + Tomcat8Resources(Context context) { + super(context); + } + + @Override + protected void addJar(String jar) { + addResourceSet(jar); + } + + @Override + protected void addDir(String dir, URL url) { + addResourceSet(url.toString()); + } + + private void addResourceSet(String resource) { + try { + if (isInsideNestedJar(resource)) { + // It's a nested jar but we now don't want the suffix because Tomcat + // is going to try and locate it as a root URL (not the resource + // inside it) + resource = resource.substring(0, resource.length() - 2); + } + URL url = new URL(resource); + String path = "/META-INF/resources"; + getContext().getResources().createWebResourceSet( + ResourceSetType.RESOURCE_JAR, "/", url, path); + } + catch (Exception ex) { + // Ignore (probably not a directory) + } + } + + private boolean isInsideNestedJar(String dir) { + return dir.indexOf("!/") < dir.lastIndexOf("!/"); + } + + } + +} 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 f33fc180c4e..6c87135cf4e 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; @@ -464,11 +465,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()])); } /** @@ -618,12 +643,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;