001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.resource; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.URI; 023import java.net.URL; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.Comparator; 028import java.util.LinkedHashMap; 029import java.util.List; 030import java.util.Map; 031import javax.servlet.http.HttpServletRequest; 032 033import org.springframework.core.io.AbstractResource; 034import org.springframework.core.io.Resource; 035import org.springframework.util.AntPathMatcher; 036import org.springframework.util.StringUtils; 037 038/** 039 * Resolves request paths containing a version string that can be used as part 040 * of an HTTP caching strategy in which a resource is cached with a date in the 041 * distant future (e.g. 1 year) and cached until the version, and therefore the 042 * URL, is changed. 043 * 044 * <p>Different versioning strategies exist, and this resolver must be configured 045 * with one or more such strategies along with path mappings to indicate which 046 * strategy applies to which resources. 047 * 048 * <p>{@code ContentVersionStrategy} is a good default choice except in cases 049 * where it cannot be used. Most notably the {@code ContentVersionStrategy} 050 * cannot be combined with JavaScript module loaders. For such cases the 051 * {@code FixedVersionStrategy} is a better choice. 052 * 053 * <p>Note that using this resolver to serve CSS files means that the 054 * {@link CssLinkResourceTransformer} should also be used in order to modify 055 * links within CSS files to also contain the appropriate versions generated 056 * by this resolver. 057 * 058 * @author Brian Clozel 059 * @author Rossen Stoyanchev 060 * @since 4.1 061 * @see VersionStrategy 062 */ 063public class VersionResourceResolver extends AbstractResourceResolver { 064 065 private AntPathMatcher pathMatcher = new AntPathMatcher(); 066 067 /** Map from path pattern -> VersionStrategy */ 068 private final Map<String, VersionStrategy> versionStrategyMap = new LinkedHashMap<String, VersionStrategy>(); 069 070 071 /** 072 * Set a Map with URL paths as keys and {@code VersionStrategy} as values. 073 * <p>Supports direct URL matches and Ant-style pattern matches. For syntax 074 * details, see the {@link org.springframework.util.AntPathMatcher} javadoc. 075 * @param map map with URLs as keys and version strategies as values 076 */ 077 public void setStrategyMap(Map<String, VersionStrategy> map) { 078 this.versionStrategyMap.clear(); 079 this.versionStrategyMap.putAll(map); 080 } 081 082 /** 083 * Return the map with version strategies keyed by path pattern. 084 */ 085 public Map<String, VersionStrategy> getStrategyMap() { 086 return this.versionStrategyMap; 087 } 088 089 /** 090 * Insert a content-based version in resource URLs that match the given path 091 * patterns. The version is computed from the content of the file, e.g. 092 * {@code "css/main-e36d2e05253c6c7085a91522ce43a0b4.css"}. This is a good 093 * default strategy to use except when it cannot be, for example when using 094 * JavaScript module loaders, use {@link #addFixedVersionStrategy} instead 095 * for serving JavaScript files. 096 * @param pathPatterns one or more resource URL path patterns, 097 * relative to the pattern configured with the resource handler 098 * @return the current instance for chained method invocation 099 * @see ContentVersionStrategy 100 */ 101 public VersionResourceResolver addContentVersionStrategy(String... pathPatterns) { 102 addVersionStrategy(new ContentVersionStrategy(), pathPatterns); 103 return this; 104 } 105 106 /** 107 * Insert a fixed, prefix-based version in resource URLs that match the given 108 * path patterns, for example: <code>"{version}/js/main.js"</code>. This is useful (vs. 109 * content-based versions) when using JavaScript module loaders. 110 * <p>The version may be a random number, the current date, or a value 111 * fetched from a git commit sha, a property file, or environment variable 112 * and set with SpEL expressions in the configuration (e.g. see {@code @Value} 113 * in Java config). 114 * <p>If not done already, variants of the given {@code pathPatterns}, prefixed with 115 * the {@code version} will be also configured. For example, adding a {@code "/js/**"} path pattern 116 * will also cofigure automatically a {@code "/v1.0.0/js/**"} with {@code "v1.0.0"} the 117 * {@code version} String given as an argument. 118 * @param version a version string 119 * @param pathPatterns one or more resource URL path patterns, 120 * relative to the pattern configured with the resource handler 121 * @return the current instance for chained method invocation 122 * @see FixedVersionStrategy 123 */ 124 public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) { 125 List<String> patternsList = Arrays.asList(pathPatterns); 126 List<String> prefixedPatterns = new ArrayList<String>(pathPatterns.length); 127 String versionPrefix = "/" + version; 128 for (String pattern : patternsList) { 129 prefixedPatterns.add(pattern); 130 if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) { 131 prefixedPatterns.add(versionPrefix + pattern); 132 } 133 } 134 return addVersionStrategy(new FixedVersionStrategy(version), StringUtils.toStringArray(prefixedPatterns)); 135 } 136 137 /** 138 * Register a custom VersionStrategy to apply to resource URLs that match the 139 * given path patterns. 140 * @param strategy the custom strategy 141 * @param pathPatterns one or more resource URL path patterns, 142 * relative to the pattern configured with the resource handler 143 * @return the current instance for chained method invocation 144 * @see VersionStrategy 145 */ 146 public VersionResourceResolver addVersionStrategy(VersionStrategy strategy, String... pathPatterns) { 147 for (String pattern : pathPatterns) { 148 getStrategyMap().put(pattern, strategy); 149 } 150 return this; 151 } 152 153 154 @Override 155 protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath, 156 List<? extends Resource> locations, ResourceResolverChain chain) { 157 158 Resource resolved = chain.resolveResource(request, requestPath, locations); 159 if (resolved != null) { 160 return resolved; 161 } 162 163 VersionStrategy versionStrategy = getStrategyForPath(requestPath); 164 if (versionStrategy == null) { 165 return null; 166 } 167 168 String candidateVersion = versionStrategy.extractVersion(requestPath); 169 if (StringUtils.isEmpty(candidateVersion)) { 170 if (logger.isTraceEnabled()) { 171 logger.trace("No version found in path \"" + requestPath + "\""); 172 } 173 return null; 174 } 175 176 String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion); 177 if (logger.isTraceEnabled()) { 178 logger.trace("Extracted version from path, re-resolving without version: \"" + simplePath + "\""); 179 } 180 181 Resource baseResource = chain.resolveResource(request, simplePath, locations); 182 if (baseResource == null) { 183 return null; 184 } 185 186 String actualVersion = versionStrategy.getResourceVersion(baseResource); 187 if (candidateVersion.equals(actualVersion)) { 188 if (logger.isTraceEnabled()) { 189 logger.trace("Resource matches extracted version [" + candidateVersion + "]"); 190 } 191 return new FileNameVersionedResource(baseResource, candidateVersion); 192 } 193 else { 194 if (logger.isTraceEnabled()) { 195 logger.trace("Potential resource found for \"" + requestPath + "\", but version [" + 196 candidateVersion + "] does not match"); 197 } 198 return null; 199 } 200 } 201 202 @Override 203 protected String resolveUrlPathInternal(String resourceUrlPath, List<? extends Resource> locations, ResourceResolverChain chain) { 204 String baseUrl = chain.resolveUrlPath(resourceUrlPath, locations); 205 if (StringUtils.hasText(baseUrl)) { 206 VersionStrategy versionStrategy = getStrategyForPath(resourceUrlPath); 207 if (versionStrategy == null) { 208 return baseUrl; 209 } 210 if (logger.isTraceEnabled()) { 211 logger.trace("Getting the original resource to determine version for path \"" + resourceUrlPath + "\""); 212 } 213 Resource resource = chain.resolveResource(null, baseUrl, locations); 214 String version = versionStrategy.getResourceVersion(resource); 215 if (logger.isTraceEnabled()) { 216 logger.trace("Determined version [" + version + "] for " + resource); 217 } 218 return versionStrategy.addVersion(baseUrl, version); 219 } 220 return baseUrl; 221 } 222 223 /** 224 * Find a {@code VersionStrategy} for the request path of the requested resource. 225 * @return an instance of a {@code VersionStrategy} or null if none matches that request path 226 */ 227 protected VersionStrategy getStrategyForPath(String requestPath) { 228 String path = "/".concat(requestPath); 229 List<String> matchingPatterns = new ArrayList<String>(); 230 for (String pattern : this.versionStrategyMap.keySet()) { 231 if (this.pathMatcher.match(pattern, path)) { 232 matchingPatterns.add(pattern); 233 } 234 } 235 if (!matchingPatterns.isEmpty()) { 236 Comparator<String> comparator = this.pathMatcher.getPatternComparator(path); 237 Collections.sort(matchingPatterns, comparator); 238 return this.versionStrategyMap.get(matchingPatterns.get(0)); 239 } 240 return null; 241 } 242 243 244 private class FileNameVersionedResource extends AbstractResource implements VersionedResource { 245 246 private final Resource original; 247 248 private final String version; 249 250 public FileNameVersionedResource(Resource original, String version) { 251 this.original = original; 252 this.version = version; 253 } 254 255 @Override 256 public boolean exists() { 257 return this.original.exists(); 258 } 259 260 @Override 261 public boolean isReadable() { 262 return this.original.isReadable(); 263 } 264 265 @Override 266 public boolean isOpen() { 267 return this.original.isOpen(); 268 } 269 270 @Override 271 public URL getURL() throws IOException { 272 return this.original.getURL(); 273 } 274 275 @Override 276 public URI getURI() throws IOException { 277 return this.original.getURI(); 278 } 279 280 @Override 281 public File getFile() throws IOException { 282 return this.original.getFile(); 283 } 284 285 @Override 286 public String getFilename() { 287 return this.original.getFilename(); 288 } 289 290 @Override 291 public long contentLength() throws IOException { 292 return this.original.contentLength(); 293 } 294 295 @Override 296 public long lastModified() throws IOException { 297 return this.original.lastModified(); 298 } 299 300 @Override 301 public Resource createRelative(String relativePath) throws IOException { 302 return this.original.createRelative(relativePath); 303 } 304 305 @Override 306 public String getDescription() { 307 return this.original.getDescription(); 308 } 309 310 @Override 311 public InputStream getInputStream() throws IOException { 312 return this.original.getInputStream(); 313 } 314 315 @Override 316 public String getVersion() { 317 return this.version; 318 } 319 } 320 321}