Extract base interface for VersionStrategy

This change splits out a base VersionPathStrategy interface that
focuses on where in the URL path the version is embedded.
This commit is contained in:
Rossen Stoyanchev
2014-07-28 15:48:21 -04:00
committed by Brian Clozel
parent 9f3c1cf762
commit d917b8a923
15 changed files with 307 additions and 346 deletions

View File

@@ -6,6 +6,7 @@ import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -20,73 +21,106 @@ import org.springframework.util.StringUtils;
* version string.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 4.1
*/
public abstract class AbstractVersionStrategy implements VersionStrategy {
private static final Pattern pattern = Pattern.compile("-(\\S*)\\.");
protected final Log logger = LogFactory.getLog(getClass());
private final VersionPathStrategy pathStrategy;
protected AbstractVersionStrategy(VersionPathStrategy pathStrategy) {
Assert.notNull(pathStrategy, "'pathStrategy' is required");
this.pathStrategy = pathStrategy;
}
public VersionPathStrategy getVersionPathStrategy() {
return this.pathStrategy;
}
@Override
public String extractVersion(String requestPath) {
return this.pathStrategy.extractVersion(requestPath);
}
@Override
public String removeVersion(String requestPath, String version) {
return this.pathStrategy.removeVersion(requestPath, version);
}
@Override
public String addVersion(String requestPath, String version) {
return this.pathStrategy.addVersion(requestPath, version);
}
/**
* 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
* A prefix-based {@code VersionPathStrategy},
* e.g. {@code "{version}/path/foo.js"}.
*/
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;
protected static class PrefixVersionPathStrategy implements VersionPathStrategy {
private final String prefix;
public PrefixVersionPathStrategy(String version) {
Assert.hasText(version, "'version' is required and must not be empty");
this.prefix = version;
}
else {
return "";
@Override
public String extractVersion(String requestPath) {
return (requestPath.startsWith(this.prefix) ? this.prefix : null);
}
}
/**
* 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;
@Override
public String removeVersion(String requestPath, String version) {
return requestPath.substring(this.prefix.length());
}
return "";
@Override
public String addVersion(String path, String version) {
return (this.prefix.endsWith("/") || path.startsWith("/") ? this.prefix + path : this.prefix + "/" + path);
}
}
/**
* Deletes the given candidate version string from the request path.
* The version string should be a prefix in the request path.
* File name-based {@code VersionPathStrategy},
* e.g. {@code "path/foo-{version}.css"}.
*/
protected String deleteVersionAsPrefix(String requestPath, String version) {
return requestPath.substring(version.length());
}
protected static class FileNameVersionPathStrategy implements VersionPathStrategy {
/**
* 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;
private static final Pattern pattern = Pattern.compile("-(\\S*)\\.");
@Override
public String extractVersion(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 null;
}
}
@Override
public String removeVersion(String requestPath, String version) {
return StringUtils.delete(requestPath, "-" + version);
}
@Override
public String addVersion(String requestPath, String version) {
String baseFilename = StringUtils.stripFilenameExtension(requestPath);
String extension = StringUtils.getFilenameExtension(requestPath);
return baseFilename + "-" + version + "." + extension;
}
}
}

View File

@@ -1,81 +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 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.
*
* <p>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<? extends Resource> 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 "";
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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 org.springframework.core.io.Resource;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils;
/**
* A {@code VersionStrategy} that calculates an Hex MD5 hashes from the content
* of the resource and appends it to the file name, e.g.
* {@code "styles/main-e36d2e05253c6c7085a91522ce43a0b4.css"}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 4.1
* @see VersionResourceResolver
*/
public class ContentVersionStrategy extends AbstractVersionStrategy {
public ContentVersionStrategy() {
super(new FileNameVersionPathStrategy());
}
@Override
public String getResourceVersion(Resource resource) {
try {
byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
return DigestUtils.md5DigestAsHex(content);
}
catch (IOException ex) {
throw new IllegalStateException("Failed to calculate hash for resource [" + resource + "]", ex);
}
}
}

View File

@@ -15,28 +15,18 @@
*/
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.
* A {@code VersionStrategy} that relies on a fixed version applied as a request
* path prefix, e.g. reduced SHA, version name, release date, etc.
*
* <p>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.
*
* <p>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.
* <p>This is useful for example when {@link ContentVersionStrategy} cannot be
* used such as when using JavaScript module loaders which are in charge of
* loading the JavaScript resources and need to know their relative paths.
*
* @author Brian Clozel
* @author Sam Brannen
* @author Rossen Stoyanchev
* @since 4.1
* @see VersionResourceResolver
*/
@@ -46,44 +36,17 @@ public class FixedVersionStrategy extends AbstractVersionStrategy {
/**
* Create a new FixedVersionStrategy with the given version string.
* @param fixedVersion the fixed version string to use
* @param version 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 + "/";
public FixedVersionStrategy(String version) {
super(new PrefixVersionPathStrategy(version));
this.version = version;
}
/**
* Create a new FixedVersionStrategy and get the version string to use by
* calling the given {@code Callable} instance.
*/
public FixedVersionStrategy(Callable<String> 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<? extends Resource> locations,
ResourceResolverChain chain) {
return addVersionAsPrefix(baseUrl, this.version);
public String getResourceVersion(Resource resource) {
return this.version;
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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;
/**
* A strategy for extracting and embedding a resource version in its URL path.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 4.1
*/
public interface VersionPathStrategy {
/**
* Extract the resource version from the request path.
* @param requestPath the request path to check
* @return the version string or {@code null} if none was found
*/
String extractVersion(String requestPath);
/**
* Remove the version from the request path. It is assumed that the given
* version was extracted via {@link #extractVersion(String)}.
* @param requestPath the request path of the resource being resolved
* @param version the version obtained from {@link #extractVersion(String)}
* @return the request path with the version removed
*/
String removeVersion(String requestPath, String version);
/**
* Add a version to the given request path.
* @param requestPath the requestPath
* @param version the version
* @return the requestPath updated with a version string
*/
String addVersion(String requestPath, String version);
}

View File

@@ -50,6 +50,7 @@ public class VersionResourceResolver extends AbstractResourceResolver {
private Map<String, VersionStrategy> versionStrategyMap = Collections.emptyMap();
/**
* Set a Map with URL paths as keys and {@code VersionStrategy}
* as values.
@@ -59,10 +60,11 @@ public class VersionResourceResolver extends AbstractResourceResolver {
*
* @param versionStrategyMap map with URLs as keys and version strategies as values
*/
public void setVersionStrategyMap(Map<String, VersionStrategy> versionStrategyMap) {
public void setStrategyMap(Map<String, VersionStrategy> versionStrategyMap) {
this.versionStrategyMap = versionStrategyMap;
}
@Override
protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
@@ -72,12 +74,12 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return resolved;
}
VersionStrategy versionStrategy = findStrategy(requestPath);
VersionStrategy versionStrategy = getStrategyForPath(requestPath);
if (versionStrategy == null) {
return null;
}
String candidateVersion = versionStrategy.extractVersionFromPath(requestPath);
String candidateVersion = versionStrategy.extractVersion(requestPath);
if (StringUtils.isEmpty(candidateVersion)) {
if (logger.isTraceEnabled()) {
logger.trace("No version found in path=\"" + requestPath + "\"");
@@ -85,7 +87,7 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return null;
}
String simplePath = versionStrategy.deleteVersionFromPath(requestPath, candidateVersion);
String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion);
if (logger.isTraceEnabled()) {
logger.trace("Extracted version from path, re-resolving without version, path=\"" + simplePath + "\"");
@@ -96,7 +98,8 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return null;
}
if (versionStrategy.resourceVersionMatches(baseResource, candidateVersion)) {
String actualVersion = versionStrategy.getResourceVersion(baseResource);
if (candidateVersion.equals(actualVersion)) {
if (logger.isTraceEnabled()) {
logger.trace("resource matches extracted version");
}
@@ -113,11 +116,19 @@ public class VersionResourceResolver extends AbstractResourceResolver {
protected String resolveUrlPathInternal(String resourceUrlPath, List<? extends Resource> locations, ResourceResolverChain chain) {
String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations);
if (StringUtils.hasText(baseUrl)) {
VersionStrategy versionStrategy = findStrategy(resourceUrlPath);
VersionStrategy versionStrategy = getStrategyForPath(resourceUrlPath);
if (versionStrategy == null) {
return null;
}
return versionStrategy.addVersionToUrl(baseUrl, locations, chain);
if (logger.isTraceEnabled()) {
logger.trace("Getting the original resource to determine version");
}
Resource resource = chain.resolveResource(null, baseUrl, locations);
String version = versionStrategy.getResourceVersion(resource);
if (logger.isTraceEnabled()) {
logger.trace("Version=" + version);
}
return versionStrategy.addVersion(baseUrl, version);
}
return baseUrl;
}
@@ -126,7 +137,7 @@ public class VersionResourceResolver extends AbstractResourceResolver {
* 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) {
protected VersionStrategy getStrategyForPath(String requestPath) {
String path = "/".concat(requestPath);
List<String> matchingPatterns = new ArrayList<String>();
for (String pattern : this.versionStrategyMap.keySet()) {

View File

@@ -15,50 +15,24 @@
*/
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}.
* An extension of {@link VersionPathStrategy} that adds a method to determine the
* actual version of a {@link org.springframework.core.io.Resource Resource}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 4.1
* @see VersionResourceResolver
*/
public interface VersionStrategy {
public interface VersionStrategy extends VersionPathStrategy {
/**
* 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
* Get the version for the given resource.
* @param resource the resource to check
* @return the version, never {@code null}
*/
String extractVersionFromPath(String requestPath);
String getResourceVersion(Resource resource);
/**
* 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<? extends Resource> locations, ResourceResolverChain chain);
}