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}