001/*
002 * Copyright 2002-2019 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.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023import java.util.stream.Collectors;
024
025import javax.servlet.http.HttpServletRequest;
026
027import org.springframework.cache.Cache;
028import org.springframework.cache.CacheManager;
029import org.springframework.core.io.Resource;
030import org.springframework.http.HttpHeaders;
031import org.springframework.lang.Nullable;
032import org.springframework.util.Assert;
033import org.springframework.util.StringUtils;
034
035/**
036 * A {@link org.springframework.web.servlet.resource.ResourceResolver} that
037 * resolves resources from a {@link org.springframework.cache.Cache} or otherwise
038 * delegates to the resolver chain and saves the result in the cache.
039 *
040 * @author Rossen Stoyanchev
041 * @author Brian Clozel
042 * @since 4.1
043 */
044public class CachingResourceResolver extends AbstractResourceResolver {
045
046        /**
047         * The prefix used for resolved resource cache keys.
048         */
049        public static final String RESOLVED_RESOURCE_CACHE_KEY_PREFIX = "resolvedResource:";
050
051        /**
052         * The prefix used for resolved URL path cache keys.
053         */
054        public static final String RESOLVED_URL_PATH_CACHE_KEY_PREFIX = "resolvedUrlPath:";
055
056
057        private final Cache cache;
058
059        private final List<String> contentCodings = new ArrayList<>(EncodedResourceResolver.DEFAULT_CODINGS);
060
061
062        public CachingResourceResolver(Cache cache) {
063                Assert.notNull(cache, "Cache is required");
064                this.cache = cache;
065        }
066
067        public CachingResourceResolver(CacheManager cacheManager, String cacheName) {
068                Cache cache = cacheManager.getCache(cacheName);
069                if (cache == null) {
070                        throw new IllegalArgumentException("Cache '" + cacheName + "' not found");
071                }
072                this.cache = cache;
073        }
074
075
076        /**
077         * Return the configured {@code Cache}.
078         */
079        public Cache getCache() {
080                return this.cache;
081        }
082
083        /**
084         * Configure the supported content codings from the
085         * {@literal "Accept-Encoding"} header for which to cache resource variations.
086         * <p>The codings configured here are generally expected to match those
087         * configured on {@link EncodedResourceResolver#setContentCodings(List)}.
088         * <p>By default this property is set to {@literal ["br", "gzip"]} based on
089         * the value of {@link EncodedResourceResolver#DEFAULT_CODINGS}.
090         * @param codings one or more supported content codings
091         * @since 5.1
092         */
093        public void setContentCodings(List<String> codings) {
094                Assert.notEmpty(codings, "At least one content coding expected");
095                this.contentCodings.clear();
096                this.contentCodings.addAll(codings);
097        }
098
099        /**
100         * Return a read-only list with the supported content codings.
101         * @since 5.1
102         */
103        public List<String> getContentCodings() {
104                return Collections.unmodifiableList(this.contentCodings);
105        }
106
107
108        @Override
109        protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
110                        List<? extends Resource> locations, ResourceResolverChain chain) {
111
112                String key = computeKey(request, requestPath);
113                Resource resource = this.cache.get(key, Resource.class);
114
115                if (resource != null) {
116                        if (logger.isTraceEnabled()) {
117                                logger.trace("Resource resolved from cache");
118                        }
119                        return resource;
120                }
121
122                resource = chain.resolveResource(request, requestPath, locations);
123                if (resource != null) {
124                        this.cache.put(key, resource);
125                }
126
127                return resource;
128        }
129
130        protected String computeKey(@Nullable HttpServletRequest request, String requestPath) {
131                if (request != null) {
132                        String codingKey = getContentCodingKey(request);
133                        if (StringUtils.hasText(codingKey)) {
134                                return RESOLVED_RESOURCE_CACHE_KEY_PREFIX + requestPath + "+encoding=" + codingKey;
135                        }
136                }
137                return RESOLVED_RESOURCE_CACHE_KEY_PREFIX + requestPath;
138        }
139
140        @Nullable
141        private String getContentCodingKey(HttpServletRequest request) {
142                String header = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
143                if (!StringUtils.hasText(header)) {
144                        return null;
145                }
146                return Arrays.stream(StringUtils.tokenizeToStringArray(header, ","))
147                                .map(token -> {
148                                        int index = token.indexOf(';');
149                                        return (index >= 0 ? token.substring(0, index) : token).trim().toLowerCase();
150                                })
151                                .filter(this.contentCodings::contains)
152                                .sorted()
153                                .collect(Collectors.joining(","));
154        }
155
156        @Override
157        protected String resolveUrlPathInternal(String resourceUrlPath,
158                        List<? extends Resource> locations, ResourceResolverChain chain) {
159
160                String key = RESOLVED_URL_PATH_CACHE_KEY_PREFIX + resourceUrlPath;
161                String resolvedUrlPath = this.cache.get(key, String.class);
162
163                if (resolvedUrlPath != null) {
164                        if (logger.isTraceEnabled()) {
165                                logger.trace("Path resolved from cache");
166                        }
167                        return resolvedUrlPath;
168                }
169
170                resolvedUrlPath = chain.resolveUrlPath(resourceUrlPath, locations);
171                if (resolvedUrlPath != null) {
172                        this.cache.put(key, resolvedUrlPath);
173                }
174
175                return resolvedUrlPath;
176        }
177
178}