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.
This commit is contained in:
Rossen Stoyanchev
2017-11-03 21:06:42 -04:00
parent 97bc2762e1
commit 9470719cdb
13 changed files with 396 additions and 36 deletions

View File

@@ -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,7 +26,11 @@ 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.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;
@@ -40,7 +46,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();
@@ -59,6 +65,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, Object source) {
registerBeanNameUrlHandlerMapping(parserContext, source);
@@ -221,5 +229,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<Resource> outputLocations, Map<Resource, Charset> 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);
}
}
}
}

View File

@@ -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.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
@@ -91,7 +97,10 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
registerUrlProvider(parserContext, source);
String resourceHandlerName = registerResourceHandler(parserContext, element, source);
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, parserContext, source);
RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, parserContext, source);
String resourceHandlerName = registerResourceHandler(parserContext, element, pathHelperRef, source);
if (resourceHandlerName == null) {
return null;
}
@@ -104,9 +113,6 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
urlMap.put(resourceRequestPath, resourceHandlerName);
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, parserContext, source);
RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, parserContext, source);
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
@@ -153,22 +159,37 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
}
private String registerResourceHandler(ParserContext parserContext, Element element, Object source) {
private String registerResourceHandler(ParserContext parserContext, Element element,
RuntimeBeanReference pathHelperRef, Object source) {
String locationAttr = element.getAttribute("location");
if (!StringUtils.hasText(locationAttr)) {
parserContext.getReaderContext().error("The 'location' attribute is required.", parserContext.extractSource(element));
return null;
}
ManagedList<String> locations = new ManagedList<String>();
locations.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(locationAttr)));
String[] locationValues = StringUtils.commaDelimitedListToStringArray(locationAttr);
ManagedList<Object> locations = new ManagedList<Object>();
Map<Resource, Charset> locationCharsets = new HashMap<Resource, Charset>();
ResourceLoader resourceLoader = parserContext.getReaderContext().getResourceLoader();
if (resourceLoader != null) {
List<Resource> resources = new ArrayList<Resource>();
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)) {

View File

@@ -16,14 +16,18 @@
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;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.http.CacheControl;
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;
@@ -43,6 +47,8 @@ public class ResourceHandlerRegistration {
private final List<Resource> locations = new ArrayList<Resource>();
private final Map<Resource, Charset> locationCharsets = new HashMap<Resource, Charset>();
private Integer cachePeriod;
private CacheControl cacheControl;
@@ -61,20 +67,27 @@ public class ResourceHandlerRegistration {
this.pathPatterns = pathPatterns;
}
/**
* 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.
* <p>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
* <p>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.
* <p>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;
}
@@ -165,6 +178,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);
}

View File

@@ -32,6 +32,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
@@ -57,6 +58,8 @@ public class ResourceHandlerRegistry {
private final ContentNegotiationManager contentNegotiationManager;
private final UrlPathHelper pathHelper;
private final List<ResourceHandlerRegistration> registrations = new ArrayList<ResourceHandlerRegistration>();
private int order = Integer.MAX_VALUE -1;
@@ -81,10 +84,24 @@ public class ResourceHandlerRegistry {
public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext,
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, UrlPathHelper pathHelper) {
Assert.notNull(applicationContext, "ApplicationContext is required");
this.applicationContext = applicationContext;
this.servletContext = servletContext;
this.contentNegotiationManager = contentNegotiationManager;
this.pathHelper = pathHelper;
}
@@ -140,9 +157,14 @@ 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);
}
handler.setServletContext(this.servletContext);
handler.setApplicationContext(this.applicationContext);
handler.setContentNegotiationManager(this.contentNegotiationManager);
try {
handler.afterPropertiesSet();
}

View File

@@ -54,6 +54,7 @@ import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConvert
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import org.springframework.validation.Errors;
@@ -439,8 +440,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
*/
@Bean
public HandlerMapping resourceHandlerMapping() {
Assert.state(this.applicationContext != null, "No ApplicationContext set");
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();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,15 @@
package org.springframework.web.servlet.resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
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;
@@ -27,6 +33,8 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
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
@@ -42,8 +50,15 @@ import org.springframework.web.context.support.ServletContextResource;
*/
public class PathResourceResolver extends AbstractResourceResolver {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Resource[] allowedLocations;
private final Map<Resource, Charset> locationCharsets = new HashMap<Resource, Charset>(4);
private UrlPathHelper urlPathHelper;
/**
* By default when a Resource is found, the path of the resolved resource is
@@ -70,28 +85,73 @@ 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
* <p><strong>Note:</strong> 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<Resource, Charset> locationCharsets) {
this.locationCharsets.clear();
this.locationCharsets.putAll(locationCharsets);
}
/**
* Return charsets associated with static resource locations.
* @since 4.3.13
*/
public Map<Resource, Charset> 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(UrlPathHelper urlPathHelper) {
this.urlPathHelper = urlPathHelper;
}
/**
* The configured {@link UrlPathHelper}.
* @since 4.3.13
*/
public UrlPathHelper getUrlPathHelper() {
return this.urlPathHelper;
}
@Override
protected Resource resolveResourceInternal(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);
}
private Resource getResource(String resourcePath, List<? extends Resource> locations) {
private Resource getResource(String resourcePath, 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);
@@ -203,4 +263,37 @@ public class PathResourceResolver extends AbstractResourceResolver {
return true;
}
private String encodeIfNecessary(String path, HttpServletRequest request, Resource location) {
if (shouldEncodeRelativePath(location) && request != null) {
Charset charset = this.locationCharsets.get(location);
charset = charset != null ? charset : DEFAULT_CHARSET;
StringBuilder sb = new StringBuilder();
StringTokenizer tokenizer = new StringTokenizer(path, "/");
while (tokenizer.hasMoreTokens()) {
String value = null;
try {
value = UriUtils.encode(tokenizer.nextToken(), charset.name());
}
catch (UnsupportedEncodingException ex) {
// Should never happen
throw new IllegalStateException("Unexpected error", ex);
}
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();
}
}

View File

@@ -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;
@@ -56,6 +58,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
@@ -101,6 +104,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
private final List<Resource> locations = new ArrayList<Resource>(4);
private final Map<Resource, Charset> locationCharsets = new HashMap<Resource, Charset>(4);
private final List<ResourceResolver> resourceResolvers = new ArrayList<ResourceResolver>(4);
private final List<ResourceTransformer> resourceTransformers = new ArrayList<ResourceTransformer>(4);
@@ -115,6 +120,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
private CorsConfiguration corsConfiguration;
private UrlPathHelper urlPathHelper;
public ResourceHttpRequestHandler() {
super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
@@ -139,6 +146,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.
* <p><strong>Note:</strong> 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<Resource,Charset> locationCharsets) {
this.locationCharsets.clear();
this.locationCharsets.putAll(locationCharsets);
}
/**
* Return charsets associated with static resource locations.
* @since 4.3.13
*/
public Map<Resource, Charset> getLocationCharsets() {
return Collections.unmodifiableMap(locationCharsets);
}
/**
* Configure the list of {@link ResourceResolver}s to use.
* <p>By default {@link PathResourceResolver} is configured. If using this property,
@@ -245,6 +277,25 @@ 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(UrlPathHelper urlPathHelper) {
this.urlPathHelper = urlPathHelper;
}
/**
* The configured {@link UrlPathHelper}.
* @since 4.3.13
*/
public UrlPathHelper getUrlPathHelper() {
return this.urlPathHelper;
}
@Override
public void afterPropertiesSet() throws Exception {
@@ -283,6 +334,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;
}
}

View File

@@ -634,6 +634,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".
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>