Browse Source

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
pull/548/head
Rossen Stoyanchev 12 years ago
parent
commit
6966e89578
  1. 26
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
  2. 36
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java
  3. 2
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java
  4. 8
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java
  5. 79
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceTransformer.java
  6. 259
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java
  7. 6
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java
  8. 84
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceTransformerChain.java
  9. 40
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
  10. 2
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java
  11. 1
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java
  12. 47
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java
  13. 52
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerChain.java
  14. 2
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java
  15. 63
      spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java
  16. 36
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java
  17. 4
      spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CachingResourceResolverTests.java
  18. 92
      spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java
  19. 11
      spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java
  20. 1
      spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less
  21. BIN
      spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/images/image.png
  22. 11
      spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/main.css
  23. 1
      spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less

26
spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java

@ -27,6 +27,7 @@ import org.springframework.util.CollectionUtils; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

36
spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java

@ -33,6 +33,7 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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();
}

2
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java

@ -25,7 +25,7 @@ import java.util.List; @@ -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

8
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java

@ -33,9 +33,9 @@ import java.util.List; @@ -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 { @@ -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 { @@ -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) {

79
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceTransformer.java

@ -0,0 +1,79 @@ @@ -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;
}
}

259
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java

@ -0,0 +1,259 @@ @@ -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;
}
}
}

6
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java

@ -50,7 +50,7 @@ class DefaultResourceResolverChain implements ResourceResolverChain { @@ -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 { @@ -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 { @@ -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");

84
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceTransformerChain.java

@ -0,0 +1,84 @@ @@ -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);
}
}

40
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java

@ -90,6 +90,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H @@ -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 @@ -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 @@ -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 @@ -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;
}
/**

2
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java

@ -44,7 +44,7 @@ public interface ResourceResolver { @@ -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,

1
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java

@ -30,7 +30,6 @@ import org.springframework.core.io.Resource; @@ -30,7 +30,6 @@ import org.springframework.core.io.Resource;
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 4.1
* @see ResourceResolver
*/
public interface ResourceResolverChain {

47
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java

@ -0,0 +1,47 @@ @@ -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;
}

52
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerChain.java

@ -0,0 +1,52 @@ @@ -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;
}

2
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java

@ -215,7 +215,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed @@ -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);

63
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java

@ -0,0 +1,63 @@ @@ -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;
}
}

36
spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java

@ -29,11 +29,12 @@ import org.springframework.web.servlet.HandlerMapping; @@ -29,11 +29,12 @@ import org.springframework.web.servlet.HandlerMapping;
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;
import static org.junit.Assert.*;
/**
* Test fixture with a {@link ResourceHandlerRegistry}.
* Unit tests for {@link ResourceHandlerRegistry}.
*
* @author Rossen Stoyanchev
*/
@ -45,18 +46,19 @@ public class ResourceHandlerRegistryTests { @@ -45,18 +46,19 @@ public class ResourceHandlerRegistryTests {
private MockHttpServletResponse response;
@Before
public void setUp() {
registry = new ResourceHandlerRegistry(new GenericWebApplicationContext(), new MockServletContext());
registration = registry.addResourceHandler("/resources/**");
registration.addResourceLocations("classpath:org/springframework/web/servlet/config/annotation/");
response = new MockHttpServletResponse();
this.registry = new ResourceHandlerRegistry(new GenericWebApplicationContext(), new MockServletContext());
this.registration = registry.addResourceHandler("/resources/**");
this.registration.addResourceLocations("classpath:org/springframework/web/servlet/config/annotation/");
this.response = new MockHttpServletResponse();
}
@Test
public void noResourceHandlers() throws Exception {
registry = new ResourceHandlerRegistry(new GenericWebApplicationContext(), new MockServletContext());
assertNull(registry.getHandlerMapping());
this.registry = new ResourceHandlerRegistry(new GenericWebApplicationContext(), new MockServletContext());
assertNull(this.registry.getHandlerMapping());
}
@Test
@ -66,16 +68,16 @@ public class ResourceHandlerRegistryTests { @@ -66,16 +68,16 @@ public class ResourceHandlerRegistryTests {
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/testStylesheet.css");
ResourceHttpRequestHandler handler = getHandler("/resources/**");
handler.handleRequest(request, response);
handler.handleRequest(request, this.response);
assertEquals("test stylesheet content", response.getContentAsString());
assertEquals("test stylesheet content", this.response.getContentAsString());
}
@Test
public void cachePeriod() {
assertEquals(-1, getHandler("/resources/**").getCacheSeconds());
registration.setCachePeriod(0);
this.registration.setCachePeriod(0);
assertEquals(0, getHandler("/resources/**").getCacheSeconds());
}
@ -89,23 +91,27 @@ public class ResourceHandlerRegistryTests { @@ -89,23 +91,27 @@ public class ResourceHandlerRegistryTests {
@Test
public void hasMappingForPattern() {
assertTrue(registry.hasMappingForPattern("/resources/**"));
assertFalse(registry.hasMappingForPattern("/whatever"));
assertTrue(this.registry.hasMappingForPattern("/resources/**"));
assertFalse(this.registry.hasMappingForPattern("/whatever"));
}
@Test
public void resourceResolversAndTransformers() {
ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
registry.setResourceResolvers(resolver);
this.registry.setResourceResolvers(resolver);
ResourceTransformer transformer = Mockito.mock(ResourceTransformer.class);
this.registry.setResourceTransformers(transformer);
SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) registry.getHandlerMapping();
SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) hm.getUrlMap().values().iterator().next();
assertEquals(Arrays.asList(resolver), handler.getResourceResolvers());
assertEquals(Arrays.asList(transformer), handler.getResourceTransformers());
}
private ResourceHttpRequestHandler getHandler(String pathPattern) {
SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) registry.getHandlerMapping();
SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping();
return (ResourceHttpRequestHandler) handlerMapping.getUrlMap().get(pathPattern);
}

4
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CachingResourceResolverTests.java

@ -74,7 +74,7 @@ public class CachingResourceResolverTests { @@ -74,7 +74,7 @@ public class CachingResourceResolverTests {
public void resolveResourceInternalFromCache() {
Resource expected = Mockito.mock(Resource.class);
this.cache.put("requestPath:bar.css", expected);
this.cache.put(CachingResourceResolver.RESOLVED_RESOURCE_CACHE_KEY_PREFIX + "bar.css", expected);
String file = "bar.css";
Resource actual = this.chain.resolveResource(null, file, this.locations);
@ -98,7 +98,7 @@ public class CachingResourceResolverTests { @@ -98,7 +98,7 @@ public class CachingResourceResolverTests {
@Test
public void resolverUrlPathFromCache() {
String expected = "cached-imaginary.css";
this.cache.put("resourceUrlPath:imaginary.css", expected);
this.cache.put(CachingResourceResolver.RESOLVED_URL_PATH_CACHE_KEY_PREFIX + "imaginary.css", expected);
String actual = this.chain.resolveUrlPath("imaginary.css", this.locations);
assertEquals(expected, actual);

92
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/*
* 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.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.mock.web.test.MockHttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
/**
* Unit tests for
* {@link org.springframework.web.servlet.resource.CssLinkResourceTransformer}.
*
* @author Rossen Stoyanchev
* @since 4.1
*/
public class CssLinkResourceTransformerTests {
private ResourceTransformerChain transformerChain;
private MockHttpServletRequest request;
@Before
public void setUp() {
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(new FingerprintResourceResolver());
resolvers.add(new PathResourceResolver());
ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers);
List<ResourceTransformer> transformers = new ArrayList<>();
transformers.add(new CssLinkResourceTransformer());
this.transformerChain = new DefaultResourceTransformerChain(resolverChain, transformers);
this.request = new MockHttpServletRequest();
}
@Test
public void transformNotCss() throws Exception {
Resource expected = new ClassPathResource("test/images/image.png", getClass());
Resource actual = this.transformerChain.transform(this.request, expected);
assertSame(expected, actual);
}
@Test
public void transform() throws Exception {
Resource mainCss = new ClassPathResource("test/main.css", getClass());
Resource resource = this.transformerChain.transform(this.request, mainCss);
TransformedResource transformedResource = (TransformedResource) resource;
String expected = "\n" +
"@import url(\"bar-11e16cf79faee7ac698c805cf28248d2.css\");\n" +
"@import url('bar-11e16cf79faee7ac698c805cf28248d2.css');\n" +
"@import url(bar-11e16cf79faee7ac698c805cf28248d2.css);\n\n" +
"@import \"foo-e36d2e05253c6c7085a91522ce43a0b4.css\";\n" +
"@import 'foo-e36d2e05253c6c7085a91522ce43a0b4.css';\n\n" +
"body { background: url(\"images/image-f448cd1d5dba82b774f3202c878230b3.png\") }\n\n" +
"li { list-style: url(http://www.example.com/redball.png) disc }\n";
assertEquals(expected, new String(transformedResource.getByteArray(), "UTF-8"));
}
@Test
public void transformNoLinks() throws Exception {
Resource expected = new ClassPathResource("test/foo.css", getClass());
Resource actual = this.transformerChain.transform(this.request, expected);
assertSame(expected, actual);
}
}

11
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java

@ -48,10 +48,11 @@ public class FingerprintResourceResolverTests { @@ -48,10 +48,11 @@ public class FingerprintResourceResolverTests {
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(resolver);
resolvers.add(new PathResourceResolver());
chain = new DefaultResourceResolverChain(resolvers);
locations = new ArrayList<Resource>();
locations.add(new ClassPathResource("test/", getClass()));
locations.add(new ClassPathResource("testalternatepath/", getClass()));
this.chain = new DefaultResourceResolverChain(resolvers);
this.locations = new ArrayList<Resource>();
this.locations.add(new ClassPathResource("test/", getClass()));
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
}
@ -73,7 +74,7 @@ public class FingerprintResourceResolverTests { @@ -73,7 +74,7 @@ public class FingerprintResourceResolverTests {
@Test
public void resolveStaticFingerprintedResource() throws Exception {
String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css";
Resource expected = new ClassPathResource("test/"+file, getClass());
Resource expected = new ClassPathResource("test/" + file, getClass());
Resource actual = chain.resolveResource(null, file, locations);
assertEquals(expected, actual);

1
spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less

@ -1 +0,0 @@ @@ -1 +0,0 @@
h1 { color:red; }

BIN
spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/images/image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

11
spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/main.css

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
@import url("bar.css");
@import url('bar.css');
@import url(bar.css);
@import "foo.css";
@import 'foo.css';
body { background: url("images/image.png") }
li { list-style: url(http://www.example.com/redball.png) disc }

1
spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less

@ -1 +0,0 @@ @@ -1 +0,0 @@
h1 { color:red; }
Loading…
Cancel
Save