From 6cb9a144db46af981fea4829e84044f05ce60a78 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Oct 2013 21:36:38 -0400 Subject: [PATCH] Expand static resource handling mechanism An initial commit with expanded support for static resource handling: - Add ResourceResolver strategy for resolving a request to a Resource along with a few implementations. - Add PublicResourceUrlProvider to get URLs for client-side use. - Add ResourceUrlEncodingFilter and PublicResourceUrlProviderExposingInterceptor along with initial MVC Java config support. Issue: SPR-10933 --- build.gradle | 1 + gradle/wrapper/gradle-wrapper.properties | 4 +- .../AbstractMessageConverterTests.java | 135 +++++++++++ .../ResourceHandlerRegistration.java | 33 ++- .../annotation/ResourceHandlerRegistry.java | 32 ++- .../WebMvcConfigurationSupport.java | 52 +++- .../DefaultResourceResolverChain.java | 115 +++++++++ .../web/servlet/resource/EncodedResource.java | 42 ++++ .../resource/FingerprintResourceResolver.java | 124 ++++++++++ .../resource/GzipResourceResolver.java | 149 ++++++++++++ .../resource/PathResourceResolver.java | 81 +++++++ .../resource/PrefixResourceResolver.java | 65 +++++ .../resource/PublicResourceUrlProvider.java | 223 ++++++++++++++++++ ...esourceUrlProviderExposingInterceptor.java | 58 +++++ .../resource/ResourceHttpRequestHandler.java | 72 ++++-- .../servlet/resource/ResourceResolver.java | 66 ++++++ .../resource/ResourceResolverChain.java | 61 +++++ .../resource/ResourceUrlEncodingFilter.java | 79 +++++++ .../ResourceHandlerRegistryTests.java | 16 ++ ...MvcConfigurationSupportExtensionTests.java | 4 +- .../WebMvcConfigurationSupportTests.java | 4 +- .../web/servlet/handler/resources/js/bar.js | 1 - .../web/servlet/handler/resources/js/foo.js | 1 - .../FingerprintResourceResolverTests.java | 123 ++++++++++ .../resource/GzipResourceResolverTests.java | 110 +++++++++ .../resource/PrefixResourceResolverTests.java | 70 ++++++ ...licResourceUrlProviderJavaConfigTests.java | 139 +++++++++++ .../PublicResourceUrlProviderTests.java | 85 +++++++ .../ResourceHttpRequestHandlerTests.java | 3 +- .../web/servlet/resource/test/bar.min.css} | 0 .../servlet/resource/test/foo-bar/foo-bar.css | 1 + .../foo-e36d2e05253c6c7085a91522ce43a0b4.css} | 0 .../web/servlet/resource/test/foo.css.less | 1 + .../web/servlet/resource/test/zoo.css.less | 1 + 34 files changed, 1898 insertions(+), 53 deletions(-) create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/converter/AbstractMessageConverterTests.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProvider.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderExposingInterceptor.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/bar.js delete mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderJavaConfigTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderTests.java rename spring-webmvc/src/test/{java/org/springframework/web/servlet/handler/resources/bar.css => resources/org/springframework/web/servlet/resource/test/bar.min.css} (100%) create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-bar/foo-bar.css rename spring-webmvc/src/test/{java/org/springframework/web/servlet/handler/resources/foo.css => resources/org/springframework/web/servlet/resource/test/foo-e36d2e05253c6c7085a91522ce43a0b4.css} (100%) create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less diff --git a/build.gradle b/build.gradle index 34a6cd4f3f..b80b4ab75c 100644 --- a/build.gradle +++ b/build.gradle @@ -677,6 +677,7 @@ project("spring-orm-hibernate4") { optional("org.hibernate:hibernate-core:${hibernate4Version}") optional("org.hibernate:hibernate-entitymanager:${hibernate4Version}") optional("javax.servlet:javax.servlet-api:3.0.1") + optional("aopalliance:aopalliance:1.0") } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b964f07083..0178eaa66d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Feb 12 23:28:21 CET 2014 +#Thu Mar 06 18:45:45 CET 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/AbstractMessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/AbstractMessageConverterTests.java new file mode 100644 index 0000000000..463f1657bd --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/AbstractMessageConverterTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2013 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.messaging.converter; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +import static org.junit.Assert.*; + +/** + * Test fixture for {@link org.springframework.messaging.converter.AbstractMessageConverter}. + * + * @author Rossen Stoyanchev + */ +public class AbstractMessageConverterTests { + + private TestMessageConverter converter; + + + @Before + public void setup() { + this.converter = new TestMessageConverter(); + this.converter.setContentTypeResolver(new DefaultContentTypeResolver()); + } + + @Test + public void supportsTargetClass() { + Message message = MessageBuilder.withPayload("ABC").build(); + + assertEquals("success-from", this.converter.fromMessage(message, String.class)); + assertNull(this.converter.fromMessage(message, Integer.class)); + } + + @Test + public void supportsMimeType() { + Message message = MessageBuilder.withPayload( + "ABC").setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN).build(); + + assertEquals("success-from", this.converter.fromMessage(message, String.class)); + } + + @Test + public void supportsMimeTypeNotSupported() { + Message message = MessageBuilder.withPayload( + "ABC").setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON).build(); + + assertNull(this.converter.fromMessage(message, String.class)); + } + + @Test + public void supportsMimeTypeNotSpecified() { + Message message = MessageBuilder.withPayload("ABC").build(); + assertEquals("success-from", this.converter.fromMessage(message, String.class)); + } + + @Test + public void supportsMimeTypeNoneConfigured() { + + Message message = MessageBuilder.withPayload( + "ABC").setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON).build(); + + this.converter = new TestMessageConverter(Collections.emptyList()); + this.converter.setContentTypeResolver(new DefaultContentTypeResolver()); + + assertEquals("success-from", this.converter.fromMessage(message, String.class)); + } + + @Test + public void toMessageHeadersCopied() { + Map map = new HashMap(); + map.put("foo", "bar"); + MessageHeaders headers = new MessageHeaders(map ); + Message message = this.converter.toMessage("ABC", headers); + + assertEquals("bar", message.getHeaders().get("foo")); + } + + @Test + public void toMessageContentTypeHeader() { + Message message = this.converter.toMessage("ABC", null); + assertEquals(MimeTypeUtils.TEXT_PLAIN, message.getHeaders().get(MessageHeaders.CONTENT_TYPE)); + } + + + private static class TestMessageConverter extends AbstractMessageConverter { + + public TestMessageConverter() { + super(MimeTypeUtils.TEXT_PLAIN); + } + + public TestMessageConverter(Collection supportedMimeTypes) { + super(supportedMimeTypes); + } + + @Override + protected boolean supports(Class clazz) { + return String.class.equals(clazz); + } + + @Override + public Object convertFromInternal(Message message, Class targetClass) { + return "success-from"; + } + + @Override + public Object convertToInternal(Object payload, MessageHeaders headers) { + return "success-to"; + } + } + +} 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 8ce3914180..38daf41462 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -23,7 +23,9 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; 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; /** * Encapsulates information required to create a resource handlers. @@ -43,6 +45,9 @@ public class ResourceHandlerRegistration { private Integer cachePeriod; + private List resourceResolvers; + + /** * Create a {@link ResourceHandlerRegistration} instance. * @param resourceLoader a resource loader for turning a String location into a {@link Resource} @@ -70,6 +75,17 @@ public class ResourceHandlerRegistration { return this; } + /** + * Configure the list of {@link ResourceResolver}s to use. + *

+ * 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(List resourceResolvers) { + this.resourceResolvers = resourceResolvers; + } + /** * Specify the cache period for the resources served by the resource handler, in seconds. The default is to not * send any cache headers but to rely on last-modified timestamps only. Set to 0 in order to send cache headers @@ -86,7 +102,11 @@ public class ResourceHandlerRegistration { * Returns the URL path patterns for the resource handler. */ protected String[] getPathPatterns() { - return pathPatterns; + return this.pathPatterns; + } + + protected List getResourceResolvers() { + return this.resourceResolvers; } /** @@ -95,9 +115,12 @@ public class ResourceHandlerRegistration { protected ResourceHttpRequestHandler getRequestHandler() { Assert.isTrue(!CollectionUtils.isEmpty(locations), "At least one location is required for resource handling."); ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); - requestHandler.setLocations(locations); - if (cachePeriod != null) { - requestHandler.setCacheSeconds(cachePeriod); + if (this.resourceResolvers != null) { + requestHandler.setResourceResolvers(this.resourceResolvers); + } + requestHandler.setLocations(this.locations); + if (this.cachePeriod != null) { + requestHandler.setCacheSeconds(this.cachePeriod); } return requestHandler; } 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 8d0e8bb7fa..b56e591df2 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -24,6 +24,7 @@ import java.util.Map; import javax.servlet.ServletContext; +import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; import org.springframework.web.HttpRequestHandler; @@ -31,6 +32,7 @@ import org.springframework.web.servlet.HandlerMapping; 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; /** * Stores registrations of resource handlers for serving static resources such as images, css files and others @@ -57,8 +59,11 @@ public class ResourceHandlerRegistry { private final List registrations = new ArrayList(); + private List resourceResolvers; + private int order = Integer.MAX_VALUE -1; + public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext) { Assert.notNull(applicationContext, "ApplicationContext is required"); this.applicationContext = applicationContext; @@ -98,6 +103,14 @@ public class ResourceHandlerRegistry { return this; } + /** + * Configure the {@link ResourceResolver}s to use by default in resource handlers that + * don't have them set. + */ + public void setResourceResolvers(List resourceResolvers) { + this.resourceResolvers = resourceResolvers; + } + /** * Return a handler mapping with the mapped resource handlers; or {@code null} in case of no registrations. */ @@ -109,10 +122,19 @@ public class ResourceHandlerRegistry { Map urlMap = new LinkedHashMap(); for (ResourceHandlerRegistration registration : registrations) { for (String pathPattern : registration.getPathPatterns()) { - ResourceHttpRequestHandler requestHandler = registration.getRequestHandler(); - requestHandler.setServletContext(servletContext); - requestHandler.setApplicationContext(applicationContext); - urlMap.put(pathPattern, requestHandler); + ResourceHttpRequestHandler handler = registration.getRequestHandler(); + handler.setServletContext(servletContext); + handler.setApplicationContext(applicationContext); + if ((this.resourceResolvers != null) && (registration.getResourceResolvers() == null)) { + handler.setResourceResolvers(this.resourceResolvers); + } + try { + handler.afterPropertiesSet(); + } + catch (Exception e) { + throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", e); + } + urlMap.put(pathPattern, handler); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index ded06b2d7f..13e3d1a009 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -48,6 +48,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; import org.springframework.validation.Errors; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -76,6 +77,9 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.servlet.resource.PublicResourceUrlProvider; +import org.springframework.web.servlet.resource.PublicResourceUrlProviderExposingInterceptor; +import org.springframework.web.util.UrlPathHelper; /** * This is the main class providing the configuration behind the MVC Java config. @@ -163,6 +167,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private List> messageConverters; + private PathMatchConfigurer pathMatchConfigurer; + /** * Set the {@link javax.servlet.ServletContext}, e.g. for resource handling, @@ -187,12 +193,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv */ @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { - PathMatchConfigurer configurer = new PathMatchConfigurer(); - configurePathMatch(configurer); + RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); + + PathMatchConfigurer configurer = getPathMatchConfigurer(); if(configurer.isUseSuffixPatternMatch() != null) { handlerMapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch()); } @@ -211,6 +218,22 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv return handlerMapping; } + protected PathMatchConfigurer getPathMatchConfigurer() { + if (this.pathMatchConfigurer == null) { + this.pathMatchConfigurer = new PathMatchConfigurer(); + configurePathMatch(this.pathMatchConfigurer); + } + return this.pathMatchConfigurer; + } + + /** + * Override this method to configure path matching options. + * @see PathMatchConfigurer + * @since 4.0.3 + */ + public void configurePathMatch(PathMatchConfigurer configurer) { + } + /** * Provide access to the shared handler interceptors used to configure * {@link HandlerMapping} instances with. This method cannot be overridden, @@ -221,6 +244,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv InterceptorRegistry registry = new InterceptorRegistry(); addInterceptors(registry); registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService())); + registry.addInterceptor(new PublicResourceUrlProviderExposingInterceptor(resourceUrlPathTranslator())); this.interceptors = registry.getInterceptors(); } return this.interceptors.toArray(); @@ -318,7 +342,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv */ @Bean public HandlerMapping resourceHandlerMapping() { - ResourceHandlerRegistry registry = new ResourceHandlerRegistry(applicationContext, servletContext); + ResourceHandlerRegistry registry = new ResourceHandlerRegistry( + this.applicationContext, this.servletContext); addResourceHandlers(registry); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); handlerMapping = handlerMapping != null ? handlerMapping : new EmptyHandlerMapping(); @@ -332,6 +357,20 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void addResourceHandlers(ResourceHandlerRegistry registry) { } + @Bean + public PublicResourceUrlProvider resourceUrlPathTranslator() { + PublicResourceUrlProvider translator = new PublicResourceUrlProvider(); + UrlPathHelper pathHelper = getPathMatchConfigurer().getUrlPathHelper(); + if (pathHelper != null) { + translator.setUrlPathHelper(pathHelper); + } + PathMatcher pathMatcher = getPathMatchConfigurer().getPathMatcher(); + if (pathMatcher != null) { + translator.setPathMatcher(pathMatcher); + } + return translator; + } + /** * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped * default servlet handler. To configure "default" Servlet handling, @@ -573,13 +612,6 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv public void configureAsyncSupport(AsyncSupportConfigurer configurer) { } - /** - * Override this method to configure path matching options. - * @see PathMatchConfigurer - * @since 4.0.3 - */ - public void configurePathMatch(PathMatchConfigurer configurer) { - } /** * Return an instance of {@link CompositeUriComponentsContributor} for use with 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 new file mode 100644 index 0000000000..1cd5ac3bad --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java @@ -0,0 +1,115 @@ +/* + * 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.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + + +/** + * A default implementation of + * {@link org.springframework.web.servlet.resource.ResourceResolverChain ResourceResolverChain} + * for invoking a list of {@link ResourceResolver}s. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + */ +class DefaultResourceResolverChain implements ResourceResolverChain { + + private static Log logger = LogFactory.getLog(DefaultResourceResolverChain.class); + + private final List resolvers = new ArrayList(); + + private int index = -1; + + + public DefaultResourceResolverChain(List resolvers) { + this.resolvers.addAll((resolvers != null) ? resolvers : new ArrayList()); + } + + + @Override + public Resource resolveResource(HttpServletRequest request, String requestPath, List locations) { + ResourceResolver resolver = getNextResolver(); + if (resolver == null) { + return null; + } + try { + logBefore(resolver); + Resource resource = resolver.resolveResource(request, requestPath, locations, this); + logAfter(resolver, resource); + return resource; + } + finally { + this.index--; + } + } + + @Override + public String resolveUrlPath(String resourcePath, List locations) { + ResourceResolver resolver = getNextResolver(); + if (resolver == null) { + return null; + } + try { + logBefore(resolver); + String urlPath = resolver.getPublicUrlPath(resourcePath, locations, this); + logAfter(resolver, urlPath); + return urlPath; + } + finally { + this.index--; + } + } + + private ResourceResolver getNextResolver() { + + Assert.state(this.index <= this.resolvers.size(), + "Current index exceeds the number of configured ResourceResolver's"); + + if (this.index == (this.resolvers.size() - 1)) { + if (logger.isTraceEnabled()) { + logger.trace("No more ResourceResolver's to delegate to, returning null"); + } + return null; + } + + this.index++; + return this.resolvers.get(this.index); + } + + private void logBefore(ResourceResolver resolver) { + if (logger.isTraceEnabled()) { + logger.trace("Calling " + resolver.getClass().getSimpleName() + " index=" + this.index); + } + } + + private void logAfter(ResourceResolver resolver, Object result) { + if (logger.isTraceEnabled()) { + logger.trace(resolver.getClass().getSimpleName() + " returned " + result); + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java new file mode 100644 index 0000000000..91c0a54228 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java @@ -0,0 +1,42 @@ +/* + * 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; + + +/** + * Interface for a resource descriptor that describes the encoding + * applied to the entire resource content. + * + *

This information is required if the client consuming that resource + * needs additional decoding capabilities to retrieve the resource's content. + * + * @author Jeremy Grelle + * @since 4.1 + * @see RFC2616 HTTP Content-Encoding + */ +public interface EncodedResource extends Resource { + + /** + * The content coding value, as defined in the IANA registry + * @return the content encoding + * @see RFC 2616 Content codings + */ + public String getContentEncoding(); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java new file mode 100644 index 0000000000..7159ea97c2 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java @@ -0,0 +1,124 @@ +/* + * 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.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.util.DigestUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + + +/** + * A {@code ResourceResolver} that resolves request paths containing an additional + * MD5 hash in the file name. + * + *

For example the path "styles/foo-e36d2e05253c6c7085a91522ce43a0b4.css" will + * match to "styles/foo.css" assuming the hash computed from the content of + * "foo.css" matches the hash in the path. + * + *

The resolver first delegates to the chain so that if + * "foo-e36d2e05253c6c7085a91522ce43a0b4.css" has been written to disk (e.g. at + * build time) it is simply found. Or if the chain does not find an existing + * resource, this resolver removes the hash, attempts to find a matching resource + * with the resulting file name ("foo.css") and then compares the hash from the + * request path to the hash computed from the file content. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class FingerprintResourceResolver implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(FingerprintResourceResolver.class); + + private final Pattern pattern = Pattern.compile("-(\\S*)\\."); + + + @Override + public Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain) { + + Resource resolved = chain.resolveResource(request, requestPath, locations); + if (resolved != null) { + return resolved; + } + + String hash = extractHash(requestPath); + if (StringUtils.isEmpty(hash)) { + return null; + } + + String simplePath = StringUtils.delete(requestPath, "-" + hash); + Resource baseResource = chain.resolveResource(request, simplePath, locations); + if (baseResource == null) { + return null; + } + + String candidateHash = calculateHash(baseResource); + if (candidateHash.equals(hash)) { + return baseResource; + } + else { + logger.debug("Potential resource found for ["+requestPath+"], but fingerprint doesn't match."); + return null; + } + } + + private String extractHash(String path) { + Matcher matcher = this.pattern.matcher(path); + if (matcher.find()) { + String match = matcher.group(1); + return match.contains("-") ? match.substring(match.lastIndexOf("-") + 1) : match; + } + else { + return ""; + } + } + + private String calculateHash(Resource resource) { + try { + byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream()); + return DigestUtils.md5DigestAsHex(content); + } + catch (IOException e) { + logger.error("Failed to calculate hash on resource [" + resource.toString()+"]"); + return ""; + } + } + + @Override + public String getPublicUrlPath(String resourceUrlPath, List locations, ResourceResolverChain chain) { + String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); + if (StringUtils.hasText(baseUrl)) { + Resource original = chain.resolveResource(null, resourceUrlPath, locations); + String hash = calculateHash(original); + return StringUtils.stripFilenameExtension(baseUrl) + + "-" + hash + "." + StringUtils.getFilenameExtension(baseUrl); + } + return baseUrl; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java new file mode 100644 index 0000000000..d4c2b8aa8c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java @@ -0,0 +1,149 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.Resource; + + +/** + * A {@code ResourceResolver} that delegates to the chain to locate a resource + * and then attempts to find a variation with the ".gz" extension. + * + *

The resolver gets involved only if the "Accept-Encoding" request header + * contains the value "gzip" indicating the client accepts gzipped responses. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class GzipResourceResolver implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(GzipResourceResolver.class); + + + @Override + public Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain) { + + Resource resource = chain.resolveResource(request, requestPath, locations); + if ((resource == null) || !isGzipAccepted(request)) { + return resource; + } + + try { + Resource gzipped = new GzippedResource(resource); + if (gzipped.exists()) { + return gzipped; + } + } + catch (IOException e) { + logger.trace("No gzipped resource for [" + resource.getFilename() + "]", e); + } + + return resource; + } + + private boolean isGzipAccepted(HttpServletRequest request) { + String value = request.getHeader("Accept-Encoding"); + return ((value != null) && value.toLowerCase().contains("gzip")); + } + + @Override + public String getPublicUrlPath(String resourceUrlPath, List locations, ResourceResolverChain chain) { + return chain.resolveUrlPath(resourceUrlPath, locations); + } + + + private static final class GzippedResource extends AbstractResource implements EncodedResource { + + private final Resource original; + + private final Resource gzipped; + + + public GzippedResource(Resource original) throws IOException { + this.original = original; + this.gzipped = original.createRelative(original.getFilename() + ".gz"); + } + + + public InputStream getInputStream() throws IOException { + return this.gzipped.getInputStream(); + } + + public boolean exists() { + return this.gzipped.exists(); + } + + public boolean isReadable() { + return this.gzipped.isReadable(); + } + + public boolean isOpen() { + return this.gzipped.isOpen(); + } + + public URL getURL() throws IOException { + return this.gzipped.getURL(); + } + + public URI getURI() throws IOException { + return this.gzipped.getURI(); + } + + public File getFile() throws IOException { + return this.gzipped.getFile(); + } + + public long contentLength() throws IOException { + return this.gzipped.contentLength(); + } + + public long lastModified() throws IOException { + return this.gzipped.lastModified(); + } + + public Resource createRelative(String relativePath) throws IOException { + return this.gzipped.createRelative(relativePath); + } + + public String getFilename() { + return this.original.getFilename(); + } + + public String getDescription() { + return this.gzipped.getDescription(); + } + + public String getContentEncoding() { + return "gzip"; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java new file mode 100644 index 0000000000..bd7748b4b8 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java @@ -0,0 +1,81 @@ +/* + * 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.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; + + +/** + * A simple {@code ResourceResolver} that tries to find a resource under the given + * locations matching to the request path. + * + *

This resolver does not delegate to the {@code ResourceResolverChain} and is + * expected to be configured at the end in a chain of resolvers. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class PathResourceResolver implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(PathResourceResolver.class); + + + @Override + public Resource resolveResource(HttpServletRequest request, + String requestPath, List locations, ResourceResolverChain chain) { + + return getResource(requestPath, locations); + } + + @Override + public String getPublicUrlPath(String resourceUrlPath, List locations, ResourceResolverChain chain) { + return (getResource(resourceUrlPath, locations) != null) ? resourceUrlPath : null; + } + + private Resource getResource(String path, List locations) { + for (Resource location : locations) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Looking for \"" + path + "\" under " + location); + } + Resource resource = location.createRelative(path); + if (resource.exists() && resource.isReadable()) { + if (logger.isDebugEnabled()) { + logger.debug("Resource exists and is readable"); + } + return resource; + } + else if (logger.isTraceEnabled()) { + logger.trace("Relative resource doesn't exist or isn't readable: " + resource); + } + } + catch (IOException ex) { + logger.debug("Failed to create relative resource - trying next resource location", ex); + } + } + return null; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java new file mode 100644 index 0000000000..393ef3798b --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java @@ -0,0 +1,65 @@ +/* + * 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.util.List; + +/** + * A {@code ResourceResolver} configured with a prefix to be removed from the + * request path before delegating to the chain to find a matching resource. + * + *

Enables inserting a unique virtual prefix (e.g. reduced SHA, version number, + * release date) into resource paths so that when a new version of the application + * is released, clients are forced to reload application resources. + * + *

This is useful when changing resource names is not an option (e.g. when + * using JavaScript module loaders). If that's not the case, the use of + * {@link FingerprintResourceResolver} provides more optimal performance since + * it causes only actually modified resources to be reloaded. + * + * @author Brian Clozel + * @since 4.1 + */ +public class PrefixResourceResolver implements ResourceResolver { + + private final String prefix; + + public PrefixResourceResolver(String prefix) { + Assert.hasText(prefix, "resource path prefix should not be null"); + this.prefix = prefix.startsWith("/") ? prefix : "/" + prefix; + } + + @Override + public Resource resolveResource(HttpServletRequest request, + String requestPath, List locations, ResourceResolverChain chain) { + + if(requestPath.startsWith(this.prefix)) { + requestPath = requestPath.substring(this.prefix.length()); + } + + return chain.resolveResource(request, requestPath, locations); + } + + @Override + public String getPublicUrlPath(String resourceUrlPath, List locations, ResourceResolverChain chain) { + String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); + return this.prefix + (baseUrl.startsWith("/") ? baseUrl : "/" + baseUrl); + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProvider.java new file mode 100644 index 0000000000..1da3e21143 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProvider.java @@ -0,0 +1,223 @@ +/* + * 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.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.util.UrlPathHelper; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + + +/** + * A central component aware of Spring MVC handler mappings for serving static + * resources that provides methods to determine the public URL path clients + * should to access static resource. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class PublicResourceUrlProvider implements ApplicationListener { + + protected final Log logger = LogFactory.getLog(getClass()); + + + private UrlPathHelper pathHelper = new UrlPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + private final Map handlerMap = new HashMap(); + + private boolean autodetect = true; + + + /** + * Configure a {@code UrlPathHelper} to use in + * {@link #getForRequestUrl(javax.servlet.http.HttpServletRequest, String)} + * in order to derive the lookup path for a target request URL path. + */ + public void setUrlPathHelper(UrlPathHelper pathHelper) { + this.pathHelper = pathHelper; + } + + /** + * @return the configured {@code UrlPathHelper}. + */ + public UrlPathHelper getPathHelper() { + return this.pathHelper; + } + + /** + * Configure a {@code PathMatcher} to use when comparing target lookup path + * against resource mappings. + */ + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + /** + * @return the configured {@code PathMatcher}. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + /** + * Manually configure the resource mappings. + * + *

Note: by default resource mappings are auto-detected + * from the Spring {@code ApplicationContext}. However if this property is + * used, the auto-detection is turned off. + */ + public void setHandlerMap(Map handlerMap) { + if (handlerMap != null) { + this.handlerMap.clear(); + this.handlerMap.putAll(handlerMap); + this.autodetect = false; + } + } + + /** + * @return the resource mappings, either manually configured or auto-detected + * when the Spring {@code ApplicationContext} is refreshed. + */ + public Map getHandlerMap() { + return this.handlerMap; + } + + /** + * @return {@code false} if resource mappings were manually configured, + * {@code true} otherwise. + */ + public boolean isAutodetect() { + return this.autodetect; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (isAutodetect()) { + this.handlerMap.clear(); + detectResourceHandlers(event.getApplicationContext()); + if (this.handlerMap.isEmpty() && logger.isDebugEnabled()) { + logger.debug("No resource handling mappings found"); + } + } + } + + protected void detectResourceHandlers(ApplicationContext applicationContext) { + logger.debug("Looking for resource handler mappings"); + Map beans = applicationContext.getBeansOfType(SimpleUrlHandlerMapping.class); + for (SimpleUrlHandlerMapping hm : beans.values()) { + for (String pattern : hm.getUrlMap().keySet()) { + Object handler = hm.getUrlMap().get(pattern); + if (handler instanceof ResourceHttpRequestHandler) { + ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler; + if (logger.isDebugEnabled()) { + logger.debug("Found pattern=\"" + pattern + "\" mapped to locations " + + resourceHandler.getLocations() + " with resolvers: " + + resourceHandler.getResourceResolvers()); + } + this.handlerMap.put(pattern, resourceHandler); + } + } + } + } + + + /** + * A variation on {@link #getForLookupPath(String)} that accepts a full request + * URL path (i.e. including context and servlet path) and returns the full request + * URL path to expose for public use. + * + * @param request the current request + * @param requestUrl the request URL path to translate + * @return the translated resource URL path or {@code null} + */ + public final String getForRequestUrl(HttpServletRequest request, String requestUrl) { + if (logger.isDebugEnabled()) { + logger.debug("Checking requestURL=" + requestUrl); + } + + String pathWithinMapping = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + if (pathWithinMapping == null) { + logger.debug("Request attribute with lookup path not found, calculating instead."); + pathWithinMapping = getPathHelper().getLookupPathForRequest(request); + } + + int index = getPathHelper().getRequestUri(request).indexOf(pathWithinMapping); + Assert.state(index > 0 && index < requestUrl.length(), "Failed to determine lookup path: " + requestUrl); + + String prefix = requestUrl.substring(0, index); + String lookupPath = requestUrl.substring(index); + String resolvedPath = getForLookupPath(lookupPath); + + return (resolvedPath != null) ? prefix + resolvedPath : null; + } + + /** + * Compare the given path against configured resource handler mappings and + * if a match is found use the {@code ResourceResolver} chain of the matched + * {@code ResourceHttpRequestHandler} to determine the URL path to expose for + * public use. + * + *

It is expected the given path is what Spring MVC would use for request + * mapping purposes, i.e. excluding context and servlet path portions. + * + * @param lookupPath the look path to check + * + * @return the resolved URL path or {@code null} if unresolved + */ + public final String getForLookupPath(String lookupPath) { + if (logger.isDebugEnabled()) { + logger.debug("Checking lookup path=" + lookupPath); + } + for (String pattern : this.handlerMap.keySet()) { + if (!getPathMatcher().match(pattern, lookupPath)) { + continue; + } + String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath); + String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping)); + if (logger.isDebugEnabled()) { + logger.debug("Found matching resource mapping=\"" + pattern + "\", " + + "resource URL path=\"" + pathWithinMapping + "\""); + } + ResourceHttpRequestHandler handler = this.handlerMap.get(pattern); + ResourceResolverChain chain = handler.createResourceResolverChain(); + String resolved = chain.resolveUrlPath(pathWithinMapping, handler.getLocations()); + if (resolved == null) { + throw new IllegalStateException("Failed to get public resource URL path for " + pathWithinMapping); + } + if (logger.isDebugEnabled()) { + logger.debug("Returning public resource URL path=\"" + resolved + "\""); + } + return pathMapping + resolved; + } + logger.debug("No matching resource mapping"); + return null; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderExposingInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderExposingInterceptor.java new file mode 100644 index 0000000000..23cdfb8fec --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderExposingInterceptor.java @@ -0,0 +1,58 @@ +/* + * 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.util.Assert; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * An interceptor that exposes the + * {@link PublicResourceUrlProvider ResourceUrlPathTranslator} + * instance it is configured with as a request attribute. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class PublicResourceUrlProviderExposingInterceptor extends HandlerInterceptorAdapter { + + /** + * Name of request attribute that holds + * {@link PublicResourceUrlProvider ResourceUrlPathTranslator}. + */ + public static final String RESOURCE_URL_PROVIDER_ATTR = PublicResourceUrlProvider.class.getName().toString(); + + + private final PublicResourceUrlProvider resourceUrlProvider; + + + public PublicResourceUrlProviderExposingInterceptor(PublicResourceUrlProvider resourceUrlProvider) { + Assert.notNull(resourceUrlProvider, "'resourceUrlProvider' is required"); + this.resourceUrlProvider = resourceUrlProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + + request.setAttribute(RESOURCE_URL_PROVIDER_ATTR, this.resourceUrlProvider); + return true; + } + +} 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 df64b50545..8576469ab5 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 @@ -18,6 +18,7 @@ package org.springframework.web.servlet.resource; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; @@ -63,8 +64,13 @@ import org.springframework.web.servlet.support.WebContentGenerator; * is used in the URL mapping pattern that selects this handler. Such patterns can be easily parameterized * using Spring EL. See the reference manual for further examples of this approach. * - *

Rather than being directly configured as a bean, this handler will typically be configured - * through use of the {@code } XML configuration element. + *

For various front-end needs, such as ensuring that users with a primed browser cache get the latest changes, + * or serving variations of resources (minified versions for example- - + * {@link org.springframework.web.servlet.resource.ResourceResolver}s can be configured.

+ * + *

This handler can be configured through use of a + * {@link org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry} or the {@code } + * XML configuration element. * * @author Keith Donald * @author Jeremy Grelle @@ -78,13 +84,19 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H private static final boolean jafPresent = ClassUtils.isPresent("javax.activation.FileTypeMap", ResourceHttpRequestHandler.class.getClassLoader()); + private static final String CONTENT_ENCODING = "Content-Encoding"; + private List locations; + private final List resourceResolvers = new ArrayList(); + public ResourceHttpRequestHandler() { super(METHOD_GET, METHOD_HEAD); + this.resourceResolvers.add(new PathResourceResolver()); } + /** * Set a {@code List} of {@code Resource} paths to use as sources * for serving static resources. @@ -94,6 +106,28 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H this.locations = locations; } + public List getLocations() { + return this.locations; + } + + /** + * Configure the list of {@link ResourceResolver}s to use. + *

+ * 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) { + this.resourceResolvers.clear(); + if (resourceResolvers != null) { + this.resourceResolvers.addAll(resourceResolvers); + } + } + + public List getResourceResolvers() { + return this.resourceResolvers; + } + + @Override public void afterPropertiesSet() throws Exception { if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) { @@ -155,41 +189,23 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H writeContent(response, resource); } - protected Resource getResource(HttpServletRequest request) { + protected Resource getResource(HttpServletRequest request) throws IOException{ String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); if (path == null) { throw new IllegalStateException("Required request attribute '" + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set"); } - if (!StringUtils.hasText(path) || isInvalidPath(path)) { if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid resource path [" + path + "]"); } return null; } + return createResourceResolverChain().resolveResource(request, path, getLocations()); + } - for (Resource location : this.locations) { - try { - if (logger.isDebugEnabled()) { - logger.debug("Trying relative path [" + path + "] against base location: " + location); - } - Resource resource = location.createRelative(path); - if (resource.exists() && resource.isReadable()) { - if (logger.isDebugEnabled()) { - logger.debug("Found matching resource: " + resource); - } - return resource; - } - else if (logger.isTraceEnabled()) { - logger.trace("Relative resource doesn't exist or isn't readable: " + resource); - } - } - catch (IOException ex) { - logger.debug("Failed to create relative resource - trying next resource location", ex); - } - } - return null; + ResourceResolverChain createResourceResolverChain() { + return new DefaultResourceResolverChain(getResourceResolvers()); } /** @@ -241,6 +257,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H if (mediaType != null) { response.setContentType(mediaType.toString()); } + + if (resource instanceof EncodedResource) { + response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding()); + } } /** @@ -308,4 +328,4 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } } -} + } 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 new file mode 100644 index 0000000000..c6539749de --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java @@ -0,0 +1,66 @@ +/* + * 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.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; + + +/** + * A strategy for matching a request to a server-side resource. Provides a mechanism + * for resolving an incoming request to an actual + * {@link org.springframework.core.io.Resource} and also for obtaining the public + * URL path clients should use when requesting the resource. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + * + * @see org.springframework.web.servlet.resource.ResourceResolverChain + */ +public interface ResourceResolver { + + /** + * Resolve the input request and request path to a {@link Resource} that + * exists under one of the given resource locations. + * + * @param request the current request + * @param requestPath the portion of the request path to use + * @param locations locations where to look for resources + * @param chain a chain with other resolvers to delegate to + * + * @return the resolved resource or {@code null} if unresolved + */ + Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain); + + /** + * Get the externally facing public URL path for clients to use to access the + * resource located at the resource URL path. + * + * @param resourceUrlPath the candidate resource URL path + * @param locations the configured locations where to look up resources + * @param chain the chain with remaining resolvers to delegate to + * + * @return the resolved URL path or {@code null} if unresolved + */ + String getPublicUrlPath(String resourceUrlPath, List locations, ResourceResolverChain chain); + +} 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 new file mode 100644 index 0000000000..3cd9975c4c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java @@ -0,0 +1,61 @@ +/* + * 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.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; + + +/** + * A contract for invoking a chain of {@link ResourceResolver}s where each resolver + * is given a reference to the chain allowing it to delegate when necessary. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + */ +public interface ResourceResolverChain { + + /** + * Resolve the URL path of an incoming request to an actual {@link Resource} + * to serve in the response. + * + * @param request the current request + * @param requestPath the portion of the request path to use + * @param locations the configured locations where to look up resources + * + * @return the resolved {@link Resource} or {@code null} if this resolver + * could not resolve the resource + */ + Resource resolveResource(HttpServletRequest request, String requestPath, List locations); + + /** + * Resolve the given resource path to a URL path. This is useful when rendering + * URL links to clients to determine the actual URL to use. + * + * @param resourcePath the resource path + * @param locations the configured locations where to look up resources + * + * @return the resolved URL path or {@code null} if this resolver could not + * resolve the given resource path + */ + String resolveUrlPath(String resourcePath, List locations); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java new file mode 100644 index 0000000000..bf4eeb9f6f --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.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 java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.filter.OncePerRequestFilter; + + +/** + * A filter that wraps the {@link HttpServletResponse} and overrides its + * {@link HttpServletResponse#encodeURL(String) encodeURL} method in order to + * translate resource request URLs. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class ResourceUrlEncodingFilter extends OncePerRequestFilter { + + private static Log logger = LogFactory.getLog(ResourceUrlEncodingFilter.class); + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + filterChain.doFilter(request, new ResourceUrlEncodingResponseWrapper(request, response)); + } + + private class ResourceUrlEncodingResponseWrapper extends HttpServletResponseWrapper { + + private HttpServletRequest request; + + private ResourceUrlEncodingResponseWrapper(HttpServletRequest request, HttpServletResponse wrapped) { + super(wrapped); + this.request = request; + } + + @Override + public String encodeURL(String url) { + String name = PublicResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR; + PublicResourceUrlProvider translator = (PublicResourceUrlProvider) this.request.getAttribute(name); + if (translator != null) { + String translatedUrl = translator.getForRequestUrl(this.request, url); + if (translatedUrl != null) { + return super.encodeURL(translatedUrl); + } + } + else { + logger.debug("Request attribute exposing ResourceUrlPathProvider not found"); + } + return super.encodeURL(url); + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java index fdaed8a8b4..552261f37e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java @@ -16,8 +16,12 @@ package org.springframework.web.servlet.config.annotation; +import java.util.Arrays; +import java.util.List; + import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockServletContext; @@ -25,6 +29,7 @@ import org.springframework.web.context.support.GenericWebApplicationContext; 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 static org.junit.Assert.*; @@ -89,6 +94,17 @@ public class ResourceHandlerRegistryTests { assertFalse(registry.hasMappingForPattern("/whatever")); } + @Test + public void resourceResolversAndTransformers() { + ResourceResolver resolver = Mockito.mock(ResourceResolver.class); + List resolvers = Arrays.asList(resolver); + registry.setResourceResolvers(resolvers); + + SimpleUrlHandlerMapping hm = (SimpleUrlHandlerMapping) registry.getHandlerMapping(); + ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) hm.getUrlMap().values().iterator().next(); + + assertEquals(resolvers, handler.getResourceResolvers()); + } private ResourceHttpRequestHandler getHandler(String pathPattern) { SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) registry.getHandlerMapping(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index 40f10ea7e6..c70a992361 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -62,6 +62,7 @@ import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.servlet.resource.PublicResourceUrlProviderExposingInterceptor; /** * A test fixture with a sub-class of {@link WebMvcConfigurationSupport} that @@ -94,9 +95,10 @@ public class WebMvcConfigurationSupportExtensionTests { rmHandlerMapping.afterPropertiesSet(); HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/")); assertNotNull(chain.getInterceptors()); - assertEquals(2, chain.getInterceptors().length); + assertEquals(3, chain.getInterceptors().length); assertEquals(LocaleChangeInterceptor.class, chain.getInterceptors()[0].getClass()); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); + assertEquals(PublicResourceUrlProviderExposingInterceptor.class, chain.getInterceptors()[2].getClass()); AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) webConfig.viewControllerHandlerMapping(); handlerMapping.setApplicationContext(webAppContext); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 942d58e160..a4af8d7dad 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -55,6 +55,7 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.servlet.resource.PublicResourceUrlProviderExposingInterceptor; import static org.junit.Assert.*; @@ -116,8 +117,9 @@ public class WebMvcConfigurationSupportTests { HandlerExecutionChain chain = handlerMapping.getHandler(request); assertNotNull(chain.getInterceptors()); - assertEquals(2, chain.getInterceptors().length); + assertEquals(3, chain.getInterceptors().length); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); + assertEquals(PublicResourceUrlProviderExposingInterceptor.class, chain.getInterceptors()[2].getClass()); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/bar.js b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/bar.js deleted file mode 100644 index 8c3dd5bf82..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/bar.js +++ /dev/null @@ -1 +0,0 @@ -function foo() { console.log("hello bar"); } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js deleted file mode 100644 index 0a694588c5..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js +++ /dev/null @@ -1 +0,0 @@ -function foo() { console.log("hello world"); } \ No newline at end of file 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 new file mode 100644 index 0000000000..e9c202a3a8 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2013 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.lang.reflect.Method; +import java.util.ArrayList; +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.util.DigestUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + + +/** + * + * @author Jeremy Grelle + */ +public class FingerprintResourceResolverTests { + + private ResourceResolverChain chain; + + private FingerprintResourceResolver resolver = new FingerprintResourceResolver(); + + private List locations; + + @Before + public void setUp() { + 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())); + } + + + @Test + public void resolveWithoutHash() throws Exception { + String file = "bar.css"; + Resource expected = new ClassPathResource("test/" + file, getClass()); + Resource actual = chain.resolveResource(null, file, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithHashNoMatch() throws Exception { + String file = "bogus-e36d2e05253c6c7085a91522ce43a0b4.css"; + assertNull(chain.resolveResource(null, file, locations)); + } + + @Test + public void resolveStaticFingerprintedResource() throws Exception { + String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; + Resource expected = new ClassPathResource("test/"+file, getClass()); + Resource actual = chain.resolveResource(null, file, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveDynamicFingerprintedResource() throws Exception { + Resource expected = new ClassPathResource("test/bar.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/bar-" + hash + ".css"; + Resource actual = chain.resolveResource(null, path, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithMultipleExtensions() throws Exception { + Resource expected = new ClassPathResource("test/bar.min.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/bar.min-" + hash + ".css"; + Resource actual = chain.resolveResource(null, path, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithMultipleHyphens() throws Exception { + Resource expected = new ClassPathResource("test/foo-bar/foo-bar.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/foo-bar/foo-bar-" + hash + ".css"; + Resource actual = chain.resolveResource(null, path, locations); + + assertEquals(expected, actual); + } + + @Test + public void extractHash() throws Exception { + String hash = "7fbe76cdac6093784895bb4989203e5a"; + String path = "font-awesome/css/font-awesome.min-" + hash + ".css"; + + Method method = ReflectionUtils.findMethod(resolver.getClass(), "extractHash", String.class); + ReflectionUtils.makeAccessible(method); + String result = (String) ReflectionUtils.invokeMethod(method, resolver, path); + + assertEquals(hash, result); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java new file mode 100644 index 0000000000..61f161ba47 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2013 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.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.util.FileCopyUtils; + +import static org.junit.Assert.*; + + +/** + * + * @author Jeremy Grelle + */ +public class GzipResourceResolverTests { + + private ResourceResolverChain resolver; + + private List locations; + + @BeforeClass + public static void createGzippedResources() throws IOException { + Resource location = new ClassPathResource("test/", GzipResourceResolverTests.class); + Resource jsFile = new FileSystemResource(location.createRelative("/js/foo.js").getFile()); + Resource gzJsFile = jsFile.createRelative("foo.js.gz"); + Resource fingerPrintedFile = new FileSystemResource(location.createRelative("foo-e36d2e05253c6c7085a91522ce43a0b4.css").getFile()); + Resource gzFingerPrintedFile = fingerPrintedFile.createRelative("foo-e36d2e05253c6c7085a91522ce43a0b4.css.gz"); + + if (gzJsFile.getFile().createNewFile()) { + GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(gzJsFile.getFile())); + FileCopyUtils.copy(jsFile.getInputStream(), out); + } + + if (gzFingerPrintedFile.getFile().createNewFile()) { + GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(gzFingerPrintedFile.getFile())); + FileCopyUtils.copy(fingerPrintedFile.getInputStream(), out); + } + + assertTrue(gzJsFile.exists()); + assertTrue(gzFingerPrintedFile.exists()); + } + + @Before + public void setUp() { + List resolvers = new ArrayList(); + resolvers.add(new GzipResourceResolver()); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); + resolver = new DefaultResourceResolverChain(resolvers); + locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); + } + + @Test + public void resolveGzippedFile() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept-Encoding", "gzip"); + String file = "js/foo.js"; + String gzFile = file+".gz"; + Resource resource = new ClassPathResource("test/"+gzFile, getClass()); + Resource resolved = resolver.resolveResource(request, file, locations); + + assertEquals(resource.getDescription(), resolved.getDescription()); + assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename()); + assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class, + resolved instanceof EncodedResource); + } + + @Test + public void resolveFingerprintedGzippedFile() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept-Encoding", "gzip"); + String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; + String gzFile = file+".gz"; + Resource resource = new ClassPathResource("test/"+gzFile, getClass()); + Resource resolved = resolver.resolveResource(request, file, locations); + + assertEquals(resource.getDescription(), resolved.getDescription()); + assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename()); + assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class, + resolved instanceof EncodedResource); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java new file mode 100644 index 0000000000..dc099d85d1 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java @@ -0,0 +1,70 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Test fixture for {@link PrefixResourceResolver} + * + * @author Brian Clozel + */ +public class PrefixResourceResolverTests { + + private ResourceResolverChain resolver; + + private List locations; + + private final String shaPrefix = "1df341f"; + + @Before + public void setUp() { + List resolvers = new ArrayList(); + resolvers.add(new PrefixResourceResolver(this.shaPrefix)); + resolvers.add(new PathResourceResolver()); + this.resolver = new DefaultResourceResolverChain(resolvers); + this.locations = new ArrayList(); + this.locations.add(new ClassPathResource("test/", getClass())); + } + + @Test + public void testResolveResource() { + String resourceId = "foo.css"; + Resource expected = new ClassPathResource("test/foo.css", getClass()); + Resource actual = this.resolver.resolveResource(null, "/" + this.shaPrefix + "/" + resourceId, this.locations); + assertEquals(expected, actual); + } + + @Test + public void testResolveUrlPath() { + String resourceId = "/foo.css"; + String url = "/" + this.shaPrefix + resourceId; + assertEquals(url, resolver.resolveUrlPath(resourceId, locations)); + } + + @Test(expected = IllegalArgumentException.class) + public void testFailPrefixResolverConstructor() { + PrefixResourceResolver resolver = new PrefixResourceResolver(""); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderJavaConfigTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderJavaConfigTests.java new file mode 100644 index 0000000000..6275f0d31b --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderJavaConfigTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2013 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.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.test.MockFilterChain; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.config.annotation.*; + +import static org.junit.Assert.*; + + +/** + * Integration tests using {@link ResourceUrlEncodingFilter} and + * {@link PublicResourceUrlProvider} with the latter configured in Spring MVC Java config. + * + * @author Rossen Stoyanchev + */ +public class PublicResourceUrlProviderJavaConfigTests { + + private MockFilterChain filterChain; + + private TestServlet servlet; + + private MockHttpServletRequest request; + + + @Before + public void setup() throws Exception { + + this.servlet = new TestServlet(); + this.filterChain = new MockFilterChain(this.servlet, new ResourceUrlEncodingFilter()); + + AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); + cxt.setServletContext(new MockServletContext()); + cxt.register(WebConfig.class); + cxt.refresh(); + + PublicResourceUrlProvider urlProvider = cxt.getBean(PublicResourceUrlProvider.class); + + this.request = new MockHttpServletRequest("GET", "/"); + request.setAttribute(PublicResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR, urlProvider); + } + + + @Test + public void rootServletMapping() throws Exception { + + this.request.setRequestURI("/"); + this.request.setMethod("GET"); + this.request.setRequestURI("/myapp/index.html"); + this.request.setContextPath("/myapp"); + this.request.setServletPath("/index.html"); + this.filterChain.doFilter(request, new MockHttpServletResponse()); + + String actual = this.servlet.response.encodeURL("/myapp/resources/foo.css"); + assertEquals("/myapp/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", actual); + } + + @Test + public void prefixServletMapping() throws Exception { + + this.request.setRequestURI("/myapp/myservlet/index.html"); + this.request.setContextPath("/myapp"); + this.request.setServletPath("/myservlet"); + this.filterChain.doFilter(request, new MockHttpServletResponse()); + + String actual = this.servlet.response.encodeURL("/myapp/myservlet/resources/foo.css"); + assertEquals("/myapp/myservlet/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", actual); + } + + @Test + public void extensionServletMapping() throws Exception { + + this.request.setRequestURI("/myapp/index.html"); + this.request.setContextPath("/myapp"); + this.request.setServletPath("/index.html"); + this.filterChain.doFilter(request, new MockHttpServletResponse()); + + String actual = this.servlet.response.encodeURL("/myapp/resources/foo.css"); + assertEquals("/myapp/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", actual); + } + + + @Configuration + static class WebConfig extends WebMvcConfigurationSupport { + + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + + List resourceResolvers = new ArrayList<>(); + resourceResolvers.add(new FingerprintResourceResolver()); + resourceResolvers.add(new PathResourceResolver()); + + registry.addResourceHandler("/resources/**") + .addResourceLocations("classpath:org/springframework/web/servlet/resource/test/") + .setResourceResolvers(resourceResolvers); + } + } + + @SuppressWarnings("serial") + private static class TestServlet extends HttpServlet { + + private HttpServletResponse response; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + this.response = response; + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderTests.java new file mode 100644 index 0000000000..539e83ae87 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PublicResourceUrlProviderTests.java @@ -0,0 +1,85 @@ +/* + * 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.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.junit.Assert.*; + + +/** + * Unit tests for {@link org.springframework.web.servlet.resource.PublicResourceUrlProvider}. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + */ +public class PublicResourceUrlProviderTests { + + private PublicResourceUrlProvider translator; + + private ResourceHttpRequestHandler handler; + + private Map handlerMap; + + + @Before + public void setUp() { + List locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); + + this.handler = new ResourceHttpRequestHandler(); + this.handler.setLocations(locations); + + this.handlerMap = new HashMap(); + this.handlerMap.put("/resources/**", this.handler); + } + + @Test + public void getStaticResourceUrl() { + initTranslator(); + + String url = this.translator.getForLookupPath("/resources/foo.css"); + assertEquals("/resources/foo.css", url); + } + + @Test + public void getFingerprintedResourceUrl() { + List resolvers = new ArrayList(); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); + this.handler.setResourceResolvers(resolvers); + initTranslator(); + + String url = this.translator.getForLookupPath("/resources/foo.css"); + assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url); + } + + private void initTranslator() { + this.translator = new PublicResourceUrlProvider(); + this.translator.setHandlerMap(this.handlerMap); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java index 8dd54a643f..5fe75ca2de 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -43,7 +43,7 @@ public class ResourceHttpRequestHandlerTests { private ResourceHttpRequestHandler handler; @Before - public void setUp() { + public void setUp() throws Exception { List resourcePaths = new ArrayList(); resourcePaths.add(new ClassPathResource("test/", getClass())); resourcePaths.add(new ClassPathResource("testalternatepath/", getClass())); @@ -51,6 +51,7 @@ public class ResourceHttpRequestHandlerTests { handler.setLocations(resourcePaths); handler.setCacheSeconds(3600); handler.setServletContext(new TestServletContext()); + handler.afterPropertiesSet(); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/bar.css b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/bar.min.css similarity index 100% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/bar.css rename to spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/bar.min.css diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-bar/foo-bar.css b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-bar/foo-bar.css new file mode 100644 index 0000000000..d1fdea69ce --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-bar/foo-bar.css @@ -0,0 +1 @@ +h2 { color:white; } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/foo.css b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-e36d2e05253c6c7085a91522ce43a0b4.css similarity index 100% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/foo.css rename to spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-e36d2e05253c6c7085a91522ce43a0b4.css 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 new file mode 100644 index 0000000000..e2f0b1c742 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less @@ -0,0 +1 @@ +h1 { color:red; } \ No newline at end of file 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 new file mode 100644 index 0000000000..e2f0b1c742 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less @@ -0,0 +1 @@ +h1 { color:red; } \ No newline at end of file