From 369d33c3d00569a8a439910bcff6fca0f4aaec37 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 1 Nov 2017 03:52:32 -0400 Subject: [PATCH] Support charset config by (static) resource location This commit adds support for configuring static resource locations with a charset to be applied to relative paths. --- .../web/util/UrlPathHelper.java | 8 ++ .../web/servlet/config/MvcNamespaceUtils.java | 42 ++++++++- .../config/ResourcesBeanDefinitionParser.java | 35 +++++-- .../ResourceHandlerRegistration.java | 33 +++++-- .../annotation/ResourceHandlerRegistry.java | 21 +++++ .../WebMvcConfigurationSupport.java | 2 +- .../resource/PathResourceResolver.java | 92 ++++++++++++++++++- .../resource/ResourceHttpRequestHandler.java | 59 +++++++++++- .../web/servlet/config/spring-mvc.xsd | 3 + .../web/servlet/config/MvcNamespaceTests.java | 13 ++- .../ResourceHandlerRegistryTests.java | 41 +++++++-- .../resource/PathResourceResolverTests.java | 61 +++++++++++- .../config/mvc-config-resources-chain.xml | 2 +- 13 files changed, 378 insertions(+), 34 deletions(-) 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 3994f20927..5e46df7fba 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 37b3c3479b..4000c75c49 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 c1aa6e023d..9719af77c8 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 4370b8a22f..7d23e4880e 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 60f7663b97..6ba5272f56 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 c5e1242e98..aab1cd03e4 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 040a905bd8..db9c283c1c 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 locations, ResourceResolverChain chain) { - return getResource(requestPath, locations); + return getResource(requestPath, request, locations); } @Override protected String resolveUrlPathInternal(String resourcePath, List 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 locations) { + private Resource getResource(String resourcePath, @Nullable HttpServletRequest request, + List 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 b96eda1e02..83f6d4423c 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 dae43c1a3a..efc126fee1 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 2e1803f05e..3300c94148 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 83c032fb6c..43473ab46f 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 e7e6dcc58c..03fc7c566e 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 42c4844437..32d0c2a32a 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"> - + -- GitLab