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:
Rossen Stoyanchev
2014-05-27 20:43:57 -04:00
parent e3e8a3eb40
commit 6966e89578
23 changed files with 808 additions and 55 deletions

View File

@@ -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 {
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 {
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 {
@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);
SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) registry.getHandlerMapping();
ResourceTransformer transformer = Mockito.mock(ResourceTransformer.class);
this.registry.setResourceTransformers(transformer);
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);
}

View File

@@ -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 {
@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);

View File

@@ -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);
}
}

View File

@@ -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 {
@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);