diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
index 3994f209279..5e46df7fba4 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java
@@ -104,6 +104,14 @@ public class UrlPathHelper {
this.urlDecode = urlDecode;
}
+ /**
+ * Whether to decode the request URI when determining the lookup path.
+ * @since 4.3.13
+ */
+ public boolean isUrlDecode() {
+ return this.urlDecode;
+ }
+
/**
* Set if ";" (semicolon) content should be stripped from the request URI.
*
Default is "true".
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
index 37b3c3479b0..4000c75c49c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
@@ -16,7 +16,9 @@
package org.springframework.web.servlet.config;
+import java.nio.charset.Charset;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -24,8 +26,12 @@ import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.UrlResource;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
+import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
@@ -41,7 +47,7 @@ import org.springframework.web.util.UrlPathHelper;
* @author Brian Clozel
* @since 3.1
*/
-abstract class MvcNamespaceUtils {
+public abstract class MvcNamespaceUtils {
private static final String BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME =
BeanNameUrlHandlerMapping.class.getName();
@@ -60,6 +66,8 @@ abstract class MvcNamespaceUtils {
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
+ private static final String URL_RESOURCE_CHARSET_PREFIX = "[charset=";
+
public static void registerDefaultComponents(ParserContext parserContext, @Nullable Object source) {
registerBeanNameUrlHandlerMapping(parserContext, source);
@@ -224,5 +232,37 @@ abstract class MvcNamespaceUtils {
return null;
}
+ /**
+ * Load the {@link Resource}'s for the given locations with the given
+ * {@link ResourceLoader} and add them to the output list. Also for
+ * {@link org.springframework.core.io.UrlResource URL-based resources} (e.g.
+ * files, HTTP URLs, etc) this method supports a special prefix to indicate
+ * the charset associated with the URL so that relative paths appended to it
+ * can be encoded correctly, e.g.
+ * {@code [charset=Windows-31J]http://example.org/path}. The charsets, if
+ * any, are added to the output map.
+ * @since 4.3.13
+ */
+ public static void loadResourceLocations(String[] locations, ResourceLoader resourceLoader,
+ List outputLocations, Map outputLocationCharsets) {
+
+ for (String location : locations) {
+ Charset charset = null;
+ location = location.trim();
+ if (location.startsWith(URL_RESOURCE_CHARSET_PREFIX)) {
+ int endIndex = location.indexOf("]", URL_RESOURCE_CHARSET_PREFIX.length());
+ Assert.isTrue(endIndex != -1, "Invalid charset syntax in location: " + location);
+ String value = location.substring(URL_RESOURCE_CHARSET_PREFIX.length(), endIndex);
+ charset = Charset.forName(value);
+ location = location.substring(endIndex + 1);
+ }
+ Resource resource = resourceLoader.getResource(location);
+ outputLocations.add(resource);
+ if (charset != null) {
+ Assert.isInstanceOf(UrlResource.class, resource, "Unexpected charset for: " + resource);
+ outputLocationCharsets.put(resource, charset);
+ }
+ }
+ }
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
index c1aa6e023d5..9719af77c89 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
@@ -16,7 +16,11 @@
package org.springframework.web.servlet.config;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -34,6 +38,8 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.core.Ordered;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
import org.springframework.http.CacheControl;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
@@ -92,7 +98,10 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
registerUrlProvider(context, source);
- String resourceHandlerName = registerResourceHandler(context, element, source);
+ RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source);
+ RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, context, source);
+
+ String resourceHandlerName = registerResourceHandler(context, element, pathHelperRef, source);
if (resourceHandlerName == null) {
return null;
}
@@ -105,9 +114,6 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
urlMap.put(resourceRequestPath, resourceHandlerName);
- RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source);
- RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, context, source);
-
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
@@ -156,7 +162,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
@Nullable
- private String registerResourceHandler(ParserContext context, Element element, @Nullable Object source) {
+ private String registerResourceHandler(ParserContext context, Element element,
+ RuntimeBeanReference pathHelperRef, @Nullable Object source) {
+
String locationAttr = element.getAttribute("location");
if (!StringUtils.hasText(locationAttr)) {
String message = "The 'location' attribute is required.";
@@ -164,15 +172,28 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
return null;
}
- ManagedList locations = new ManagedList<>();
- locations.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(locationAttr)));
+ String[] locationValues = StringUtils.commaDelimitedListToStringArray(locationAttr);
+ ManagedList locations = new ManagedList<>();
+ Map locationCharsets = new HashMap<>();
+ ResourceLoader resourceLoader = context.getReaderContext().getResourceLoader();
+
+ if (resourceLoader != null) {
+ List resources = new ArrayList<>();
+ MvcNamespaceUtils.loadResourceLocations(locationValues, resourceLoader, resources, locationCharsets);
+ locations.addAll(resources);
+ }
+ else {
+ locations.addAll(Arrays.asList(locationValues));
+ }
RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class);
resourceHandlerDef.setSource(source);
resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
MutablePropertyValues values = resourceHandlerDef.getPropertyValues();
+ values.add("urlPathHelper", pathHelperRef);
values.add("locations", locations);
+ values.add("locationCharsets", locationCharsets);
String cacheSeconds = element.getAttribute("cache-period");
if (StringUtils.hasText(cacheSeconds)) {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
index 4370b8a22fb..7d23e4880e3 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
@@ -16,8 +16,11 @@
package org.springframework.web.servlet.config.annotation;
+import java.nio.charset.Charset;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.springframework.cache.Cache;
import org.springframework.core.io.Resource;
@@ -25,6 +28,7 @@ import org.springframework.core.io.ResourceLoader;
import org.springframework.http.CacheControl;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
+import org.springframework.web.servlet.config.MvcNamespaceUtils;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
@@ -44,6 +48,8 @@ public class ResourceHandlerRegistration {
private final List locations = new ArrayList<>();
+ private final Map locationCharsets = new HashMap<>();
+
@Nullable
private Integer cachePeriod;
@@ -67,18 +73,26 @@ public class ResourceHandlerRegistration {
/**
- * Add one or more resource locations from which to serve static content. Each location must point to a valid
- * directory. Multiple locations may be specified as a comma-separated list, and the locations will be checked
+ * Add one or more resource locations from which to serve static content.
+ * Each location must point to a valid directory. Multiple locations may
+ * be specified as a comma-separated list, and the locations will be checked
* for a given resource in the order specified.
- * For example, {{@code "/"}, {@code "classpath:/META-INF/public-web-resources/"}} allows resources to
- * be served both from the web application root and from any JAR on the classpath that contains a
- * {@code /META-INF/public-web-resources/} directory, with resources in the web application root taking precedence.
- * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
+ *
For example, {{@code "/"}, {@code "classpath:/META-INF/public-web-resources/"}}
+ * allows resources to be served both from the web application root and
+ * from any JAR on the classpath that contains a
+ * {@code /META-INF/public-web-resources/} directory, with resources in the
+ * web application root taking precedence.
+ *
For {@link org.springframework.core.io.UrlResource URL-based resources}
+ * (e.g. files, HTTP URLs, etc) this method supports a special prefix to
+ * indicate the charset associated with the URL so that relative paths
+ * appended to it can be encoded correctly, e.g.
+ * {@code [charset=Windows-31J]http://example.org/path}.
+ * @return the same {@link ResourceHandlerRegistration} instance, for
+ * chained method invocation
*/
public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
- for (String location : resourceLocations) {
- this.locations.add(resourceLoader.getResource(location));
- }
+ MvcNamespaceUtils.loadResourceLocations(
+ resourceLocations, this.resourceLoader, this.locations, this.locationCharsets);
return this;
}
@@ -169,6 +183,7 @@ public class ResourceHandlerRegistration {
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
}
handler.setLocations(this.locations);
+ handler.setLocationCharsets(this.locationCharsets);
if (this.cacheControl != null) {
handler.setCacheControl(this.cacheControl);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java
index 60f7663b97e..6ba5272f560 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java
@@ -33,6 +33,7 @@ import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
+import org.springframework.web.util.UrlPathHelper;
/**
* Stores registrations of resource handlers for serving static resources such as images, css files and others
@@ -59,6 +60,9 @@ public class ResourceHandlerRegistry {
@Nullable
private final ContentNegotiationManager contentNegotiationManager;
+ @Nullable
+ private final UrlPathHelper pathHelper;
+
private final List registrations = new ArrayList<>();
private int order = Integer.MAX_VALUE -1;
@@ -83,10 +87,24 @@ public class ResourceHandlerRegistry {
public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext,
@Nullable ContentNegotiationManager contentNegotiationManager) {
+ this(applicationContext, servletContext, contentNegotiationManager, null);
+ }
+
+ /**
+ * A variant of
+ * {@link #ResourceHandlerRegistry(ApplicationContext, ServletContext, ContentNegotiationManager)}
+ * that also accepts the {@link UrlPathHelper} used for mapping requests
+ * to static resources.
+ * @since 4.3.13
+ */
+ public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext,
+ ContentNegotiationManager contentNegotiationManager, @Nullable UrlPathHelper pathHelper) {
+
Assert.notNull(applicationContext, "ApplicationContext is required");
this.applicationContext = applicationContext;
this.servletContext = servletContext;
this.contentNegotiationManager = contentNegotiationManager;
+ this.pathHelper = pathHelper;
}
@@ -143,6 +161,9 @@ public class ResourceHandlerRegistry {
for (ResourceHandlerRegistration registration : this.registrations) {
for (String pathPattern : registration.getPathPatterns()) {
ResourceHttpRequestHandler handler = registration.getRequestHandler();
+ if (this.pathHelper != null) {
+ handler.setUrlPathHelper(this.pathHelper);
+ }
if (this.contentNegotiationManager != null) {
handler.setContentNegotiationManager(this.contentNegotiationManager);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
index c5e1242e98b..aab1cd03e47 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
@@ -481,7 +481,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
Assert.state(this.servletContext != null, "No ServletContext set");
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
- this.servletContext, mvcContentNegotiationManager());
+ this.servletContext, mvcContentNegotiationManager(), mvcUrlPathHelper());
addResourceHandlers(registry);
AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java
index 040a905bd8c..db9c283c1c6 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java
@@ -18,8 +18,14 @@ package org.springframework.web.servlet.resource;
import java.io.IOException;
import java.net.URLDecoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.ClassPathResource;
@@ -28,6 +34,8 @@ import org.springframework.core.io.UrlResource;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.ServletContextResource;
+import org.springframework.web.util.UriUtils;
+import org.springframework.web.util.UrlPathHelper;
/**
* A simple {@code ResourceResolver} that tries to find a resource under the given
@@ -46,6 +54,11 @@ public class PathResourceResolver extends AbstractResourceResolver {
@Nullable
private Resource[] allowedLocations;
+ private final Map locationCharsets = new HashMap<>(4);
+
+ @Nullable
+ private UrlPathHelper urlPathHelper;
+
/**
* By default when a Resource is found, the path of the resolved resource is
@@ -73,29 +86,75 @@ public class PathResourceResolver extends AbstractResourceResolver {
return this.allowedLocations;
}
+ /**
+ * Configure charsets associated with locations. If a static resource is found
+ * under a {@link org.springframework.core.io.UrlResource URL resource}
+ * location the charset is used to encode the relative path
+ * Note: the charset is used only if the
+ * {@link #setUrlPathHelper urlPathHelper} property is also configured and
+ * its {@code urlDecode} property is set to true.
+ * @param locationCharsets charsets by location
+ * @since 4.3.13
+ */
+ public void setLocationCharsets(Map locationCharsets) {
+ this.locationCharsets.clear();
+ this.locationCharsets.putAll(locationCharsets);
+ }
+
+ /**
+ * Return charsets associated with static resource locations.
+ * @since 4.3.13
+ */
+ public Map getLocationCharsets() {
+ return Collections.unmodifiableMap(locationCharsets);
+ }
+
+ /**
+ * Provide a reference to the {@link UrlPathHelper} used to map requests to
+ * static resources. This helps to derive information about the lookup path
+ * such as whether it is decoded or not.
+ * @param urlPathHelper a reference to the path helper
+ * @since 4.3.13
+ */
+ public void setUrlPathHelper(@Nullable UrlPathHelper urlPathHelper) {
+ this.urlPathHelper = urlPathHelper;
+ }
+
+ /**
+ * The configured {@link UrlPathHelper}.
+ * @since 4.3.13
+ */
+ @Nullable
+ public UrlPathHelper getUrlPathHelper() {
+ return this.urlPathHelper;
+ }
@Override
protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
List extends Resource> locations, ResourceResolverChain chain) {
- return getResource(requestPath, locations);
+ return getResource(requestPath, request, locations);
}
@Override
protected String resolveUrlPathInternal(String resourcePath, List extends Resource> locations,
ResourceResolverChain chain) {
- return (StringUtils.hasText(resourcePath) && getResource(resourcePath, locations) != null ? resourcePath : null);
+ return (StringUtils.hasText(resourcePath) &&
+ getResource(resourcePath, null, locations) != null ? resourcePath : null);
}
@Nullable
- private Resource getResource(String resourcePath, List extends Resource> locations) {
+ private Resource getResource(String resourcePath, @Nullable HttpServletRequest request,
+ List extends Resource> locations) {
+
for (Resource location : locations) {
try {
if (logger.isTraceEnabled()) {
logger.trace("Checking location: " + location);
}
- Resource resource = getResource(resourcePath, location);
+ String pathToUse = encodeIfNecessary(resourcePath, request, location);
+ Resource resource = getResource(pathToUse, location);
if (resource != null) {
if (logger.isTraceEnabled()) {
logger.trace("Found match: " + resource);
@@ -210,4 +269,29 @@ public class PathResourceResolver extends AbstractResourceResolver {
return true;
}
+ private String encodeIfNecessary(String path, @Nullable HttpServletRequest request, Resource location) {
+ if (shouldEncodeRelativePath(location) && request != null) {
+ Charset charset = this.locationCharsets.getOrDefault(location, StandardCharsets.UTF_8);
+ StringBuilder sb = new StringBuilder();
+ StringTokenizer tokenizer = new StringTokenizer(path, "/");
+ while (tokenizer.hasMoreTokens()) {
+ String value = UriUtils.encode(tokenizer.nextToken(), charset);
+ sb.append(value);
+ sb.append("/");
+ }
+ if (!path.endsWith("/")) {
+ sb.setLength(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+ else {
+ return path;
+ }
+ }
+
+ private boolean shouldEncodeRelativePath(Resource location) {
+ return location instanceof UrlResource &&
+ this.urlPathHelper != null && this.urlPathHelper.isUrlDecode();
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
index b96eda1e02d..83f6d4423c5 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
@@ -18,7 +18,9 @@ package org.springframework.web.servlet.resource;
import java.io.IOException;
import java.net.URLDecoder;
+import java.nio.charset.Charset;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -31,7 +33,6 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
-import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange;
@@ -55,6 +56,7 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.support.WebContentGenerator;
+import org.springframework.web.util.UrlPathHelper;
/**
* {@code HttpRequestHandler} that serves static resources in an optimized way
@@ -96,6 +98,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
private final List locations = new ArrayList<>(4);
+ private final Map locationCharsets = new HashMap<>(4);
+
private final List resourceResolvers = new ArrayList<>(4);
private final List resourceTransformers = new ArrayList<>(4);
@@ -115,6 +119,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
@Nullable
private CorsConfiguration corsConfiguration;
+ @Nullable
+ private UrlPathHelper urlPathHelper;
+
public ResourceHttpRequestHandler() {
super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
@@ -124,6 +131,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/**
* Set the {@code List} of {@code Resource} paths to use as sources
* for serving static resources.
+ * @see #setLocationCharsets(Map)
*/
public void setLocations(List locations) {
Assert.notNull(locations, "Locations list must not be null");
@@ -139,6 +147,31 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return this.locations;
}
+ /**
+ * Specify charsets associated with the configured {@link #setLocations(List)
+ * locations}. This is supported for
+ * {@link org.springframework.core.io.UrlResource URL resources} such as a
+ * file or an HTTP URL location and is used in {@link PathResourceResolver}
+ * to correctly encode paths relative to the location.
+ * Note: the charset is used only if the
+ * {@link #setUrlPathHelper urlPathHelper} property is also configured and
+ * its {@code urlDecode} property is set to true.
+ * @param locationCharsets charsets by location
+ * @since 4.3.13
+ */
+ public void setLocationCharsets(Map locationCharsets) {
+ this.locationCharsets.clear();
+ this.locationCharsets.putAll(locationCharsets);
+ }
+
+ /**
+ * Return charsets associated with static resource locations.
+ * @since 4.3.13
+ */
+ public Map getLocationCharsets() {
+ return Collections.unmodifiableMap(locationCharsets);
+ }
+
/**
* Configure the list of {@link ResourceResolver}s to use.
* By default {@link PathResourceResolver} is configured. If using this property,
@@ -249,6 +282,26 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return this.corsConfiguration;
}
+ /**
+ * Provide a reference to the {@link UrlPathHelper} used to map requests to
+ * static resources. This helps to derive information about the lookup path
+ * such as whether it is decoded or not.
+ * @param urlPathHelper a reference to the path helper
+ * @since 4.3.13
+ */
+ public void setUrlPathHelper(@Nullable UrlPathHelper urlPathHelper) {
+ this.urlPathHelper = urlPathHelper;
+ }
+
+ /**
+ * The configured {@link UrlPathHelper}.
+ * @since 4.3.13
+ */
+ @Nullable
+ public UrlPathHelper getUrlPathHelper() {
+ return this.urlPathHelper;
+ }
+
@Override
public void afterPropertiesSet() throws Exception {
@@ -287,6 +340,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) {
pathResolver.setAllowedLocations(getLocations().toArray(new Resource[getLocations().size()]));
}
+ if (this.urlPathHelper != null) {
+ pathResolver.setLocationCharsets(this.locationCharsets);
+ pathResolver.setUrlPathHelper(this.urlPathHelper);
+ }
break;
}
}
diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd
index dae43c1a3a5..efc126fee13 100644
--- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd
+++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd
@@ -639,6 +639,9 @@
"/, classpath:/META-INF/public-web-resources/" will allow resources to be served both from the web app
root and from any JAR on the classpath that contains a /META-INF/public-web-resources/ directory,
with resources in the web app root taking precedence.
+ For URL-based resources (e.g. files, HTTP URLs, etc) this property supports a special prefix to
+ indicate the charset associated with the URL so that relative paths appended to it can be encoded
+ correctly, e.g. "[charset=Windows-31J]http://example.org/path".
]]>
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java
index 2e1803f05ea..3300c94148f 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java
@@ -20,7 +20,6 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
@@ -418,10 +417,14 @@ public class MvcNamespaceTests {
SimpleUrlHandlerMapping mapping = appContext.getBean(SimpleUrlHandlerMapping.class);
assertNotNull(mapping);
assertNotNull(mapping.getUrlMap().get("/resources/**"));
- ResourceHttpRequestHandler handler = appContext.getBean((String) mapping.getUrlMap().get("/resources/**"),
- ResourceHttpRequestHandler.class);
+ String beanName = (String) mapping.getUrlMap().get("/resources/**");
+ ResourceHttpRequestHandler handler = appContext.getBean(beanName, ResourceHttpRequestHandler.class);
assertNotNull(handler);
+ assertNotNull(handler.getUrlPathHelper());
+ assertEquals(1, handler.getLocationCharsets().size());
+ assertEquals(StandardCharsets.ISO_8859_1, handler.getLocationCharsets().values().iterator().next());
+
List resolvers = handler.getResourceResolvers();
assertThat(resolvers, Matchers.hasSize(4));
assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class));
@@ -439,6 +442,10 @@ public class MvcNamespaceTests {
assertThat(versionResolver.getStrategyMap().get("/**"),
Matchers.instanceOf(ContentVersionStrategy.class));
+ PathResourceResolver pathResolver = (PathResourceResolver) resolvers.get(3);
+ assertEquals(1, pathResolver.getLocationCharsets().size());
+ assertEquals(StandardCharsets.ISO_8859_1, handler.getLocationCharsets().values().iterator().next());
+
List transformers = handler.getResourceTransformers();
assertThat(transformers, Matchers.hasSize(3));
assertThat(transformers.get(0), Matchers.instanceOf(CachingResourceTransformer.class));
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java
index 83c032fb6ca..43473ab46fc 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java
@@ -16,6 +16,7 @@
package org.springframework.web.servlet.config.annotation;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import org.hamcrest.Matchers;
@@ -24,10 +25,12 @@ import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.cache.concurrent.ConcurrentMapCache;
+import org.springframework.core.io.UrlResource;
+import org.springframework.http.CacheControl;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
-import org.springframework.http.CacheControl;
+import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
@@ -41,8 +44,14 @@ import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
+import org.springframework.web.util.UrlPathHelper;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
/**
* Unit tests for {@link ResourceHandlerRegistry}.
@@ -60,8 +69,10 @@ public class ResourceHandlerRegistryTests {
@Before
public void setUp() {
- this.registry = new ResourceHandlerRegistry(new GenericWebApplicationContext(), new MockServletContext());
- this.registration = registry.addResourceHandler("/resources/**");
+ this.registry = new ResourceHandlerRegistry(new GenericWebApplicationContext(),
+ new MockServletContext(), new ContentNegotiationManager(), new UrlPathHelper());
+
+ this.registration = this.registry.addResourceHandler("/resources/**");
this.registration.addResourceLocations("classpath:org/springframework/web/servlet/config/annotation/");
this.response = new MockHttpServletResponse();
}
@@ -211,9 +222,27 @@ public class ResourceHandlerRegistryTests {
assertThat(transformers.get(2), Matchers.sameInstance(cssLinkTransformer));
}
+ @Test
+ public void urlResourceWithCharset() throws Exception {
+ this.registration.addResourceLocations("[charset=ISO-8859-1]file:///tmp");
+ this.registration.resourceChain(true);
+
+ ResourceHttpRequestHandler handler = getHandler("/resources/**");
+ UrlResource resource = (UrlResource) handler.getLocations().get(1);
+ assertEquals("file:/tmp", resource.getURL().toString());
+ assertNotNull(handler.getUrlPathHelper());
+ assertEquals(1, handler.getLocationCharsets().size());
+ assertEquals(StandardCharsets.ISO_8859_1, handler.getLocationCharsets().get(resource));
+
+ List resolvers = handler.getResourceResolvers();
+ PathResourceResolver resolver = (PathResourceResolver) resolvers.get(resolvers.size()-1);
+ assertEquals(1, resolver.getLocationCharsets().size());
+ assertEquals(StandardCharsets.ISO_8859_1, handler.getLocationCharsets().values().iterator().next());
+ }
+
private ResourceHttpRequestHandler getHandler(String pathPattern) {
- SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
- return (ResourceHttpRequestHandler) handlerMapping.getUrlMap().get(pathPattern);
+ SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
+ return (ResourceHttpRequestHandler) hm.getUrlMap().get(pathPattern);
}
}
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PathResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PathResourceResolverTests.java
index e7e6dcc58c8..03fc7c566ed 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PathResourceResolverTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PathResourceResolverTests.java
@@ -16,17 +16,28 @@
package org.springframework.web.servlet.resource;
import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
+import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.support.ServletContextResource;
+import org.springframework.web.util.UrlPathHelper;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/**
* Unit tests for
@@ -132,4 +143,52 @@ public class PathResourceResolverTests {
assertNull(path);
}
+
+ @Test
+ public void relativePathEncodedForUrlResource() throws Exception {
+ TestUrlResource location = new TestUrlResource("file:///tmp");
+ List locations = Collections.singletonList(location);
+
+ // ISO-8859-1
+ this.resolver.setUrlPathHelper(new UrlPathHelper());
+ this.resolver.setLocationCharsets(Collections.singletonMap(location, StandardCharsets.ISO_8859_1));
+ this.resolver.resolveResource(new MockHttpServletRequest(), "/Ä ;ä.txt", locations, null);
+
+ assertEquals("%C4%20%3B%E4.txt", location.getSavedRelativePath());
+
+ // UTF-8
+ this.resolver.setLocationCharsets(Collections.singletonMap(location, StandardCharsets.UTF_8));
+ this.resolver.resolveResource(new MockHttpServletRequest(), "/Ä ;ä.txt", locations, null);
+
+ assertEquals("%C3%84%20%3B%C3%A4.txt", location.getSavedRelativePath());
+
+ // UTF-8 by default
+ this.resolver.setLocationCharsets(Collections.emptyMap());
+ this.resolver.resolveResource(new MockHttpServletRequest(), "/Ä ;ä.txt", locations, null);
+
+ assertEquals("%C3%84%20%3B%C3%A4.txt", location.getSavedRelativePath());
+ }
+
+
+ private static class TestUrlResource extends UrlResource {
+
+ private String relativePath;
+
+
+ public TestUrlResource(String path) throws MalformedURLException {
+ super(path);
+ }
+
+
+ public String getSavedRelativePath() {
+ return this.relativePath;
+ }
+
+ @Override
+ public Resource createRelative(String relativePath) throws MalformedURLException {
+ this.relativePath = relativePath;
+ return this;
+ }
+ }
+
}
diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain.xml
index 42c4844437f..32d0c2a32ab 100644
--- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain.xml
+++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-resources-chain.xml
@@ -21,7 +21,7 @@
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
-
+