Add ResourceTransformer and CSS link implementation
This change adds a ResourceTransformer that can be invoked in a chain after resource resolution. The CssLinkResourceTransformer modifies a CSS file being served in order to update its @import and url() links (e.g. to images or other CSS files) to match the resource resolution strategy (e.g. adding MD5 content-based hashes). Issue: SPR-11800
This commit is contained in:
@@ -27,6 +27,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
|
||||
import org.springframework.web.servlet.resource.ResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceTransformer;
|
||||
|
||||
/**
|
||||
* Encapsulates information required to create a resource handlers.
|
||||
@@ -48,6 +49,8 @@ public class ResourceHandlerRegistration {
|
||||
|
||||
private List<ResourceResolver> resourceResolvers;
|
||||
|
||||
private List<ResourceTransformer> resourceTransformers;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link ResourceHandlerRegistration} instance.
|
||||
@@ -78,13 +81,23 @@ public class ResourceHandlerRegistration {
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceResolver}s to use.
|
||||
* <p>
|
||||
* By default {@link PathResourceResolver} is configured. If using this property, it
|
||||
* <p>By default {@link PathResourceResolver} is configured. If using this property, it
|
||||
* is recommended to add {@link PathResourceResolver} as the last resolver.
|
||||
* @since 4.1
|
||||
*/
|
||||
public void setResourceResolvers(ResourceResolver... resourceResolvers) {
|
||||
public ResourceHandlerRegistration setResourceResolvers(ResourceResolver... resourceResolvers) {
|
||||
this.resourceResolvers = Arrays.asList(resourceResolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceTransformer}s to use.
|
||||
* <p>By default no transformers are configured.
|
||||
* @since 4.1
|
||||
*/
|
||||
public ResourceHandlerRegistration setResourceTransformers(ResourceTransformer... transformers) {
|
||||
this.resourceTransformers = Arrays.asList(transformers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,6 +123,10 @@ public class ResourceHandlerRegistration {
|
||||
return this.resourceResolvers;
|
||||
}
|
||||
|
||||
protected List<ResourceTransformer> getResourceTransformers() {
|
||||
return this.resourceTransformers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link ResourceHttpRequestHandler} instance.
|
||||
*/
|
||||
@@ -119,6 +136,9 @@ public class ResourceHandlerRegistration {
|
||||
if (this.resourceResolvers != null) {
|
||||
requestHandler.setResourceResolvers(this.resourceResolvers);
|
||||
}
|
||||
if (this.resourceTransformers != null) {
|
||||
requestHandler.setResourceTransformers(this.resourceTransformers);
|
||||
}
|
||||
requestHandler.setLocations(this.locations);
|
||||
if (this.cachePeriod != null) {
|
||||
requestHandler.setCacheSeconds(this.cachePeriod);
|
||||
|
||||
@@ -33,6 +33,7 @@ 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.servlet.resource.ResourceResolver;
|
||||
import org.springframework.web.servlet.resource.ResourceTransformer;
|
||||
|
||||
/**
|
||||
* Stores registrations of resource handlers for serving static resources such as images, css files and others
|
||||
@@ -55,18 +56,20 @@ public class ResourceHandlerRegistry {
|
||||
|
||||
private final ServletContext servletContext;
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ApplicationContext appContext;
|
||||
|
||||
private final List<ResourceHandlerRegistration> registrations = new ArrayList<ResourceHandlerRegistration>();
|
||||
|
||||
private List<ResourceResolver> resourceResolvers;
|
||||
|
||||
private List<ResourceTransformer> resourceTransformers;
|
||||
|
||||
private int order = Integer.MAX_VALUE -1;
|
||||
|
||||
|
||||
public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext is required");
|
||||
this.applicationContext = applicationContext;
|
||||
this.appContext = applicationContext;
|
||||
this.servletContext = servletContext;
|
||||
}
|
||||
|
||||
@@ -77,8 +80,8 @@ public class ResourceHandlerRegistry {
|
||||
* @return A {@link ResourceHandlerRegistration} to use to further configure the registered resource handler.
|
||||
*/
|
||||
public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) {
|
||||
ResourceHandlerRegistration registration = new ResourceHandlerRegistration(applicationContext, pathPatterns);
|
||||
registrations.add(registration);
|
||||
ResourceHandlerRegistration registration = new ResourceHandlerRegistration(this.appContext, pathPatterns);
|
||||
this.registrations.add(registration);
|
||||
return registration;
|
||||
}
|
||||
|
||||
@@ -86,7 +89,7 @@ public class ResourceHandlerRegistry {
|
||||
* Whether a resource handler has already been registered for the given pathPattern.
|
||||
*/
|
||||
public boolean hasMappingForPattern(String pathPattern) {
|
||||
for (ResourceHandlerRegistration registration : registrations) {
|
||||
for (ResourceHandlerRegistration registration : this.registrations) {
|
||||
if (Arrays.asList(registration.getPathPatterns()).contains(pathPattern)) {
|
||||
return true;
|
||||
}
|
||||
@@ -104,11 +107,21 @@ public class ResourceHandlerRegistry {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceResolver}s to use by default in resource handlers that
|
||||
* don't have them set.
|
||||
* Configure the {@link ResourceResolver}s to use by default, that is in
|
||||
* resource handlers aren't already configured explicitly with resolvers.
|
||||
*/
|
||||
public void setResourceResolvers(ResourceResolver... resourceResolvers) {
|
||||
this.resourceResolvers = Arrays.asList(resourceResolvers);
|
||||
public ResourceHandlerRegistry setResourceResolvers(ResourceResolver... resolvers) {
|
||||
this.resourceResolvers = Arrays.asList(resolvers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceTransformer}s to use by default, that is in
|
||||
* resource handlers aren't already configured explicitly with transformers.
|
||||
*/
|
||||
public ResourceHandlerRegistry setResourceTransformers(ResourceTransformer... transformers) {
|
||||
this.resourceTransformers = Arrays.asList(transformers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,10 +137,13 @@ public class ResourceHandlerRegistry {
|
||||
for (String pathPattern : registration.getPathPatterns()) {
|
||||
ResourceHttpRequestHandler handler = registration.getRequestHandler();
|
||||
handler.setServletContext(this.servletContext);
|
||||
handler.setApplicationContext(this.applicationContext);
|
||||
handler.setApplicationContext(this.appContext);
|
||||
if ((this.resourceResolvers != null) && (registration.getResourceResolvers() == null)) {
|
||||
handler.setResourceResolvers(this.resourceResolvers);
|
||||
}
|
||||
if ((this.resourceResolvers != null) && (registration.getResourceTransformers() == null)) {
|
||||
handler.setResourceTransformers(this.resourceTransformers);
|
||||
}
|
||||
try {
|
||||
handler.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class for {@link org.springframework.web.servlet.resource.ResourceResolver}
|
||||
* implementations.
|
||||
* implementations. Provides consistent logging.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
|
||||
@@ -33,9 +33,9 @@ import java.util.List;
|
||||
*/
|
||||
public class CachingResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private static final String REQUEST_PATH_PREFIX = "requestPath:";
|
||||
public static final String RESOLVED_RESOURCE_CACHE_KEY_PREFIX = "resolvedResource:";
|
||||
|
||||
private static final String RESOURCE_URL_PATH_PREFIX = "resourceUrlPath:";
|
||||
public static final String RESOLVED_URL_PATH_CACHE_KEY_PREFIX = "resolvedUrlPath:";
|
||||
|
||||
|
||||
private final Cache cache;
|
||||
@@ -57,7 +57,7 @@ public class CachingResourceResolver extends AbstractResourceResolver {
|
||||
protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String key = REQUEST_PATH_PREFIX + requestPath;
|
||||
String key = RESOLVED_RESOURCE_CACHE_KEY_PREFIX + requestPath;
|
||||
Resource resource = this.cache.get(key, Resource.class);
|
||||
|
||||
if (resource != null) {
|
||||
@@ -82,7 +82,7 @@ public class CachingResourceResolver extends AbstractResourceResolver {
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String key = RESOURCE_URL_PATH_PREFIX + resourceUrlPath;
|
||||
String key = RESOLVED_URL_PATH_CACHE_KEY_PREFIX + resourceUrlPath;
|
||||
String resolvedUrlPath = this.cache.get(key, String.class);
|
||||
|
||||
if (resolvedUrlPath != null) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link org.springframework.web.servlet.resource.ResourceTransformer} that
|
||||
* checks a {@link org.springframework.cache.Cache} to see if a previously
|
||||
* transformed or otherwise
|
||||
* delegates to the resolver chain and saves the result in the cache.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
*/
|
||||
public class CachingResourceTransformer implements ResourceTransformer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(CachingResourceTransformer.class);
|
||||
|
||||
private final Cache cache;
|
||||
|
||||
|
||||
public CachingResourceTransformer(Cache cache) {
|
||||
Assert.notNull(cache, "'cache' is required");
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured {@code Cache}.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
|
||||
throws IOException {
|
||||
|
||||
Resource transformed = this.cache.get(resource, Resource.class);
|
||||
if (transformed != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Found match");
|
||||
}
|
||||
return transformed;
|
||||
}
|
||||
|
||||
transformed = transformerChain.transform(request, resource);
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Putting transformed resource in cache");
|
||||
}
|
||||
this.cache.put(resource, transformed);
|
||||
|
||||
return transformed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@link ResourceTransformer} implementation that modifies links in a CSS
|
||||
* file to match the public URL paths that should be exposed to clients (e.g.
|
||||
* with an MD5 content-based hash inserted in the URL).
|
||||
*
|
||||
* <p>The implementation looks for links in CSS {@code @import} statements and
|
||||
* also inside CSS {@code url()} functions. All links are then passed through the
|
||||
* {@link ResourceResolverChain} and resolved relative to the location of the
|
||||
* containing CSS file. If successfully resolved, the link is modified, otherwise
|
||||
* the original link is preserved.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
*/
|
||||
public class CssLinkResourceTransformer implements ResourceTransformer {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class);
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
|
||||
private final List<CssLinkParser> linkParsers = new ArrayList<CssLinkParser>();
|
||||
|
||||
|
||||
public CssLinkResourceTransformer() {
|
||||
this.linkParsers.add(new ImportStatementCssLinkParser());
|
||||
this.linkParsers.add(new UrlFunctionCssLinkParser());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
|
||||
throws IOException {
|
||||
|
||||
resource = transformerChain.transform(request, resource);
|
||||
|
||||
String filename = resource.getFilename();
|
||||
if (!"css".equals(StringUtils.getFilenameExtension(filename))) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Transforming resource: " + resource);
|
||||
}
|
||||
|
||||
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
|
||||
String content = new String(bytes, DEFAULT_CHARSET);
|
||||
|
||||
Set<CssLinkInfo> infos = new HashSet<CssLinkInfo>(5);
|
||||
for (CssLinkParser parser : this.linkParsers) {
|
||||
parser.parseLink(content, infos);
|
||||
}
|
||||
|
||||
if (infos.isEmpty()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No links found.");
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
List<CssLinkInfo> sortedInfos = new ArrayList<CssLinkInfo>(infos);
|
||||
Collections.sort(sortedInfos);
|
||||
|
||||
int index = 0;
|
||||
StringWriter writer = new StringWriter();
|
||||
for (CssLinkInfo info : sortedInfos) {
|
||||
writer.write(content.substring(index, info.getStart()));
|
||||
String link = content.substring(info.getStart(), info.getEnd());
|
||||
String newLink = transformerChain.getResolverChain().resolveUrlPath(link, Arrays.asList(resource));
|
||||
if (logger.isTraceEnabled()) {
|
||||
if (newLink != null && !link.equals(newLink)) {
|
||||
logger.trace("Link modified: " + newLink + " (original: " + link + ")");
|
||||
}
|
||||
else {
|
||||
logger.trace("Link not modified: " + link);
|
||||
}
|
||||
}
|
||||
writer.write(newLink != null ? newLink : link);
|
||||
index = info.getEnd();
|
||||
}
|
||||
writer.write(content.substring(index));
|
||||
|
||||
return new TransformedResource(resource, writer.toString().getBytes(DEFAULT_CHARSET));
|
||||
}
|
||||
|
||||
|
||||
protected static interface CssLinkParser {
|
||||
|
||||
void parseLink(String content, Set<CssLinkInfo> linkInfos);
|
||||
|
||||
}
|
||||
|
||||
protected static abstract class AbstractCssLinkParser implements CssLinkParser {
|
||||
|
||||
/**
|
||||
* Return the keyword to use to search for links.
|
||||
*/
|
||||
protected abstract String getKeyword();
|
||||
|
||||
@Override
|
||||
public void parseLink(String content, Set<CssLinkInfo> linkInfos) {
|
||||
int index = 0;
|
||||
do {
|
||||
index = content.indexOf(getKeyword(), index);
|
||||
if (index == -1) {
|
||||
break;
|
||||
}
|
||||
index = skipWhitespace(content, index + getKeyword().length());
|
||||
if (content.charAt(index) == '\'') {
|
||||
index = addLink(index, "'", content, linkInfos);
|
||||
}
|
||||
else if (content.charAt(index) == '"') {
|
||||
index = addLink(index, "\"", content, linkInfos);
|
||||
}
|
||||
else {
|
||||
index = extractLink(index, content, linkInfos);
|
||||
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
private int skipWhitespace(String content, int index) {
|
||||
while (true) {
|
||||
if (Character.isWhitespace(content.charAt(index))) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
protected int addLink(int index, String endKey, String content, Set<CssLinkInfo> linkInfos) {
|
||||
int start = index + 1;
|
||||
int end = content.indexOf(endKey, start);
|
||||
linkInfos.add(new CssLinkInfo(start, end));
|
||||
return end + endKey.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked after a keyword match, after whitespaces removed, and when
|
||||
* the next char is neither a single nor double quote.
|
||||
*/
|
||||
protected abstract int extractLink(int index, String content, Set<CssLinkInfo> linkInfos);
|
||||
|
||||
}
|
||||
|
||||
private static class ImportStatementCssLinkParser extends AbstractCssLinkParser {
|
||||
|
||||
@Override
|
||||
protected String getKeyword() {
|
||||
return "@import";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) {
|
||||
if (content.substring(index, index + 4).equals("url(")) {
|
||||
// Ignore, UrlLinkParser will take care
|
||||
}
|
||||
else if (logger.isErrorEnabled()) {
|
||||
logger.error("Unexpected syntax for @import link at index " + index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
private static class UrlFunctionCssLinkParser extends AbstractCssLinkParser {
|
||||
|
||||
@Override
|
||||
protected String getKeyword() {
|
||||
return "url(";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) {
|
||||
// A url() function without unquoted
|
||||
return addLink(index - 1, ")", content, linkInfos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class CssLinkInfo implements Comparable<CssLinkInfo> {
|
||||
|
||||
private final int start;
|
||||
|
||||
private final int end;
|
||||
|
||||
|
||||
private CssLinkInfo(int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return this.start;
|
||||
}
|
||||
|
||||
public int getEnd() {
|
||||
return this.end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CssLinkInfo other) {
|
||||
return Integer.compare(this.start, other.start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj != null && obj instanceof CssLinkInfo) {
|
||||
CssLinkInfo other = (CssLinkInfo) obj;
|
||||
return (this.start == other.start && this.end == other.end);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.start * 31 + this.end;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,7 +50,7 @@ class DefaultResourceResolverChain implements ResourceResolverChain {
|
||||
|
||||
@Override
|
||||
public Resource resolveResource(HttpServletRequest request, String requestPath, List<? extends Resource> locations) {
|
||||
ResourceResolver resolver = getNextResolver();
|
||||
ResourceResolver resolver = getNext();
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class DefaultResourceResolverChain implements ResourceResolverChain {
|
||||
|
||||
@Override
|
||||
public String resolveUrlPath(String resourcePath, List<? extends Resource> locations) {
|
||||
ResourceResolver resolver = getNextResolver();
|
||||
ResourceResolver resolver = getNext();
|
||||
if (resolver == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class DefaultResourceResolverChain implements ResourceResolverChain {
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceResolver getNextResolver() {
|
||||
private ResourceResolver getNext() {
|
||||
|
||||
Assert.state(this.index <= this.resolvers.size(),
|
||||
"Current index exceeds the number of configured ResourceResolver's");
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ResourceTransformerChain} for invoking a list
|
||||
* of {@link ResourceTransformer}s.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
*/
|
||||
class DefaultResourceTransformerChain implements ResourceTransformerChain {
|
||||
|
||||
private final ResourceResolverChain resolverChain;
|
||||
|
||||
private final List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>();
|
||||
|
||||
private int index = -1;
|
||||
|
||||
public DefaultResourceTransformerChain(ResourceResolverChain resolverChain,
|
||||
List<ResourceTransformer> transformers) {
|
||||
|
||||
Assert.notNull(resolverChain, "'resolverChain' is required");
|
||||
this.resolverChain = resolverChain;
|
||||
if (transformers != null) {
|
||||
this.transformers.addAll(transformers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ResourceResolverChain getResolverChain() {
|
||||
return this.resolverChain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource transform(HttpServletRequest request, Resource resource) throws IOException {
|
||||
ResourceTransformer transformer = getNext();
|
||||
if (transformer == null) {
|
||||
return resource;
|
||||
}
|
||||
try {
|
||||
return transformer.transform(request, resource, this);
|
||||
}
|
||||
finally {
|
||||
this.index--;
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceTransformer getNext() {
|
||||
|
||||
Assert.state(this.index <= this.transformers.size(),
|
||||
"Current index exceeds the number of configured ResourceTransformer's");
|
||||
|
||||
if (this.index == (this.transformers.size() - 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.index++;
|
||||
return this.transformers.get(this.index);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -90,6 +90,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
|
||||
|
||||
private final List<ResourceResolver> resourceResolvers = new ArrayList<ResourceResolver>();
|
||||
|
||||
private final List<ResourceTransformer> resourceTransformers = new ArrayList<ResourceTransformer>();
|
||||
|
||||
|
||||
public ResourceHttpRequestHandler() {
|
||||
super(METHOD_GET, METHOD_HEAD);
|
||||
@@ -112,8 +114,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceResolver}s to use.
|
||||
* <p>
|
||||
* By default {@link PathResourceResolver} is configured. If using this property, it
|
||||
*
|
||||
* <p>By default {@link PathResourceResolver} is configured. If using this property, it
|
||||
* is recommended to add {@link PathResourceResolver} as the last resolver.
|
||||
*/
|
||||
public void setResourceResolvers(List<ResourceResolver> resourceResolvers) {
|
||||
@@ -123,10 +125,31 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource resolvers.
|
||||
*/
|
||||
public List<ResourceResolver> getResourceResolvers() {
|
||||
return this.resourceResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the list of {@link ResourceTransformer}s to use.
|
||||
* <p>By default no transformers are configured for use.
|
||||
*/
|
||||
public void setResourceTransformers(List<ResourceTransformer> resourceTransformers) {
|
||||
this.resourceTransformers.clear();
|
||||
if (resourceTransformers != null) {
|
||||
this.resourceTransformers.addAll(resourceTransformers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of configured resource transformers.
|
||||
*/
|
||||
public List<ResourceTransformer> getResourceTransformers() {
|
||||
return this.resourceTransformers;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
@@ -201,11 +224,14 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return createResourceResolverChain().resolveResource(request, path, getLocations());
|
||||
}
|
||||
|
||||
ResourceResolverChain createResourceResolverChain() {
|
||||
return new DefaultResourceResolverChain(getResourceResolvers());
|
||||
ResourceResolverChain resolveChain = new DefaultResourceResolverChain(getResourceResolvers());
|
||||
Resource resource = resolveChain.resolveResource(request, path, getLocations());
|
||||
if (resource == null || getResourceTransformers().isEmpty()) {
|
||||
return resource;
|
||||
}
|
||||
ResourceTransformerChain transformChain = new DefaultResourceTransformerChain(resolveChain, getResourceTransformers());
|
||||
resource = transformChain.transform(request, resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,7 @@ public interface ResourceResolver {
|
||||
* @param request the current request
|
||||
* @param requestPath the portion of the request path to use
|
||||
* @param locations the locations to search in when looking up resources
|
||||
* @param chain the chain of resolvers to delegate to
|
||||
* @param chain the chain of remaining resolvers to delegate to
|
||||
* @return the resolved resource or {@code null} if unresolved
|
||||
*/
|
||||
Resource resolveResource(HttpServletRequest request, String requestPath, List<? extends Resource> locations,
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.springframework.core.io.Resource;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @since 4.1
|
||||
* @see ResourceResolver
|
||||
*/
|
||||
public interface ResourceResolverChain {
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An abstraction for transforming the content of a resource.
|
||||
*
|
||||
* @author Jeremy Grelle
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface ResourceTransformer {
|
||||
|
||||
/**
|
||||
* Transform the given resource.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param resource the resource to transform
|
||||
* @param transformerChain the chain of remaining transformers to delegate to
|
||||
* @return the transformed resource, never {@code null}
|
||||
*
|
||||
* @throws IOException if the transformation fails
|
||||
*/
|
||||
Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
|
||||
throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A contract for invoking a chain of {@link ResourceTransformer}s where each resolver
|
||||
* is given a reference to the chain allowing it to delegate when necessary.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface ResourceTransformerChain {
|
||||
|
||||
/**
|
||||
* Return the {@code ResourceResolverChain} that was used to resolve the
|
||||
* {@code Resource} being transformed. This may be needed for resolving
|
||||
* related resources, e.g. links to other resources.
|
||||
*/
|
||||
ResourceResolverChain getResolverChain();
|
||||
|
||||
/**
|
||||
* Transform the given resource.
|
||||
*
|
||||
* @param request the current request
|
||||
* @param resource the candidate resource to transform
|
||||
* @return the transformed or the same resource, never {@code null}
|
||||
*
|
||||
* @throws java.io.IOException if transformation fails
|
||||
*/
|
||||
Resource transform(HttpServletRequest request, Resource resource) throws IOException;
|
||||
|
||||
}
|
||||
@@ -215,7 +215,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
|
||||
logger.trace("Invoking ResourceResolverChain for URL pattern=\"" + pattern + "\"");
|
||||
}
|
||||
ResourceHttpRequestHandler handler = this.handlerMap.get(pattern);
|
||||
ResourceResolverChain chain = handler.createResourceResolverChain();
|
||||
ResourceResolverChain chain = new DefaultResourceResolverChain(handler.getResourceResolvers());
|
||||
String resolved = chain.resolveUrlPath(pathWithinMapping, handler.getLocations());
|
||||
if (resolved == null) {
|
||||
throw new IllegalStateException("Failed to get public resource URL path for " + pathWithinMapping);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An extension of {@link org.springframework.core.io.ByteArrayResource} that a
|
||||
* {@link ResourceTransformer} can use to represent an original resource
|
||||
* preserving all other information except the content.
|
||||
*
|
||||
* @author Jeremy Grelle
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.1
|
||||
*/
|
||||
public class TransformedResource extends ByteArrayResource {
|
||||
|
||||
private final String filename;
|
||||
|
||||
private final long lastModified;
|
||||
|
||||
|
||||
public TransformedResource(Resource original, byte[] transformedContent) {
|
||||
super(transformedContent);
|
||||
this.filename = original.getFilename();
|
||||
try {
|
||||
this.lastModified = original.lastModified();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// should never happen
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return this.filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return this.lastModified;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user