001/* 002 * Copyright 2002-2020 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.Collections; 021import java.util.Comparator; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025 026import javax.servlet.http.HttpServletRequest; 027 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030 031import org.springframework.context.ApplicationContext; 032import org.springframework.context.ApplicationListener; 033import org.springframework.context.event.ContextRefreshedEvent; 034import org.springframework.core.annotation.AnnotationAwareOrderComparator; 035import org.springframework.util.AntPathMatcher; 036import org.springframework.util.PathMatcher; 037import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; 038import org.springframework.web.util.UrlPathHelper; 039 040/** 041 * A central component to use to obtain the public URL path that clients should 042 * use to access a static resource. 043 * 044 * <p>This class is aware of Spring MVC handler mappings used to serve static 045 * resources and uses the {@code ResourceResolver} chains of the configured 046 * {@code ResourceHttpRequestHandler}s to make its decisions. 047 * 048 * @author Rossen Stoyanchev 049 * @since 4.1 050 */ 051public class ResourceUrlProvider implements ApplicationListener<ContextRefreshedEvent> { 052 053 protected final Log logger = LogFactory.getLog(getClass()); 054 055 private UrlPathHelper urlPathHelper = UrlPathHelper.defaultInstance; 056 057 private PathMatcher pathMatcher = new AntPathMatcher(); 058 059 private final Map<String, ResourceHttpRequestHandler> handlerMap = new LinkedHashMap<String, ResourceHttpRequestHandler>(); 060 061 private boolean autodetect = true; 062 063 064 /** 065 * Configure a {@code UrlPathHelper} to use in 066 * {@link #getForRequestUrl(javax.servlet.http.HttpServletRequest, String)} 067 * in order to derive the lookup path for a target request URL path. 068 */ 069 public void setUrlPathHelper(UrlPathHelper urlPathHelper) { 070 this.urlPathHelper = urlPathHelper; 071 } 072 073 /** 074 * Return the configured {@code UrlPathHelper}. 075 * @since 4.2.8 076 */ 077 public UrlPathHelper getUrlPathHelper() { 078 return this.urlPathHelper; 079 } 080 081 /** 082 * @deprecated as of Spring 4.2.8, in favor of {@link #getUrlPathHelper} 083 */ 084 @Deprecated 085 public UrlPathHelper getPathHelper() { 086 return this.urlPathHelper; 087 } 088 089 /** 090 * Configure a {@code PathMatcher} to use when comparing target lookup path 091 * against resource mappings. 092 */ 093 public void setPathMatcher(PathMatcher pathMatcher) { 094 this.pathMatcher = pathMatcher; 095 } 096 097 /** 098 * Return the configured {@code PathMatcher}. 099 */ 100 public PathMatcher getPathMatcher() { 101 return this.pathMatcher; 102 } 103 104 /** 105 * Manually configure the resource mappings. 106 * <p><strong>Note:</strong> by default resource mappings are auto-detected 107 * from the Spring {@code ApplicationContext}. However if this property is 108 * used, the auto-detection is turned off. 109 */ 110 public void setHandlerMap(Map<String, ResourceHttpRequestHandler> handlerMap) { 111 if (handlerMap != null) { 112 this.handlerMap.clear(); 113 this.handlerMap.putAll(handlerMap); 114 this.autodetect = false; 115 } 116 } 117 118 /** 119 * Return the resource mappings, either manually configured or auto-detected 120 * when the Spring {@code ApplicationContext} is refreshed. 121 */ 122 public Map<String, ResourceHttpRequestHandler> getHandlerMap() { 123 return this.handlerMap; 124 } 125 126 /** 127 * Return {@code false} if resource mappings were manually configured, 128 * {@code true} otherwise. 129 */ 130 public boolean isAutodetect() { 131 return this.autodetect; 132 } 133 134 @Override 135 public void onApplicationEvent(ContextRefreshedEvent event) { 136 if (isAutodetect()) { 137 this.handlerMap.clear(); 138 detectResourceHandlers(event.getApplicationContext()); 139 if (this.handlerMap.isEmpty() && logger.isDebugEnabled()) { 140 logger.debug("No resource handling mappings found"); 141 } 142 if (!this.handlerMap.isEmpty()) { 143 this.autodetect = false; 144 } 145 } 146 } 147 148 149 protected void detectResourceHandlers(ApplicationContext appContext) { 150 logger.debug("Looking for resource handler mappings"); 151 152 Map<String, SimpleUrlHandlerMapping> beans = appContext.getBeansOfType(SimpleUrlHandlerMapping.class); 153 List<SimpleUrlHandlerMapping> mappings = new ArrayList<SimpleUrlHandlerMapping>(beans.values()); 154 AnnotationAwareOrderComparator.sort(mappings); 155 156 for (SimpleUrlHandlerMapping mapping : mappings) { 157 for (String pattern : mapping.getHandlerMap().keySet()) { 158 Object handler = mapping.getHandlerMap().get(pattern); 159 if (handler instanceof ResourceHttpRequestHandler) { 160 ResourceHttpRequestHandler resourceHandler = (ResourceHttpRequestHandler) handler; 161 if (logger.isDebugEnabled()) { 162 logger.debug("Found resource handler mapping: URL pattern=\"" + pattern + "\", " + 163 "locations=" + resourceHandler.getLocations() + ", " + 164 "resolvers=" + resourceHandler.getResourceResolvers()); 165 } 166 this.handlerMap.put(pattern, resourceHandler); 167 } 168 } 169 } 170 } 171 172 /** 173 * A variation on {@link #getForLookupPath(String)} that accepts a full request 174 * URL path (i.e. including context and servlet path) and returns the full request 175 * URL path to expose for public use. 176 * @param request the current request 177 * @param requestUrl the request URL path to resolve 178 * @return the resolved public URL path, or {@code null} if unresolved 179 */ 180 public final String getForRequestUrl(HttpServletRequest request, String requestUrl) { 181 if (logger.isTraceEnabled()) { 182 logger.trace("Getting resource URL for request URL \"" + requestUrl + "\""); 183 } 184 int prefixIndex = getLookupPathIndex(request); 185 int suffixIndex = getEndPathIndex(requestUrl); 186 if (prefixIndex >= suffixIndex) { 187 return null; 188 } 189 String prefix = requestUrl.substring(0, prefixIndex); 190 String suffix = requestUrl.substring(suffixIndex); 191 String lookupPath = requestUrl.substring(prefixIndex, suffixIndex); 192 String resolvedLookupPath = getForLookupPath(lookupPath); 193 return (resolvedLookupPath != null ? prefix + resolvedLookupPath + suffix : null); 194 } 195 196 private int getLookupPathIndex(HttpServletRequest request) { 197 UrlPathHelper pathHelper = getUrlPathHelper(); 198 String requestUri = pathHelper.getRequestUri(request); 199 String lookupPath = pathHelper.getLookupPathForRequest(request); 200 return requestUri.indexOf(lookupPath); 201 } 202 203 private int getEndPathIndex(String lookupPath) { 204 int suffixIndex = lookupPath.length(); 205 int queryIndex = lookupPath.indexOf('?'); 206 if (queryIndex > 0) { 207 suffixIndex = queryIndex; 208 } 209 int hashIndex = lookupPath.indexOf('#'); 210 if (hashIndex > 0) { 211 suffixIndex = Math.min(suffixIndex, hashIndex); 212 } 213 return suffixIndex; 214 } 215 216 /** 217 * Compare the given path against configured resource handler mappings and 218 * if a match is found use the {@code ResourceResolver} chain of the matched 219 * {@code ResourceHttpRequestHandler} to resolve the URL path to expose for 220 * public use. 221 * <p>It is expected that the given path is what Spring MVC would use for 222 * request mapping purposes, i.e. excluding context and servlet path portions. 223 * <p>If several handler mappings match, the handler used will be the one 224 * configured with the most specific pattern. 225 * @param lookupPath the lookup path to check 226 * @return the resolved public URL path, or {@code null} if unresolved 227 */ 228 public final String getForLookupPath(String lookupPath) { 229 if (logger.isTraceEnabled()) { 230 logger.trace("Getting resource URL for lookup path \"" + lookupPath + "\""); 231 } 232 233 List<String> matchingPatterns = new ArrayList<String>(); 234 for (String pattern : this.handlerMap.keySet()) { 235 if (getPathMatcher().match(pattern, lookupPath)) { 236 matchingPatterns.add(pattern); 237 } 238 } 239 240 if (!matchingPatterns.isEmpty()) { 241 Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath); 242 Collections.sort(matchingPatterns, patternComparator); 243 for (String pattern : matchingPatterns) { 244 String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath); 245 String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping)); 246 if (logger.isTraceEnabled()) { 247 logger.trace("Invoking ResourceResolverChain for URL pattern \"" + pattern + "\""); 248 } 249 ResourceHttpRequestHandler handler = this.handlerMap.get(pattern); 250 ResourceResolverChain chain = new DefaultResourceResolverChain(handler.getResourceResolvers()); 251 String resolved = chain.resolveUrlPath(pathWithinMapping, handler.getLocations()); 252 if (resolved == null) { 253 continue; 254 } 255 if (logger.isTraceEnabled()) { 256 logger.trace("Resolved public resource URL path \"" + resolved + "\""); 257 } 258 return pathMapping + resolved; 259 } 260 } 261 262 if (logger.isDebugEnabled()) { 263 logger.debug("No matching resource mapping for lookup path \"" + lookupPath + "\""); 264 } 265 return null; 266 } 267 268}