Merge branch '1.5.x'

pull/8551/merge
Andy Wilkinson 8 years ago
commit b527e71474

@ -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<String> 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<String> 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)));
}
}

@ -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<Class<? extends AbstractApplicationLauncher>> applicationLaunchers) {
List<Object> 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<Object> createParameters(String packaging, String container,
String... versions) {
List<Object> parameters = new ArrayList<>();
List<Class<? extends AbstractApplicationLauncher>> applicationLaunchers) {
List<Object> parameters = new ArrayList<Object>();
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<? extends AbstractApplicationLauncher> 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;
}

@ -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<String> getArguments(File archive) {
try {
explodeArchive(archive);
deleteLauncherClasses();
File targetClasses = populateTargetClasses(archive);
File dependencies = populateDependencies(archive);
populateSrcMainWebapp();
List<String> classpath = new ArrayList<String>();
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<String> 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<String> 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<JarEntry> 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";
}
}

@ -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<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void metaInfResourceFromDependencyIsAvailableViaServletContext()
throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}

@ -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,

@ -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<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void metaInfResourceFromDependencyIsAvailableViaServletContext()
throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void webappResourcesAreAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest.getForEntity("/webapp-resource.txt",
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}

@ -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,

@ -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<String> getArguments(File archive) {
String mainClass = archive.getName().endsWith(".war")

@ -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<String> getArguments(File archive) {
try {
explodeArchive(archive, this.exploded);
deleteLauncherClasses();
File targetClasses = populateTargetClasses(archive);
File dependencies = populateDependencies(archive);
File resourcesProject = explodedResourcesProject(dependencies);
populateSrcMainWebapp();
List<String> classpath = new ArrayList<String>();
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<String> 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<String> 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<JarEntry> 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();
}
}

@ -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<String> getArguments(File archive) {
return Arrays.asList("-jar", archive.getAbsolutePath());

@ -96,15 +96,23 @@ public abstract class AbstractEmbeddedServletContainerFactory
if (classLoader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
try {
if ("file".equals(url.getProtocol())) {
File file = new File(url.getFile());
if (file.isDirectory()
&& new File(file, "META-INF/resources").isDirectory()) {
staticResourceUrls.add(url);
}
else if (isResourcesJar(file)) {
staticResourceUrls.add(url);
}
}
else {
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 (isResourcesJar((JarURLConnection) connection)) {
staticResourceUrls.add(url);
}
jar.close();
}
}
}
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);

@ -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}

@ -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);

@ -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<URL> 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("!/");
}
}
}

@ -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<URL> metaInfResourceJarUrls = getUrlsOfJarsWithMetaInfResources();
List<URL> metaInfResourceUrls = getUrlsOfJarsWithMetaInfResources();
List<URL> resourceJarUrls = new ArrayList<URL>();
List<ResourceManager> resourceManagers = new ArrayList<ResourceManager>();
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,14 +643,19 @@ public class UndertowEmbeddedServletContainerFactory
}
@Override
public Resource getResource(String path) throws IOException {
public Resource getResource(String path) {
for (URL url : this.metaInfResourceJarUrls) {
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;
}

Loading…
Cancel
Save