From 61e61bd5fdd2d5b1e2d5bf132dca59ee83f0da5e Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Sun, 22 Sep 2013 15:17:15 -0400 Subject: [PATCH 1/4] Asset pipeline proposal. --- build.gradle | 2 + .../resource/AbstractResourceResolver.java | 50 +++++++ .../DefaultResourceResolverChain.java | 111 ++++++++++++++ .../web/servlet/resource/EncodedResource.java | 29 ++++ .../ExtensionMappingResourceResolver.java | 117 +++++++++++++++ .../FingerprintingResourceResolver.java | 116 +++++++++++++++ .../resource/GzipResourceResolver.java | 135 ++++++++++++++++++ .../resource/LessResourceTransformer.java | 66 +++++++++ .../resource/ResourceHttpRequestHandler.java | 63 ++++---- .../servlet/resource/ResourceResolver.java | 36 +++++ .../resource/ResourceResolverChain.java | 40 ++++++ .../servlet/resource/ResourceTransformer.java | 37 +++++ .../resource/ResourceUrlEncodingFilter.java | 91 ++++++++++++ .../servlet/resource/ResourceUrlMapper.java | 104 ++++++++++++++ .../servlet/resource/TransformedResource.java | 68 +++++++++ .../web/servlet/handler/resources/js/bar.js | 1 - .../web/servlet/handler/resources/js/foo.js | 1 - ...ExtensionMappingResourceResolverTests.java | 64 +++++++++ .../FingerprintingResourceResolverTests.java | 107 ++++++++++++++ .../resource/GzipResourceResolverTests.java | 105 ++++++++++++++ .../ResourceHttpRequestHandlerTests.java | 3 +- .../resource/ResourceUrlMapperTests.java | 96 +++++++++++++ .../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 + 27 files changed, 1417 insertions(+), 28 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.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/ExtensionMappingResourceResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintingResourceResolver.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/LessResourceTransformer.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/ResourceTransformer.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.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/ExtensionMappingResourceResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintingResourceResolverTests.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/ResourceUrlMapperTests.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 094daef0b28..3bfebe7dca9 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ configure(allprojects) { project -> apply plugin: "java" apply plugin: "test-source-set-dependencies" apply from: "${gradleScriptDir}/ide.gradle" + apply plugin: "maven" [compileJava, compileTestJava]*.options*.compilerArgs = [ "-Xlint:serial", @@ -631,6 +632,7 @@ project("spring-webmvc") { optional("org.freemarker:freemarker:2.3.19") optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12") optional("com.fasterxml.jackson.core:jackson-databind:2.2.0") + optional("org.lesscss:lesscss:1.3.3") provided("javax.servlet:jstl:1.2") provided("javax.servlet:javax.servlet-api:3.0.1") provided("javax.servlet.jsp:jsp-api:2.1") 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 new file mode 100644 index 00000000000..350b0ce9e1a --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java @@ -0,0 +1,50 @@ +/* + * 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.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; + + +/** + * + * @author Jeremy Grelle + */ +public abstract class AbstractResourceResolver implements ResourceResolver { + + @Override + public final Resource resolve(HttpServletRequest request, String path, + List locations, ResourceResolverChain chain) { + + Resource candidate = chain.next(this).resolve(request, path, locations, chain); + + return resolveInternal(request, path, locations, chain, candidate); + } + + protected abstract Resource resolveInternal(HttpServletRequest request, String path, + List locations, ResourceResolverChain chain, Resource resolved); + + @Override + 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 new file mode 100644 index 00000000000..4a21053d723 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java @@ -0,0 +1,111 @@ +/* + * 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.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; + + +/** + * + * @author Jeremy Grelle + */ +public class DefaultResourceResolverChain implements ResourceResolverChain{ + + private static final ResourceResolver DEFAULT_RESOLVER = new PathMappingResourceResolver(); + + 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; + } + + @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; + } + + @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) { + 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 new file mode 100644 index 00000000000..11efabbca5d --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java @@ -0,0 +1,29 @@ +/* + * 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 org.springframework.core.io.Resource; + + +/** + * + * @author Jeremy Grelle + */ +public interface EncodedResource extends Resource { + + public String getEncoding(); +} 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 new file mode 100644 index 00000000000..48f59cd062d --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolver.java @@ -0,0 +1,117 @@ +/* + * 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.File; +import java.io.FilenameFilter; +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; +import org.springframework.util.StringUtils; + + +/** + * + * @author Jeremy Grelle + */ +public class ExtensionMappingResourceResolver extends AbstractResourceResolver { + + private final Log logger = LogFactory.getLog(getClass()); + + 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; + } + + 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))) { + //Always use the first match + Resource matched = basePath.createRelative(fileName); + if (resolved == null || matched.lastModified() > resolved.lastModified()) { + return matched; + } else { + return resolved; + } + } + } + } + catch (IOException e) { + this.logger.trace("Error occurred locating resource based on file extension mapping", e); + } + + } + return resolved; + } + + @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; + } + return null; + } + + + + private static final class ExtensionFilter implements FilenameFilter{ + + 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(); + } + + @Override + public boolean accept(File dir, String name) { + return name.contains(baseExtension) && baseFilename.equals(name.substring(0, name.lastIndexOf(baseExtension) + this.baseExtLen)); + } + } +} 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/FingerprintingResourceResolver.java new file mode 100644 index 00000000000..4166d3f6251 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintingResourceResolver.java @@ -0,0 +1,116 @@ +/* + * 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 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; + + +/** + * + * @author Jeremy Grelle + */ +public class FingerprintingResourceResolver extends AbstractResourceResolver { + + 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 + if (resolved != null) { + return resolved; + } + + //Now try extracting and matching the hash for dev mode + String hash = extractHash(path); + String simplePath = !StringUtils.isEmpty(hash) ? StringUtils.delete(path, "-" + hash) : path; + Resource baseResource = chain.next(this).resolve(request, simplePath, locations, chain); + + if (StringUtils.isEmpty(hash) || baseResource == null) { + 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."); + 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); + } + 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()); + 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 ""; + } + } + +} 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 00000000000..834429906ed --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.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.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; + + +/** + * + * @author Jeremy Grelle + */ +public class GzipResourceResolver extends AbstractResourceResolver { + + private final Log logger = LogFactory.getLog(getClass()); + + @Override + protected Resource resolveInternal(HttpServletRequest request, String path, + List locations, ResourceResolverChain chain, Resource resolved) { + + if (!isGzipAccepted(request) || resolved == null) { + return resolved; + } + + try { + Resource gzipped = new GzippedResource(resolved); + if (gzipped.exists()) { + return gzipped; + } + } catch (IOException e) { + this.logger.trace("Error occurred locating gzipped resource", e); + } + return resolved; + } + + /** + * @param request + * @return + */ + private boolean isGzipAccepted(HttpServletRequest request) { + String val = request.getHeader("Accept-Encoding"); + return val != null && val.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"); + } + + public InputStream getInputStream() throws IOException { + return gzipped.getInputStream(); + } + + public boolean exists() { + return gzipped.exists(); + } + + public boolean isReadable() { + return gzipped.isReadable(); + } + + public boolean isOpen() { + return gzipped.isOpen(); + } + + public URL getURL() throws IOException { + return gzipped.getURL(); + } + + public URI getURI() throws IOException { + return gzipped.getURI(); + } + + public File getFile() throws IOException { + return gzipped.getFile(); + } + + public long contentLength() throws IOException { + return gzipped.contentLength(); + } + + public long lastModified() throws IOException { + return gzipped.lastModified(); + } + + public Resource createRelative(String relativePath) throws IOException { + return gzipped.createRelative(relativePath); + } + + public String getFilename() { + return original.getFilename(); + } + + public String getDescription() { + return gzipped.getDescription(); + } + + public String getEncoding() { + 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 new file mode 100644 index 00000000000..9169730fa53 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java @@ -0,0 +1,66 @@ +/* + * 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 javax.servlet.http.HttpServletRequest; + +import org.lesscss.LessCompiler; +import org.lesscss.LessException; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + + +/** + * + * @author Jeremy Grelle + */ +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; + try { + 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()); + } + catch (LessException le) { + //TODO - Nicely print out the compilation error + le.printStackTrace(); + return null; + } + + return transformed; + } + + @Override + public boolean handles(HttpServletRequest request, Resource original) { + return LESS_EXT.equals(StringUtils.getFilenameExtension(original.getFilename())); + } + +} 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 f668d2a9c18..526d5b58a90 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; @@ -78,13 +79,32 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H private static final boolean jafPresent = ClassUtils.isPresent("javax.activation.FileTypeMap", ResourceHttpRequestHandler.class.getClassLoader()); - private List locations; + private static final String CONTENT_ENCODING = "Content-Encoding"; + private List locations; + private List resourceResolvers = new ArrayList(); + + private List resourceTransformers = new ArrayList(); + + private ResourceResolverChain resolverChain; + public ResourceHttpRequestHandler() { super(METHOD_GET, METHOD_HEAD); } + 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. @@ -93,12 +113,21 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H Assert.notEmpty(locations, "Locations list must not be empty"); this.locations = locations; } + + public void setResourceResolvers(List resourceResolvers) { + this.resourceResolvers = resourceResolvers; + } + + public void setResourceTransformers(List resourceTransformers) { + this.resourceTransformers = resourceTransformers; + } @Override public void afterPropertiesSet() throws Exception { if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) { logger.warn("Locations list is empty. No resources will be served"); } + this.resolverChain = new DefaultResourceResolverChain(this.resourceResolvers, this.resourceTransformers); } /** @@ -155,7 +184,7 @@ 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 '" + @@ -169,27 +198,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return null; } - 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; + return resolverChain.resolveAndTransform(request, path, locations); } /** @@ -241,6 +250,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).getEncoding()); + } } /** @@ -297,5 +310,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 new file mode 100644 index 00000000000..0dd7b2fa12e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java @@ -0,0 +1,36 @@ +/* + * 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.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; + + +/** + * + * @author Jeremy Grelle + */ +public interface ResourceResolver { + + public Resource resolve(HttpServletRequest request, String path, 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 new file mode 100644 index 00000000000..1666335b16a --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java @@ -0,0 +1,40 @@ +/* + * 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.springframework.core.io.Resource; + + + +/** + * + * @author Jeremy Grelle + */ +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 new file mode 100644 index 00000000000..e8808d1aa63 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java @@ -0,0 +1,37 @@ +/* + * 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 javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; + + +/** + * More than meets the eye. + * + * @author Jeremy Grelle + */ +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 new file mode 100644 index 00000000000..b8745d86af0 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java @@ -0,0 +1,91 @@ +/* + * 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 javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.UrlPathHelper; + + +/** + * + * @author Jeremy Grelle + */ +public class ResourceUrlEncodingFilter extends OncePerRequestFilter { + + private ResourceUrlMapper mapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + filterChain.doFilter(request, new ResourceUrlResponseWrapper(request, response)); + } + + @Override + protected void initFilterBean() throws ServletException { + WebApplicationContext appContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + this.mapper = appContext.getBean(ResourceUrlMapper.class); + } + + 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); + //This accounts for the behavior when servlet is mapped to "/" + if (!servletPath.equals(appPath)) { + this.pathPrefix += pathHelper.getServletPath(request); + } + } + + @Override + public String encodeURL(String url) { + if(url.startsWith(pathPrefix)) { + String relativeUrl = url.replaceFirst(pathPrefix, ""); + if (!relativeUrl.startsWith("/")) { + relativeUrl = "/" + relativeUrl; + } + if (mapper.isResourceUrl(relativeUrl)) { + String resourceUrl = mapper.getUrlForResource(relativeUrl); + if (resourceUrl != null) { + return resourceUrl; + } + } + } + return super.encodeURL(url); + } + + } + +} 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 new file mode 100644 index 00000000000..5c896ed6afc --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java @@ -0,0 +1,104 @@ +/* + * 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.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.OrderComparator; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + + +/** + * + * @author Jeremy Grelle + */ +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 { + if (ClassUtils.isAssignableValue(SimpleUrlHandlerMapping.class, bean)) { + SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) bean; + for(Entry mappingEntry : mapping.getUrlMap().entrySet()) { + Object val = mappingEntry.getValue(); + if (val instanceof ResourceHttpRequestHandler) { + this.mappings.add(mapping); + } + } + } + return bean; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + OrderComparator.sort(this.mappings); + for (SimpleUrlHandlerMapping mapping : mappings) { + for(Entry mappingEntry : mapping.getUrlMap().entrySet()) { + Object val = mappingEntry.getValue(); + this.handlers.put(mappingEntry.getKey(), (ResourceHttpRequestHandler) val); + } + } + } + + public String getUrlForResource(String resourcePath) { + for (Entry mapping : this.handlers.entrySet()) { + if (matcher.match(mapping.getKey(), resourcePath)) { + ResourceHttpRequestHandler handler = mapping.getValue(); + String nestedPath = matcher.extractPathWithinPattern(mapping.getKey(), resourcePath); + String prefix = resourcePath.replace(nestedPath, ""); + String url = new DefaultResourceResolverChain(handler.getResourceResolvers(), handler. + getResourceTransformers()).resolveUrl(nestedPath, handler.getLocations()); + if (url != null) { + return prefix + url; + } + } + } + return null; + } + + public boolean isResourceUrl(String relativeUrl) { + for (String mapping : this.handlers.keySet()) { + if (matcher.match(mapping, relativeUrl)) { + return true; + } + } + return false; + } +} 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 new file mode 100644 index 00000000000..a70b0e39549 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java @@ -0,0 +1,68 @@ +/* + * 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.io.UnsupportedEncodingException; +import java.util.Date; + +import org.springframework.core.io.ByteArrayResource; + + +/** + * + * @author Jeremy Grelle + */ +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; + this.lastModified = lastModified; + } + + @Override + public String getFilename() { + return this.filename; + } + + @Override + public long lastModified() throws IOException { + return this.lastModified; + } + + public String getContentAsString() { + try { + return new String(getByteArray(), "UTF-8"); + } + catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return ""; + } + } + +} 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 8c3dd5bf82f..00000000000 --- 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 0a694588c5a..00000000000 --- 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/ExtensionMappingResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java new file mode 100644 index 00000000000..e9f99cb5020 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java @@ -0,0 +1,64 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +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()); + 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()); + Resource resolved = resolver.resolveAndTransform(null, resourceId, locations); + assertEquals(resource, resolved); + } + + @Test + public void resolveLessUrl() { + String resourceId = "zoo.css"; + String url = "zoo.css"; + assertEquals(url, resolver.resolveUrl(resourceId, locations)); + } +} 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/FingerprintingResourceResolverTests.java new file mode 100644 index 00000000000..50fee80a1f9 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintingResourceResolverTests.java @@ -0,0 +1,107 @@ +/* + * 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 FingerprintingResourceResolverTests { + + private ResourceResolverChain chain; + + private FingerprintingResourceResolver resolver = new FingerprintingResourceResolver(); + + private List locations; + + @Before + public void setUp() { + List resolvers = new ArrayList(); + resolvers.add(resolver); + chain = new DefaultResourceResolverChain(resolvers, new ArrayList()); + locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + 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 00000000000..0cd88f333fa --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java @@ -0,0 +1,105 @@ +/* + * 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 FingerprintingResourceResolver()); + 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(); + request.addHeader("Accept-Encoding", "gzip"); + String file = "js/foo.js"; + 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); + } + + @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.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); + } +} 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 8dd54a643fc..5fe75ca2dea 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/resource/ResourceUrlMapperTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java new file mode 100644 index 00000000000..cac485bfc54 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java @@ -0,0 +1,96 @@ +/* + * 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.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 org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + +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()); + 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()); + handler.setResourceResolvers(resolvers); + resetMapper(); + + String url = mapper.getUrlForResource("/resources/zoo.css"); + assertEquals("/resources/zoo.css", url); + } + +} 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 00000000000..d1fdea69ce1 --- /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 00000000000..e2f0b1c742a --- /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 00000000000..e2f0b1c742a --- /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 From 0e58125b157f0be6440ffdf1a38a0d4957a6f4a2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 24 Sep 2013 20:53:08 -0400 Subject: [PATCH 2/4] Polish --- .../annotation/ResourceHandlerRegistry.java | 7 ++ .../resource/AbstractResourceResolver.java | 18 ++-- .../DefaultResourceResolverChain.java | 78 ++++----------- .../web/servlet/resource/EncodedResource.java | 6 +- .../ExtensionMappingResourceResolver.java | 76 +++++++------- ....java => FingerprintResourceResolver.java} | 92 ++++++++--------- .../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, 430 insertions(+), 319 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) { - return baseResource; + if (baseResource == null) { + logger.debug("Failed to find resource after removing fingerprint: " + simplePath); + return null; } - + String candidateHash = calculateHash(baseResource); - if (candidateHash.equals(hash)) { - this.logger.debug("Fingerprint match succeeded."); + 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); } - + } From 3c48b421065607689068676eab005269a4d06ed6 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 Sep 2013 16:51:51 -0400 Subject: [PATCH 3/4] Refactor ResourceResolverChain + resolver class names This change splits out resource transformation out from the ResourceResolverChain so that chain is focused entirely on resource resolution (as its name suggests). The invocation of transformers is left as a separate step, it uses a different (recursive) algorithm in any case and iterates over a different set of objects. Also ResourceResolverChain is now limited strictly to methods that a ResourceResolver should be able to use to delegate to remaining resolvers. Furthermore, ResourceResolverChain now maintains an internal index of the "current" resolver so that resolvers don't have to pass the chain when invoking it much like a (Servlet API) FilterChain works. If the last resolver calls the chain again, a null value is returned. --- .../resource/AbstractResourceResolver.java | 50 ------------ .../DefaultResourceResolverChain.java | 80 ++++++++++++++----- .../resource/FingerprintResourceResolver.java | 21 ++--- .../resource/GzipResourceResolver.java | 12 ++- .../resource/LessResourceTransformer.java | 2 +- ...ava => PathExtensionResourceResolver.java} | 24 +++--- .../resource/PathResourceResolver.java | 23 +++--- .../resource/ResourceHttpRequestHandler.java | 24 ++++-- .../servlet/resource/ResourceResolver.java | 33 +++++++- .../resource/ResourceResolverChain.java | 33 ++++++-- .../servlet/resource/ResourceTransformer.java | 22 ++++- .../resource/ResourceUrlEncodingFilter.java | 8 +- .../servlet/resource/ResourceUrlMapper.java | 5 +- ...ExtensionMappingResourceResolverTests.java | 8 +- .../FingerprintResourceResolverTests.java | 14 ++-- .../resource/GzipResourceResolverTests.java | 6 +- .../resource/ResourceUrlMapperTests.java | 2 +- spring-webmvc/src/test/resources/log4j.xml | 4 + 18 files changed, 221 insertions(+), 150 deletions(-) delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java rename spring-webmvc/src/main/java/org/springframework/web/servlet/resource/{ExtensionMappingResourceResolver.java => PathExtensionResourceResolver.java} (76%) 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 deleted file mode 100644 index 627d5e88e53..00000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractResourceResolver.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.List; - -import javax.servlet.http.HttpServletRequest; - -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 requestPath, - List locations, ResourceResolverChain chain) { - - 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) { - 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 dcc53bf9d2c..44e8fc343d5 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 @@ -16,13 +16,15 @@ package org.springframework.web.servlet.resource; -import java.io.IOException; 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; /** @@ -34,42 +36,78 @@ import org.springframework.core.io.Resource; */ class DefaultResourceResolverChain implements ResourceResolverChain { - private final List resolvers; + private static Log logger = LogFactory.getLog(DefaultResourceResolverChain.class); - private List transformers = new ArrayList(); + private final List resolvers = new ArrayList(); + private int index = -1; - public DefaultResourceResolverChain(List resolvers, List transformers) { - this.resolvers = (resolvers != null) ? resolvers : new ArrayList(); - this.transformers = (transformers != null) ? transformers : new ArrayList(); + + public DefaultResourceResolverChain(List resolvers) { + this.resolvers.addAll((resolvers != null) ? resolvers : new ArrayList()); } @Override - public ResourceResolver next(ResourceResolver current) { - return this.resolvers.get(this.resolvers.indexOf(current) + 1); + 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 Resource resolveAndTransform(HttpServletRequest request, String path, List locations) - throws IOException { + public String resolveUrlPath(String resourcePath, List locations) { + ResourceResolver resolver = getNextResolver(); + if (resolver == null) { + return null; + } + try { + logBefore(resolver); + String urlPath = resolver.resolveUrlPath(resourcePath, locations, this); + logAfter(resolver, urlPath); + return urlPath; + } + finally { + this.index--; + } + } - Resource resource = this.resolvers.get(0).resolve(request, path, locations, this); - return resource != null ? applyTransformers(request, resource) : resource; + 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); } - @Override - public String resolveUrl(String resourcePath, List locations) { - return this.resolvers.get(0).resolveUrl(resourcePath, locations, this); + private void logBefore(ResourceResolver resolver) { + if (logger.isTraceEnabled()) { + logger.trace("Calling " + resolver.getClass().getName() + " at index [" + this.index + "]"); + } } - 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)); - } + private void logAfter(ResourceResolver resolver, Object result) { + if (logger.isTraceEnabled()) { + logger.trace(resolver.getClass().getName() + " returned " + result); } - return resource; } } 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 index 2daf1080ccb..528a9e9bf75 100644 --- 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 @@ -34,9 +34,10 @@ import org.springframework.util.StringUtils; /** * * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 4.0 */ -public class FingerprintResourceResolver extends AbstractResourceResolver { +public class FingerprintResourceResolver implements ResourceResolver { private static final Log logger = LogFactory.getLog(FingerprintResourceResolver.class); @@ -44,23 +45,25 @@ public class FingerprintResourceResolver extends AbstractResourceResolver { @Override - protected Resource resolveInternal(HttpServletRequest request, String path, List locations, - ResourceResolverChain chain, Resource resolved) { + public Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain) { // 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 + + Resource resolved = chain.resolveResource(request, requestPath, locations); if (resolved != null) { return resolved; } // Now try extracting and matching the hash for dev mode - String hash = extractHash(path); + String hash = extractHash(requestPath); if (StringUtils.isEmpty(hash)) { return null; } - String simplePath = StringUtils.delete(path, "-" + hash); - Resource baseResource = chain.next(this).resolve(request, simplePath, locations, chain); + String simplePath = StringUtils.delete(requestPath, "-" + hash); + Resource baseResource = chain.resolveResource(request, simplePath, locations); if (baseResource == null) { logger.debug("Failed to find resource after removing fingerprint: " + simplePath); return null; @@ -101,11 +104,11 @@ public class FingerprintResourceResolver extends AbstractResourceResolver { } @Override - public String resolveUrl(String resourcePath, List locations, ResourceResolverChain chain) { + public String resolveUrlPath(String resourcePath, List locations, ResourceResolverChain chain) { // TODO - Consider caching here for better efficiency - String baseUrl = chain.next(this).resolveUrl(resourcePath, locations, chain); + String baseUrl = chain.resolveUrlPath(resourcePath, locations); if (StringUtils.hasText(baseUrl)) { - Resource original = chain.next(this).resolve(null, resourcePath, locations, chain); + Resource original = chain.resolveResource(null, resourcePath, locations); String hash = calculateHash(original); return StringUtils.stripFilenameExtension(baseUrl) + "-" + hash + "." + StringUtils.getFilenameExtension(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 e18d4b05469..3569e1f30b3 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 @@ -41,15 +41,16 @@ import org.springframework.core.io.Resource; * @author Rossen Stoyanchev * @since 4.0 */ -public class GzipResourceResolver extends AbstractResourceResolver { +public class GzipResourceResolver implements ResourceResolver { private static final Log logger = LogFactory.getLog(GzipResourceResolver.class); @Override - protected Resource resolveInternal(HttpServletRequest request, String path, - List locations, ResourceResolverChain chain, Resource resource) { + 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; } @@ -72,6 +73,11 @@ public class GzipResourceResolver extends AbstractResourceResolver { return ((value != null) && value.toLowerCase().contains("gzip")); } + @Override + public String resolveUrlPath(String resourcePath, List locations, ResourceResolverChain chain) { + return chain.resolveUrlPath(resourcePath, locations); + } + private static final class GzippedResource extends AbstractResource implements EncodedResource { 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 b759a79f223..2268a3fdde0 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 @@ -61,7 +61,7 @@ public class LessResourceTransformer implements ResourceTransformer { } @Override - public boolean handles(HttpServletRequest request, Resource original) { + public boolean willTransform(HttpServletRequest request, Resource original) { return LESS_EXT.equals(StringUtils.getFilenameExtension(original.getFilename())); } 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/PathExtensionResourceResolver.java similarity index 76% rename from spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolver.java rename to spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathExtensionResourceResolver.java index 80ea21fbe7b..f9e02e7091c 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/PathExtensionResourceResolver.java @@ -32,36 +32,38 @@ import org.springframework.util.StringUtils; /** * * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 4.0 */ -public class ExtensionMappingResourceResolver extends AbstractResourceResolver { +public class PathExtensionResourceResolver implements ResourceResolver { - private static final Log logger = LogFactory.getLog(ExtensionMappingResourceResolver.class); + private static final Log logger = LogFactory.getLog(PathExtensionResourceResolver.class); private final boolean compareTimeStamp; - public ExtensionMappingResourceResolver() { + public PathExtensionResourceResolver() { this.compareTimeStamp = false; } - public ExtensionMappingResourceResolver(boolean compareTimeStamp) { + public PathExtensionResourceResolver(boolean compareTimeStamp) { this.compareTimeStamp = compareTimeStamp; } @Override - protected Resource resolveInternal(HttpServletRequest request, String path, - List locations, ResourceResolverChain chain, Resource resource) { + public Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain) { + Resource resource = chain.resolveResource(request, requestPath, locations); if ((resource != null) && !this.compareTimeStamp) { return resource; } for (Resource location : locations) { - String baseFilename = StringUtils.getFilename(path); + String baseFilename = StringUtils.getFilename(requestPath); try { - Resource basePath = location.createRelative(StringUtils.delete(path, baseFilename)); + Resource basePath = location.createRelative(StringUtils.delete(requestPath, baseFilename)); if (basePath.getFile().isDirectory()) { for (String fileName : basePath.getFile().list(new ExtensionFilenameFilter(baseFilename))) { //Always use the first match @@ -84,15 +86,15 @@ public class ExtensionMappingResourceResolver extends AbstractResourceResolver { } @Override - public String resolveUrl(String resourcePath, List locations, + public String resolveUrlPath(String resourcePath, List locations, ResourceResolverChain chain) { - String resolved = super.resolveUrl(resourcePath, locations, chain); + String resolved = chain.resolveUrlPath(resourcePath, locations); if (StringUtils.hasText(resolved)) { return resolved; } - Resource mappedResource = resolveInternal(null, resourcePath, locations, chain, null); + Resource mappedResource = resolveResource(null, resourcePath, locations, chain); if (mappedResource != null) { return resourcePath; } 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 index a7a7381b5df..2a55e7e0a86 100644 --- 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 @@ -28,7 +28,7 @@ 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. + * configured Resource location and checks if such a resource exists. * * @author Jeremy Grelle * @author Rossen Stoyanchev @@ -40,13 +40,18 @@ public class PathResourceResolver implements ResourceResolver { @Override - public Resource resolve(HttpServletRequest request, String requestPath, List locations, - ResourceResolverChain chain) { + public Resource resolveResource(HttpServletRequest request, + String requestPath, List locations, ResourceResolverChain chain) { - return resolveInternal(requestPath, locations); + return getResource(requestPath, locations); } - private Resource resolveInternal(String path, List locations) { + @Override + public String resolveUrlPath(String resourcePath, List locations, ResourceResolverChain chain) { + return (getResource(resourcePath, locations) != null) ? resourcePath : null; + } + + private Resource getResource(String path, List locations) { for (Resource location : locations) { try { if (logger.isDebugEnabled()) { @@ -70,12 +75,4 @@ public class PathResourceResolver implements ResourceResolver { 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 ca9c8386233..48f48e80a40 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 @@ -85,9 +85,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H private List resourceResolvers = new ArrayList(); - private List resourceTransformers; - - private ResourceResolverChain resolverChain; + private List resourceTransformers = new ArrayList(); public ResourceHttpRequestHandler() { @@ -123,21 +121,19 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return this.resourceResolvers; } - public void setResourceTransformers(List resourceTransformers) { - this.resourceTransformers = resourceTransformers; + public void setResourceTransformers(List transformers) { + this.resourceTransformers = (transformers != null) ? transformers : new ArrayList(); } public List getResourceTransformers() { return this.resourceTransformers; } - @Override public void afterPropertiesSet() throws Exception { if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) { logger.warn("Locations list is empty. No resources will be served"); } - this.resolverChain = new DefaultResourceResolverChain(this.resourceResolvers, this.resourceTransformers); } /** @@ -208,7 +204,19 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return null; } - return this.resolverChain.resolveAndTransform(request, path, this.locations); + ResourceResolverChain chain = new DefaultResourceResolverChain(this.resourceResolvers); + Resource resource = chain.resolveResource(request, path, this.locations); + + return (resource != null) ? applyTransformers(request, resource) : null; + } + + private Resource applyTransformers(HttpServletRequest request, Resource resource) throws IOException { + for (ResourceTransformer transformer : this.resourceTransformers) { + if (transformer.willTransform(request, resource)) { + return applyTransformers(request, transformer.transform(resource)); + } + } + return resource; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java index 52d8771aef4..bec80b02db2 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,15 +24,40 @@ import org.springframework.core.io.Resource; /** + * A strategy for two way resolution of URL paths to actual {@link Resource}s located + * from one or more configured locations. * * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 4.0 */ public interface ResourceResolver { - public Resource resolve(HttpServletRequest request, String requestPath, - List locations, ResourceResolverChain chain); + /** + * Resolve the URL path of an incoming request to an actual {@link Resource}. + * + * @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 + * @param chain the chain with remaining resolvers to delegate to + * + * @return the resolved {@link Resource} or {@code null} if this resolver could not + * resolve the resource + */ + Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain); + + /** + * 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 + * @param chain the chain with remaining resolvers to delegate to + * + * @return the resolved URL path or {@code null} if this resolver could not resolve + * the given resource path + */ + String resolveUrlPath(String resourcePath, 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 be82752b662..339082faa64 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 @@ -16,7 +16,6 @@ package org.springframework.web.servlet.resource; -import java.io.IOException; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -24,18 +23,38 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.core.io.Resource; - /** + * A contract for invoking a chain of {@link ResourceResolver}s. Each resolver is passed a + * reference to the chain allowing it delegate to the remaining resolvers. * * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 4.0 */ public interface ResourceResolverChain { - public Resource resolveAndTransform(HttpServletRequest request, String path, List locations) - throws IOException; - - public ResourceResolver next(ResourceResolver current); + /** + * Resolve the URL path of an incoming request to an actual {@link Resource}. + * + * @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); - 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 4d9b57d5e8e..b457bb568ef 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,30 @@ import org.springframework.core.io.Resource; /** + * A strategy for transforming a resource. * * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 4.0 */ public interface ResourceTransformer { - public Resource transform(Resource original) throws IOException; - - public boolean handles(HttpServletRequest request, Resource original); + /** + * Whether this transformer can transform the given resource. + * + * @param request the context request + * @param resource the candidate resource to transform + */ + boolean willTransform(HttpServletRequest request, Resource resource); + + /** + * Transform the given resource and return a new resource. + * + * @param resource the resource to transform + * @return the transformed resource, never {@code null} + * + * @throws IOException if the transformation fails + */ + Resource transform(Resource resource) throws IOException; } 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 e95773f9779..84008cd029b 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 @@ -39,10 +39,11 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { private ResourceUrlMapper mapper; + @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + filterChain.doFilter(request, new ResourceUrlResponseWrapper(request, response)); } @@ -52,6 +53,7 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { this.mapper = appContext.getBean(ResourceUrlMapper.class); } + private class ResourceUrlResponseWrapper extends HttpServletResponseWrapper { private final UrlPathHelper pathHelper = new UrlPathHelper(); 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 43b9c3dbd00..4885d05ebae 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 @@ -84,8 +84,9 @@ public class ResourceUrlMapper implements BeanPostProcessor, ApplicationListener ResourceHttpRequestHandler handler = mapping.getValue(); String nestedPath = matcher.extractPathWithinPattern(mapping.getKey(), resourcePath); String prefix = resourcePath.replace(nestedPath, ""); - String url = new DefaultResourceResolverChain(handler.getResourceResolvers(), handler. - getResourceTransformers()).resolveUrl(nestedPath, handler.getLocations()); + List resolvers = handler.getResourceResolvers(); + DefaultResourceResolverChain chain = new DefaultResourceResolverChain(resolvers); + String url = chain.resolveUrlPath(nestedPath, handler.getLocations()); if (url != null) { return prefix + url; } 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 2cd5a890a29..fb4841ab08e 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 @@ -40,9 +40,9 @@ public class ExtensionMappingResourceResolverTests { @Before public void setUp() { List resolvers = new ArrayList(); - resolvers.add(new ExtensionMappingResourceResolver()); + resolvers.add(new PathExtensionResourceResolver()); resolvers.add(new PathResourceResolver()); - resolver = new DefaultResourceResolverChain(resolvers, new ArrayList()); + resolver = new DefaultResourceResolverChain(resolvers); locations = new ArrayList(); locations.add(new ClassPathResource("test/", getClass())); locations.add(new ClassPathResource("testalternatepath/", getClass())); @@ -52,7 +52,7 @@ public class ExtensionMappingResourceResolverTests { public void resolveLessResource() throws Exception { String resourceId = "zoo.css"; Resource resource = new ClassPathResource("test/" + resourceId + ".less", getClass()); - Resource resolved = resolver.resolveAndTransform(null, resourceId, locations); + Resource resolved = resolver.resolveResource(null, resourceId, locations); assertEquals(resource, resolved); } @@ -60,6 +60,6 @@ public class ExtensionMappingResourceResolverTests { public void resolveLessUrl() { String resourceId = "zoo.css"; String url = "zoo.css"; - assertEquals(url, resolver.resolveUrl(resourceId, locations)); + assertEquals(url, resolver.resolveUrlPath(resourceId, locations)); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java index 8e8f4c9993f..e9c202a3a8d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java @@ -48,7 +48,7 @@ public class FingerprintResourceResolverTests { List resolvers = new ArrayList(); resolvers.add(resolver); resolvers.add(new PathResourceResolver()); - chain = new DefaultResourceResolverChain(resolvers, new ArrayList()); + chain = new DefaultResourceResolverChain(resolvers); locations = new ArrayList(); locations.add(new ClassPathResource("test/", getClass())); locations.add(new ClassPathResource("testalternatepath/", getClass())); @@ -59,7 +59,7 @@ public class FingerprintResourceResolverTests { public void resolveWithoutHash() throws Exception { String file = "bar.css"; Resource expected = new ClassPathResource("test/" + file, getClass()); - Resource actual = chain.resolveAndTransform(null, file, locations); + Resource actual = chain.resolveResource(null, file, locations); assertEquals(expected, actual); } @@ -67,14 +67,14 @@ public class FingerprintResourceResolverTests { @Test public void resolveWithHashNoMatch() throws Exception { String file = "bogus-e36d2e05253c6c7085a91522ce43a0b4.css"; - assertNull(chain.resolveAndTransform(null, file, locations)); + 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.resolveAndTransform(null, file, locations); + Resource actual = chain.resolveResource(null, file, locations); assertEquals(expected, actual); } @@ -84,7 +84,7 @@ public class FingerprintResourceResolverTests { 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); + Resource actual = chain.resolveResource(null, path, locations); assertEquals(expected, actual); } @@ -94,7 +94,7 @@ public class FingerprintResourceResolverTests { 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); + Resource actual = chain.resolveResource(null, path, locations); assertEquals(expected, actual); } @@ -104,7 +104,7 @@ public class FingerprintResourceResolverTests { 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); + Resource actual = chain.resolveResource(null, path, locations); assertEquals(expected, actual); } 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 0e05aeedea7..61f161ba476 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 @@ -72,7 +72,7 @@ public class GzipResourceResolverTests { resolvers.add(new GzipResourceResolver()); resolvers.add(new FingerprintResourceResolver()); resolvers.add(new PathResourceResolver()); - resolver = new DefaultResourceResolverChain(resolvers, new ArrayList()); + resolver = new DefaultResourceResolverChain(resolvers); locations = new ArrayList(); locations.add(new ClassPathResource("test/", getClass())); locations.add(new ClassPathResource("testalternatepath/", getClass())); @@ -85,7 +85,7 @@ public class GzipResourceResolverTests { String file = "js/foo.js"; String gzFile = file+".gz"; Resource resource = new ClassPathResource("test/"+gzFile, getClass()); - Resource resolved = resolver.resolveAndTransform(request, file, locations); + Resource resolved = resolver.resolveResource(request, file, locations); assertEquals(resource.getDescription(), resolved.getDescription()); assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename()); @@ -100,7 +100,7 @@ public class GzipResourceResolverTests { String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; String gzFile = file+".gz"; Resource resource = new ClassPathResource("test/"+gzFile, getClass()); - Resource resolved = resolver.resolveAndTransform(request, file, locations); + Resource resolved = resolver.resolveResource(request, file, locations); assertEquals(resource.getDescription(), resolved.getDescription()); assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename()); 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 f0397af5fc2..2c6ee8f4b08 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 @@ -86,7 +86,7 @@ public class ResourceUrlMapperTests { @Test public void getExtensionMappedResourceUrl() { List resolvers = new ArrayList(); - resolvers.add(new ExtensionMappingResourceResolver()); + resolvers.add(new PathExtensionResourceResolver()); resolvers.add(new PathResourceResolver()); handler.setResourceResolvers(resolvers); resetMapper(); diff --git a/spring-webmvc/src/test/resources/log4j.xml b/spring-webmvc/src/test/resources/log4j.xml index 785c094dde1..1f7f3d0980e 100644 --- a/spring-webmvc/src/test/resources/log4j.xml +++ b/spring-webmvc/src/test/resources/log4j.xml @@ -19,6 +19,10 @@ + + + + From 5a2e30c18bee91343ca9547930efc678a08b0cac Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 27 Sep 2013 21:02:17 -0400 Subject: [PATCH 4/4] Refactor Resource URL generation and Servlet Filter Renamed ResourceUrlMapper to ResourceUrlGenerator and refactored it to be configured with Resource-serving HandlerMappings as opposed to having them detected in the ApplicationContext through the BeanPostProcessor contact. Renamed and polished ResourceUrlEncodingFilter to ResourceUrlFilter and added tests. --- .../ResourceHandlerRegistration.java | 37 ++++- ...dingFilter.java => ResourceUrlFilter.java} | 40 +++-- .../resource/ResourceUrlGenerator.java | 129 +++++++++++++++ .../servlet/resource/ResourceUrlMapper.java | 106 ------------ .../resource/ResourceUrlFilterTests.java | 154 ++++++++++++++++++ ...ts.java => ResourceUrlGeneratorTests.java} | 34 ++-- 6 files changed, 357 insertions(+), 143 deletions(-) rename spring-webmvc/src/main/java/org/springframework/web/servlet/resource/{ResourceUrlEncodingFilter.java => ResourceUrlFilter.java} (62%) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlGenerator.java delete mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlFilterTests.java rename spring-webmvc/src/test/java/org/springframework/web/servlet/resource/{ResourceUrlMapperTests.java => ResourceUrlGeneratorTests.java} (74%) 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 8ce3914180e..0a876df93fd 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 @@ -23,7 +23,10 @@ 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; +import org.springframework.web.servlet.resource.ResourceTransformer; /** * Encapsulates information required to create a resource handlers. @@ -43,6 +46,11 @@ public class ResourceHandlerRegistration { private Integer cachePeriod; + private List resourceResolvers; + + private List resourceTransformers; + + /** * Create a {@link ResourceHandlerRegistration} instance. * @param resourceLoader a resource loader for turning a String location into a {@link Resource} @@ -70,6 +78,23 @@ 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. + */ + public void setResourceResolvers(List resourceResolvers) { + this.resourceResolvers = resourceResolvers; + } + + /** + * Configure the list of {@link ResourceTransformer}s to use. + */ + public void setResourceTransformers(List transformers) { + this.resourceTransformers = transformers; + } + /** * 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 @@ -95,9 +120,15 @@ 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); + } + if (this.resourceTransformers != null) { + requestHandler.setResourceTransformers(this.resourceTransformers); + } + 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/resource/ResourceUrlEncodingFilter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlFilter.java similarity index 62% rename from spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java rename to spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlFilter.java index 84008cd029b..0d571c6d434 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/ResourceUrlFilter.java @@ -17,6 +17,9 @@ package org.springframework.web.servlet.resource; import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -31,13 +34,17 @@ import org.springframework.web.util.UrlPathHelper; /** + * A filter that wraps the {@link HttpServletResponse} and overrides its + * {@link HttpServletResponse#encodeURL(String) encodeURL} method in order to generate + * resource URL links via {@link ResourceUrlGenerator}. * * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 4.0 */ -public class ResourceUrlEncodingFilter extends OncePerRequestFilter { +public class ResourceUrlFilter extends OncePerRequestFilter { - private ResourceUrlMapper mapper; + private Set resourceUrlGenerators; @Override @@ -49,8 +56,10 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { @Override protected void initFilterBean() throws ServletException { - WebApplicationContext appContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); - this.mapper = appContext.getBean(ResourceUrlMapper.class); + WebApplicationContext cxt = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + Map beans = cxt.getBeansOfType(ResourceUrlGenerator.class); + this.resourceUrlGenerators = new LinkedHashSet(); + this.resourceUrlGenerators.addAll(beans.values()); } @@ -60,35 +69,30 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { 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); - //This accounts for the behavior when servlet is mapped to "/" - if (!servletPath.equals(appPath)) { - this.pathPrefix += pathHelper.getServletPath(request); - } + String requestUri = this.pathHelper.getRequestUri(request); + String lookupPath = this.pathHelper.getLookupPathForRequest(request); + this.pathPrefix = requestUri.replace(lookupPath, ""); } @Override public String encodeURL(String url) { - if(url.startsWith(pathPrefix)) { - String relativeUrl = url.replaceFirst(pathPrefix, ""); + if(url.startsWith(this.pathPrefix)) { + String relativeUrl = url.replaceFirst(this.pathPrefix, ""); if (!relativeUrl.startsWith("/")) { relativeUrl = "/" + relativeUrl; } - if (mapper.isResourceUrl(relativeUrl)) { - String resourceUrl = mapper.getUrlForResource(relativeUrl); + for (ResourceUrlGenerator generator : resourceUrlGenerators) { + String resourceUrl = generator.getResourceUrl(relativeUrl); if (resourceUrl != null) { - return resourceUrl; + return super.encodeURL(this.pathPrefix + resourceUrl); } } } return super.encodeURL(url); } - } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlGenerator.java new file mode 100644 index 00000000000..f4086b875aa --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlGenerator.java @@ -0,0 +1,129 @@ +/* + * 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 java.util.Map.Entry; + +import org.springframework.core.io.Resource; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + + +/** + * A helper class for generating the URL for a resource. Given knowledge of all configured + * resource handler mappings (see {@link #setResourceHandlerMappings(List)}), it can + * determine whether a given path is a path to a resource, as well as what URL should be + * sent to the client to access that resource. This is essentially the reverse of + * resolving an incoming request URL to a resource. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class ResourceUrlGenerator { + + private final List resourceMappings = new ArrayList(); + + + /** + * Configure this instance with the handler mappings used to serve resources. It is + * expected that the handler mapping URL map contains handlers of type + * {@link ResourceHttpRequestHandler}. + * + * @param handlerMappings resource handler mappings + */ + public void setResourceHandlerMappings(List handlerMappings) { + this.resourceMappings.clear(); + if (handlerMappings == null) { + return; + } + for (SimpleUrlHandlerMapping handlerMapping : handlerMappings) { + PathMatcher pathMatcher = handlerMapping.getPathMatcher(); + + for(Entry entry : handlerMapping.getUrlMap().entrySet()) { + Object value = entry.getValue(); + if (value instanceof ResourceHttpRequestHandler) { + ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) value; + + String pattern = entry.getKey(); + List resolvers = handler.getResourceResolvers(); + List locations = handler.getLocations(); + this.resourceMappings.add(new ResourceMapping(pattern, pathMatcher, resolvers, 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 candidatePath the resource path to resolve + * + * @return the resolved URL path or {@code null} if the given path does not match to + * any resource or otherwise could not be resolved to a resource URL path + */ + public String getResourceUrl(String candidatePath) { + for (ResourceMapping mapping : this.resourceMappings) { + String url = mapping.getUrlForResource(candidatePath); + if (url != null) { + return url; + } + } + return null; + } + + + private static class ResourceMapping { + + private final String pattern; + + private final PathMatcher pathMatcher; + + private final List resolvers; + + private final List locations; + + + public ResourceMapping(String pattern, PathMatcher pathMatcher, + List resolvers, List locations) { + + this.pattern = pattern; + this.pathMatcher = pathMatcher; + this.resolvers = resolvers; + this.locations = locations; + } + + public String getUrlForResource(String candidatePath) { + + if (this.pathMatcher.match(this.pattern, candidatePath)) { + + String pathWithinMapping = this.pathMatcher.extractPathWithinPattern(this.pattern, candidatePath); + String pathMapping = candidatePath.replace(pathWithinMapping, ""); + + DefaultResourceResolverChain chain = new DefaultResourceResolverChain(this.resolvers); + String url = chain.resolveUrlPath(pathWithinMapping, this.locations); + if (url != null) { + return pathMapping + url; + } + } + return null; + } + } + +} 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 deleted file mode 100644 index 4885d05ebae..00000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlMapper.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.core.OrderComparator; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.ClassUtils; -import org.springframework.util.PathMatcher; -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 { - if (ClassUtils.isAssignableValue(SimpleUrlHandlerMapping.class, bean)) { - SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) bean; - for(Entry mappingEntry : mapping.getUrlMap().entrySet()) { - Object val = mappingEntry.getValue(); - if (val instanceof ResourceHttpRequestHandler) { - this.mappings.add(mapping); - } - } - } - return bean; - } - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - OrderComparator.sort(this.mappings); - for (SimpleUrlHandlerMapping mapping : mappings) { - for(Entry mappingEntry : mapping.getUrlMap().entrySet()) { - Object val = mappingEntry.getValue(); - this.handlers.put(mappingEntry.getKey(), (ResourceHttpRequestHandler) val); - } - } - } - - public String getUrlForResource(String resourcePath) { - for (Entry mapping : this.handlers.entrySet()) { - if (matcher.match(mapping.getKey(), resourcePath)) { - ResourceHttpRequestHandler handler = mapping.getValue(); - String nestedPath = matcher.extractPathWithinPattern(mapping.getKey(), resourcePath); - String prefix = resourcePath.replace(nestedPath, ""); - List resolvers = handler.getResourceResolvers(); - DefaultResourceResolverChain chain = new DefaultResourceResolverChain(resolvers); - String url = chain.resolveUrlPath(nestedPath, handler.getLocations()); - if (url != null) { - return prefix + url; - } - } - } - return null; - } - - public boolean isResourceUrl(String relativeUrl) { - for (String mapping : this.handlers.keySet()) { - if (matcher.match(mapping, relativeUrl)) { - return true; - } - } - return false; - } -} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlFilterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlFilterTests.java new file mode 100644 index 00000000000..6fce5731419 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlFilterTests.java @@ -0,0 +1,154 @@ +/* + * 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.Collections; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.springframework.context.annotation.Bean; +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.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + +import static org.junit.Assert.*; + + +/** + * + * @author Rossen Stoyanchev + */ +public class ResourceUrlFilterTests { + + private MockFilterChain filterChain; + + private TestServlet servlet; + + + @Test + public void rootServletMapping() throws Exception { + + initFilterChain(WebConfig.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.setRequestURI("/myapp/index.html"); + request.setContextPath("/myapp"); + 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 { + + initFilterChain(WebConfig.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.setRequestURI("/myapp/myservlet/index.html"); + request.setContextPath("/myapp"); + 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 { + + initFilterChain(WebConfig.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.setRequestURI("/myapp/index.html"); + request.setContextPath("/myapp"); + 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); + } + + private void initFilterChain(Class configClass) throws ServletException { + + MockServletContext servletContext = new MockServletContext(); + + AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); + cxt.setServletContext(servletContext); + cxt.register(configClass); + cxt.refresh(); + + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, cxt); + + ResourceUrlFilter filter = new ResourceUrlFilter(); + filter.setServletContext(servletContext); + filter.initFilterBean(); + + this.servlet = new TestServlet(); + this.filterChain = new MockFilterChain(servlet, filter); + } + + + @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); + } + + @Bean + public ResourceUrlGenerator resourceUrlGenerator() { + ResourceUrlGenerator generator = new ResourceUrlGenerator(); + SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) resourceHandlerMapping(); + generator.setResourceHandlerMappings(Collections.singletonList(handlerMapping)); + return generator; + } + } + + 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/ResourceUrlMapperTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlGeneratorTests.java similarity index 74% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java rename to spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlGeneratorTests.java index 2c6ee8f4b08..2c3a2149229 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/ResourceUrlGeneratorTests.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.resource; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,41 +34,42 @@ import static org.junit.Assert.*; /** * * @author Jeremy Grelle + * @author Rossen Stoyanchev */ -public class ResourceUrlMapperTests { +public class ResourceUrlGeneratorTests { ResourceHttpRequestHandler handler; SimpleUrlHandlerMapping mapping; - ResourceUrlMapper mapper; + ResourceUrlGenerator generator; + @Before public void setUp() { - List resourcePaths = new ArrayList(); - resourcePaths.add(new ClassPathResource("test/", getClass())); - resourcePaths.add(new ClassPathResource("testalternatepath/", getClass())); + List locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); Map urlMap = new HashMap(); handler = new ResourceHttpRequestHandler(); - handler.setLocations(resourcePaths); + handler.setLocations(locations); urlMap.put("/resources/**", handler); mapping = new SimpleUrlHandlerMapping(); mapping.setUrlMap(urlMap); } - private void resetMapper() { - mapper = new ResourceUrlMapper(); - mapper.postProcessAfterInitialization(mapping, "resourceMapping"); - mapper.onApplicationEvent(null); + private void initGenerator() { + generator = new ResourceUrlGenerator(); + generator.setResourceHandlerMappings(Collections.singletonList(this.mapping)); } @Test public void getStaticResourceUrl() { - resetMapper(); + initGenerator(); - String url = mapper.getUrlForResource("/resources/foo.css"); + String url = generator.getResourceUrl("/resources/foo.css"); assertEquals("/resources/foo.css", url); } @@ -77,9 +79,9 @@ public class ResourceUrlMapperTests { resolvers.add(new FingerprintResourceResolver()); resolvers.add(new PathResourceResolver()); handler.setResourceResolvers(resolvers); - resetMapper(); + initGenerator(); - String url = mapper.getUrlForResource("/resources/foo.css"); + String url = generator.getResourceUrl("/resources/foo.css"); assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url); } @@ -89,9 +91,9 @@ public class ResourceUrlMapperTests { resolvers.add(new PathExtensionResourceResolver()); resolvers.add(new PathResourceResolver()); handler.setResourceResolvers(resolvers); - resetMapper(); + initGenerator(); - String url = mapper.getUrlForResource("/resources/zoo.css"); + String url = generator.getResourceUrl("/resources/zoo.css"); assertEquals("/resources/zoo.css", url); }