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