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}