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