From a2cf0455fdddb22380fd695688bbf0b7fef3e403 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 9 Mar 2017 12:22:55 +0000 Subject: [PATCH] Fix static resource handling when run in IDE or using Maven or Gradle The changes made for gh-8299 attempted to make static resource handling consistent across Jetty, Tomcat, and Undertow. They did so for application's launched using JarLauncher or WarLauncher but did not consider application's launched in an IDE or using spring-boot:run in Maven or bootRun in Gradle. Running in an IDE or via Maven or Gradle introduces two new resource locations: - Jars on the classpath with file protocol URLs (they are always jar protocol URLs when using either launcher) - Directories on the classpath from a project that is depended upon and contains resources in META-INF/resources This commit updates the factories for all three containers to handle these new resources locations. The integration tests have also been updated. --- .../embedded/AbstractApplicationLauncher.java | 24 ++- ...eddedServletContainerIntegrationTests.java | 39 +++-- .../embedded/BootRunApplicationLauncher.java | 141 ++++++++++++++++ ...ntainerJarDevelopmentIntegrationTests.java | 67 ++++++++ ...ContainerJarPackagingIntegrationTests.java | 6 +- ...ntainerWarDevelopmentIntegrationTests.java | 74 +++++++++ ...ContainerWarPackagingIntegrationTests.java | 6 +- .../embedded/ExplodedApplicationLauncher.java | 14 +- .../embedded/IdeApplicationLauncher.java | 152 ++++++++++++++++++ .../embedded/PackagedApplicationLauncher.java | 10 ++ ...stractEmbeddedServletContainerFactory.java | 50 +++++- .../JettyEmbeddedServletContainerFactory.java | 14 +- .../embedded/tomcat/TomcatResources.java | 6 +- ...dertowEmbeddedServletContainerFactory.java | 46 +++++- 14 files changed, 604 insertions(+), 45 deletions(-) create mode 100644 spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/BootRunApplicationLauncher.java create mode 100644 spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java create mode 100644 spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java create mode 100644 spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java 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;