diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 50a8a4ba6b4..b744cc66cc1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -287,7 +287,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor private Resource createResource(URL url) throws IOException { if ("file".equals(url.getProtocol())) { - File file = new File(url.getFile()); + File file = new File(getDecodedFile(url)); if (file.isFile()) { return Resource.newResource("jar:" + url + "!/META-INF/resources"); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index c428c64bc68..dcc0090746f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -682,7 +682,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto private void addResourceJars(List resourceJarUrls) { for (URL url : resourceJarUrls) { - String file = url.getFile(); + String file = getDecodedFile(url); if (file.endsWith(".jar") || file.endsWith(".jar!/")) { String jar = url.toString(); if (!jar.startsWith("jar:")) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 7e412861141..f756e5997c7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -367,7 +367,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac : new LoaderHidingResourceManager(rootResourceManager)); for (URL url : metaInfResourceUrls) { if ("file".equals(url.getProtocol())) { - File file = new File(url.getFile()); + File file = new File(getDecodedFile(url)); if (file.isFile()) { try { resourceJarUrls.add(new URL("jar:" + url + "!/")); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java index 00ce07717d9..ba979340542 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java @@ -17,7 +17,9 @@ package org.springframework.boot.web.servlet.server; import java.io.File; +import java.io.UnsupportedEncodingException; import java.net.URL; +import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -280,6 +282,16 @@ public abstract class AbstractServletWebServerFactory return this.staticResourceJars.getUrls(); } + protected final String getDecodedFile(URL url) { + try { + return URLDecoder.decode(url.getFile(), "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException( + "Failed to decode '" + url.getFile() + "' using UTF-8"); + } + } + protected final File getValidSessionStoreDir() { return getValidSessionStoreDir(true); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/StaticResourceJars.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/StaticResourceJars.java index 74ca4d7214f..72c6594d26c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/StaticResourceJars.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/StaticResourceJars.java @@ -18,15 +18,18 @@ package org.springframework.boot.web.servlet.server; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.lang.management.ManagementFactory; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import java.util.jar.JarFile; +import java.util.stream.Stream; /** * Logic to extract URLs of static resource jars (those containing @@ -37,21 +40,25 @@ import java.util.jar.JarFile; */ class StaticResourceJars { - public final List getUrls() { + List getUrls() { ClassLoader classLoader = getClass().getClassLoader(); - List urls = new ArrayList<>(); if (classLoader instanceof URLClassLoader) { - for (URL url : ((URLClassLoader) classLoader).getURLs()) { - addUrl(urls, url); - } + return getUrlsFrom(((URLClassLoader) classLoader).getURLs()); } else { - for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath() - .split(File.pathSeparator)) { - addUrl(urls, toUrl(entry)); - } + return getUrlsFrom(Stream + .of(ManagementFactory.getRuntimeMXBean().getClassPath() + .split(File.pathSeparator)) + .map(this::toUrl).toArray(URL[]::new)); } - return urls; + } + + List getUrlsFrom(URL... urls) { + List resourceJarUrls = new ArrayList<>(); + for (URL url : urls) { + addUrl(resourceJarUrls, url); + } + return resourceJarUrls; } private URL toUrl(String classPathEntry) { @@ -67,7 +74,7 @@ class StaticResourceJars { private void addUrl(List urls, URL url) { try { if ("file".equals(url.getProtocol())) { - addUrlFile(urls, url, new File(url.getFile())); + addUrlFile(urls, url, new File(getDecodedFile(url))); } else { addUrlConnection(urls, url, url.openConnection()); @@ -78,6 +85,16 @@ class StaticResourceJars { } } + private String getDecodedFile(URL url) { + try { + return URLDecoder.decode(url.getFile(), "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException( + "Failed to decode '" + url.getFile() + "' using UTF-8"); + } + } + private void addUrlFile(List urls, URL url, File file) { if ((file.isDirectory() && new File(file, "META-INF/resources").isDirectory()) || isResourcesJar(file)) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java new file mode 100644 index 00000000000..8ae5788c145 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2018 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.web.servlet.server; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StaticResourceJars}. + * + * @author Rupert Madden-Abbott + * @author Andy Wilkinson + */ +public class StaticResourceJarsTests { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void includeJarWithStaticResources() throws Exception { + File jarFile = createResourcesJar("test-resources.jar"); + List staticResourceJarUrls = new StaticResourceJars() + .getUrlsFrom(jarFile.toURI().toURL()); + assertThat(staticResourceJarUrls).hasSize(1); + } + + @Test + public void includeJarWithStaticResourcesWithUrlEncodedSpaces() throws Exception { + File jarFile = createResourcesJar("test resources.jar"); + List staticResourceJarUrls = new StaticResourceJars() + .getUrlsFrom(jarFile.toURI().toURL()); + assertThat(staticResourceJarUrls).hasSize(1); + } + + @Test + public void excludeJarWithoutStaticResources() throws Exception { + File jarFile = createJar("dependency.jar"); + List staticResourceJarUrls = new StaticResourceJars() + .getUrlsFrom(jarFile.toURI().toURL()); + assertThat(staticResourceJarUrls).hasSize(0); + } + + private File createResourcesJar(String name) throws IOException { + return createJar(name, (output) -> { + JarEntry jarEntry = new JarEntry("META-INF/resources"); + try { + output.putNextEntry(jarEntry); + output.closeEntry(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + } + + private File createJar(String name) throws IOException { + return createJar(name, null); + } + + private File createJar(String name, Consumer customizer) + throws IOException { + File jarFile = this.temporaryFolder.newFile(name); + JarOutputStream jarOutputStream = new JarOutputStream( + new FileOutputStream(jarFile)); + if (customizer != null) { + customizer.accept(jarOutputStream); + } + jarOutputStream.close(); + return jarFile; + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java index 949c8cb145a..bec7657a1b6 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/IdeApplicationLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -40,7 +40,7 @@ import org.springframework.util.StringUtils; */ class IdeApplicationLauncher extends AbstractApplicationLauncher { - private final File exploded = new File("target/ide"); + private final File exploded = new File("target/ide application"); IdeApplicationLauncher(ApplicationBuilder applicationBuilder) { super(applicationBuilder);