diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java index da6634fd86a..ae0666ab540 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java @@ -58,7 +58,7 @@ import org.springframework.util.StringUtils; * applications spec * @since 4.1 */ -public class AppCacheManifestTransformer implements ResourceTransformer { +public class AppCacheManifestTransformer extends ResourceTransformerSupport { private static final String MANIFEST_HEADER = "CACHE MANIFEST"; @@ -129,7 +129,7 @@ public class AppCacheManifestTransformer implements ResourceTransformer { hashBuilder.appendString(line); } else { - contentWriter.write(currentTransformer.transform(line, hashBuilder, resource, transformerChain) + "\n"); + contentWriter.write(currentTransformer.transform(line, hashBuilder, resource, transformerChain, request) + "\n"); } } @@ -151,14 +151,14 @@ public class AppCacheManifestTransformer implements ResourceTransformer { * for the current manifest section (CACHE, NETWORK, FALLBACK, etc). */ String transform(String line, HashBuilder builder, Resource resource, - ResourceTransformerChain transformerChain) throws IOException; + ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException; } private static class NoOpSection implements SectionTransformer { - public String transform(String line, HashBuilder builder, Resource resource, ResourceTransformerChain transformerChain) - throws IOException { + public String transform(String line, HashBuilder builder, Resource resource, + ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException { builder.appendString(line); return line; @@ -166,17 +166,18 @@ public class AppCacheManifestTransformer implements ResourceTransformer { } - private static class CacheSection implements SectionTransformer { + private class CacheSection implements SectionTransformer { private final String COMMENT_DIRECTIVE = "#"; @Override - public String transform(String line, HashBuilder builder, - Resource resource, ResourceTransformerChain transformerChain) throws IOException { + public String transform(String line, HashBuilder builder, Resource resource, + ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException { if (isLink(line) && !hasScheme(line)) { - Resource appCacheResource = transformerChain.getResolverChain().resolveResource(null, line, Arrays.asList(resource)); - String path = transformerChain.getResolverChain().resolveUrlPath(line, Arrays.asList(resource)); + ResourceResolverChain resolverChain = transformerChain.getResolverChain(); + Resource appCacheResource = resolverChain.resolveResource(null, line, Arrays.asList(resource)); + String path = resolveUrlPath(line, request, resource, transformerChain); builder.appendResource(appCacheResource); if (logger.isTraceEnabled()) { logger.trace("Link modified: " + path + " (original: " + line + ")"); 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 index 83be432b6d2..7fd4be40a58 100644 --- 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 @@ -27,7 +27,6 @@ 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; @@ -47,7 +46,7 @@ import java.util.Set; * @author Rossen Stoyanchev * @since 4.1 */ -public class CssLinkResourceTransformer implements ResourceTransformer { +public class CssLinkResourceTransformer extends ResourceTransformerSupport { private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class); @@ -103,7 +102,7 @@ public class CssLinkResourceTransformer implements ResourceTransformer { String link = content.substring(info.getStart(), info.getEnd()); String newLink = null; if (!hasScheme(link)) { - newLink = transformerChain.getResolverChain().resolveUrlPath(link, Arrays.asList(resource)); + newLink = resolveUrlPath(link, request, resource, transformerChain); } if (logger.isTraceEnabled()) { if (newLink != null && !link.equals(newLink)) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerSupport.java new file mode 100644 index 00000000000..a3826974769 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformerSupport.java @@ -0,0 +1,98 @@ +/* + * 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 java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; + +/** + * A base class for a {@code ResourceTransformer} with an optional helper method + * for resolving public links within a transformed resource. + * + * @author Brian Clozel + * @author Rossen Stoyanchev + * @since 4.1 + */ +public abstract class ResourceTransformerSupport implements ResourceTransformer { + + private ResourceUrlProvider resourceUrlProvider; + + + /** + * Configure a {@link ResourceUrlProvider} to use when resolving the public + * URL of links in a transformed resource (e.g. import links in a CSS file). + * This is required only for links expressed as full paths, i.e. including + * context and servlet path, and not for relative links. + * + *

By default this property is not set. In that case if a + * {@code ResourceUrlProvider} is needed an attempt is made to find the + * {@code ResourceUrlProvider} exposed through the + * {@link org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor + * ResourceUrlProviderExposingInterceptor} (configured by default in the MVC + * Java config and XML namespace). Therefore explicitly configuring this + * property should not be needed in most cases. + * @param resourceUrlProvider the URL provider to use + */ + public void setResourceUrlProvider(ResourceUrlProvider resourceUrlProvider) { + this.resourceUrlProvider = resourceUrlProvider; + } + + /** + * @return the configured {@code ResourceUrlProvider}. + */ + public ResourceUrlProvider getResourceUrlProvider() { + return this.resourceUrlProvider; + } + + + /** + * A transformer can use this method when a resource being transformed + * contains links to other resources. Such links need to be replaced with the + * public facing link as determined by the resource resolver chain (e.g. the + * public URL may have a version inserted). + * + * @param resourcePath the path to a resource that needs to be re-written + * @param request the current request + * @param resource the resource being transformed + * @param transformerChain the transformer chain + * @return the resolved URL or null + */ + protected String resolveUrlPath(String resourcePath, HttpServletRequest request, + Resource resource, ResourceTransformerChain transformerChain) { + + if (!resourcePath.startsWith("/")) { + // try resolving as relative path + return transformerChain.getResolverChain().resolveUrlPath(resourcePath, Arrays.asList(resource)); + } + else { + // full resource path + ResourceUrlProvider urlProvider = findResourceUrlProvider(request); + return (urlProvider != null ? urlProvider.getForRequestUrl(request, resourcePath) : null); + } + } + + private ResourceUrlProvider findResourceUrlProvider(HttpServletRequest request) { + if (this.resourceUrlProvider != null) { + return this.resourceUrlProvider; + } + return (ResourceUrlProvider) request.getAttribute( + ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LinkRewriteTransformerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LinkRewriteTransformerTests.java new file mode 100644 index 00000000000..8751e87d73e --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/LinkRewriteTransformerTests.java @@ -0,0 +1,127 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +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 org.springframework.web.servlet.HandlerMapping; + +import javax.servlet.http.HttpServletRequest; + +/** + * Unit tests for {@code LinkRewriteTransformer} + * + * @author Brian Clozel + * @author Rossen Stoyanchev + */ +public class LinkRewriteTransformerTests { + + private ResourceTransformerChain transformerChain; + + private TestTransformer transformer; + + private MockHttpServletRequest request; + + + @Before + public void setUp() { + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy())); + + List resolvers = new ArrayList<>(); + resolvers.add(versionResolver); + resolvers.add(new PathResourceResolver()); + this.transformerChain = new DefaultResourceTransformerChain(new DefaultResourceResolverChain(resolvers), null); + + List locations = new ArrayList<>(); + locations.add(new ClassPathResource("test/", getClass())); + + ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); + handler.setLocations(locations); + handler.setResourceResolvers(resolvers); + + ResourceUrlProvider urlProvider = new ResourceUrlProvider(); + urlProvider.setHandlerMap(Collections.singletonMap("/resources/**", handler)); + + this.transformer = new TestTransformer(); + this.transformer.setResourceUrlProvider(urlProvider); + + this.request = new MockHttpServletRequest(); + } + + @Test + public void rewriteAbsolutePath() throws Exception { + this.request.setRequestURI("/servlet/context/resources/main.css"); + this.request.setMethod("GET"); + this.request.setServletPath("/servlet"); + this.request.setContextPath("/context"); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/resources/main.css"); + + String resourcePath = "/servlet/context/resources/bar.css"; + Resource mainCss = new ClassPathResource("test/main.css", getClass()); + String actual = this.transformer.resolveUrlPath(resourcePath, this.request, mainCss, this.transformerChain); + assertEquals("/servlet/context/resources/bar-11e16cf79faee7ac698c805cf28248d2.css", actual); + + actual = this.transformer.resolveUrlPath("bar.css", this.request, mainCss, this.transformerChain); + assertEquals("bar-11e16cf79faee7ac698c805cf28248d2.css", actual); + } + + @Test + public void rewriteRelativePath() throws Exception { + this.request.setRequestURI("/servlet/context/resources/main.css"); + this.request.setMethod("GET"); + this.request.setServletPath("/servlet"); + this.request.setContextPath("/context"); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/resources/main.css"); + + Resource mainCss = new ClassPathResource("test/main.css", getClass()); + String actual = this.transformer.resolveUrlPath("bar.css", this.request, mainCss, this.transformerChain); + assertEquals("bar-11e16cf79faee7ac698c805cf28248d2.css", actual); + } + + @Test + public void rewriteRelativePathUpperLevel() throws Exception { + this.request.setRequestURI("/servlet/context/resources/images/image.png"); + this.request.setMethod("GET"); + this.request.setServletPath("/servlet"); + this.request.setContextPath("/context"); + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/resources/images/image.png"); + + Resource imagePng = new ClassPathResource("test/images/image.png", getClass()); + String actual = this.transformer.resolveUrlPath("../bar.css", this.request, imagePng, this.transformerChain); + assertEquals("../bar-11e16cf79faee7ac698c805cf28248d2.css", actual); + } + + + private static class TestTransformer extends ResourceTransformerSupport { + + @Override + public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain chain) { + throw new IllegalStateException("Should never be called"); + } + } + +}