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/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/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/DefaultResourceResolverChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java new file mode 100644 index 00000000000..44e8fc343d5 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + + +/** + * + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.0 + */ +class DefaultResourceResolverChain implements ResourceResolverChain { + + private static Log logger = LogFactory.getLog(DefaultResourceResolverChain.class); + + private final List resolvers = new ArrayList(); + + private int index = -1; + + + public DefaultResourceResolverChain(List resolvers) { + this.resolvers.addAll((resolvers != null) ? resolvers : new ArrayList()); + } + + + @Override + public Resource resolveResource(HttpServletRequest request, String requestPath, List locations) { + ResourceResolver resolver = getNextResolver(); + if (resolver == null) { + return null; + } + try { + logBefore(resolver); + Resource resource = resolver.resolveResource(request, requestPath, locations, this); + logAfter(resolver, resource); + return resource; + } + finally { + this.index--; + } + } + + @Override + public String resolveUrlPath(String resourcePath, List locations) { + ResourceResolver resolver = getNextResolver(); + if (resolver == null) { + return null; + } + try { + logBefore(resolver); + String urlPath = resolver.resolveUrlPath(resourcePath, locations, this); + logAfter(resolver, urlPath); + return urlPath; + } + finally { + this.index--; + } + } + + private ResourceResolver getNextResolver() { + + Assert.state(this.index <= this.resolvers.size(), + "Current index exceeds the number of configured ResourceResolver's"); + + if (this.index == (this.resolvers.size() - 1)) { + if (logger.isTraceEnabled()) { + logger.trace("No more ResourceResolver's to delegate to, returning null"); + } + return null; + } + + this.index++; + return this.resolvers.get(this.index); + } + + private void logBefore(ResourceResolver resolver) { + if (logger.isTraceEnabled()) { + logger.trace("Calling " + resolver.getClass().getName() + " at index [" + this.index + "]"); + } + } + + private void logAfter(ResourceResolver resolver, Object result) { + if (logger.isTraceEnabled()) { + logger.trace(resolver.getClass().getName() + " returned " + result); + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java new file mode 100644 index 00000000000..dcf9644ce87 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/EncodedResource.java @@ -0,0 +1,31 @@ +/* + * 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 + * @since 4.0 + */ +public interface EncodedResource extends Resource { + + public String getContentEncoding(); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java new file mode 100644 index 00000000000..528a9e9bf75 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java @@ -0,0 +1,119 @@ +/* + * 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 + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class FingerprintResourceResolver implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(FingerprintResourceResolver.class); + + private Pattern pattern = Pattern.compile("-(\\S*)\\."); + + + @Override + 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(requestPath); + if (StringUtils.isEmpty(hash)) { + return null; + } + + 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; + } + + String candidateHash = calculateHash(baseResource); + if (candidateHash.equals(hash)) { + logger.debug("Fingerprint match succeeded."); + return baseResource; + } + else { + logger.debug("Potential resource found, but fingerprint doesn't match."); + return null; + } + } + + 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 ""; + } + } + + private String calculateHash(Resource resource) { + try { + byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream()); + return DigestUtils.md5DigestAsHex(content); + } + catch (IOException e) { + logger.error("Failed to calculate hash on resource " + resource.toString()); + return ""; + } + } + + @Override + public String resolveUrlPath(String resourcePath, List locations, ResourceResolverChain chain) { + // TODO - Consider caching here for better efficiency + String baseUrl = chain.resolveUrlPath(resourcePath, locations); + if (StringUtils.hasText(baseUrl)) { + Resource original = chain.resolveResource(null, resourcePath, locations); + String hash = calculateHash(original); + return StringUtils.stripFilenameExtension(baseUrl) + + "-" + hash + "." + StringUtils.getFilenameExtension(baseUrl); + } + return baseUrl; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java new file mode 100644 index 00000000000..3569e1f30b3 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/GzipResourceResolver.java @@ -0,0 +1,148 @@ +/* + * 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; + + +/** + * 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 implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(GzipResourceResolver.class); + + + @Override + public Resource resolveResource(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain) { + + Resource resource = chain.resolveResource(request, requestPath, locations); + if ((resource == null) || !isGzipAccepted(request)) { + return resource; + } + + try { + Resource gzipped = new GzippedResource(resource); + if (gzipped.exists()) { + return gzipped; + } + } + catch (IOException e) { + logger.trace("No gzipped resource for " + resource.getFilename(), e); + } + + return resource; + } + + private boolean isGzipAccepted(HttpServletRequest request) { + String value = request.getHeader("Accept-Encoding"); + return ((value != null) && value.toLowerCase().contains("gzip")); + } + + @Override + public String resolveUrlPath(String resourcePath, List locations, ResourceResolverChain chain) { + return chain.resolveUrlPath(resourcePath, locations); + } + + + private static final class GzippedResource extends AbstractResource implements EncodedResource { + + private final Resource original; + + private final Resource gzipped; + + + public GzippedResource(Resource original) throws IOException { + this.original = original; + this.gzipped = original.createRelative(original.getFilename() + ".gz"); + } + + + public InputStream getInputStream() throws IOException { + return this.gzipped.getInputStream(); + } + + public boolean exists() { + return this.gzipped.exists(); + } + + public boolean isReadable() { + return this.gzipped.isReadable(); + } + + public boolean isOpen() { + return this.gzipped.isOpen(); + } + + public URL getURL() throws IOException { + return this.gzipped.getURL(); + } + + public URI getURI() throws IOException { + return this.gzipped.getURI(); + } + + public File getFile() throws IOException { + return this.gzipped.getFile(); + } + + public long contentLength() throws IOException { + return this.gzipped.contentLength(); + } + + public long lastModified() throws IOException { + return this.gzipped.lastModified(); + } + + public Resource createRelative(String relativePath) throws IOException { + return this.gzipped.createRelative(relativePath); + } + + public String getFilename() { + return this.original.getFilename(); + } + + public String getDescription() { + return this.gzipped.getDescription(); + } + + public String getContentEncoding() { + return "gzip"; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.java new file mode 100644 index 00000000000..2268a3fdde0 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/LessResourceTransformer.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 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 + * @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; + try { + String content = ""; + if (original instanceof TransformedResource) { + content = ((TransformedResource) original).getContentAsString(); + } + else { + content = this.compiler.compile(original.getFile()); + } + transformed = new TransformedResource(original.getFilename().replace( + "." + LESS_EXT, ""), content.getBytes("UTF-8"), original.lastModified()); + } + catch (LessException ex) { + //TODO - Nicely print out the compilation error + ex.printStackTrace(); + return null; + } + return transformed; + } + + @Override + 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/PathExtensionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathExtensionResourceResolver.java new file mode 100644 index 00000000000..f9e02e7091c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathExtensionResourceResolver.java @@ -0,0 +1,127 @@ +/* + * 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 + * @author Rossen Stoyanchev + * @since 4.0 + */ +public class PathExtensionResourceResolver implements ResourceResolver { + + private static final Log logger = LogFactory.getLog(PathExtensionResourceResolver.class); + + private final boolean compareTimeStamp; + + + public PathExtensionResourceResolver() { + this.compareTimeStamp = false; + } + + public PathExtensionResourceResolver(boolean compareTimeStamp) { + this.compareTimeStamp = compareTimeStamp; + } + + + @Override + 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(requestPath); + try { + 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 + Resource matched = basePath.createRelative(fileName); + if ((resource == null) || (matched.lastModified() > resource.lastModified())) { + return matched; + } + else { + return resource; + } + } + } + } + catch (IOException e) { + logger.trace("Error occurred locating resource based on file extension mapping", e); + } + } + + return resource; + } + + @Override + public String resolveUrlPath(String resourcePath, List locations, + ResourceResolverChain chain) { + + String resolved = chain.resolveUrlPath(resourcePath, locations); + if (StringUtils.hasText(resolved)) { + return resolved; + } + + Resource mappedResource = resolveResource(null, resourcePath, locations, chain); + if (mappedResource != null) { + return resourcePath; + } + return null; + } + + + private static final class ExtensionFilenameFilter implements FilenameFilter { + + private final String filename; + + 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 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/PathResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java new file mode 100644 index 00000000000..2a55e7e0a86 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java @@ -0,0 +1,78 @@ +/* + * 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 resolveResource(HttpServletRequest request, + String requestPath, List locations, ResourceResolverChain chain) { + + return getResource(requestPath, 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()) { + 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; + } + +} 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..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 @@ -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,21 @@ 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(); + public ResourceHttpRequestHandler() { super(METHOD_GET, METHOD_HEAD); + this.resourceResolvers.add(new PathResourceResolver()); } + /** * Set a {@code List} of {@code Resource} paths to use as sources * for serving static resources. @@ -94,6 +103,32 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H this.locations = locations; } + public List getLocations() { + return this.locations; + } + + /** + * Configure the list of {@link ResourceResolver}s to use. + *

+ * By default {@link PathResourceResolver} is configured. If using this property, it + * is recommended to add {@link PathResourceResolver} as the last resolver. + */ + public void setResourceResolvers(List resourceResolvers) { + this.resourceResolvers = resourceResolvers; + } + + public List getResourceResolvers() { + return this.resourceResolvers; + } + + 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)) { @@ -155,7 +190,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 +204,19 @@ 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); + 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 null; + return resource; } /** @@ -241,6 +268,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H if (mediaType != null) { response.setContentType(mediaType.toString()); } + + if (resource instanceof EncodedResource) { + response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding()); + } } /** @@ -298,4 +329,4 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } } -} + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java new file mode 100644 index 00000000000..bec80b02db2 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolver.java @@ -0,0 +1,63 @@ +/* + * 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; + + +/** + * 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 { + + /** + * 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); + +} 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..339082faa64 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceResolverChain.java @@ -0,0 +1,60 @@ +/* + * 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; + + +/** + * 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 { + + /** + * 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); + +} 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..b457bb568ef --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java @@ -0,0 +1,53 @@ +/* + * 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; + + +/** + * A strategy for transforming a resource. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 4.0 + */ +public interface ResourceTransformer { + + /** + * 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/ResourceUrlFilter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlFilter.java new file mode 100644 index 00000000000..0d571c6d434 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlFilter.java @@ -0,0 +1,98 @@ +/* + * 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.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +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; + + +/** + * 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 ResourceUrlFilter extends OncePerRequestFilter { + + private Set resourceUrlGenerators; + + + @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 cxt = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + Map beans = cxt.getBeansOfType(ResourceUrlGenerator.class); + this.resourceUrlGenerators = new LinkedHashSet(); + this.resourceUrlGenerators.addAll(beans.values()); + } + + + private class ResourceUrlResponseWrapper extends HttpServletResponseWrapper { + + private final UrlPathHelper pathHelper = new UrlPathHelper(); + + private String pathPrefix; + + + private ResourceUrlResponseWrapper(HttpServletRequest request, HttpServletResponse wrapped) { + super(wrapped); + 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(this.pathPrefix)) { + String relativeUrl = url.replaceFirst(this.pathPrefix, ""); + if (!relativeUrl.startsWith("/")) { + relativeUrl = "/" + relativeUrl; + } + for (ResourceUrlGenerator generator : resourceUrlGenerators) { + String resourceUrl = generator.getResourceUrl(relativeUrl); + if (resourceUrl != null) { + 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/TransformedResource.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java new file mode 100644 index 00000000000..d7bae844078 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/TransformedResource.java @@ -0,0 +1,69 @@ +/* + * 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 + * @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; + 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..fb4841ab08e --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ExtensionMappingResourceResolverTests.java @@ -0,0 +1,65 @@ +/* + * 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 PathExtensionResourceResolver()); + resolvers.add(new PathResourceResolver()); + resolver = new DefaultResourceResolverChain(resolvers); + locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); + } + + @Test + public void resolveLessResource() throws Exception { + String resourceId = "zoo.css"; + Resource resource = new ClassPathResource("test/" + resourceId + ".less", getClass()); + Resource resolved = resolver.resolveResource(null, resourceId, locations); + assertEquals(resource, resolved); + } + + @Test + public void resolveLessUrl() { + String resourceId = "zoo.css"; + String url = "zoo.css"; + 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 new file mode 100644 index 00000000000..e9c202a3a8d --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.DigestUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + + +/** + * + * @author Jeremy Grelle + */ +public class FingerprintResourceResolverTests { + + private ResourceResolverChain chain; + + private FingerprintResourceResolver resolver = new FingerprintResourceResolver(); + + private List locations; + + @Before + public void setUp() { + List resolvers = new ArrayList(); + resolvers.add(resolver); + resolvers.add(new PathResourceResolver()); + chain = new DefaultResourceResolverChain(resolvers); + locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); + } + + + @Test + public void resolveWithoutHash() throws Exception { + String file = "bar.css"; + Resource expected = new ClassPathResource("test/" + file, getClass()); + Resource actual = chain.resolveResource(null, file, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithHashNoMatch() throws Exception { + String file = "bogus-e36d2e05253c6c7085a91522ce43a0b4.css"; + assertNull(chain.resolveResource(null, file, locations)); + } + + @Test + public void resolveStaticFingerprintedResource() throws Exception { + String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; + Resource expected = new ClassPathResource("test/"+file, getClass()); + Resource actual = chain.resolveResource(null, file, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveDynamicFingerprintedResource() throws Exception { + Resource expected = new ClassPathResource("test/bar.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/bar-" + hash + ".css"; + Resource actual = chain.resolveResource(null, path, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithMultipleExtensions() throws Exception { + Resource expected = new ClassPathResource("test/bar.min.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/bar.min-" + hash + ".css"; + Resource actual = chain.resolveResource(null, path, locations); + + assertEquals(expected, actual); + } + + @Test + public void resolveWithMultipleHyphens() throws Exception { + Resource expected = new ClassPathResource("test/foo-bar/foo-bar.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "/foo-bar/foo-bar-" + hash + ".css"; + Resource actual = chain.resolveResource(null, path, locations); + + assertEquals(expected, actual); + } + + @Test + public void extractHash() throws Exception { + String hash = "7fbe76cdac6093784895bb4989203e5a"; + String path = "font-awesome/css/font-awesome.min-" + hash + ".css"; + + Method method = ReflectionUtils.findMethod(resolver.getClass(), "extractHash", String.class); + ReflectionUtils.makeAccessible(method); + String result = (String) ReflectionUtils.invokeMethod(method, resolver, path); + + assertEquals(hash, result); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java new file mode 100644 index 00000000000..61f161ba476 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.util.FileCopyUtils; + +import static org.junit.Assert.*; + + +/** + * + * @author Jeremy Grelle + */ +public class GzipResourceResolverTests { + + private ResourceResolverChain resolver; + + private List locations; + + @BeforeClass + public static void createGzippedResources() throws IOException { + Resource location = new ClassPathResource("test/", GzipResourceResolverTests.class); + Resource jsFile = new FileSystemResource(location.createRelative("/js/foo.js").getFile()); + Resource gzJsFile = jsFile.createRelative("foo.js.gz"); + Resource fingerPrintedFile = new FileSystemResource(location.createRelative("foo-e36d2e05253c6c7085a91522ce43a0b4.css").getFile()); + Resource gzFingerPrintedFile = fingerPrintedFile.createRelative("foo-e36d2e05253c6c7085a91522ce43a0b4.css.gz"); + + if (gzJsFile.getFile().createNewFile()) { + GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(gzJsFile.getFile())); + FileCopyUtils.copy(jsFile.getInputStream(), out); + } + + if (gzFingerPrintedFile.getFile().createNewFile()) { + GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(gzFingerPrintedFile.getFile())); + FileCopyUtils.copy(fingerPrintedFile.getInputStream(), out); + } + + assertTrue(gzJsFile.exists()); + assertTrue(gzFingerPrintedFile.exists()); + } + + @Before + public void setUp() { + List resolvers = new ArrayList(); + resolvers.add(new GzipResourceResolver()); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); + resolver = new DefaultResourceResolverChain(resolvers); + locations = new ArrayList(); + locations.add(new ClassPathResource("test/", getClass())); + locations.add(new ClassPathResource("testalternatepath/", getClass())); + } + + @Test + public void resolveGzippedFile() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept-Encoding", "gzip"); + String file = "js/foo.js"; + String gzFile = file+".gz"; + Resource resource = new ClassPathResource("test/"+gzFile, getClass()); + Resource resolved = resolver.resolveResource(request, file, locations); + + assertEquals(resource.getDescription(), resolved.getDescription()); + assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename()); + assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class, + resolved instanceof EncodedResource); + } + + @Test + public void resolveFingerprintedGzippedFile() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept-Encoding", "gzip"); + String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css"; + String gzFile = file+".gz"; + Resource resource = new ClassPathResource("test/"+gzFile, getClass()); + Resource resolved = resolver.resolveResource(request, file, locations); + + assertEquals(resource.getDescription(), resolved.getDescription()); + assertEquals(new ClassPathResource("test/"+file).getFilename(), resolved.getFilename()); + assertTrue("Expected " + resolved + " to be of type " + EncodedResource.class, + resolved instanceof EncodedResource); + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/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/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/ResourceUrlGeneratorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlGeneratorTests.java new file mode 100644 index 00000000000..2c3a2149229 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlGeneratorTests.java @@ -0,0 +1,100 @@ +/* + * 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.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 + * @author Rossen Stoyanchev + */ +public class ResourceUrlGeneratorTests { + + ResourceHttpRequestHandler handler; + + SimpleUrlHandlerMapping mapping; + + ResourceUrlGenerator generator; + + + @Before + public void setUp() { + 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(locations); + urlMap.put("/resources/**", handler); + + mapping = new SimpleUrlHandlerMapping(); + mapping.setUrlMap(urlMap); + } + + private void initGenerator() { + generator = new ResourceUrlGenerator(); + generator.setResourceHandlerMappings(Collections.singletonList(this.mapping)); + } + + @Test + public void getStaticResourceUrl() { + initGenerator(); + + String url = generator.getResourceUrl("/resources/foo.css"); + assertEquals("/resources/foo.css", url); + } + + @Test + public void getFingerprintedResourceUrl() { + List resolvers = new ArrayList(); + resolvers.add(new FingerprintResourceResolver()); + resolvers.add(new PathResourceResolver()); + handler.setResourceResolvers(resolvers); + initGenerator(); + + String url = generator.getResourceUrl("/resources/foo.css"); + assertEquals("/resources/foo-e36d2e05253c6c7085a91522ce43a0b4.css", url); + } + + @Test + public void getExtensionMappedResourceUrl() { + List resolvers = new ArrayList(); + resolvers.add(new PathExtensionResourceResolver()); + resolvers.add(new PathResourceResolver()); + handler.setResourceResolvers(resolvers); + initGenerator(); + + String url = generator.getResourceUrl("/resources/zoo.css"); + assertEquals("/resources/zoo.css", url); + } + +} 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 @@ + + + + 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