You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2019/09/08 15:28:24 UTC

[juneau] branch master updated: JUNEAU-142, JUNEAU-143

This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 9882259  JUNEAU-142, JUNEAU-143
9882259 is described below

commit 98822599ea5ca9bd267b125681997b1076c4675b
Author: JamesBognar <ja...@apache.org>
AuthorDate: Sun Sep 8 11:28:09 2019 -0400

    JUNEAU-142, JUNEAU-143
    
    Static files mapping doesn't properly handle override scenarios.
    Add support for absolute paths on @RestResource(staticFiles)
---
 .../org/apache/juneau/PropertyStoreBuilder.java    |  6 +-
 .../utils/ClasspathResourceFinderSimple.java       | 15 +---
 juneau-doc/docs/ReleaseNotes/8.1.1.html            |  9 +++
 .../07.juneau-rest-server/26.StaticFiles.html      |  2 +-
 juneau-doc/docs/docs.txt                           |  1 +
 .../annotation/RestResourceStaticFilesTest.java    | 48 +++++++++++-
 .../java/org/apache/juneau/rest/RestContext.java   | 87 +++++++++++++++-------
 .../org/apache/juneau/rest/StaticFileMapping.java  | 36 +++++++--
 .../java/org/apache/juneau/rest/StaticFiles.java   | 76 +++++++++++++++++++
 9 files changed, 232 insertions(+), 48 deletions(-)

diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
index 55d9f02..05fd053 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStoreBuilder.java
@@ -341,6 +341,7 @@ public class PropertyStoreBuilder {
 	 * 		Out-of-range indexes are simply 'adjusted' to the beginning or the end of the list.
 	 * 		So, for example, a value of <js>"-100"</js> will always just cause the entry to be added to the beginning
 	 * 		of the list.
+	 * 		<br>NOTE:  If <jk>null</jk>, value will be inserted at position 0.
 	 * 	<br>For MAPs, this can be <jk>null</jk> if we're adding a map, or a string key if we're adding an entry.
 	 * @param value
 	 * 	The new value to add to the property.
@@ -370,11 +371,14 @@ public class PropertyStoreBuilder {
 	}
 
 	/**
-	 * Adds a value to a SET, LIST, or MAP property.
+	 * Adds/prepends a value to a SET, LIST, or MAP property.
 	 *
 	 * <p>
 	 * Shortcut for calling <code>addTo(key, <jk>null</jk>, value);</code>.
 	 *
+	 * <p>
+	 * NOTE:  When adding to a list, the value is inserted at the beginning of the list.
+	 *
 	 * @param key The property key.
 	 * @param value
 	 * 	The new value to add to the property.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
index 7eea5f3..eb97c8f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/ClasspathResourceFinderSimple.java
@@ -65,10 +65,10 @@ public class ClasspathResourceFinderSimple implements ClasspathResourceFinder {
 	 * @throws IOException Thrown by underlying stream.
 	 */
 	protected InputStream findClasspathResource(Class<?> baseClass, String name, Locale locale) throws IOException {
-		
-		if (locale == null) 
+
+		if (locale == null)
 			return getResourceAsStream(baseClass, name);
-		
+
 		for (String n : getCandidateFileNames(name, locale)) {
 			InputStream is = getResourceAsStream(baseClass, n);
 			if (is != null)
@@ -78,14 +78,7 @@ public class ClasspathResourceFinderSimple implements ClasspathResourceFinder {
 	}
 
 	private InputStream getResourceAsStream(Class<?> baseClass, String name) {
-		InputStream is = baseClass.getResourceAsStream(name);
-		if (is != null)
-			return is;
-		if (! name.startsWith("/"))
-			is = baseClass.getResourceAsStream("/" + name);
-		if (is != null)
-			return is;
-		return null;
+		return baseClass.getResourceAsStream(name);
 	}
 
 	/**
diff --git a/juneau-doc/docs/ReleaseNotes/8.1.1.html b/juneau-doc/docs/ReleaseNotes/8.1.1.html
index fe2213d..ca60e8f 100644
--- a/juneau-doc/docs/ReleaseNotes/8.1.1.html
+++ b/juneau-doc/docs/ReleaseNotes/8.1.1.html
@@ -74,6 +74,15 @@
 				<li class='jm'>{@link oajr.BasicRest#log(Level,Throwable,String,Object[]) log(Level,Throwable,String,Object[])}
 			</ul>
 		</ul>
+	<li>
+		The <c>@RestResource(staticFiles)</c> annotation now supports absolute path locations:
+		<p class='bpcode w800'>
+	<jc>// Resolves static files in root package "htdocs" or working directory "htdocs".
+	<ja>@RestResource</ja>(staticFiles=<js>"htdocs:/htdocs"</js>)
+		</p>
+	<li>
+		Fixed a bug in <c>@RestResource(staticFiles)</c> where the order of lookup between parent and child resources
+		was wrong.
 </ul>
 
 <h5 class='topic w800'>juneau-rest-client</h5>
diff --git a/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html b/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
index bf55663..dcf82d7 100644
--- a/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
+++ b/juneau-doc/docs/Topics/07.juneau-rest-server/26.StaticFiles.html
@@ -13,7 +13,7 @@
  ***************************************************************************************************************************/
  -->
 
-Static files
+{todo} Static files
 
 <p>
 	The {@link oajr.annotation.RestResource#staticFiles @RestResource(staticFiles)} 
diff --git a/juneau-doc/docs/docs.txt b/juneau-doc/docs/docs.txt
index 56d6f0b..4563d0f 100644
--- a/juneau-doc/docs/docs.txt
+++ b/juneau-doc/docs/docs.txt
@@ -4,6 +4,7 @@ PojoCategories = #juneau-marshall.PojoCategories, POJO Categories
 PojosConveribleToStrings = #PojosConveribleToStrings, POJOs Convertible to/from Strings
 PojosConveribleToOtherTypes = #PojosConveribleToOtherTypes, POJOs Convertible to/from Other Types
 ConfigurableProperties = #juneau-marshall.ConfigurableProperties, Configurable Properties
+SimpleJson = #juneau-marshall.JsonDetails.SimplifiedJson, Simple JSON
 
 SwaggerIO.v2 = https://swagger.io/specification/v2
 SwaggerIO.v3 = https://swagger.io/specification
diff --git a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
index 2a5b5e3..f9817b3 100644
--- a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
+++ b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceStaticFilesTest.java
@@ -23,9 +23,9 @@ import org.junit.runners.*;
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class RestResourceStaticFilesTest {
 
-	//====================================================================================================
+	//------------------------------------------------------------------------------------------------------------------
 	// Basic tests
-	//====================================================================================================
+	//------------------------------------------------------------------------------------------------------------------
 
 	@RestResource(staticFiles={"xdocs:xdocs","xdocs2:xdocs2:{Foo:'Bar'}"})
 	public static class A {
@@ -47,9 +47,9 @@ public class RestResourceStaticFilesTest {
 		a.get("/xdocs/xsubdocs/%2E%2E/test.txt?noTrace=true").execute().assertStatus(404);
 	}
 
-	//====================================================================================================
+	//------------------------------------------------------------------------------------------------------------------
 	// Static files with response headers.
-	//====================================================================================================
+	//------------------------------------------------------------------------------------------------------------------
 
 	@RestResource(staticFiles={"xdocs:xdocs:{Foo:'Bar'}"})
 	public static class B {
@@ -64,4 +64,44 @@ public class RestResourceStaticFilesTest {
 	public void b01() throws Exception {
 		b.get("/xdocs/test.txt").execute().assertHeader("Foo","Bar").assertBodyContains("OK-1");
 	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Class hierarchy
+	//------------------------------------------------------------------------------------------------------------------
+
+	@RestResource(staticFiles={"xdocs:xdocs"})
+	public static class C1 {
+		@RestMethod
+		public String c01() {
+			return null;
+		}
+	}
+
+	@RestResource(staticFiles={"xdocs:/xdocs"})
+	public static class C2 extends C1 {
+		@RestMethod
+		public String c02() {
+			return null;
+		}
+	}
+
+	static MockRest c1 = MockRest.build(C1.class);
+	static MockRest c2 = MockRest.build(C2.class);
+
+	@Test
+	public void c01() throws Exception {
+		// Should resolve to relative xdocs folder.
+		c1.get("/xdocs/test.txt").execute().assertBodyContains("OK-1");
+		c1.get("/xdocs/xsubdocs/test.txt").execute().assertBodyContains("OK-2");
+
+		// Should be overridden to absolute xdocs folder.
+		c2.get("/xdocs/test.txt").execute().assertBodyContains("OK-3");
+		c2.get("/xdocs/xsubdocs/test.txt").execute().assertBodyContains("OK-4");
+
+		// Should pick up from file system.
+		c1.get("/xdocs/test2.txt").execute().assertBodyContains("OK-5");
+		c2.get("/xdocs/test2.txt").execute().assertBodyContains("OK-5");
+		c1.get("/xdocs/xsubdocs/test2.txt").execute().assertBodyContains("OK-6");
+		c2.get("/xdocs/xsubdocs/test2.txt").execute().assertBodyContains("OK-6");
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index 7291510..15b3fd7 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -50,7 +50,6 @@ import org.apache.juneau.http.annotation.Query;
 import org.apache.juneau.http.annotation.Response;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.httppart.bean.*;
-import org.apache.juneau.internal.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.jsonschema.*;
 import org.apache.juneau.msgpack.*;
@@ -2755,6 +2754,16 @@ public final class RestContext extends BeanContext {
 	 * 	}
 	 * </p>
 	 *
+	 * <p>
+	 * Note that headers can also be specified per path-mapping via the {@link RestResource#staticFiles() @RestResource(staticFiles)} annotation.
+	 * <p class='bcode w800'>
+	 * 	<ja>@RestResource</ja>(
+	 * 		staticFiles={
+	 * 			<js>"htdocs:docs:{'Cache-Control':'max-age=86400, public'}"</js>
+	 * 		}
+	 * 	)
+	 * </p>
+	 *
 	 * <ul class='seealso'>
 	 * 	<li class='jf'>{@link #REST_staticFiles} for information about statically-served files.
 	 * </ul>
@@ -2790,6 +2799,13 @@ public final class RestContext extends BeanContext {
 	 * from the classpath or file system.
 	 *
 	 * <p>
+	 * The format of the value is one of the following:
+	 * <ol class='spaced-list'>
+	 * 	<li><js>"path:location"</js>
+	 * 	<li><js>"path:location:headers"</js>
+	 * </ol>
+	 *
+	 * <p>
 	 * An example where this class is used is in the {@link RestResource#staticFiles} annotation:
 	 * <p class='bcode w800'>
 	 * 	<jk>package</jk> com.foo.mypackage;
@@ -2809,12 +2825,41 @@ public final class RestContext extends BeanContext {
 	 * <p class='bcode w800'>
 	 *  	/myresource/htdocs/foobar.html
 	 * </p>
-	 * <br>...the servlet will attempt to find the <c>foobar.html</c> file in the following ordered locations:
+	 * <br>...the servlet will attempt to find the <c>foobar.html</c> file in the following location:
 	 * <ol class='spaced-list'>
 	 * 	<li><c>com.foo.mypackage.docs</c> package.
-	 * 	<li><c>[working-dir]/docs</c> directory.
 	 * </ol>
 	 *
+	 * <p>
+	 * The location is interpreted as an absolute path if it starts with <js>'/'</js>.
+	 * <p class='bcode w800'>
+	 * 	<ja>@RestResource</ja>(
+	 * 		staticFiles={
+	 * 			<js>"htdocs:/docs"</js>
+	 * 		}
+	 * 	)
+	 * </p>
+	 * <p>
+	 * In the example above, given a GET request to the following URL...
+	 * <p class='bcode w800'>
+	 *  	/myresource/htdocs/foobar.html
+	 * </p>
+	 * <br>...the servlet will attempt to find the <c>foobar.html</c> file in the following location:
+	 * <ol class='spaced-list'>
+	 * 	<li><c>docs</c> package (typically under <c>src/main/resources/docs</c> in your workspace).
+	 * 	<li><c>[working-dir]/docs</c> directory at runtime.
+	 * </ol>
+	 *
+	 * <p>
+	 * Response headers can be specified for served files by adding a 3rd section that consists of a {@doc SimpleJson} object.
+	 * <p class='bcode w800'>
+	 * 	<ja>@RestResource</ja>(
+	 * 		staticFiles={
+	 * 			<js>"htdocs:docs:{'Cache-Control':'max-age=86400, public'}"</js>
+	 * 		}
+	 * 	)
+	 * </p>
+	 *
 	 * <ul class='seealso'>
 	 * 	<li class='jf'>{@link #REST_classpathResourceFinder} for configuring how classpath resources are located and retrieved.
 	 * 	<li class='jf'>{@link #REST_mimeTypes} for configuring the media types based on file extension.
@@ -2828,6 +2873,8 @@ public final class RestContext extends BeanContext {
 	 * 		Mappings are cumulative from super classes.
 	 * 	<li>
 	 * 		Child resources can override mappings made on parent class resources.
+	 * 		<br>When both parent and child resources map against the same path, files will be search in the child location
+	 * 		and then the parent location.
 	 * </ul>
 	 */
 	public static final String REST_staticFiles = PREFIX + ".staticFiles.lo";
@@ -3485,7 +3532,7 @@ public final class RestContext extends BeanContext {
 	private final ObjectMap defaultRequestAttributes;
 	private final ResponseHandler[] responseHandlers;
 	private final MimetypesFileTypeMap mimetypesFileTypeMap;
-	private final StaticFileMapping[] staticFiles;
+	private final StaticFiles[] staticFiles;
 	private final String[] staticFilesPaths;
 	private final MessageBundle msgs;
 	private final Config config;
@@ -3688,10 +3735,14 @@ public final class RestContext extends BeanContext {
 			consumes = getListProperty(REST_consumes, MediaType.class, parsers.getSupportedMediaTypes());
 			produces = getListProperty(REST_produces, MediaType.class, serializers.getSupportedMediaTypes());
 
-			staticFiles = ArrayUtils.reverse(getArrayProperty(REST_staticFiles, StaticFileMapping.class));
+			StaticFileMapping[] staticFileMappings = getArrayProperty(REST_staticFiles, StaticFileMapping.class, new StaticFileMapping[0]);
+			staticFiles = new StaticFiles[staticFileMappings.length];
+			for (int i = 0; i < staticFiles.length; i++)
+				staticFiles[i] = new StaticFiles(staticFileMappings[i], staticResourceManager, mimetypesFileTypeMap, staticFileResponseHeaders);
+
 			Set<String> s = new TreeSet<>();
-			for (StaticFileMapping sfm : staticFiles)
-				s.add(sfm.path);
+			for (StaticFiles sf : staticFiles)
+				s.add(sf.getPath());
 			staticFilesPaths = s.toArray(new String[s.size()]);
 
 			MessageBundleLocation[] mbl = getInstanceArrayProperty(REST_messages, MessageBundleLocation.class, new MessageBundleLocation[0]);
@@ -4085,24 +4136,10 @@ public final class RestContext extends BeanContext {
 			if (p.indexOf("..") != -1)
 				throw new NotFound("Invalid path");
 			StreamResource sr = null;
-			for (StaticFileMapping sfm : staticFiles) {
-				String path = sfm.path;
-				if (p.startsWith(path)) {
-					String remainder = (p.equals(path) ? "" : p.substring(path.length()));
-					if (remainder.isEmpty() || remainder.startsWith("/")) {
-						String p2 = sfm.location + remainder;
-						try (InputStream is = getClasspathResource(sfm.resourceClass, p2, null)) {
-							if (is != null) {
-								int i = p2.lastIndexOf('/');
-								String name = (i == -1 ? p2 : p2.substring(i+1));
-								String mediaType = mimetypesFileTypeMap.getContentType(name);
-								Map<String,Object> responseHeaders = sfm.responseHeaders != null ? sfm.responseHeaders : staticFileResponseHeaders;
-								sr = new StreamResource(MediaType.forString(mediaType), responseHeaders, true, is);
-								break;
-							}
-						}
-					}
-				}
+			for (StaticFiles sf : staticFiles) {
+				sr = sf.resolve(p);
+				if (sr != null)
+					break;
 			}
 			StaticFile sf = new StaticFile(sr);
 			if (useClasspathResourceCaching) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
index 2bdec39..3fa0424 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
@@ -13,11 +13,12 @@
 package org.apache.juneau.rest;
 
 import static org.apache.juneau.internal.CollectionUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.util.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.utils.*;
@@ -86,8 +87,8 @@ public class StaticFileMapping {
 	 */
 	public StaticFileMapping(Class<?> resourceClass, String path, String location, Map<String,Object> responseHeaders) {
 		this.resourceClass = resourceClass;
-		this.path = StringUtils.trimSlashes(path);
-		this.location = StringUtils.trimSlashes(location);
+		this.path = trimSlashes(path);
+		this.location = trimTrailingSlashes(location);
 		this.responseHeaders = immutableMap(responseHeaders);
 	}
 
@@ -109,11 +110,11 @@ public class StaticFileMapping {
 	 */
 	public StaticFileMapping(Class<?> resourceClass, String mappingString) {
 		this.resourceClass = resourceClass;
-		String[] parts = StringUtils.split(mappingString, ':', 3);
+		String[] parts = split(mappingString, ':', 3);
 		if (parts == null || parts.length <= 1)
 			throw new FormattedRuntimeException("Invalid mapping string format: ''{0}'' on resource class ''{1}''", mappingString, resourceClass.getName());
-		this.path = StringUtils.trimSlashes(parts[0]);
-		this.location = StringUtils.trimSlashes(parts[1]);
+		this.path = trimSlashes(parts[0]);
+		this.location = trimTrailingSlashes(parts[1]);
 		if (parts.length == 3) {
 			try {
 				responseHeaders = unmodifiableMap(new ObjectMap(parts[2]));
@@ -124,4 +125,27 @@ public class StaticFileMapping {
 			responseHeaders = null;
 		}
 	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other methods
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Override /* Object */
+	public String toString() {
+		return SimpleJsonSerializer.DEFAULT_READABLE.toString(toMap());
+	}
+
+	/**
+	 * Returns the properties defined on this bean as a simple map for debugging purposes.
+	 *
+	 * @return A new map containing the properties defined on this bean.
+	 */
+	public ObjectMap toMap() {
+		return new DefaultFilteringObjectMap()
+			.append("resourceClass", resourceClass)
+			.append("path", path)
+			.append("location", location)
+			.append("responseHeaders", responseHeaders)
+		;
+	}
 }
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
new file mode 100644
index 0000000..e7d859d
--- /dev/null
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
@@ -0,0 +1,76 @@
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you 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.                                              *
+// ***************************************************************************************************************************
+// ***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
+// * to you 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.apache.juneau.rest;
+
+import java.io.*;
+import java.util.*;
+
+import javax.activation.*;
+
+import org.apache.juneau.http.*;
+import org.apache.juneau.utils.*;
+
+/**
+ * The static file resource resolver for a single {@link StaticFileMapping}.
+ */
+class StaticFiles {
+	private final Class<?> resourceClass;
+	private final String path, location;
+	private final Map<String,Object> responseHeaders;
+
+	private final ClasspathResourceManager staticResourceManager;
+	private final MimetypesFileTypeMap mimetypesFileTypeMap;
+
+	StaticFiles(StaticFileMapping sfm, ClasspathResourceManager staticResourceManager, MimetypesFileTypeMap mimetypesFileTypeMap, Map<String,Object> staticFileResponseHeaders) {
+		this.resourceClass = sfm.resourceClass;
+		this.path = sfm.path;
+		this.location = sfm.location;
+		this.responseHeaders = sfm.responseHeaders != null ? sfm.responseHeaders : staticFileResponseHeaders;
+		this.staticResourceManager = staticResourceManager;
+		this.mimetypesFileTypeMap = mimetypesFileTypeMap;
+	}
+
+	String getPath() {
+		return path;
+	}
+
+	StreamResource resolve(String p) throws IOException {
+		if (p.startsWith(path)) {
+			String remainder = (p.equals(path) ? "" : p.substring(path.length()));
+			if (remainder.isEmpty() || remainder.startsWith("/")) {
+				String p2 = location + remainder;
+				try (InputStream is = staticResourceManager.getStream(resourceClass, p2, null)) {
+					if (is != null) {
+						int i = p2.lastIndexOf('/');
+						String name = (i == -1 ? p2 : p2.substring(i+1));
+						String mediaType = mimetypesFileTypeMap.getContentType(name);
+						return new StreamResource(MediaType.forString(mediaType), responseHeaders, true, is);
+					}
+				}
+			}
+		}
+		return null;
+	}
+}