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