From 61e61bd5fdd2d5b1e2d5bf132dca59ee83f0da5e Mon Sep 17 00:00:00 2001 From: Jeremy Grelle Date: Sun, 22 Sep 2013 15:17:15 -0400 Subject: [PATCH] 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 094daef0b2..3bfebe7dca 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 0000000000..350b0ce9e1 --- /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 0000000000..4a21053d72 --- /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 0000000000..11efabbca5 --- /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 0000000000..48f59cd062 --- /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 0000000000..4166d3f625 --- /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 0000000000..834429906e --- /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 0000000000..9169730fa5 --- /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 f668d2a9c1..526d5b58a9 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 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 0000000000..0dd7b2fa12 --- /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 0000000000..1666335b16 --- /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 0000000000..e8808d1aa6 --- /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 0000000000..b8745d86af --- /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 0000000000..5c896ed6af --- /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 0000000000..a70b0e3954 --- /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 8c3dd5bf82..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/bar.js +++ /dev/null @@ -1 +0,0 @@ -function foo() { console.log("hello bar"); } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js deleted file mode 100644 index 0a694588c5..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/js/foo.js +++ /dev/null @@ -1 +0,0 @@ -function foo() { console.log("hello world"); } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java new file mode 100644 index 0000000000..e9f99cb502 --- /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 0000000000..50fee80a1f --- /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 0000000000..0cd88f333f --- /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 8dd54a643f..5fe75ca2de 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java @@ -43,7 +43,7 @@ public class ResourceHttpRequestHandlerTests { private ResourceHttpRequestHandler handler; @Before - public void setUp() { + public void setUp() throws Exception { List resourcePaths = new ArrayList(); resourcePaths.add(new ClassPathResource("test/", getClass())); resourcePaths.add(new ClassPathResource("testalternatepath/", getClass())); @@ -51,6 +51,7 @@ public class ResourceHttpRequestHandlerTests { handler.setLocations(resourcePaths); handler.setCacheSeconds(3600); handler.setServletContext(new TestServletContext()); + handler.afterPropertiesSet(); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlMapperTests.java new file mode 100644 index 0000000000..cac485bfc5 --- /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 0000000000..d1fdea69ce --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-bar/foo-bar.css @@ -0,0 +1 @@ +h2 { color:white; } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/foo.css b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-e36d2e05253c6c7085a91522ce43a0b4.css similarity index 100% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/handler/resources/foo.css rename to spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo-e36d2e05253c6c7085a91522ce43a0b4.css diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less new file mode 100644 index 0000000000..e2f0b1c742 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/foo.css.less @@ -0,0 +1 @@ +h1 { color:red; } \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less new file mode 100644 index 0000000000..e2f0b1c742 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/zoo.css.less @@ -0,0 +1 @@ +h1 { color:red; } \ No newline at end of file