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}