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.handler; 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; 027import javax.servlet.http.HttpServletResponse; 028 029import org.springframework.beans.BeansException; 030import org.springframework.context.ApplicationContext; 031import org.springframework.lang.Nullable; 032import org.springframework.util.Assert; 033import org.springframework.util.CollectionUtils; 034import org.springframework.util.StringUtils; 035import org.springframework.web.servlet.HandlerExecutionChain; 036 037/** 038 * Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping} 039 * implementations. Provides infrastructure for mapping handlers to URLs and configurable 040 * URL lookup. For information on the latter, see "alwaysUseFullPath" property. 041 * 042 * <p>Supports direct matches, e.g. a registered "/test" matches "/test", and 043 * various Ant-style pattern matches, e.g. a registered "/t*" pattern matches 044 * both "/test" and "/team", "/test/*" matches all paths in the "/test" directory, 045 * "/test/**" matches all paths below "/test". For details, see the 046 * {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc. 047 * 048 * <p>Will search all path patterns to find the most exact match for the 049 * current request path. The most exact match is defined as the longest 050 * path pattern that matches the current request path. 051 * 052 * @author Juergen Hoeller 053 * @author Arjen Poutsma 054 * @since 16.04.2003 055 */ 056public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping { 057 058 @Nullable 059 private Object rootHandler; 060 061 private boolean useTrailingSlashMatch = false; 062 063 private boolean lazyInitHandlers = false; 064 065 private final Map<String, Object> handlerMap = new LinkedHashMap<>(); 066 067 068 /** 069 * Set the root handler for this handler mapping, that is, 070 * the handler to be registered for the root path ("/"). 071 * <p>Default is {@code null}, indicating no root handler. 072 */ 073 public void setRootHandler(@Nullable Object rootHandler) { 074 this.rootHandler = rootHandler; 075 } 076 077 /** 078 * Return the root handler for this handler mapping (registered for "/"), 079 * or {@code null} if none. 080 */ 081 @Nullable 082 public Object getRootHandler() { 083 return this.rootHandler; 084 } 085 086 /** 087 * Whether to match to URLs irrespective of the presence of a trailing slash. 088 * If enabled a URL pattern such as "/users" also matches to "/users/". 089 * <p>The default value is {@code false}. 090 */ 091 public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { 092 this.useTrailingSlashMatch = useTrailingSlashMatch; 093 } 094 095 /** 096 * Whether to match to URLs irrespective of the presence of a trailing slash. 097 */ 098 public boolean useTrailingSlashMatch() { 099 return this.useTrailingSlashMatch; 100 } 101 102 /** 103 * Set whether to lazily initialize handlers. Only applicable to 104 * singleton handlers, as prototypes are always lazily initialized. 105 * Default is "false", as eager initialization allows for more efficiency 106 * through referencing the controller objects directly. 107 * <p>If you want to allow your controllers to be lazily initialized, 108 * make them "lazy-init" and set this flag to true. Just making them 109 * "lazy-init" will not work, as they are initialized through the 110 * references from the handler mapping in this case. 111 */ 112 public void setLazyInitHandlers(boolean lazyInitHandlers) { 113 this.lazyInitHandlers = lazyInitHandlers; 114 } 115 116 /** 117 * Look up a handler for the URL path of the given request. 118 * @param request current HTTP request 119 * @return the handler instance, or {@code null} if none found 120 */ 121 @Override 122 @Nullable 123 protected Object getHandlerInternal(HttpServletRequest request) throws Exception { 124 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); 125 request.setAttribute(LOOKUP_PATH, lookupPath); 126 Object handler = lookupHandler(lookupPath, request); 127 if (handler == null) { 128 // We need to care for the default handler directly, since we need to 129 // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. 130 Object rawHandler = null; 131 if (StringUtils.matchesCharacter(lookupPath, '/')) { 132 rawHandler = getRootHandler(); 133 } 134 if (rawHandler == null) { 135 rawHandler = getDefaultHandler(); 136 } 137 if (rawHandler != null) { 138 // Bean name or resolved handler? 139 if (rawHandler instanceof String) { 140 String handlerName = (String) rawHandler; 141 rawHandler = obtainApplicationContext().getBean(handlerName); 142 } 143 validateHandler(rawHandler, request); 144 handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); 145 } 146 } 147 return handler; 148 } 149 150 /** 151 * Look up a handler instance for the given URL path. 152 * <p>Supports direct matches, e.g. a registered "/test" matches "/test", 153 * and various Ant-style pattern matches, e.g. a registered "/t*" matches 154 * both "/test" and "/team". For details, see the AntPathMatcher class. 155 * <p>Looks for the most exact pattern, where most exact is defined as 156 * the longest path pattern. 157 * @param urlPath the URL the bean is mapped to 158 * @param request current HTTP request (to expose the path within the mapping to) 159 * @return the associated handler instance, or {@code null} if not found 160 * @see #exposePathWithinMapping 161 * @see org.springframework.util.AntPathMatcher 162 */ 163 @Nullable 164 protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { 165 // Direct match? 166 Object handler = this.handlerMap.get(urlPath); 167 if (handler != null) { 168 // Bean name or resolved handler? 169 if (handler instanceof String) { 170 String handlerName = (String) handler; 171 handler = obtainApplicationContext().getBean(handlerName); 172 } 173 validateHandler(handler, request); 174 return buildPathExposingHandler(handler, urlPath, urlPath, null); 175 } 176 177 // Pattern match? 178 List<String> matchingPatterns = new ArrayList<>(); 179 for (String registeredPattern : this.handlerMap.keySet()) { 180 if (getPathMatcher().match(registeredPattern, urlPath)) { 181 matchingPatterns.add(registeredPattern); 182 } 183 else if (useTrailingSlashMatch()) { 184 if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { 185 matchingPatterns.add(registeredPattern + "/"); 186 } 187 } 188 } 189 190 String bestMatch = null; 191 Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath); 192 if (!matchingPatterns.isEmpty()) { 193 matchingPatterns.sort(patternComparator); 194 if (logger.isTraceEnabled() && matchingPatterns.size() > 1) { 195 logger.trace("Matching patterns " + matchingPatterns); 196 } 197 bestMatch = matchingPatterns.get(0); 198 } 199 if (bestMatch != null) { 200 handler = this.handlerMap.get(bestMatch); 201 if (handler == null) { 202 if (bestMatch.endsWith("/")) { 203 handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); 204 } 205 if (handler == null) { 206 throw new IllegalStateException( 207 "Could not find handler for best pattern match [" + bestMatch + "]"); 208 } 209 } 210 // Bean name or resolved handler? 211 if (handler instanceof String) { 212 String handlerName = (String) handler; 213 handler = obtainApplicationContext().getBean(handlerName); 214 } 215 validateHandler(handler, request); 216 String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); 217 218 // There might be multiple 'best patterns', let's make sure we have the correct URI template variables 219 // for all of them 220 Map<String, String> uriTemplateVariables = new LinkedHashMap<>(); 221 for (String matchingPattern : matchingPatterns) { 222 if (patternComparator.compare(bestMatch, matchingPattern) == 0) { 223 Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); 224 Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars); 225 uriTemplateVariables.putAll(decodedVars); 226 } 227 } 228 if (logger.isTraceEnabled() && uriTemplateVariables.size() > 0) { 229 logger.trace("URI variables " + uriTemplateVariables); 230 } 231 return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables); 232 } 233 234 // No handler found... 235 return null; 236 } 237 238 /** 239 * Validate the given handler against the current request. 240 * <p>The default implementation is empty. Can be overridden in subclasses, 241 * for example to enforce specific preconditions expressed in URL mappings. 242 * @param handler the handler object to validate 243 * @param request current HTTP request 244 * @throws Exception if validation failed 245 */ 246 protected void validateHandler(Object handler, HttpServletRequest request) throws Exception { 247 } 248 249 /** 250 * Build a handler object for the given raw handler, exposing the actual 251 * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, as well as 252 * the {@link #URI_TEMPLATE_VARIABLES_ATTRIBUTE} before executing the handler. 253 * <p>The default implementation builds a {@link HandlerExecutionChain} 254 * with a special interceptor that exposes the path attribute and uri template variables 255 * @param rawHandler the raw handler to expose 256 * @param pathWithinMapping the path to expose before executing the handler 257 * @param uriTemplateVariables the URI template variables, can be {@code null} if no variables found 258 * @return the final handler object 259 */ 260 protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern, 261 String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables) { 262 263 HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler); 264 chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping)); 265 if (!CollectionUtils.isEmpty(uriTemplateVariables)) { 266 chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); 267 } 268 return chain; 269 } 270 271 /** 272 * Expose the path within the current mapping as request attribute. 273 * @param pathWithinMapping the path within the current mapping 274 * @param request the request to expose the path to 275 * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE 276 */ 277 protected void exposePathWithinMapping(String bestMatchingPattern, String pathWithinMapping, 278 HttpServletRequest request) { 279 280 request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatchingPattern); 281 request.setAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); 282 } 283 284 /** 285 * Expose the URI templates variables as request attribute. 286 * @param uriTemplateVariables the URI template variables 287 * @param request the request to expose the path to 288 * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE 289 */ 290 protected void exposeUriTemplateVariables(Map<String, String> uriTemplateVariables, HttpServletRequest request) { 291 request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables); 292 } 293 294 @Override 295 @Nullable 296 public RequestMatchResult match(HttpServletRequest request, String pattern) { 297 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request, LOOKUP_PATH); 298 if (getPathMatcher().match(pattern, lookupPath)) { 299 return new RequestMatchResult(pattern, lookupPath, getPathMatcher()); 300 } 301 else if (useTrailingSlashMatch()) { 302 if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", lookupPath)) { 303 return new RequestMatchResult(pattern + "/", lookupPath, getPathMatcher()); 304 } 305 } 306 return null; 307 } 308 309 /** 310 * Register the specified handler for the given URL paths. 311 * @param urlPaths the URLs that the bean should be mapped to 312 * @param beanName the name of the handler bean 313 * @throws BeansException if the handler couldn't be registered 314 * @throws IllegalStateException if there is a conflicting handler registered 315 */ 316 protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException { 317 Assert.notNull(urlPaths, "URL path array must not be null"); 318 for (String urlPath : urlPaths) { 319 registerHandler(urlPath, beanName); 320 } 321 } 322 323 /** 324 * Register the specified handler for the given URL path. 325 * @param urlPath the URL the bean should be mapped to 326 * @param handler the handler instance or handler bean name String 327 * (a bean name will automatically be resolved into the corresponding handler bean) 328 * @throws BeansException if the handler couldn't be registered 329 * @throws IllegalStateException if there is a conflicting handler registered 330 */ 331 protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException { 332 Assert.notNull(urlPath, "URL path must not be null"); 333 Assert.notNull(handler, "Handler object must not be null"); 334 Object resolvedHandler = handler; 335 336 // Eagerly resolve handler if referencing singleton via name. 337 if (!this.lazyInitHandlers && handler instanceof String) { 338 String handlerName = (String) handler; 339 ApplicationContext applicationContext = obtainApplicationContext(); 340 if (applicationContext.isSingleton(handlerName)) { 341 resolvedHandler = applicationContext.getBean(handlerName); 342 } 343 } 344 345 Object mappedHandler = this.handlerMap.get(urlPath); 346 if (mappedHandler != null) { 347 if (mappedHandler != resolvedHandler) { 348 throw new IllegalStateException( 349 "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath + 350 "]: There is already " + getHandlerDescription(mappedHandler) + " mapped."); 351 } 352 } 353 else { 354 if (urlPath.equals("/")) { 355 if (logger.isTraceEnabled()) { 356 logger.trace("Root mapping to " + getHandlerDescription(handler)); 357 } 358 setRootHandler(resolvedHandler); 359 } 360 else if (urlPath.equals("/*")) { 361 if (logger.isTraceEnabled()) { 362 logger.trace("Default mapping to " + getHandlerDescription(handler)); 363 } 364 setDefaultHandler(resolvedHandler); 365 } 366 else { 367 this.handlerMap.put(urlPath, resolvedHandler); 368 if (logger.isTraceEnabled()) { 369 logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler)); 370 } 371 } 372 } 373 } 374 375 private String getHandlerDescription(Object handler) { 376 return (handler instanceof String ? "'" + handler + "'" : handler.toString()); 377 } 378 379 380 /** 381 * Return the registered handlers as an unmodifiable Map, with the registered path 382 * as key and the handler object (or handler bean name in case of a lazy-init handler) 383 * as value. 384 * @see #getDefaultHandler() 385 */ 386 public final Map<String, Object> getHandlerMap() { 387 return Collections.unmodifiableMap(this.handlerMap); 388 } 389 390 /** 391 * Indicates whether this handler mapping support type-level mappings. Default to {@code false}. 392 */ 393 protected boolean supportsTypeLevelMappings() { 394 return false; 395 } 396 397 398 /** 399 * Special interceptor for exposing the 400 * {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} attribute. 401 * @see AbstractUrlHandlerMapping#exposePathWithinMapping 402 */ 403 private class PathExposingHandlerInterceptor extends HandlerInterceptorAdapter { 404 405 private final String bestMatchingPattern; 406 407 private final String pathWithinMapping; 408 409 public PathExposingHandlerInterceptor(String bestMatchingPattern, String pathWithinMapping) { 410 this.bestMatchingPattern = bestMatchingPattern; 411 this.pathWithinMapping = pathWithinMapping; 412 } 413 414 @Override 415 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 416 exposePathWithinMapping(this.bestMatchingPattern, this.pathWithinMapping, request); 417 request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, handler); 418 request.setAttribute(INTROSPECT_TYPE_LEVEL_MAPPING, supportsTypeLevelMappings()); 419 return true; 420 } 421 422 } 423 424 /** 425 * Special interceptor for exposing the 426 * {@link AbstractUrlHandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE} attribute. 427 * @see AbstractUrlHandlerMapping#exposePathWithinMapping 428 */ 429 private class UriTemplateVariablesHandlerInterceptor extends HandlerInterceptorAdapter { 430 431 private final Map<String, String> uriTemplateVariables; 432 433 public UriTemplateVariablesHandlerInterceptor(Map<String, String> uriTemplateVariables) { 434 this.uriTemplateVariables = uriTemplateVariables; 435 } 436 437 @Override 438 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 439 exposeUriTemplateVariables(this.uriTemplateVariables, request); 440 return true; 441 } 442 } 443 444}