diff --git a/spring-boot-integration-tests/pom.xml b/spring-boot-integration-tests/pom.xml index f6b6fd9bce..6b62d0d12f 100644 --- a/spring-boot-integration-tests/pom.xml +++ b/spring-boot-integration-tests/pom.xml @@ -22,6 +22,7 @@ spring-boot-devtools-tests + spring-boot-integration-tests-embedded-servlet-container spring-boot-gradle-tests spring-boot-launch-script-tests spring-boot-security-tests diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/pom.xml b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/pom.xml new file mode 100644 index 0000000000..59fb0a5da6 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-integration-tests + 2.0.0.BUILD-SNAPSHOT + + spring-boot-integration-tests-embedded-servlet-container + jar + Spring Boot Embedded Servlet Container Integration Tests + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.boot + spring-boot-starter-logging + test + + + org.springframework.boot + spring-boot-starter-test + test + + + com.samskivert + jmustache + test + + + javax.servlet + javax.servlet-api + test + + + org.apache.maven.shared + maven-invoker + 3.0.0 + test + + + org.springframework + spring-web + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${maven.home} + + + + + + diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/com/example/ResourceHandlingApplication.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/com/example/ResourceHandlingApplication.java new file mode 100644 index 0000000000..7ee6783e6e --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/com/example/ResourceHandlingApplication.java @@ -0,0 +1,72 @@ +/* + * 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 com.example; + +import java.io.IOException; +import java.net.URL; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.system.EmbeddedServerPortFileWriter; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; + +/** + * Test application for verifying an embedded container's static resource handling. + * + * @author Andy Wilkinson + */ +@SpringBootApplication +public class ResourceHandlingApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(ResourceHandlingApplication.class) + .properties("server.port:0") + .listeners(new EmbeddedServerPortFileWriter("target/server.port")) + .run(args); + } + + @Bean + public ServletRegistrationBean resourceServletRegistration() { + ServletRegistrationBean registration = new ServletRegistrationBean( + new HttpServlet() { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + URL resource = getServletContext() + .getResource(req.getQueryString()); + if (resource == null) { + resp.sendError(404); + } + else { + resp.getWriter().println(resource); + resp.getWriter().flush(); + } + } + + }); + registration.addUrlMappings("/servletContext"); + return registration; + } + +} 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 new file mode 100644 index 0000000000..bd16fdf01b --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractApplicationLauncher.java @@ -0,0 +1,96 @@ +/* + * 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.FileReader; +import java.lang.ProcessBuilder.Redirect; +import java.util.ArrayList; +import java.util.List; + +import org.junit.rules.ExternalResource; + +import org.springframework.util.FileCopyUtils; + +/** + * Base {@link ExternalResource} for launching a Spring Boot application as part of a + * JUnit test. + * + * @author Andy Wilkinson + */ +abstract class AbstractApplicationLauncher extends ExternalResource { + + private final File serverPortFile = new File("target/server.port"); + + private final ApplicationBuilder applicationBuilder; + + private Process process; + + private int httpPort; + + protected AbstractApplicationLauncher(ApplicationBuilder applicationBuilder) { + this.applicationBuilder = applicationBuilder; + } + + @Override + protected final void before() throws Throwable { + this.process = startApplication(); + } + + @Override + protected final void after() { + this.process.destroy(); + } + + public final int getHttpPort() { + return this.httpPort; + } + + protected abstract List getArguments(File archive); + + private Process startApplication() throws Exception { + this.serverPortFile.delete(); + File archive = this.applicationBuilder.buildApplication(); + List arguments = new ArrayList(); + arguments.add(System.getProperty("java.home") + "/bin/java"); + arguments.addAll(getArguments(archive)); + ProcessBuilder processBuilder = new ProcessBuilder( + arguments.toArray(new String[arguments.size()])); + processBuilder.redirectOutput(Redirect.INHERIT); + processBuilder.redirectError(Redirect.INHERIT); + Process process = processBuilder.start(); + this.httpPort = awaitServerPort(process); + return process; + } + + private int awaitServerPort(Process process) throws Exception { + long end = System.currentTimeMillis() + 30000; + while (this.serverPortFile.length() == 0) { + if (System.currentTimeMillis() > end) { + throw new IllegalStateException( + "server.port file was not written within 30 seconds"); + } + if (!process.isAlive()) { + throw new IllegalStateException("Application failed to launch"); + } + Thread.sleep(100); + } + return Integer.parseInt( + FileCopyUtils.copyToString(new FileReader(this.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 new file mode 100644 index 0000000000..f318cf3c07 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerIntegrationTests.java @@ -0,0 +1,105 @@ +/* + * 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.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.codehaus.plexus.util.StringUtils; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriTemplateHandler; + +/** + * Base class for embedded servlet container integration tests. + * + * @author Andy Wilkinson + */ +public abstract class AbstractEmbeddedServletContainerIntegrationTests { + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Rule + public final AbstractApplicationLauncher launcher; + + protected final RestTemplate rest = new RestTemplate(); + + public static Object[] parameters(String packaging) { + List parameters = new ArrayList(); + parameters.addAll(createParameters(packaging, "jetty")); + parameters.addAll(createParameters(packaging, "tomcat")); + parameters.addAll(createParameters(packaging, "undertow")); + return parameters.toArray(new Object[parameters.size()]); + } + + private static List createParameters(String packaging, String container, + String... versions) { + 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) }); + return parameters; + } + + protected AbstractEmbeddedServletContainerIntegrationTests(String name, + AbstractApplicationLauncher launcher) { + this.launcher = launcher; + this.rest.setErrorHandler(new ResponseErrorHandler() { + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return false; + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + + } + + }); + this.rest.setUriTemplateHandler(new UriTemplateHandler() { + + @Override + public URI expand(String uriTemplate, Object... uriVariables) { + return URI.create( + "http://localhost:" + launcher.getHttpPort() + uriTemplate); + } + + @Override + public URI expand(String uriTemplate, Map uriVariables) { + return URI.create( + "http://localhost:" + launcher.getHttpPort() + uriTemplate); + } + + }); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java new file mode 100644 index 0000000000..67d0a5356e --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2016 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.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import com.samskivert.mustache.Mustache; +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.rules.TemporaryFolder; + +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Builds a Spring Boot application using Maven. To use this class, the {@code maven.home} + * system property must be set. + * + * @author Andy Wilkinson + */ +class ApplicationBuilder { + + private final TemporaryFolder temp; + + private final String packaging; + + private final String container; + + ApplicationBuilder(TemporaryFolder temp, String packaging, String container) { + this.temp = temp; + this.packaging = packaging; + this.container = container; + } + + File buildApplication() throws Exception { + File containerFolder = new File(this.temp.getRoot(), this.container); + if (containerFolder.exists()) { + return new File(containerFolder, "app/target/app-0.0.1." + this.packaging); + } + return doBuildApplication(containerFolder); + } + + private File doBuildApplication(File containerFolder) + throws IOException, FileNotFoundException, MavenInvocationException { + File resourcesJar = createResourcesJar(); + File appFolder = new File(containerFolder, "app"); + appFolder.mkdirs(); + writePom(appFolder, resourcesJar); + copyApplicationSource(appFolder); + packageApplication(appFolder); + return new File(appFolder, "target/app-0.0.1." + this.packaging); + } + + private File createResourcesJar() throws IOException, FileNotFoundException { + File resourcesJar = new File(this.temp.getRoot(), "resources.jar"); + if (resourcesJar.exists()) { + return resourcesJar; + } + JarOutputStream resourcesJarStream = new JarOutputStream( + new FileOutputStream(resourcesJar)); + resourcesJarStream.putNextEntry(new ZipEntry("META-INF/resources/")); + resourcesJarStream.closeEntry(); + resourcesJarStream.putNextEntry( + new ZipEntry("META-INF/resources/nested-meta-inf-resource.txt")); + resourcesJarStream.write("nested".getBytes()); + resourcesJarStream.closeEntry(); + resourcesJarStream.close(); + return resourcesJar; + } + + private void writePom(File appFolder, File resourcesJar) + throws FileNotFoundException, IOException { + Map context = new HashMap(); + context.put("packaging", this.packaging); + context.put("container", this.container); + context.put("bootVersion", Versions.getBootVersion()); + context.put("resourcesJarPath", resourcesJar.getAbsolutePath()); + FileWriter out = new FileWriter(new File(appFolder, "pom.xml")); + Mustache.compiler().escapeHTML(false) + .compile(new FileReader("src/test/resources/pom-template.xml")) + .execute(context, out); + out.close(); + } + + private void copyApplicationSource(File appFolder) throws IOException { + File examplePackage = new File(appFolder, "src/main/java/com/example"); + examplePackage.mkdirs(); + FileCopyUtils.copy( + new File("src/test/java/com/example/ResourceHandlingApplication.java"), + new File(examplePackage, "ResourceHandlingApplication.java")); + if ("war".equals(this.packaging)) { + File srcMainWebapp = new File(appFolder, "src/main/webapp"); + srcMainWebapp.mkdirs(); + FileCopyUtils.copy("webapp resource", + new FileWriter(new File(srcMainWebapp, "webapp-resource.txt"))); + } + } + + private void packageApplication(File appFolder) throws MavenInvocationException { + InvocationRequest invocation = new DefaultInvocationRequest(); + invocation.setBaseDirectory(appFolder); + invocation.setGoals(Collections.singletonList("package")); + InvocationResult execute = new DefaultInvoker().execute(invocation); + assertThat(execute.getExitCode()).isEqualTo(0); + } + +} 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 new file mode 100644 index 0000000000..fd1a7cc466 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java @@ -0,0 +1,85 @@ +/* + * 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 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 using jar + * packaging. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class EmbeddedServletContainerJarPackagingIntegrationTests + extends AbstractEmbeddedServletContainerIntegrationTests { + + @Parameters(name = "{0}") + public static Object[] parameters() { + return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar"); + } + + public EmbeddedServletContainerJarPackagingIntegrationTests(String name, + AbstractApplicationLauncher launcher) { + super(name, launcher); + } + + @Test + public void nestedMetaInfResourceIsAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void nestedMetaInfResourceIsAvailableViaServletContext() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void nestedJarIsNotAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/BOOT-INF/lib/resources-1.0.jar", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void applicationClassesAreNotAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest.getForEntity( + "/BOOT-INF/classes/com/example/ResourceHandlingApplication.class", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void launcherIsNotAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest.getForEntity( + "/org/springframework/boot/loader/Launcher.class", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + +} 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 new file mode 100644 index 0000000000..131319bf08 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java @@ -0,0 +1,85 @@ +/* + * 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 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 using war + * packaging. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public class EmbeddedServletContainerWarPackagingIntegrationTests + extends AbstractEmbeddedServletContainerIntegrationTests { + + @Parameters(name = "{0}") + public static Object[] parameters() { + return AbstractEmbeddedServletContainerIntegrationTests.parameters("war"); + } + + public EmbeddedServletContainerWarPackagingIntegrationTests(String name, + AbstractApplicationLauncher launcher) { + super(name, launcher); + } + + @Test + public void nestedMetaInfResourceIsAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void nestedMetaInfResourceIsAvailableViaServletContext() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/nested-meta-inf-resource.txt", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + public void nestedJarIsNotAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest + .getForEntity("/WEB-INF/lib/resources-1.0.jar", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void applicationClassesAreNotAvailableViaHttp() throws Exception { + ResponseEntity entity = this.rest.getForEntity( + "/WEB-INF/classes/com/example/ResourceHandlingApplication.class", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @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/ExplodedApplicationLauncher.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java new file mode 100644 index 0000000000..021c219291 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java @@ -0,0 +1,78 @@ +/* + * 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.Arrays; +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; + +/** + * {@link AbstractApplicationLauncher} that launches an exploded Spring Boot application + * using Spring Boot's Jar or War launcher. + * + * @author Andy Wilkinson + */ +class ExplodedApplicationLauncher extends AbstractApplicationLauncher { + + private final File exploded = new File("target/exploded"); + + ExplodedApplicationLauncher(ApplicationBuilder applicationBuilder) { + super(applicationBuilder); + } + + @Override + protected List getArguments(File archive) { + String mainClass = archive.getName().endsWith(".war") + ? "org.springframework.boot.loader.WarLauncher" + : "org.springframework.boot.loader.JarLauncher"; + try { + explodeArchive(archive); + return Arrays.asList("-cp", this.exploded.getAbsolutePath(), mainClass); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + 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(); + } + +} 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 new file mode 100644 index 0000000000..0c352ec733 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/PackagedApplicationLauncher.java @@ -0,0 +1,40 @@ +/* + * 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.util.Arrays; +import java.util.List; + +/** + * {@link AbstractApplicationLauncher} that launches a packaged Spring Boot application + * using {@code java -jar}. + * + * @author Andy Wilkinson + */ +class PackagedApplicationLauncher extends AbstractApplicationLauncher { + + PackagedApplicationLauncher(ApplicationBuilder applicationBuilder) { + super(applicationBuilder); + } + + @Override + protected List getArguments(File archive) { + return Arrays.asList("-jar", archive.getAbsolutePath()); + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/Versions.java b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/Versions.java new file mode 100644 index 0000000000..2f1c05e8a8 --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/Versions.java @@ -0,0 +1,56 @@ +/* + * 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.FileReader; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import org.xml.sax.InputSource; + +/** + * Provides access to dependency versions by querying the project's pom. + * + * @author Andy Wilkinson + */ +final class Versions { + + private Versions() { + } + + public static String getBootVersion() { + return evaluateExpression( + "/*[local-name()='project']/*[local-name()='parent']/*[local-name()='version']" + + "/text()"); + } + + private static String evaluateExpression(String expression) { + try { + XPathFactory xPathFactory = XPathFactory.newInstance(); + XPath xpath = xPathFactory.newXPath(); + XPathExpression expr = xpath.compile(expression); + String version = expr.evaluate(new InputSource(new FileReader("pom.xml"))); + return version; + } + catch (Exception ex) { + throw new IllegalStateException("Failed to evaluate expression", ex); + } + } + +} diff --git a/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/resources/pom-template.xml b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/resources/pom-template.xml new file mode 100644 index 0000000000..9be1b542dd --- /dev/null +++ b/spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/resources/pom-template.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + {{bootVersion}} + + + + com.example + app + 0.0.1 + {{packaging}} + + + {{resourcesJarPath}} + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-{{container}} + + + org.springframework + spring-web + + + javax.servlet + javax.servlet-api + provided + + + com.example + resources + 1.0 + system + ${resourcesJarPath} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + true + + + + + + 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 9dfbd728cd..5bb072cf42 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 @@ -20,9 +20,13 @@ import java.io.File; import java.io.IOException; import java.net.JarURLConnection; import java.net.URL; +import java.net.URLClassLoader; import java.net.URLConnection; import java.security.CodeSource; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.jar.JarFile; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -86,6 +90,31 @@ public abstract class AbstractEmbeddedServletContainerFactory return getExplodedWarFileDocumentRoot(getCodeSourceArchive()); } + protected List getUrlsOfJarsWithMetaInfResources() { + ClassLoader classLoader = getClass().getClassLoader(); + List staticResourceUrls = new ArrayList(); + 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) { + staticResourceUrls.add(url); + } + jar.close(); + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + } + return staticResourceUrls; + } + 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 44dcb82f91..67961dce50 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 @@ -57,6 +57,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.resource.JarResource; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.ThreadPool; import org.eclipse.jetty.webapp.AbstractConfiguration; @@ -386,14 +387,21 @@ public class JettyEmbeddedServletContainerFactory File root = getValidDocumentRoot(); root = (root != null ? root : createTempDir("jetty-docbase")); try { - if (!root.isDirectory()) { - Resource resource = JarResource - .newJarResource(Resource.newResource(root)); - handler.setBaseResource(resource); - } - else { - handler.setBaseResource(Resource.newResource(root.getCanonicalFile())); + List resources = new ArrayList(); + resources.add( + root.isDirectory() ? Resource.newResource(root.getCanonicalFile()) + : JarResource.newJarResource(Resource.newResource(root))); + for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) { + Resource resource = Resource + .newResource(resourceJarUrl + "META-INF/resources"); + // Jetty 9.2 and earlier do not support nested jars. See + // https://github.com/eclipse/jetty.project/issues/518 + if (resource.exists() && resource.isDirectory()) { + resources.add(resource); + } } + handler.setBaseResource(new ResourceCollection( + resources.toArray(new Resource[resources.size()]))); } catch (Exception ex) { throw new IllegalStateException(ex); 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 df1eec2691..64e2d8f22c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.boot.context.embedded.tomcat; import java.io.File; import java.io.FileNotFoundException; +import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -41,6 +42,7 @@ import org.apache.catalina.LifecycleEvent; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Manager; import org.apache.catalina.Valve; +import org.apache.catalina.WebResourceRoot.ResourceSetType; import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.loader.WebappLoader; @@ -185,7 +187,7 @@ public class TomcatEmbeddedServletContainerFactory protected void prepareContext(Host host, ServletContextInitializer[] initializers) { File docBase = getValidDocumentRoot(); docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase")); - TomcatEmbeddedContext context = new TomcatEmbeddedContext(); + final TomcatEmbeddedContext context = new TomcatEmbeddedContext(); context.setName(getContextPath()); context.setDisplayName(getDisplayName()); context.setPath(getContextPath()); @@ -214,6 +216,7 @@ public class TomcatEmbeddedServletContainerFactory addJspServlet(context); addJasperInitializer(context); } + context.addLifecycleListener(new StaticResourceConfigurer(context)); ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); configureContext(context, initializersToUse); host.addChild(context); @@ -817,4 +820,57 @@ public class TomcatEmbeddedServletContainerFactory } + private final class StaticResourceConfigurer implements LifecycleListener { + + private final Context context; + + private StaticResourceConfigurer(Context context) { + this.context = context; + } + + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { + addResourceJars(getUrlsOfJarsWithMetaInfResources()); + } + } + + private 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); + } + } + } + + private void addJar(String jar) { + try { + if (isInsideNestedJar(jar)) { + // 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); + } + URL url = new URL(jar); + String path = "/META-INF/resources"; + this.context.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/CompositeResourceManager.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/CompositeResourceManager.java new file mode 100644 index 0000000000..661f78f65a --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/undertow/CompositeResourceManager.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.undertow; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import io.undertow.UndertowMessages; +import io.undertow.server.handlers.resource.Resource; +import io.undertow.server.handlers.resource.ResourceChangeListener; +import io.undertow.server.handlers.resource.ResourceManager; + +/** + * A {@ResourceManager} that delegates to multiple {@code ResourceManager} instances. + * + * @author Andy Wilkinson + */ +class CompositeResourceManager implements ResourceManager { + + private final List resourceManagers; + + CompositeResourceManager(ResourceManager... resourceManagers) { + this.resourceManagers = Arrays.asList(resourceManagers); + } + + @Override + public void close() throws IOException { + for (ResourceManager resourceManager : this.resourceManagers) { + resourceManager.close(); + } + } + + @Override + public Resource getResource(String path) throws IOException { + for (ResourceManager resourceManager : this.resourceManagers) { + Resource resource = resourceManager.getResource(path); + if (resource != null) { + return resource; + } + } + return null; + } + + @Override + public boolean isResourceChangeListenerSupported() { + return false; + } + + @Override + public void registerResourceChangeListener(ResourceChangeListener listener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported(); + } + + @Override + public void removeResourceChangeListener(ResourceChangeListener listener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported(); + } + +} 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 8697555a13..603d7e8154 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 @@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.undertow; import java.io.File; import java.io.IOException; import java.net.URL; +import java.net.URLConnection; import java.nio.charset.Charset; import java.security.KeyManagementException; import java.security.KeyStore; @@ -49,7 +50,10 @@ import io.undertow.server.handlers.accesslog.AccessLogHandler; import io.undertow.server.handlers.accesslog.AccessLogReceiver; import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; import io.undertow.server.handlers.resource.FileResourceManager; +import io.undertow.server.handlers.resource.Resource; +import io.undertow.server.handlers.resource.ResourceChangeListener; import io.undertow.server.handlers.resource.ResourceManager; +import io.undertow.server.handlers.resource.URLResource; import io.undertow.server.session.SessionManager; import io.undertow.servlet.Servlets; import io.undertow.servlet.api.DeploymentInfo; @@ -461,13 +465,11 @@ public class UndertowEmbeddedServletContainerFactory private ResourceManager getDocumentRootResourceManager() { File root = getCanonicalDocumentRoot(); - if (root.isDirectory()) { - return new FileResourceManager(root, 0); - } - if (root.isFile()) { - return new JarResourceManager(root); - } - return ResourceManager.EMPTY_RESOURCE_MANAGER; + List metaInfResourceJarUrls = getUrlsOfJarsWithMetaInfResources(); + ResourceManager rootResourceManager = root.isDirectory() + ? new FileResourceManager(root, 0) : new JarResourceManager(root); + return new CompositeResourceManager(rootResourceManager, + new MetaInfResourcesResourceManager(metaInfResourceJarUrls)); } /** @@ -599,6 +601,49 @@ public class UndertowEmbeddedServletContainerFactory this.useForwardHeaders = useForwardHeaders; } + /** + * {@link ResourceManager} that exposes resource in {@code META-INF/resources} + * directory of nested (in {@code BOOT-INF/lib} or {@code WEB-INF/lib}) jars. + */ + private static final class MetaInfResourcesResourceManager + implements ResourceManager { + + private final List metaInfResourceJarUrls; + + private MetaInfResourcesResourceManager(List metaInfResourceJarUrls) { + this.metaInfResourceJarUrls = metaInfResourceJarUrls; + } + + @Override + public void close() throws IOException { + } + + @Override + public Resource getResource(String path) throws IOException { + 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); + } + } + return null; + } + + @Override + public boolean isResourceChangeListenerSupported() { + return false; + } + + @Override + public void registerResourceChangeListener(ResourceChangeListener listener) { + } + + @Override + public void removeResourceChangeListener(ResourceChangeListener listener) { + } + } + /** * {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer * ServletContextInitializers}.