From 0e58125b157f0be6440ffdf1a38a0d4957a6f4a2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 24 Sep 2013 20:53:08 -0400 Subject: [PATCH] Polish --- .../annotation/ResourceHandlerRegistry.java | 7 ++ .../resource/AbstractResourceResolver.java | 18 ++-- .../DefaultResourceResolverChain.java | 78 ++++----------- .../web/servlet/resource/EncodedResource.java | 6 +- .../ExtensionMappingResourceResolver.java | 76 +++++++------- ....java => FingerprintResourceResolver.java} | 96 +++++++++--------- .../resource/GzipResourceResolver.java | 81 ++++++++------- .../resource/LessResourceTransformer.java | 22 +++-- .../resource/PathResourceResolver.java | 81 +++++++++++++++ .../resource/ResourceHttpRequestHandler.java | 54 +++++----- .../servlet/resource/ResourceResolver.java | 8 +- .../resource/ResourceResolverChain.java | 5 +- .../servlet/resource/ResourceTransformer.java | 8 +- .../resource/ResourceUrlEncodingFilter.java | 13 +-- .../servlet/resource/ResourceUrlMapper.java | 15 +-- .../servlet/resource/TransformedResource.java | 11 ++- ...ExtensionMappingResourceResolverTests.java | 15 +-- ... => FingerprintResourceResolverTests.java} | 98 +++++++++++-------- .../resource/GzipResourceResolverTests.java | 29 +++--- .../resource/ResourceUrlMapperTests.java | 32 +++--- 20 files changed, 432 insertions(+), 321 deletions(-) rename spring-webmvc/src/main/java/org/springframework/web/servlet/resource/{FingerprintingResourceResolver.java => FingerprintResourceResolver.java} (66%) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java rename spring-webmvc/src/test/java/org/springframework/web/servlet/resource/{FingerprintingResourceResolverTests.java => FingerprintResourceResolverTests.java} (54%) 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 65ef9be6c53..ce383b5ff03 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 @@ -23,6 +23,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; @@ -98,6 +99,12 @@ public class ResourceHandlerRegistry { ResourceHttpRequestHandler requestHandler = registration.getRequestHandler(); requestHandler.setServletContext(servletContext); requestHandler.setApplicationContext(applicationContext); + try { + requestHandler.afterPropertiesSet(); + } + catch (Exception e) { + throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", e); + } urlMap.put(pathPattern, requestHandler); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java index 350b0ce9e1a..627d5e88e53 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java @@ -24,26 +24,26 @@ import org.springframework.core.io.Resource; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public abstract class AbstractResourceResolver implements ResourceResolver { + @Override - public final Resource resolve(HttpServletRequest request, String path, + public final Resource resolve(HttpServletRequest request, String requestPath, List locations, ResourceResolverChain chain) { - - Resource candidate = chain.next(this).resolve(request, path, locations, chain); - - return resolveInternal(request, path, locations, chain, candidate); + + Resource resource = chain.next(this).resolve(request, requestPath, locations, chain); + return resolveInternal(request, requestPath, locations, chain, resource); } - + protected abstract Resource resolveInternal(HttpServletRequest request, String path, List locations, ResourceResolverChain chain, Resource resolved); @Override - public String resolveUrl(String resourcePath, List locations, - ResourceResolverChain chain) { + public String resolveUrl(String resourcePath, List locations, ResourceResolverChain chain) { return chain.next(this).resolveUrl(resourcePath, locations, chain); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java index 4a21053d723..dcc53bf9d2c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java @@ -22,90 +22,54 @@ 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; /** - * + * + * * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.0 */ -public class DefaultResourceResolverChain implements ResourceResolverChain{ - - private static final ResourceResolver DEFAULT_RESOLVER = new PathMappingResourceResolver(); - +class DefaultResourceResolverChain implements ResourceResolverChain { + private final List resolvers; - + private List transformers = new ArrayList(); - + + public DefaultResourceResolverChain(List resolvers, List transformers) { - this.resolvers = resolvers; - this.resolvers.add(DEFAULT_RESOLVER); - this.transformers = transformers; + this.resolvers = (resolvers != null) ? resolvers : new ArrayList(); + this.transformers = (transformers != null) ? transformers : new ArrayList(); } + @Override public ResourceResolver next(ResourceResolver current) { return this.resolvers.get(this.resolvers.indexOf(current) + 1); } @Override - public Resource resolveAndTransform(HttpServletRequest request, String path, - List locations) throws IOException{ - Resource resolved = this.resolvers.get(0).resolve(request, path, locations, this); - return resolved != null ? applyTransformers(request, resolved) : resolved; + public Resource resolveAndTransform(HttpServletRequest request, String path, List locations) + throws IOException { + + Resource resource = this.resolvers.get(0).resolve(request, path, locations, this); + return resource != null ? applyTransformers(request, resource) : resource; } - + @Override public String resolveUrl(String resourcePath, List locations) { return this.resolvers.get(0).resolveUrl(resourcePath, locations, this); } - - protected Resource applyTransformers(HttpServletRequest request, Resource resource) throws IOException{ - for (ResourceTransformer transformer : transformers) { + + private Resource applyTransformers(HttpServletRequest request, Resource resource) throws IOException { + for (ResourceTransformer transformer : this.transformers) { if (transformer.handles(request, resource)) { return applyTransformers(request, transformer.transform(resource)); } } return resource; } - - private static class PathMappingResourceResolver implements ResourceResolver { - - private static final Log logger = LogFactory.getLog(PathMappingResourceResolver.class); - - @Override - public Resource resolve(HttpServletRequest request, String path, List locations, ResourceResolverChain chain) { - for (Resource location : 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; - } - @Override - public String resolveUrl(String resourcePath, List locations, ResourceResolverChain chain) { - if (resolve(null, resourcePath, locations, chain) != null) { - return resourcePath; - } - return null; - } - } } 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 index 11efabbca5d..dcf9644ce87 100644 --- 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 @@ -20,10 +20,12 @@ import org.springframework.core.io.Resource; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public interface EncodedResource extends Resource { - public String getEncoding(); + public String getContentEncoding(); + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolver.java index 48f59cd062d..80ea21fbe7b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolver.java @@ -30,63 +30,68 @@ import org.springframework.util.StringUtils; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public class ExtensionMappingResourceResolver extends AbstractResourceResolver { - - private final Log logger = LogFactory.getLog(getClass()); - + + private static final Log logger = LogFactory.getLog(ExtensionMappingResourceResolver.class); + private final boolean compareTimeStamp; - + + public ExtensionMappingResourceResolver() { this.compareTimeStamp = false; } - + public ExtensionMappingResourceResolver(boolean compareTimeStamp) { this.compareTimeStamp = compareTimeStamp; } - + + @Override protected Resource resolveInternal(HttpServletRequest request, String path, - List locations, ResourceResolverChain chain, Resource resolved) { - if (resolved != null && !compareTimeStamp) { - return resolved; + List locations, ResourceResolverChain chain, Resource resource) { + + if ((resource != null) && !this.compareTimeStamp) { + return resource; } - + for (Resource location : locations) { String baseFilename = StringUtils.getFilename(path); - try { Resource basePath = location.createRelative(StringUtils.delete(path, baseFilename)); if (basePath.getFile().isDirectory()) { - for (String fileName : basePath.getFile().list(new ExtensionFilter(baseFilename))) { + for (String fileName : basePath.getFile().list(new ExtensionFilenameFilter(baseFilename))) { //Always use the first match Resource matched = basePath.createRelative(fileName); - if (resolved == null || matched.lastModified() > resolved.lastModified()) { + if ((resource == null) || (matched.lastModified() > resource.lastModified())) { return matched; - } else { - return resolved; + } + else { + return resource; } } } } catch (IOException e) { - this.logger.trace("Error occurred locating resource based on file extension mapping", e); + logger.trace("Error occurred locating resource based on file extension mapping", e); } - } - return resolved; + + return resource; } - + @Override public String resolveUrl(String resourcePath, List locations, ResourceResolverChain chain) { + String resolved = super.resolveUrl(resourcePath, locations, chain); if (StringUtils.hasText(resolved)) { return resolved; } - + Resource mappedResource = resolveInternal(null, resourcePath, locations, chain, null); if (mappedResource != null) { return resourcePath; @@ -95,23 +100,26 @@ public class ExtensionMappingResourceResolver extends AbstractResourceResolver { } + private static final class ExtensionFilenameFilter implements FilenameFilter { - private static final class ExtensionFilter implements FilenameFilter{ + private final String filename; - private final String baseFilename; - private final String baseExtension; - private final int baseExtLen; - - - public ExtensionFilter(String baseFilename) { - this.baseFilename = baseFilename; - this.baseExtension = "." + StringUtils.getFilenameExtension(baseFilename); - this.baseExtLen = this.baseExtension.length(); + private final String extension; + + private final int extensionLength; + + + public ExtensionFilenameFilter(String filename) { + this.filename = filename; + this.extension = "." + StringUtils.getFilenameExtension(filename); + this.extensionLength = this.extension.length(); } - + @Override - public boolean accept(File dir, String name) { - return name.contains(baseExtension) && baseFilename.equals(name.substring(0, name.lastIndexOf(baseExtension) + this.baseExtLen)); + public boolean accept(File directory, String name) { + return (name.contains(this.extension) + && this.filename.equals(name.substring(0, name.lastIndexOf(this.extension) + this.extensionLength))); } } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintingResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java similarity index 66% rename from spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintingResourceResolver.java rename to spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java index 4166d3f6251..2daf1080ccb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintingResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java @@ -32,85 +32,85 @@ import org.springframework.util.StringUtils; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ -public class FingerprintingResourceResolver extends AbstractResourceResolver { +public class FingerprintResourceResolver extends AbstractResourceResolver { + + private static final Log logger = LogFactory.getLog(FingerprintResourceResolver.class); - private final Log logger = LogFactory.getLog(getClass()); - private Pattern pattern = Pattern.compile("-(\\S*)\\."); - + + @Override protected Resource resolveInternal(HttpServletRequest request, String path, List locations, ResourceResolverChain chain, Resource resolved) { - //First try the resolved full path, in case resource has been written that way to disk at build-time - //or the resource is requested without fingerprint + + // First try the resolved full path, in case resource has been written that way to disk at build-time + // or the resource is requested without fingerprint if (resolved != null) { return resolved; } - - //Now try extracting and matching the hash for dev mode + + // Now try extracting and matching the hash for dev mode String hash = extractHash(path); - String simplePath = !StringUtils.isEmpty(hash) ? StringUtils.delete(path, "-" + hash) : path; + if (StringUtils.isEmpty(hash)) { + return null; + } + + String simplePath = StringUtils.delete(path, "-" + hash); Resource baseResource = chain.next(this).resolve(request, simplePath, locations, chain); - - if (StringUtils.isEmpty(hash) || baseResource == null) { + if (baseResource == null) { + logger.debug("Failed to find resource after removing fingerprint: " + simplePath); + return null; + } + + String candidateHash = calculateHash(baseResource); + if (candidateHash.equals(hash)) { + logger.debug("Fingerprint match succeeded."); return baseResource; } - - String candidateHash = calculateHash(baseResource); - - if (candidateHash.equals(hash)) { - this.logger.debug("Fingerprint match succeeded."); - return baseResource; - } else { - this.logger.debug("Potential resource found, but fingerprint doesn't match."); + else { + logger.debug("Potential resource found, but fingerprint doesn't match."); return null; } } - - @Override - public String resolveUrl(String resourcePath, List locations, - ResourceResolverChain chain) { - //TODO - Consider caching here for better efficiency - String baseUrl = chain.next(this).resolveUrl(resourcePath, locations, chain); - if (StringUtils.hasText(baseUrl)) { - Resource original = chain.next(this).resolve(null, resourcePath, locations, chain); - String hash = calculateHash(original); - return StringUtils.stripFilenameExtension(baseUrl) + "-" + hash + "." + StringUtils.getFilenameExtension(baseUrl); + + private String extractHash(String path) { + Matcher matcher = this.pattern.matcher(path); + if (matcher.find()) { + logger.debug("Found fingerprint in path: " + matcher.group(1)); + String match = matcher.group(1); + return match.contains("-") ? match.substring(match.lastIndexOf("-") + 1) : match; + } + else { + return ""; } - return baseUrl; } - /** - * @param candidate - * @return - */ private String calculateHash(Resource resource) { try { byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream()); return DigestUtils.md5DigestAsHex(content); } catch (IOException e) { - this.logger.error("Failed to calculate hash on resource " + resource.toString()); + logger.error("Failed to calculate hash on resource " + resource.toString()); return ""; } } - /** - * @param path - * @return - */ - private String extractHash(String path) { - Matcher matcher = pattern.matcher(path); - if (matcher.find()) { - this.logger.debug("Found fingerprint in path: " + matcher.group(1)); - String match = matcher.group(1); - return match.contains("-") ? match.substring(match.lastIndexOf("-") + 1) : match; - } else { - return ""; + @Override + public String resolveUrl(String resourcePath, List locations, ResourceResolverChain chain) { + // TODO - Consider caching here for better efficiency + String baseUrl = chain.next(this).resolveUrl(resourcePath, locations, chain); + if (StringUtils.hasText(baseUrl)) { + Resource original = chain.next(this).resolve(null, resourcePath, locations, chain); + 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 index 834429906ed..e18d4b05469 100644 --- 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 @@ -32,104 +32,111 @@ import org.springframework.core.io.Resource; /** - * + * A {@link ResourceResolver} that lets the next resolver in the chain locate a Resource + * and then attempts to find a variation of that Resource with ".gz" extension. This + * resolver will only get involved if the client has indicated it supports gzipped + * responses through the "Accept-Encoding" header. + * * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.0 */ public class GzipResourceResolver extends AbstractResourceResolver { - private final Log logger = LogFactory.getLog(getClass()); - + private static final Log logger = LogFactory.getLog(GzipResourceResolver.class); + + @Override protected Resource resolveInternal(HttpServletRequest request, String path, - List locations, ResourceResolverChain chain, Resource resolved) { - - if (!isGzipAccepted(request) || resolved == null) { - return resolved; + List locations, ResourceResolverChain chain, Resource resource) { + + if ((resource == null) || !isGzipAccepted(request)) { + return resource; } - + try { - Resource gzipped = new GzippedResource(resolved); + Resource gzipped = new GzippedResource(resource); if (gzipped.exists()) { return gzipped; } - } catch (IOException e) { - this.logger.trace("Error occurred locating gzipped resource", e); } - return resolved; + catch (IOException e) { + logger.trace("No gzipped resource for " + resource.getFilename(), e); + } + + return resource; } - /** - * @param request - * @return - */ private boolean isGzipAccepted(HttpServletRequest request) { - String val = request.getHeader("Accept-Encoding"); - return val != null && val.toLowerCase().contains("gzip"); + String value = request.getHeader("Accept-Encoding"); + return ((value != null) && value.toLowerCase().contains("gzip")); } - + + 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"); + this.gzipped = original.createRelative(original.getFilename() + ".gz"); } + public InputStream getInputStream() throws IOException { - return gzipped.getInputStream(); + return this.gzipped.getInputStream(); } public boolean exists() { - return gzipped.exists(); + return this.gzipped.exists(); } public boolean isReadable() { - return gzipped.isReadable(); + return this.gzipped.isReadable(); } public boolean isOpen() { - return gzipped.isOpen(); + return this.gzipped.isOpen(); } public URL getURL() throws IOException { - return gzipped.getURL(); + return this.gzipped.getURL(); } public URI getURI() throws IOException { - return gzipped.getURI(); + return this.gzipped.getURI(); } public File getFile() throws IOException { - return gzipped.getFile(); + return this.gzipped.getFile(); } public long contentLength() throws IOException { - return gzipped.contentLength(); + return this.gzipped.contentLength(); } public long lastModified() throws IOException { - return gzipped.lastModified(); + return this.gzipped.lastModified(); } public Resource createRelative(String relativePath) throws IOException { - return gzipped.createRelative(relativePath); + return this.gzipped.createRelative(relativePath); } public String getFilename() { - return original.getFilename(); + return this.original.getFilename(); } public String getDescription() { - return gzipped.getDescription(); + return this.gzipped.getDescription(); } - public String getEncoding() { + public String getContentEncoding() { return "gzip"; } - - } + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java index 9169730fa53..b759a79f223 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java @@ -27,15 +27,17 @@ import org.springframework.util.StringUtils; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public class LessResourceTransformer implements ResourceTransformer { private static final String LESS_EXT = "less"; - + private final LessCompiler compiler = new LessCompiler(); - + + @Override public Resource transform(Resource original) throws IOException { TransformedResource transformed; @@ -43,18 +45,18 @@ public class LessResourceTransformer implements ResourceTransformer { String content = ""; if (original instanceof TransformedResource) { content = ((TransformedResource) original).getContentAsString(); - } else { - content = compiler.compile(original.getFile()); } - transformed = new TransformedResource(original.getFilename() - .replace("."+LESS_EXT, ""), content.getBytes("UTF-8"), original.lastModified()); + else { + content = this.compiler.compile(original.getFile()); + } + transformed = new TransformedResource(original.getFilename().replace( + "." + LESS_EXT, ""), content.getBytes("UTF-8"), original.lastModified()); } - catch (LessException le) { + catch (LessException ex) { //TODO - Nicely print out the compilation error - le.printStackTrace(); + ex.printStackTrace(); return null; } - return transformed; } 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 00000000000..a7a7381b5df --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java @@ -0,0 +1,81 @@ +/* + * 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.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 path-based {@link ResourceResolver} that appends the request path to each + * configured Resource location and checks if such a Resource exists. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class PathResourceResolver implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(PathResourceResolver.class); + + + @Override + public Resource resolve(HttpServletRequest request, String requestPath, List locations, + ResourceResolverChain chain) { + + return resolveInternal(requestPath, locations); + } + + private Resource resolveInternal(String path, List locations) { + for (Resource location : 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; + } + + @Override + public String resolveUrl(String resourcePath, List locations, ResourceResolverChain chain) { + if (resolveInternal(resourcePath, locations) != null) { + return resourcePath; + } + return null; + } + +} 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 526d5b58a90..ca9c8386233 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 @@ -84,27 +84,18 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H private List locations; private List resourceResolvers = new ArrayList(); - - private List resourceTransformers = new ArrayList(); - + + private List resourceTransformers; + private ResourceResolverChain resolverChain; - + + public ResourceHttpRequestHandler() { super(METHOD_GET, METHOD_HEAD); + this.resourceResolvers.add(new PathResourceResolver()); } - public List getLocations() { - return this.locations; - } - - public List getResourceResolvers() { - return this.resourceResolvers; - } - - public List getResourceTransformers() { - return this.resourceTransformers; - } - + /** * Set a {@code List} of {@code Resource} paths to use as sources * for serving static resources. @@ -113,15 +104,34 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H Assert.notEmpty(locations, "Locations list must not be empty"); 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 = resourceResolvers; } - + + public List getResourceResolvers() { + return this.resourceResolvers; + } + public void setResourceTransformers(List resourceTransformers) { this.resourceTransformers = resourceTransformers; } + public List getResourceTransformers() { + return this.resourceTransformers; + } + + @Override public void afterPropertiesSet() throws Exception { if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) { @@ -198,7 +208,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return null; } - return resolverChain.resolveAndTransform(request, path, locations); + return this.resolverChain.resolveAndTransform(request, path, this.locations); } /** @@ -250,9 +260,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H if (mediaType != null) { response.setContentType(mediaType.toString()); } - + if (resource instanceof EncodedResource) { - response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getEncoding()); + response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding()); } } @@ -310,5 +320,5 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null); } } - + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java index 0dd7b2fa12e..52d8771aef4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java @@ -24,13 +24,15 @@ import org.springframework.core.io.Resource; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public interface ResourceResolver { - public Resource resolve(HttpServletRequest request, String path, List locations, ResourceResolverChain chain); - + public Resource resolve(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain); + public String resolveUrl(String resourcePath, 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 index 1666335b16a..be82752b662 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java @@ -26,14 +26,15 @@ import org.springframework.core.io.Resource; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public interface ResourceResolverChain { public Resource resolveAndTransform(HttpServletRequest request, String path, List locations) throws IOException; - + public ResourceResolver next(ResourceResolver current); public String resolveUrl(String resourcePath, List locations); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java index e8808d1aa63..4d9b57d5e8e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java @@ -24,14 +24,14 @@ import org.springframework.core.io.Resource; /** - * More than meets the eye. - * + * * @author Jeremy Grelle + * @since 4.0 */ public interface ResourceTransformer { public Resource transform(Resource original) throws IOException; - + public boolean handles(HttpServletRequest request, Resource original); - + } 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 index b8745d86af0..e95773f9779 100644 --- 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 @@ -31,8 +31,9 @@ import org.springframework.web.util.UrlPathHelper; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { @@ -44,7 +45,7 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { throws ServletException, IOException { filterChain.doFilter(request, new ResourceUrlResponseWrapper(request, response)); } - + @Override protected void initFilterBean() throws ServletException { WebApplicationContext appContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); @@ -52,14 +53,14 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { } private class ResourceUrlResponseWrapper extends HttpServletResponseWrapper { - + private final UrlPathHelper pathHelper = new UrlPathHelper(); - + private String pathPrefix; - + private ResourceUrlResponseWrapper(HttpServletRequest request, HttpServletResponse wrapped) { super(wrapped); - + this.pathPrefix = pathHelper.getContextPath(request); String servletPath = pathHelper.getServletPath(request); String appPath = pathHelper.getPathWithinApplication(request); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java index 5c896ed6afc..43b9c3dbd00 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java @@ -34,23 +34,24 @@ import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public class ResourceUrlMapper implements BeanPostProcessor, ApplicationListener{ private final Map handlers = new LinkedHashMap(); - + private final List mappings = new ArrayList(); - + private final PathMatcher matcher = new AntPathMatcher(); - + @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } - + @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { @@ -65,7 +66,7 @@ public class ResourceUrlMapper implements BeanPostProcessor, ApplicationListener } return bean; } - + @Override public void onApplicationEvent(ContextRefreshedEvent event) { OrderComparator.sort(this.mappings); @@ -76,7 +77,7 @@ public class ResourceUrlMapper implements BeanPostProcessor, ApplicationListener } } } - + public String getUrlForResource(String resourcePath) { for (Entry mapping : this.handlers.entrySet()) { if (matcher.match(mapping.getKey(), resourcePath)) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java index a70b0e39549..d7bae844078 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java @@ -24,20 +24,21 @@ import org.springframework.core.io.ByteArrayResource; /** - * + * * @author Jeremy Grelle + * @since 4.0 */ public class TransformedResource extends ByteArrayResource { private final String filename; private final long lastModified; - + public TransformedResource(String filename, byte[] transformedContent) { super(transformedContent); this.filename = filename; this.lastModified = new Date().getTime(); } - + public TransformedResource(String filename, byte[] transformedContent, long lastModified) { super(transformedContent); this.filename = filename; @@ -48,7 +49,7 @@ public class TransformedResource extends ByteArrayResource { public String getFilename() { return this.filename; } - + @Override public long lastModified() throws IOException { return this.lastModified; @@ -64,5 +65,5 @@ public class TransformedResource extends ByteArrayResource { return ""; } } - + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java index e9f99cb5020..2cd5a890a29 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java @@ -28,33 +28,34 @@ import static org.junit.Assert.*; /** - * + * * @author Jeremy Grelle */ public class ExtensionMappingResourceResolverTests { private ResourceResolverChain resolver; - + private List locations; - + @Before public void setUp() { List resolvers = new ArrayList(); resolvers.add(new ExtensionMappingResourceResolver()); + resolvers.add(new PathResourceResolver()); resolver = new DefaultResourceResolverChain(resolvers, new ArrayList()); locations = new ArrayList(); locations.add(new ClassPathResource("test/", getClass())); locations.add(new ClassPathResource("testalternatepath/", getClass())); } - + @Test public void resolveLessResource() throws Exception { - String resourceId = "zoo.css"; - Resource resource = new ClassPathResource("test/"+resourceId+".less", getClass()); + String resourceId = "zoo.css"; + Resource resource = new ClassPathResource("test/" + resourceId + ".less", getClass()); Resource resolved = resolver.resolveAndTransform(null, resourceId, locations); assertEquals(resource, resolved); } - + @Test public void resolveLessUrl() { String resourceId = "zoo.css"; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintingResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java similarity index 54% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintingResourceResolverTests.java rename to spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java index 50fee80a1f9..8e8f4c9993f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintingResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java @@ -32,76 +32,92 @@ import static org.junit.Assert.*; /** - * + * * @author Jeremy Grelle */ -public class FingerprintingResourceResolverTests { +public class FingerprintResourceResolverTests { private ResourceResolverChain chain; - - private FingerprintingResourceResolver resolver = new FingerprintingResourceResolver(); - + + 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, new ArrayList()); 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.resolveAndTransform(null, file, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithHashNoMatch() throws Exception { + String file = "bogus-e36d2e05253c6c7085a91522ce43a0b4.css"; + assertNull(chain.resolveAndTransform(null, file, locations)); + } + @Test public void resolveStaticFingerprintedResource() throws Exception { - String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; - Resource resource = new ClassPathResource("test/"+file, getClass()); - Resource resolved = chain.resolveAndTransform(null, file, locations); - assertEquals(resource, resolved); + String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; + Resource expected = new ClassPathResource("test/"+file, getClass()); + Resource actual = chain.resolveAndTransform(null, file, locations); + + assertEquals(expected, actual); } - + @Test public void resolveDynamicFingerprintedResource() throws Exception { - Resource resource = new ClassPathResource("test/bar.css", getClass()); - String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(resource.getInputStream())); - String path = "/bar-"+hash+".css"; - - Resource resolved = chain.resolveAndTransform(null, path, locations); - - assertEquals(resource, resolved); + Resource expected = new ClassPathResource("test/bar.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/bar-" + hash + ".css"; + Resource actual = chain.resolveAndTransform(null, path, locations); + + assertEquals(expected, actual); } - + @Test public void resolveWithMultipleExtensions() throws Exception { - Resource resource = new ClassPathResource("test/bar.min.css", getClass()); - String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(resource.getInputStream())); - String path = "/bar.min-"+hash+".css"; - - Resource resolved = chain.resolveAndTransform(null, path, locations); - - assertEquals(resource, resolved); + 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.resolveAndTransform(null, path, locations); + + assertEquals(expected, actual); } - + @Test public void resolveWithMultipleHyphens() throws Exception { - Resource resource = new ClassPathResource("test/foo-bar/foo-bar.css", getClass()); - String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(resource.getInputStream())); - String path = "/foo-bar/foo-bar-"+hash+".css"; - - Resource resolved = chain.resolveAndTransform(null, path, locations); - - assertEquals(resource, resolved); + 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.resolveAndTransform(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 extractHash = ReflectionUtils.findMethod(FingerprintingResourceResolver.class, "extractHash", String.class); - ReflectionUtils.makeAccessible(extractHash); - String result = (String) ReflectionUtils.invokeMethod(extractHash, resolver, path); + 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 index 0cd88f333fa..0e05aeedea7 100644 --- 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 @@ -35,15 +35,15 @@ 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); @@ -51,32 +51,33 @@ public class GzipResourceResolverTests { 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 FingerprintingResourceResolver()); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); resolver = new DefaultResourceResolverChain(resolvers, new ArrayList()); 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(); @@ -85,11 +86,13 @@ public class GzipResourceResolverTests { String gzFile = file+".gz"; Resource resource = new ClassPathResource("test/"+gzFile, getClass()); Resource resolved = resolver.resolveAndTransform(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); + assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class, + resolved instanceof EncodedResource); } - + @Test public void resolveFingerprintedGzippedFile() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest(); @@ -98,8 +101,10 @@ public class GzipResourceResolverTests { String gzFile = file+".gz"; Resource resource = new ClassPathResource("test/"+gzFile, getClass()); Resource resolved = resolver.resolveAndTransform(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); + 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/ResourceUrlMapperTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java index cac485bfc54..f0397af5fc2 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java @@ -31,66 +31,68 @@ import static org.junit.Assert.*; /** - * + * * @author Jeremy Grelle */ public class ResourceUrlMapperTests { ResourceHttpRequestHandler handler; - + SimpleUrlHandlerMapping mapping; - + ResourceUrlMapper mapper; - + @Before public void setUp() { List resourcePaths = new ArrayList(); resourcePaths.add(new ClassPathResource("test/", getClass())); resourcePaths.add(new ClassPathResource("testalternatepath/", getClass())); - + Map urlMap = new HashMap(); handler = new ResourceHttpRequestHandler(); handler.setLocations(resourcePaths); urlMap.put("/resources/**", handler); - + mapping = new SimpleUrlHandlerMapping(); mapping.setUrlMap(urlMap); } - + private void resetMapper() { mapper = new ResourceUrlMapper(); mapper.postProcessAfterInitialization(mapping, "resourceMapping"); mapper.onApplicationEvent(null); } - + @Test public void getStaticResourceUrl() { resetMapper(); - + String url = mapper.getUrlForResource("/resources/foo.css"); assertEquals("/resources/foo.css", url); } - + @Test public void getFingerprintedResourceUrl() { List resolvers = new ArrayList(); - resolvers.add(new FingerprintingResourceResolver()); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); handler.setResourceResolvers(resolvers); resetMapper(); - + String url = mapper.getUrlForResource("/resources/foo.css"); assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url); } - + @Test public void getExtensionMappedResourceUrl() { List resolvers = new ArrayList(); resolvers.add(new ExtensionMappingResourceResolver()); + resolvers.add(new PathResourceResolver()); handler.setResourceResolvers(resolvers); resetMapper(); - + String url = mapper.getUrlForResource("/resources/zoo.css"); assertEquals("/resources/zoo.css", url); } - + }