diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index 11395bb8de9..1f060024fe3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -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 resourceResolvers; + private List resourceTransformers; + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -78,13 +81,23 @@ public class ResourceHandlerRegistration { /** * Configure the list of {@link ResourceResolver}s to use. - *

- * By default {@link PathResourceResolver} is configured. If using this property, it + *

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. + *

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 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); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java index 16b5b5e5315..bf755927bac 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java @@ -33,6 +33,7 @@ import org.springframework.web.servlet.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 registrations = new ArrayList(); private List resourceResolvers; + private List 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(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java index fa59c7da011..405760c38af 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java @@ -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 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java index a26fad51824..59d195fd223 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceResolver.java @@ -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 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 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) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceTransformer.java new file mode 100644 index 00000000000..3e27f9df122 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CachingResourceTransformer.java @@ -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; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java new file mode 100644 index 00000000000..da3b91d6a09 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java @@ -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). + * + *

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 linkParsers = new ArrayList(); + + + 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 infos = new HashSet(5); + for (CssLinkParser parser : this.linkParsers) { + parser.parseLink(content, infos); + } + + if (infos.isEmpty()) { + if (logger.isTraceEnabled()) { + logger.trace("No links found."); + } + return resource; + } + + List sortedInfos = new ArrayList(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 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 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 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 linkInfos); + + } + + private static class ImportStatementCssLinkParser extends AbstractCssLinkParser { + + @Override + protected String getKeyword() { + return "@import"; + } + + @Override + protected int extractLink(int index, String content, Set 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 linkInfos) { + // A url() function without unquoted + return addLink(index - 1, ")", content, linkInfos); + } + } + + + private static class CssLinkInfo implements Comparable { + + 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; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java index 8d2f2bc6f07..4d44acfb237 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java @@ -50,7 +50,7 @@ class DefaultResourceResolverChain implements ResourceResolverChain { @Override public Resource resolveResource(HttpServletRequest request, String requestPath, List 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 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"); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceTransformerChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceTransformerChain.java new file mode 100644 index 00000000000..eb9473e691b --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceTransformerChain.java @@ -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 transformers = new ArrayList(); + + private int index = -1; + + public DefaultResourceTransformerChain(ResourceResolverChain resolverChain, + List 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); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 0c46a57cdd9..81659155aff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -90,6 +90,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H private final List resourceResolvers = new ArrayList(); + private final List resourceTransformers = new ArrayList(); + 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. - *

- * By default {@link PathResourceResolver} is configured. If using this property, it + * + *

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 resourceResolvers) { @@ -123,10 +125,31 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } } + /** + * Return the list of configured resource resolvers. + */ public List getResourceResolvers() { return this.resourceResolvers; } + /** + * Configure the list of {@link ResourceTransformer}s to use. + *

By default no transformers are configured for use. + */ + public void setResourceTransformers(List resourceTransformers) { + this.resourceTransformers.clear(); + if (resourceTransformers != null) { + this.resourceTransformers.addAll(resourceTransformers); + } + } + + /** + * Return the list of configured resource transformers. + */ + public List 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; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java index c8d2d1bc7f6..70e13e45fc3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java @@ -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 locations, diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java index 05a0e5a02e9..ac57de820c1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java @@ -30,7 +30,6 @@ import org.springframework.core.io.Resource; * @author Rossen Stoyanchev * @author Sam Brannen * @since 4.1 - * @see ResourceResolver */ public interface ResourceResolverChain { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java new file mode 100644 index 00000000000..4078a08243c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java @@ -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; + +} \ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerChain.java new file mode 100644 index 00000000000..728fe93ad21 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerChain.java @@ -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; + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java index 8924075eeec..0b30b58bc47 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java @@ -215,7 +215,7 @@ public class ResourceUrlProvider implements ApplicationListener resolvers = new ArrayList(); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); + ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers); + + List 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); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java index e9c202a3a8d..668fbfe4686 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java @@ -48,10 +48,11 @@ public class FingerprintResourceResolverTests { List resolvers = new ArrayList(); resolvers.add(resolver); resolvers.add(new PathResourceResolver()); - chain = new DefaultResourceResolverChain(resolvers); - locations = new ArrayList(); - locations.add(new ClassPathResource("test/", getClass())); - locations.add(new ClassPathResource("testalternatepath/", getClass())); + this.chain = new DefaultResourceResolverChain(resolvers); + + this.locations = new ArrayList(); + this.locations.add(new ClassPathResource("test/", getClass())); + this.locations.add(new ClassPathResource("testalternatepath/", getClass())); } @@ -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); diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less deleted file mode 100644 index e2f0b1c742a..00000000000 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less +++ /dev/null @@ -1 +0,0 @@ -h1 { color:red; } \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/images/image.png b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/images/image.png new file mode 100644 index 00000000000..86622dc4fb4 Binary files /dev/null and b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/images/image.png differ diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/main.css b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/main.css new file mode 100644 index 00000000000..ac14294bf3e --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/main.css @@ -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 } diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less deleted file mode 100644 index e2f0b1c742a..00000000000 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less +++ /dev/null @@ -1 +0,0 @@ -h1 { color:red; } \ No newline at end of file