diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java new file mode 100644 index 0000000000..8cb3c3b5f4 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java @@ -0,0 +1,92 @@ +package org.springframework.web.servlet.resource; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Abstract base class for {@link VersionStrategy} implementations. + * Supports versions as: + * + * + *

Note: This base class does not provide support for generating the + * version string. + * + * @author Brian Clozel + * @since 4.1 + */ +public abstract class AbstractVersionStrategy implements VersionStrategy { + + private static final Pattern pattern = Pattern.compile("-(\\S*)\\."); + + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Extracts a version string from the request path, as a suffix in the resource + * file name. + * @param requestPath the request path to extract the version string from + * @return a version string or an empty string if none was found + */ + protected String extractVersionFromFilename(String requestPath) { + Matcher matcher = pattern.matcher(requestPath); + if (matcher.find()) { + String match = matcher.group(1); + return match.contains("-") ? match.substring(match.lastIndexOf("-") + 1) : match; + } + else { + return ""; + } + } + + /** + * Deletes the given candidate version string from the request path. + * The version string should be a suffix in the resource file name. + */ + protected String deleteVersionFromFilename(String requestPath, String candidateVersion) { + return StringUtils.delete(requestPath, "-" + candidateVersion); + } + + /** + * Adds the given version string to the baseUrl, as a file name suffix. + */ + protected String addVersionToFilename(String baseUrl, String version) { + String baseFilename = StringUtils.stripFilenameExtension(baseUrl); + String extension = StringUtils.getFilenameExtension(baseUrl); + return baseFilename + "-" + version + "." + extension; + } + + /** + * Extracts a version string from the request path, as a prefix in the request path. + * @param requestPath the request path to extract the version string from + * @return a version string or an empty string if none was found + */ + protected String extractVersionAsPrefix(String requestPath, String prefix) { + if (requestPath.startsWith(prefix)) { + return prefix; + } + return ""; + } + + /** + * Deletes the given candidate version string from the request path. + * The version string should be a prefix in the request path. + */ + protected String deleteVersionAsPrefix(String requestPath, String version) { + return requestPath.substring(version.length()); + } + + /** + * Adds the given version string to the baseUrl, as a prefix in the request path. + */ + protected String addVersionAsPrefix(String baseUrl, String version) { + return version + baseUrl; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategy.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategy.java new file mode 100644 index 0000000000..778e2e03e2 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategy.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.io.IOException; +import java.util.List; + +import org.springframework.core.io.Resource; +import org.springframework.util.DigestUtils; +import org.springframework.util.FileCopyUtils; + +/** + * A {@code VersionStrategy} that handles version strings as a Hex MD5 hash in resource file names. + * + *

For example the path "styles/foo-e36d2e05253c6c7085a91522ce43a0b4.css" will + * match to "styles/foo.css" assuming the hash computed from the content of + * "foo.css" matches the hash in the path. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @author Sam Brannen + * @author Brian Clozel + * @since 4.1 + * @see VersionResourceResolver + */ +public class ContentBasedVersionStrategy extends AbstractVersionStrategy { + + @Override + public String extractVersionFromPath(String requestPath) { + return extractVersionFromFilename(requestPath); + } + + @Override + public String deleteVersionFromPath(String requestPath, String candidateVersion) { + return deleteVersionFromFilename(requestPath, candidateVersion); + } + + @Override + public boolean resourceVersionMatches(Resource baseResource, String candidateVersion) { + String resourceHash = calculateHash(baseResource); + return candidateVersion.equals(resourceHash); + } + + @Override + public String addVersionToUrl(String baseUrl, List locations, ResourceResolverChain chain) { + if (logger.isTraceEnabled()) { + logger.trace("Getting the original resource to calculate hash"); + } + Resource original = chain.resolveResource(null, baseUrl, locations); + String hash = calculateHash(original); + if (logger.isTraceEnabled()) { + logger.trace("Calculated hash=" + hash); + } + return addVersionToFilename(baseUrl, hash); + } + + 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 for resource [" + resource + "]"); + return ""; + } + } + +} 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 deleted file mode 100644 index df80870a86..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FingerprintResourceResolver.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2002-2014 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.resource; - -import java.io.IOException; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.core.io.Resource; -import org.springframework.util.DigestUtils; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StringUtils; - -/** - * A {@code ResourceResolver} that resolves request paths containing an additional - * MD5 hash in the file name. - * - *

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

The resolver first delegates to the chain so that if - * "foo-e36d2e05253c6c7085a91522ce43a0b4.css" has been written to disk (e.g. at - * build time) it is simply found. Or if the chain does not find an existing - * resource, this resolver removes the hash, attempts to find a matching resource - * with the resulting file name ("foo.css") and then compares the hash from the - * request path to the hash computed from the file content. - * - * @author Jeremy Grelle - * @author Rossen Stoyanchev - * @author Sam Brannen - * @since 4.1 - */ -public class FingerprintResourceResolver extends AbstractResourceResolver { - - private static final Pattern pattern = Pattern.compile("-(\\S*)\\."); - - - @Override - protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath, - List locations, ResourceResolverChain chain) { - - Resource resolved = chain.resolveResource(request, requestPath, locations); - if (resolved != null) { - return resolved; - } - - String hash = extractHash(requestPath); - if (StringUtils.isEmpty(hash)) { - if (logger.isTraceEnabled()) { - logger.trace("No hash found"); - } - return null; - } - - String simplePath = StringUtils.delete(requestPath, "-" + hash); - - if (logger.isTraceEnabled()) { - logger.trace("Extracted hash from path, re-resolving without hash, path=\"" + simplePath + "\""); - } - - Resource baseResource = chain.resolveResource(request, simplePath, locations); - if (baseResource == null) { - return null; - } - - String candidateHash = calculateHash(baseResource); - if (candidateHash.equals(hash)) { - if (logger.isTraceEnabled()) { - logger.trace("Calculated hash matches extracted hash"); - } - return baseResource; - } - else { - logger.trace("Potential resource found for [" + requestPath + "], but fingerprint doesn't match."); - return null; - } - } - - @Override - protected String resolveUrlPathInternal(String resourceUrlPath, List locations, - ResourceResolverChain chain) { - - String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); - if (StringUtils.hasText(baseUrl)) { - if (logger.isTraceEnabled()) { - logger.trace("Getting the original resource to calculate hash"); - } - Resource original = chain.resolveResource(null, resourceUrlPath, locations); - String hash = calculateHash(original); - if (logger.isTraceEnabled()) { - logger.trace("Calculated hash=" + hash); - } - String baseFilename = StringUtils.stripFilenameExtension(baseUrl); - String extension = StringUtils.getFilenameExtension(baseUrl); - return baseFilename + "-" + hash + "." + extension; - } - return baseUrl; - } - - - private String extractHash(String path) { - Matcher matcher = pattern.matcher(path); - if (matcher.find()) { - String match = matcher.group(1); - return match.contains("-") ? match.substring(match.lastIndexOf("-") + 1) : match; - } - else { - return ""; - } - } - - private String calculateHash(Resource resource) { - try { - byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream()); - return DigestUtils.md5DigestAsHex(content); - } - catch (IOException e) { - logger.error("Failed to calculate hash for resource [" + resource + "]"); - return ""; - } - } - -} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FixedVersionStrategy.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FixedVersionStrategy.java new file mode 100644 index 0000000000..9c8d9da229 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/FixedVersionStrategy.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.util.List; +import java.util.concurrent.Callable; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * A {@code VersionStrategy} that handles unique, static, application-wide version strings + * as prefixes in the request path. + * + *

Enables inserting a unique and static version String (e.g. reduced SHA, version name, + * release date) at the beginning of resource paths so that when a new version of the application + * is released, clients are forced to reload application resources. + * + *

This is useful when changing resource names is not an option (e.g. when + * using JavaScript module loaders). If that's not the case, the use of + * {@link ContentBasedVersionStrategy} provides more optimal performance since + * version is generated on a per-resource basis: only actually modified resources are reloaded + * by the client. + * + * @author Brian Clozel + * @author Sam Brannen + * @since 4.1 + * @see VersionResourceResolver + */ +public class FixedVersionStrategy extends AbstractVersionStrategy { + + private final String version; + + /** + * Create a new FixedVersionStrategy with the given version string. + * @param fixedVersion the fixed version string to use + */ + public FixedVersionStrategy(String fixedVersion) { + Assert.hasText(fixedVersion, "version must not be null or empty"); + this.version = fixedVersion.endsWith("/") ? fixedVersion : fixedVersion + "/"; + } + + /** + * Create a new FixedVersionStrategy and get the version string to use by + * calling the given {@code Callable} instance. + */ + public FixedVersionStrategy(Callable versionInitializer) throws Exception { + String fixedVersion = versionInitializer.call(); + Assert.hasText(fixedVersion, "version must not be null or empty"); + this.version = fixedVersion.endsWith("/") ? fixedVersion : fixedVersion + "/"; + } + + @Override + public String extractVersionFromPath(String requestPath) { + + return extractVersionAsPrefix(requestPath, this.version); + } + + @Override + public String deleteVersionFromPath(String requestPath, String candidateVersion) { + return deleteVersionAsPrefix(requestPath, candidateVersion); + } + + @Override + public boolean resourceVersionMatches(Resource baseResource, String candidateVersion) { + return this.version.equals(candidateVersion); + } + + @Override + public String addVersionToUrl(String baseUrl, List locations, + ResourceResolverChain chain) { + + return addVersionAsPrefix(baseUrl, this.version); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java deleted file mode 100644 index 1637eb6f14..0000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PrefixResourceResolver.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2014 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.resource; - -import org.springframework.core.io.Resource; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -import javax.servlet.http.HttpServletRequest; -import java.util.List; - -/** - * A {@code ResourceResolver} configured with a prefix to be removed from the - * request path before delegating to the chain to find a matching resource. - * - *

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

This is useful when changing resource names is not an option (e.g. when - * using JavaScript module loaders). If that's not the case, the use of - * {@link FingerprintResourceResolver} provides more optimal performance since - * it causes only actually modified resources to be reloaded. - * - * @author Brian Clozel - * @author Sam Brannen - * @since 4.1 - */ -public class PrefixResourceResolver extends AbstractResourceResolver { - - private final String prefix; - - public PrefixResourceResolver(String prefix) { - Assert.hasText(prefix, "prefix must not be null or empty"); - this.prefix = prefix.startsWith("/") ? prefix.substring(1) : prefix; - } - - @Override - protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath, - List locations, ResourceResolverChain chain) { - - if (requestPath.startsWith(this.prefix)) { - requestPath = requestPath.substring(this.prefix.length()); - } - - return chain.resolveResource(request, requestPath, locations); - } - - @Override - protected String resolveUrlPathInternal(String resourceUrlPath, List locations, - ResourceResolverChain chain) { - - String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); - if (StringUtils.hasText(baseUrl)) { - return this.prefix + (baseUrl.startsWith("/") ? baseUrl : "/" + baseUrl); - } - return baseUrl; - } - -} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java new file mode 100644 index 0000000000..212ef71278 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.util.*; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.core.io.Resource; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StringUtils; + +/** + * A {@code ResourceResolver} that resolves request paths containing a version + * string, i.e. version information about the resource being requested. + * This resolver can be useful to set up HTTP caching strategies by changing + * resources' URLs as they are updated. + * + *

Because resource versioning depends on the resource types, this {@code ResourceResolver} + * needs to be configured with at least one {@link VersionStrategy}. The process of matching + * and generating version strings is delegated to the {@code VersionStrategy}. + * + *

When resolving resources, this resolver will first delegate to the chain to locate + * an existing resource and then attempt to extract a version string from the request path + * and then find a resource that matches that version. + * + *

When resolving URLs, this resolver will, if necessary, add a version string in the + * request path. + * + * @author Brian Clozel + * @since 4.1 + * @see VersionStrategy + */ +public class VersionResourceResolver extends AbstractResourceResolver { + + private AntPathMatcher pathMatcher = new AntPathMatcher(); + + private Map versionStrategyMap = Collections.emptyMap(); + + /** + * Set a Map with URL paths as keys and {@code VersionStrategy} + * as values. + * + *

Supports direct URL matches and Ant-style pattern matches. For syntax + * details, see the {@link org.springframework.util.AntPathMatcher} javadoc. + * + * @param versionStrategyMap map with URLs as keys and version strategies as values + */ + public void setVersionStrategyMap(Map versionStrategyMap) { + this.versionStrategyMap = versionStrategyMap; + } + + @Override + protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath, + List locations, ResourceResolverChain chain) { + + Resource resolved = chain.resolveResource(request, requestPath, locations); + if (resolved != null) { + return resolved; + } + + VersionStrategy versionStrategy = findStrategy(requestPath); + if(versionStrategy == null) { + return null; + } + + String candidateVersion = versionStrategy.extractVersionFromPath(requestPath); + if (StringUtils.isEmpty(candidateVersion)) { + if (logger.isTraceEnabled()) { + logger.trace("No version found in path=\"" + requestPath + "\""); + } + return null; + } + + String simplePath = versionStrategy.deleteVersionFromPath(requestPath, candidateVersion); + + if (logger.isTraceEnabled()) { + logger.trace("Extracted version from path, re-resolving without version, path=\"" + simplePath + "\""); + } + + Resource baseResource = chain.resolveResource(request, simplePath, locations); + if (baseResource == null) { + return null; + } + + if (versionStrategy.resourceVersionMatches(baseResource, candidateVersion)) { + if (logger.isTraceEnabled()) { + logger.trace("resource matches extracted version"); + } + return baseResource; + } + else { + logger.trace("Potential resource found for [" + requestPath + "], but version [" + + candidateVersion + "] doesn't match."); + return null; + } + } + + @Override + protected String resolveUrlPathInternal(String resourceUrlPath, List locations, ResourceResolverChain chain) { + String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); + if (StringUtils.hasText(baseUrl)) { + VersionStrategy versionStrategy = findStrategy(resourceUrlPath); + if(versionStrategy == null) { + return null; + } + return versionStrategy.addVersionToUrl(baseUrl, locations, chain); + } + return baseUrl; + } + + /** + * Finds a {@code VersionStrategy} for the request path of the requested resource. + * @return an instance of a {@code VersionStrategy} or null if none matches that request path + */ + protected VersionStrategy findStrategy(String requestPath) { + String path = "/".concat(requestPath); + List matchingPatterns = new ArrayList(); + for(String pattern : this.versionStrategyMap.keySet()) { + if(this.pathMatcher.match(pattern, path)) { + matchingPatterns.add(pattern); + } + } + if(!matchingPatterns.isEmpty()) { + Comparator comparator = this.pathMatcher.getPatternComparator(path); + Collections.sort(matchingPatterns, comparator); + return this.versionStrategyMap.get(matchingPatterns.get(0)); + } + + return null; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionStrategy.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionStrategy.java new file mode 100644 index 0000000000..5385e036e0 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionStrategy.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.util.List; + +import org.springframework.core.io.Resource; + +/** + * A strategy for handling version strings in request paths when resolving + * static resources with a {@link VersionResourceResolver}. + * + * @author Brian Clozel + * @since 4.1 + * @see VersionResourceResolver +*/ +public interface VersionStrategy { + + /** + * Extracts a version string from the request path. + * @param requestPath the request path of the resource being resolved + * @return the version string or an empty string if none was found + */ + String extractVersionFromPath(String requestPath); + + /** + * Deletes the given candidate version string from the given request path. + * @param requestPath the request path of the resource being resolved + * @param candidateVersion the candidate version string + * @return the modified request path, without the version string + */ + String deleteVersionFromPath(String requestPath, String candidateVersion); + + /** + * Checks whether the given {@code Resource} matches the candidate version string. + * Useful when the version string is managed on a per-resource basis. + * @param baseResource the resource to check against the given version + * @param candidateVersion the candidate version for the given resource + * @return true if the resource matches the version string, false otherwise + */ + boolean resourceVersionMatches(Resource baseResource, String candidateVersion); + + /** + * Adds a version string to the given baseUrl. + * @param baseUrl the baseUrl of the requested resource + * @param locations the resource locations to resolve resources from + * @param chain the chain of resource resolvers + * @return the baseUrl updated with a version string + */ + String addVersionToUrl(String baseUrl, List locations, ResourceResolverChain chain); +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java new file mode 100644 index 0000000000..9d2b0fc14d --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +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 static org.junit.Assert.*; + +/** + * Unit tests for {@link org.springframework.web.servlet.resource.ContentBasedVersionStrategy} + * @author Brian Clozel + */ +public class ContentBasedVersionStrategyTests { + + private List locations; + + private ContentBasedVersionStrategy versionStrategy = new ContentBasedVersionStrategy(); + + private ResourceResolverChain chain; + + @Before + public void setup() { + this.locations = new ArrayList(); + this.locations.add(new ClassPathResource("test/", getClass())); + this.locations.add(new ClassPathResource("testalternatepath/", getClass())); + + VersionResourceResolver versionResourceResolver = new VersionResourceResolver(); + versionResourceResolver.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); + this.chain = new DefaultResourceResolverChain(Arrays.asList(versionResourceResolver, new PathResourceResolver())); + } + + @Test + public void extractVersionFromPath() throws Exception { + String hash = "7fbe76cdac6093784895bb4989203e5a"; + String path = "font-awesome/css/font-awesome.min-" + hash + ".css"; + + assertEquals(hash, this.versionStrategy.extractVersionFromPath(path)); + assertEquals("", this.versionStrategy.extractVersionFromPath("foo/bar.css")); + } + + @Test + public void deleteVersionFromPath() throws Exception { + String file = "font-awesome/css/font-awesome.min%s%s.css"; + String hash = "7fbe76cdac6093784895bb4989203e5a"; + + assertEquals(String.format(file, "", ""), this.versionStrategy.deleteVersionFromPath(String.format(file, "-", hash), hash)); + assertEquals("", this.versionStrategy.extractVersionFromPath("foo/bar.css")); + } + + @Test + public void resourceVersionMatches() throws Exception { + Resource expected = new ClassPathResource("test/bar.css", getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String wrongHash = "wronghash"; + + assertTrue(this.versionStrategy.resourceVersionMatches(expected, hash)); + assertFalse(this.versionStrategy.resourceVersionMatches(expected, wrongHash)); + } + + @Test + public void addVersionToUrl() throws Exception { + String file = "bar.css"; + Resource expected = new ClassPathResource("test/" + file, getClass()); + String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); + String path = "bar-" + hash + ".css"; + String resultUrl = this.versionStrategy.addVersionToUrl(file, this.locations, this.chain); + + assertEquals(path, resultUrl); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java index a37c7e52da..c4d391098b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java @@ -18,7 +18,9 @@ package org.springframework.web.servlet.resource; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -47,8 +49,13 @@ public class CssLinkResourceTransformerTests { @Before public void setUp() { + Map versionStrategyMap = new HashMap<>(); + versionStrategyMap.put("/**", new ContentBasedVersionStrategy()); + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setVersionStrategyMap(versionStrategyMap); + List resolvers = new ArrayList(); - resolvers.add(new FingerprintResourceResolver()); + resolvers.add(versionResolver); resolvers.add(new PathResourceResolver()); ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers); 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 deleted file mode 100644 index 668fbfe468..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FingerprintResourceResolverTests.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2002-2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.resource; - -import java.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()); - this.chain = new DefaultResourceResolverChain(resolvers); - - this.locations = new ArrayList(); - this.locations.add(new ClassPathResource("test/", getClass())); - this.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/FixedVersionStrategyTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FixedVersionStrategyTests.java new file mode 100644 index 0000000000..b3cab06411 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/FixedVersionStrategyTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.util.Collections; +import java.util.concurrent.Callable; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +import org.springframework.core.io.Resource; + +/** + * Unit tests for {@link org.springframework.web.servlet.resource.FixedVersionStrategy} + * @author Brian Clozel + */ +public class FixedVersionStrategyTests { + + private final String version = "1df341f"; + + private final String resourceId = "js/foo.js"; + + private FixedVersionStrategy versionStrategy; + + @Before + public void setup() { + this.versionStrategy = new FixedVersionStrategy(this.version); + } + + @Test(expected = IllegalArgumentException.class) + public void constructWithEmptyPrefixVersion() throws Exception { + + FixedVersionStrategy versionStrategy = new FixedVersionStrategy(" "); + } + + @Test(expected = IllegalArgumentException.class) + public void constructWithEmptyCallableVersion() throws Exception { + + FixedVersionStrategy versionStrategy = new FixedVersionStrategy( + new Callable() { + @Override + public String call() throws Exception { + return " "; + } + }); + } + + @Test + public void extractVersionFromPath() throws Exception { + assertEquals(this.version + "/", this.versionStrategy.extractVersionFromPath(this.version + "/" + this.resourceId)); + assertEquals("", this.versionStrategy.extractVersionFromPath(this.resourceId)); + } + + @Test + public void deleteVersionFromPath() throws Exception { + assertEquals(this.resourceId, + this.versionStrategy.deleteVersionFromPath(this.version + "/" + this.resourceId, this.version + "/")); + } + + @Test + public void addVersionToUrl() throws Exception { + assertEquals(this.version + "/" + this.resourceId, + this.versionStrategy.addVersionToUrl(this.resourceId, Collections.emptyList(), null)); + + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java index 61f161ba47..40a9ad18bf 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/GzipResourceResolverTests.java @@ -19,7 +19,9 @@ package org.springframework.web.servlet.resource; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.zip.GZIPOutputStream; import org.junit.Before; @@ -68,9 +70,14 @@ public class GzipResourceResolverTests { @Before public void setUp() { + Map versionStrategyMap = new HashMap<>(); + versionStrategyMap.put("/**", new ContentBasedVersionStrategy()); + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setVersionStrategyMap(versionStrategyMap); + List resolvers = new ArrayList(); resolvers.add(new GzipResourceResolver()); - resolvers.add(new FingerprintResourceResolver()); + resolvers.add(versionResolver); resolvers.add(new PathResourceResolver()); resolver = new DefaultResourceResolverChain(resolvers); locations = new ArrayList(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java deleted file mode 100644 index 78924531a4..0000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PrefixResourceResolverTests.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2002-2014 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.resource; - -import java.util.ArrayList; -import java.util.Arrays; -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.*; - -/** - * Test fixture for {@link PrefixResourceResolver} - * - * @author Brian Clozel - * @author Sam Brannen - */ -public class PrefixResourceResolverTests { - - private final List locations = Arrays.asList(new ClassPathResource("test/", getClass())); - - private final String shaPrefix = "1df341f"; - - private ResourceResolverChain chain; - - - @Before - public void setUp() { - List resolvers = new ArrayList(); - resolvers.add(new PrefixResourceResolver(this.shaPrefix)); - resolvers.add(new PathResourceResolver()); - this.chain = new DefaultResourceResolverChain(resolvers); - } - - @Test - public void resolveResource() { - String resourceId = "foo.css"; - Resource expected = new ClassPathResource("test/foo.css", getClass()); - Resource actual = this.chain.resolveResource(null, this.shaPrefix + "/" + resourceId, this.locations); - assertEquals(expected, actual); - } - - @Test - public void resolveUrlPath() { - String resourceId = "/foo.css"; - String url = this.shaPrefix + resourceId; - assertEquals(url, chain.resolveUrlPath(resourceId, locations)); - } - - @Test(expected = IllegalArgumentException.class) - public void constructWithEmptyPrefix() { - new PrefixResourceResolver(" "); - } - -} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java index 32959ce18c..48749ff3eb 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderJavaConfigTests.java @@ -32,6 +32,9 @@ import org.springframework.web.servlet.config.annotation.*; import static org.junit.Assert.*; +import java.util.HashMap; +import java.util.Map; + /** * Integration tests using {@link ResourceUrlEncodingFilter} and @@ -111,9 +114,14 @@ public class ResourceUrlProviderJavaConfigTests { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { + Map versionStrategyMap = new HashMap<>(); + versionStrategyMap.put("/**", new ContentBasedVersionStrategy()); + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setVersionStrategyMap(versionStrategyMap); + registry.addResourceHandler("/resources/**") .addResourceLocations("classpath:org/springframework/web/servlet/resource/test/") - .setResourceResolvers(new FingerprintResourceResolver(), new PathResourceResolver()); + .setResourceResolvers(versionResolver, new PathResourceResolver()); } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderTests.java index fa35ed33f1..12e4ced623 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceUrlProviderTests.java @@ -67,8 +67,13 @@ public class ResourceUrlProviderTests { @Test public void getFingerprintedResourceUrl() { + Map versionStrategyMap = new HashMap<>(); + versionStrategyMap.put("/**", new ContentBasedVersionStrategy()); + VersionResourceResolver versionResolver = new VersionResourceResolver(); + versionResolver.setVersionStrategyMap(versionStrategyMap); + List resolvers = new ArrayList(); - resolvers.add(new FingerprintResourceResolver()); + resolvers.add(versionResolver); resolvers.add(new PathResourceResolver()); this.handler.setResourceResolvers(resolvers); initTranslator(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java new file mode 100644 index 0000000000..cbeddb0e0f --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/VersionResourceResolverTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.resource; + +import java.util.ArrayList; +import java.util.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 static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link VersionResourceResolver} + * + * @author Brian Clozel + * @author Sam Brannen + */ +public class VersionResourceResolverTests { + + private List locations; + + private VersionResourceResolver versionResourceResolver; + + private ResourceResolverChain chain; + + private VersionStrategy versionStrategy; + + + @Before + public void setup() { + this.locations = new ArrayList(); + this.locations.add(new ClassPathResource("test/", getClass())); + this.locations.add(new ClassPathResource("testalternatepath/", getClass())); + + this.versionResourceResolver = new VersionResourceResolver(); + this.chain = mock(ResourceResolverChain.class); + this.versionStrategy = mock(VersionStrategy.class); + } + + @Test + public void resolveResourceInternalExisting() throws Exception { + String file = "bar.css"; + Resource expected = new ClassPathResource("test/" + file, getClass()); + when(this.chain.resolveResource(null, file, this.locations)).thenReturn(expected); + + this.versionResourceResolver + .setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); + Resource actual = this.versionResourceResolver.resolveResourceInternal(null, file, this.locations, this.chain); + assertEquals(expected, actual); + verify(this.chain, times(1)).resolveResource(null, file, this.locations); + verify(this.versionStrategy, never()).extractVersionFromPath(file); + } + + @Test + public void resolveResourceInternalNoStrategy() throws Exception { + String file = "missing.css"; + when(this.chain.resolveResource(null, file, this.locations)).thenReturn(null); + + this.versionResourceResolver.setVersionStrategyMap(Collections.emptyMap()); + Resource actual = this.versionResourceResolver.resolveResourceInternal(null, file, this.locations, this.chain); + assertNull(actual); + verify(this.chain, times(1)).resolveResource(null, file, this.locations); + } + + @Test + public void resolveResourceInternalNoCandidateVersion() throws Exception { + + String file = "bar.css"; + when(this.chain.resolveResource(null, file, this.locations)).thenReturn(null); + when(this.versionStrategy.extractVersionFromPath(file)).thenReturn(""); + + this.versionResourceResolver + .setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); + Resource actual = this.versionResourceResolver.resolveResourceInternal(null, file, this.locations, this.chain); + assertNull(actual); + verify(this.chain, times(1)).resolveResource(null, file, this.locations); + verify(this.versionStrategy, times(1)).extractVersionFromPath(file); + } + + @Test + public void resolveResourceInternalNoBaseResource() throws Exception { + + String versionFile = "bar-version.css"; + String version = "version"; + String file = "bar.css"; + when(this.chain.resolveResource(null, versionFile, this.locations)).thenReturn(null); + when(this.chain.resolveResource(null, file, this.locations)).thenReturn(null); + when(this.versionStrategy.extractVersionFromPath(versionFile)).thenReturn(version); + when(this.versionStrategy.deleteVersionFromPath(versionFile, version)).thenReturn(file); + + this.versionResourceResolver + .setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); + Resource actual = this.versionResourceResolver.resolveResourceInternal(null, versionFile, this.locations, this.chain); + assertNull(actual); + verify(this.versionStrategy, times(1)).deleteVersionFromPath(versionFile, version); + } + + @Test + public void resolveResourceInternalNoMatch() throws Exception { + + String versionFile = "bar-version.css"; + String version = "version"; + String file = "bar.css"; + Resource expected = new ClassPathResource("test/" + file, getClass()); + when(this.chain.resolveResource(null, versionFile, this.locations)).thenReturn(null); + when(this.chain.resolveResource(null, file, this.locations)).thenReturn(expected); + when(this.versionStrategy.extractVersionFromPath(versionFile)).thenReturn(version); + when(this.versionStrategy.deleteVersionFromPath(versionFile, version)).thenReturn(file); + when(this.versionStrategy.resourceVersionMatches(expected, version)).thenReturn(false); + + this.versionResourceResolver + .setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); + Resource actual = this.versionResourceResolver.resolveResourceInternal(null, versionFile, this.locations, this.chain); + assertNull(actual); + verify(this.versionStrategy, times(1)).resourceVersionMatches(expected, version); + } + + @Test + public void resolveResourceInternalMatch() throws Exception { + + String versionFile = "bar-version.css"; + String version = "version"; + String file = "bar.css"; + Resource expected = new ClassPathResource("test/" + file, getClass()); + when(this.chain.resolveResource(null, versionFile, this.locations)).thenReturn(null); + when(this.chain.resolveResource(null, file, this.locations)).thenReturn(expected); + when(this.versionStrategy.extractVersionFromPath(versionFile)).thenReturn(version); + when(this.versionStrategy.deleteVersionFromPath(versionFile, version)).thenReturn(file); + when(this.versionStrategy.resourceVersionMatches(expected, version)).thenReturn(true); + + this.versionResourceResolver + .setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); + Resource actual = this.versionResourceResolver.resolveResourceInternal(null, versionFile, this.locations, this.chain); + assertEquals(expected, actual); + verify(this.versionStrategy, times(1)).resourceVersionMatches(expected, version); + } + + @Test + public void findStrategy() throws Exception { + Map strategies = new HashMap<>(); + VersionStrategy jsStrategy = mock(VersionStrategy.class); + VersionStrategy catchAllStrategy = mock(VersionStrategy.class); + strategies.put("/**", catchAllStrategy); + strategies.put("/**/*.js", jsStrategy); + this.versionResourceResolver.setVersionStrategyMap(strategies); + + assertEquals(catchAllStrategy, this.versionResourceResolver.findStrategy("foo.css")); + assertEquals(catchAllStrategy, this.versionResourceResolver.findStrategy("foo-js.css")); + assertEquals(jsStrategy, this.versionResourceResolver.findStrategy("foo.js")); + assertEquals(jsStrategy, this.versionResourceResolver.findStrategy("bar/foo.js")); + } + +}