001/* 002 * Copyright 2002-2015 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.mvc.annotation; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.OutputStream; 022import java.io.Reader; 023import java.io.Writer; 024import java.lang.reflect.Method; 025import java.security.Principal; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collections; 029import java.util.Comparator; 030import java.util.HashMap; 031import java.util.LinkedHashMap; 032import java.util.LinkedHashSet; 033import java.util.List; 034import java.util.Locale; 035import java.util.Map; 036import java.util.Set; 037import java.util.concurrent.ConcurrentHashMap; 038import javax.servlet.ServletException; 039import javax.servlet.ServletRequest; 040import javax.servlet.ServletResponse; 041import javax.servlet.http.Cookie; 042import javax.servlet.http.HttpServletRequest; 043import javax.servlet.http.HttpServletResponse; 044import javax.servlet.http.HttpSession; 045import javax.xml.transform.Source; 046 047import org.apache.commons.logging.Log; 048import org.apache.commons.logging.LogFactory; 049 050import org.springframework.beans.BeanUtils; 051import org.springframework.beans.factory.BeanFactory; 052import org.springframework.beans.factory.BeanFactoryAware; 053import org.springframework.beans.factory.config.BeanExpressionContext; 054import org.springframework.beans.factory.config.BeanExpressionResolver; 055import org.springframework.beans.factory.config.ConfigurableBeanFactory; 056import org.springframework.core.DefaultParameterNameDiscoverer; 057import org.springframework.core.Ordered; 058import org.springframework.core.ParameterNameDiscoverer; 059import org.springframework.core.annotation.AnnotatedElementUtils; 060import org.springframework.core.annotation.AnnotationUtils; 061import org.springframework.http.HttpEntity; 062import org.springframework.http.HttpHeaders; 063import org.springframework.http.HttpInputMessage; 064import org.springframework.http.HttpOutputMessage; 065import org.springframework.http.HttpStatus; 066import org.springframework.http.MediaType; 067import org.springframework.http.ResponseEntity; 068import org.springframework.http.converter.ByteArrayHttpMessageConverter; 069import org.springframework.http.converter.HttpMessageConverter; 070import org.springframework.http.converter.StringHttpMessageConverter; 071import org.springframework.http.converter.xml.SourceHttpMessageConverter; 072import org.springframework.http.server.ServerHttpRequest; 073import org.springframework.http.server.ServerHttpResponse; 074import org.springframework.http.server.ServletServerHttpRequest; 075import org.springframework.http.server.ServletServerHttpResponse; 076import org.springframework.ui.ExtendedModelMap; 077import org.springframework.ui.Model; 078import org.springframework.util.AntPathMatcher; 079import org.springframework.util.Assert; 080import org.springframework.util.ClassUtils; 081import org.springframework.util.ObjectUtils; 082import org.springframework.util.PathMatcher; 083import org.springframework.util.StringUtils; 084import org.springframework.validation.support.BindingAwareModelMap; 085import org.springframework.web.HttpMediaTypeNotAcceptableException; 086import org.springframework.web.HttpRequestMethodNotSupportedException; 087import org.springframework.web.HttpSessionRequiredException; 088import org.springframework.web.bind.MissingServletRequestParameterException; 089import org.springframework.web.bind.ServletRequestDataBinder; 090import org.springframework.web.bind.WebDataBinder; 091import org.springframework.web.bind.annotation.InitBinder; 092import org.springframework.web.bind.annotation.ModelAttribute; 093import org.springframework.web.bind.annotation.RequestMapping; 094import org.springframework.web.bind.annotation.RequestMethod; 095import org.springframework.web.bind.annotation.RequestParam; 096import org.springframework.web.bind.annotation.ResponseBody; 097import org.springframework.web.bind.annotation.ResponseStatus; 098import org.springframework.web.bind.annotation.SessionAttributes; 099import org.springframework.web.bind.support.DefaultSessionAttributeStore; 100import org.springframework.web.bind.support.SessionAttributeStore; 101import org.springframework.web.bind.support.WebArgumentResolver; 102import org.springframework.web.bind.support.WebBindingInitializer; 103import org.springframework.web.context.request.NativeWebRequest; 104import org.springframework.web.context.request.RequestScope; 105import org.springframework.web.context.request.ServletWebRequest; 106import org.springframework.web.multipart.MultipartRequest; 107import org.springframework.web.servlet.HandlerAdapter; 108import org.springframework.web.servlet.HandlerMapping; 109import org.springframework.web.servlet.ModelAndView; 110import org.springframework.web.servlet.View; 111import org.springframework.web.servlet.support.RequestContextUtils; 112import org.springframework.web.servlet.support.WebContentGenerator; 113import org.springframework.web.util.UrlPathHelper; 114import org.springframework.web.util.WebUtils; 115 116/** 117 * Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface 118 * that maps handler methods based on HTTP paths, HTTP methods, and request parameters 119 * expressed through the {@link RequestMapping} annotation. 120 * 121 * <p>Supports request parameter binding through the {@link RequestParam} annotation. 122 * Also supports the {@link ModelAttribute} annotation for exposing model attribute 123 * values to the view, as well as {@link InitBinder} for binder initialization methods 124 * and {@link SessionAttributes} for automatic session management of specific attributes. 125 * 126 * <p>This adapter can be customized through various bean properties. 127 * A common use case is to apply shared binder initialization logic through 128 * a custom {@link #setWebBindingInitializer WebBindingInitializer}. 129 * 130 * @author Juergen Hoeller 131 * @author Arjen Poutsma 132 * @author Sam Brannen 133 * @since 2.5 134 * @see #setPathMatcher 135 * @see #setMethodNameResolver 136 * @see #setWebBindingInitializer 137 * @see #setSessionAttributeStore 138 * @deprecated as of Spring 3.2, in favor of 139 * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter RequestMappingHandlerAdapter} 140 */ 141@Deprecated 142public class AnnotationMethodHandlerAdapter extends WebContentGenerator 143 implements HandlerAdapter, Ordered, BeanFactoryAware { 144 145 /** 146 * Log category to use when no mapped handler is found for a request. 147 * @see #pageNotFoundLogger 148 */ 149 public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; 150 151 /** 152 * Additional logger to use when no mapped handler is found for a request. 153 * @see #PAGE_NOT_FOUND_LOG_CATEGORY 154 */ 155 protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); 156 157 158 private UrlPathHelper urlPathHelper = new UrlPathHelper(); 159 160 private PathMatcher pathMatcher = new AntPathMatcher(); 161 162 private org.springframework.web.servlet.mvc.multiaction.MethodNameResolver methodNameResolver = 163 new org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver(); 164 165 private WebBindingInitializer webBindingInitializer; 166 167 private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore(); 168 169 private int cacheSecondsForSessionAttributeHandlers = 0; 170 171 private boolean synchronizeOnSession = false; 172 173 private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); 174 175 private WebArgumentResolver[] customArgumentResolvers; 176 177 private ModelAndViewResolver[] customModelAndViewResolvers; 178 179 private HttpMessageConverter<?>[] messageConverters; 180 181 private int order = Ordered.LOWEST_PRECEDENCE; 182 183 private ConfigurableBeanFactory beanFactory; 184 185 private BeanExpressionContext expressionContext; 186 187 private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache = 188 new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>(64); 189 190 private final Map<Class<?>, Boolean> sessionAnnotatedClassesCache = new ConcurrentHashMap<Class<?>, Boolean>(64); 191 192 193 public AnnotationMethodHandlerAdapter() { 194 // no restriction of HTTP methods by default 195 super(false); 196 197 // See SPR-7316 198 StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); 199 stringHttpMessageConverter.setWriteAcceptCharset(false); 200 this.messageConverters = new HttpMessageConverter<?>[] { 201 new ByteArrayHttpMessageConverter(), stringHttpMessageConverter, 202 new SourceHttpMessageConverter<Source>(), 203 new org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter() }; 204 } 205 206 207 /** 208 * Set if URL lookup should always use the full path within the current servlet 209 * context. Else, the path within the current servlet mapping is used if applicable 210 * (that is, in the case of a ".../*" servlet mapping in web.xml). 211 * <p>Default is "false". 212 * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath 213 */ 214 public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { 215 this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); 216 } 217 218 /** 219 * Set if context path and request URI should be URL-decoded. Both are returned 220 * <i>undecoded</i> by the Servlet API, in contrast to the servlet path. 221 * <p>Uses either the request encoding or the default encoding according 222 * to the Servlet spec (ISO-8859-1). 223 * @see org.springframework.web.util.UrlPathHelper#setUrlDecode 224 */ 225 public void setUrlDecode(boolean urlDecode) { 226 this.urlPathHelper.setUrlDecode(urlDecode); 227 } 228 229 /** 230 * Set the UrlPathHelper to use for resolution of lookup paths. 231 * <p>Use this to override the default UrlPathHelper with a custom subclass, 232 * or to share common UrlPathHelper settings across multiple HandlerMappings and HandlerAdapters. 233 */ 234 public void setUrlPathHelper(UrlPathHelper urlPathHelper) { 235 Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); 236 this.urlPathHelper = urlPathHelper; 237 } 238 239 /** 240 * Set the PathMatcher implementation to use for matching URL paths against registered URL patterns. 241 * <p>Default is {@link org.springframework.util.AntPathMatcher}. 242 */ 243 public void setPathMatcher(PathMatcher pathMatcher) { 244 Assert.notNull(pathMatcher, "PathMatcher must not be null"); 245 this.pathMatcher = pathMatcher; 246 } 247 248 /** 249 * Set the MethodNameResolver to use for resolving default handler methods 250 * (carrying an empty {@code @RequestMapping} annotation). 251 * <p>Will only kick in when the handler method cannot be resolved uniquely 252 * through the annotation metadata already. 253 */ 254 public void setMethodNameResolver(org.springframework.web.servlet.mvc.multiaction.MethodNameResolver methodNameResolver) { 255 this.methodNameResolver = methodNameResolver; 256 } 257 258 /** 259 * Specify a WebBindingInitializer which will apply pre-configured 260 * configuration to every DataBinder that this controller uses. 261 */ 262 public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { 263 this.webBindingInitializer = webBindingInitializer; 264 } 265 266 /** 267 * Specify the strategy to store session attributes with. 268 * <p>Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore}, 269 * storing session attributes in the HttpSession, using the same attribute name as in the model. 270 */ 271 public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { 272 Assert.notNull(sessionAttributeStore, "SessionAttributeStore must not be null"); 273 this.sessionAttributeStore = sessionAttributeStore; 274 } 275 276 /** 277 * Cache content produced by {@code @SessionAttributes} annotated handlers 278 * for the given number of seconds. Default is 0, preventing caching completely. 279 * <p>In contrast to the "cacheSeconds" property which will apply to all general handlers 280 * (but not to {@code @SessionAttributes} annotated handlers), this setting will 281 * apply to {@code @SessionAttributes} annotated handlers only. 282 * @see #setCacheSeconds 283 * @see org.springframework.web.bind.annotation.SessionAttributes 284 */ 285 public void setCacheSecondsForSessionAttributeHandlers(int cacheSecondsForSessionAttributeHandlers) { 286 this.cacheSecondsForSessionAttributeHandlers = cacheSecondsForSessionAttributeHandlers; 287 } 288 289 /** 290 * Set if controller execution should be synchronized on the session, 291 * to serialize parallel invocations from the same client. 292 * <p>More specifically, the execution of the {@code handleRequestInternal} 293 * method will get synchronized if this flag is "true". The best available 294 * session mutex will be used for the synchronization; ideally, this will 295 * be a mutex exposed by HttpSessionMutexListener. 296 * <p>The session mutex is guaranteed to be the same object during 297 * the entire lifetime of the session, available under the key defined 298 * by the {@code SESSION_MUTEX_ATTRIBUTE} constant. It serves as a 299 * safe reference to synchronize on for locking on the current session. 300 * <p>In many cases, the HttpSession reference itself is a safe mutex 301 * as well, since it will always be the same object reference for the 302 * same active logical session. However, this is not guaranteed across 303 * different servlet containers; the only 100% safe way is a session mutex. 304 * @see org.springframework.web.util.HttpSessionMutexListener 305 * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession) 306 */ 307 public void setSynchronizeOnSession(boolean synchronizeOnSession) { 308 this.synchronizeOnSession = synchronizeOnSession; 309 } 310 311 /** 312 * Set the ParameterNameDiscoverer to use for resolving method parameter names if needed 313 * (e.g. for default attribute names). 314 * <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}. 315 */ 316 public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { 317 this.parameterNameDiscoverer = parameterNameDiscoverer; 318 } 319 320 /** 321 * Set a custom WebArgumentResolvers to use for special method parameter types. 322 * <p>Such a custom WebArgumentResolver will kick in first, having a chance to resolve 323 * an argument value before the standard argument handling kicks in. 324 */ 325 public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { 326 this.customArgumentResolvers = new WebArgumentResolver[] {argumentResolver}; 327 } 328 329 /** 330 * Set one or more custom WebArgumentResolvers to use for special method parameter types. 331 * <p>Any such custom WebArgumentResolver will kick in first, having a chance to resolve 332 * an argument value before the standard argument handling kicks in. 333 */ 334 public void setCustomArgumentResolvers(WebArgumentResolver... argumentResolvers) { 335 this.customArgumentResolvers = argumentResolvers; 336 } 337 338 /** 339 * Set a custom ModelAndViewResolvers to use for special method return types. 340 * <p>Such a custom ModelAndViewResolver will kick in first, having a chance to resolve 341 * a return value before the standard ModelAndView handling kicks in. 342 */ 343 public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { 344 this.customModelAndViewResolvers = new ModelAndViewResolver[] {customModelAndViewResolver}; 345 } 346 347 /** 348 * Set one or more custom ModelAndViewResolvers to use for special method return types. 349 * <p>Any such custom ModelAndViewResolver will kick in first, having a chance to resolve 350 * a return value before the standard ModelAndView handling kicks in. 351 */ 352 public void setCustomModelAndViewResolvers(ModelAndViewResolver... customModelAndViewResolvers) { 353 this.customModelAndViewResolvers = customModelAndViewResolvers; 354 } 355 356 /** 357 * Set the message body converters to use. 358 * <p>These converters are used to convert from and to HTTP requests and responses. 359 */ 360 public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) { 361 this.messageConverters = messageConverters; 362 } 363 364 /** 365 * Return the message body converters that this adapter has been configured with. 366 */ 367 public HttpMessageConverter<?>[] getMessageConverters() { 368 return messageConverters; 369 } 370 371 /** 372 * Specify the order value for this HandlerAdapter bean. 373 * <p>Default value is {@code Integer.MAX_VALUE}, meaning that it's non-ordered. 374 * @see org.springframework.core.Ordered#getOrder() 375 */ 376 public void setOrder(int order) { 377 this.order = order; 378 } 379 380 @Override 381 public int getOrder() { 382 return this.order; 383 } 384 385 @Override 386 public void setBeanFactory(BeanFactory beanFactory) { 387 if (beanFactory instanceof ConfigurableBeanFactory) { 388 this.beanFactory = (ConfigurableBeanFactory) beanFactory; 389 this.expressionContext = new BeanExpressionContext(this.beanFactory, new RequestScope()); 390 } 391 } 392 393 394 @Override 395 public boolean supports(Object handler) { 396 return getMethodResolver(handler).hasHandlerMethods(); 397 } 398 399 @Override 400 public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) 401 throws Exception { 402 403 Class<?> clazz = ClassUtils.getUserClass(handler); 404 Boolean annotatedWithSessionAttributes = this.sessionAnnotatedClassesCache.get(clazz); 405 if (annotatedWithSessionAttributes == null) { 406 annotatedWithSessionAttributes = (AnnotationUtils.findAnnotation(clazz, SessionAttributes.class) != null); 407 this.sessionAnnotatedClassesCache.put(clazz, annotatedWithSessionAttributes); 408 } 409 410 if (annotatedWithSessionAttributes) { 411 checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); 412 } 413 else { 414 checkAndPrepare(request, response, true); 415 } 416 417 // Execute invokeHandlerMethod in synchronized block if required. 418 if (this.synchronizeOnSession) { 419 HttpSession session = request.getSession(false); 420 if (session != null) { 421 Object mutex = WebUtils.getSessionMutex(session); 422 synchronized (mutex) { 423 return invokeHandlerMethod(request, response, handler); 424 } 425 } 426 } 427 428 return invokeHandlerMethod(request, response, handler); 429 } 430 431 protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler) 432 throws Exception { 433 434 ServletHandlerMethodResolver methodResolver = getMethodResolver(handler); 435 Method handlerMethod = methodResolver.resolveHandlerMethod(request); 436 ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver); 437 ServletWebRequest webRequest = new ServletWebRequest(request, response); 438 ExtendedModelMap implicitModel = new BindingAwareModelMap(); 439 440 Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); 441 ModelAndView mav = 442 methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); 443 methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); 444 return mav; 445 } 446 447 /** 448 * This method always returns -1 since an annotated controller can have many methods, 449 * each requiring separate lastModified calculations. Instead, an 450 * {@link RequestMapping}-annotated method can calculate the lastModified value, call 451 * {@link org.springframework.web.context.request.WebRequest#checkNotModified(long)} 452 * to check it, and return {@code null} if that returns {@code true}. 453 * @see org.springframework.web.context.request.WebRequest#checkNotModified(long) 454 */ 455 @Override 456 public long getLastModified(HttpServletRequest request, Object handler) { 457 return -1; 458 } 459 460 461 /** 462 * Build a HandlerMethodResolver for the given handler type. 463 */ 464 private ServletHandlerMethodResolver getMethodResolver(Object handler) { 465 Class<?> handlerClass = ClassUtils.getUserClass(handler); 466 ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass); 467 if (resolver == null) { 468 synchronized (this.methodResolverCache) { 469 resolver = this.methodResolverCache.get(handlerClass); 470 if (resolver == null) { 471 resolver = new ServletHandlerMethodResolver(handlerClass); 472 this.methodResolverCache.put(handlerClass, resolver); 473 } 474 } 475 } 476 return resolver; 477 } 478 479 480 /** 481 * Template method for creating a new ServletRequestDataBinder instance. 482 * <p>The default implementation creates a standard ServletRequestDataBinder. 483 * This can be overridden for custom ServletRequestDataBinder subclasses. 484 * @param request current HTTP request 485 * @param target the target object to bind onto (or {@code null} 486 * if the binder is just used to convert a plain parameter value) 487 * @param objectName the objectName of the target object 488 * @return the ServletRequestDataBinder instance to use 489 * @throws Exception in case of invalid state or arguments 490 * @see ServletRequestDataBinder#bind(javax.servlet.ServletRequest) 491 * @see ServletRequestDataBinder#convertIfNecessary(Object, Class, org.springframework.core.MethodParameter) 492 */ 493 protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object target, String objectName) throws Exception { 494 return new ServletRequestDataBinder(target, objectName); 495 } 496 497 /** 498 * Template method for creating a new HttpInputMessage instance. 499 * <p>The default implementation creates a standard {@link ServletServerHttpRequest}. 500 * This can be overridden for custom {@code HttpInputMessage} implementations 501 * @param servletRequest current HTTP request 502 * @return the HttpInputMessage instance to use 503 * @throws Exception in case of errors 504 */ 505 protected HttpInputMessage createHttpInputMessage(HttpServletRequest servletRequest) throws Exception { 506 return new ServletServerHttpRequest(servletRequest); 507 } 508 509 /** 510 * Template method for creating a new HttpOutputMessage instance. 511 * <p>The default implementation creates a standard {@link ServletServerHttpResponse}. 512 * This can be overridden for custom {@code HttpOutputMessage} implementations 513 * @param servletResponse current HTTP response 514 * @return the HttpInputMessage instance to use 515 * @throws Exception in case of errors 516 */ 517 protected HttpOutputMessage createHttpOutputMessage(HttpServletResponse servletResponse) throws Exception { 518 return new ServletServerHttpResponse(servletResponse); 519 } 520 521 522 /** 523 * Servlet-specific subclass of {@code HandlerMethodResolver}. 524 */ 525 @SuppressWarnings("deprecation") 526 private class ServletHandlerMethodResolver extends org.springframework.web.bind.annotation.support.HandlerMethodResolver { 527 528 private final Map<Method, RequestMappingInfo> mappings = new HashMap<Method, RequestMappingInfo>(); 529 530 private ServletHandlerMethodResolver(Class<?> handlerType) { 531 init(handlerType); 532 } 533 534 @Override 535 protected boolean isHandlerMethod(Method method) { 536 if (this.mappings.containsKey(method)) { 537 return true; 538 } 539 RequestMapping mapping = AnnotationUtils.findAnnotation(method, RequestMapping.class); 540 if (mapping != null) { 541 String[] patterns = mapping.value(); 542 RequestMethod[] methods = new RequestMethod[0]; 543 String[] params = new String[0]; 544 String[] headers = new String[0]; 545 if (!hasTypeLevelMapping() || !Arrays.equals(mapping.method(), getTypeLevelMapping().method())) { 546 methods = mapping.method(); 547 } 548 if (!hasTypeLevelMapping() || !Arrays.equals(mapping.params(), getTypeLevelMapping().params())) { 549 params = mapping.params(); 550 } 551 if (!hasTypeLevelMapping() || !Arrays.equals(mapping.headers(), getTypeLevelMapping().headers())) { 552 headers = mapping.headers(); 553 } 554 RequestMappingInfo mappingInfo = new RequestMappingInfo(patterns, methods, params, headers); 555 this.mappings.put(method, mappingInfo); 556 return true; 557 } 558 return false; 559 } 560 561 public Method resolveHandlerMethod(HttpServletRequest request) throws ServletException { 562 String lookupPath = urlPathHelper.getLookupPathForRequest(request); 563 Comparator<String> pathComparator = pathMatcher.getPatternComparator(lookupPath); 564 Map<RequestSpecificMappingInfo, Method> targetHandlerMethods = new LinkedHashMap<RequestSpecificMappingInfo, Method>(); 565 Set<String> allowedMethods = new LinkedHashSet<String>(7); 566 String resolvedMethodName = null; 567 for (Method handlerMethod : getHandlerMethods()) { 568 RequestSpecificMappingInfo mappingInfo = new RequestSpecificMappingInfo(this.mappings.get(handlerMethod)); 569 boolean match = false; 570 if (mappingInfo.hasPatterns()) { 571 for (String pattern : mappingInfo.getPatterns()) { 572 if (!hasTypeLevelMapping() && !pattern.startsWith("/")) { 573 pattern = "/" + pattern; 574 } 575 String combinedPattern = getCombinedPattern(pattern, lookupPath, request); 576 if (combinedPattern != null) { 577 if (mappingInfo.matches(request)) { 578 match = true; 579 mappingInfo.addMatchedPattern(combinedPattern); 580 } 581 else { 582 if (!mappingInfo.matchesRequestMethod(request)) { 583 allowedMethods.addAll(mappingInfo.methodNames()); 584 } 585 break; 586 } 587 } 588 } 589 mappingInfo.sortMatchedPatterns(pathComparator); 590 } 591 else if (useTypeLevelMapping(request)) { 592 String[] typeLevelPatterns = getTypeLevelMapping().value(); 593 for (String typeLevelPattern : typeLevelPatterns) { 594 if (!typeLevelPattern.startsWith("/")) { 595 typeLevelPattern = "/" + typeLevelPattern; 596 } 597 boolean useSuffixPattern = useSuffixPattern(request); 598 if (getMatchingPattern(typeLevelPattern, lookupPath, useSuffixPattern) != null) { 599 if (mappingInfo.matches(request)) { 600 match = true; 601 mappingInfo.addMatchedPattern(typeLevelPattern); 602 } 603 else { 604 if (!mappingInfo.matchesRequestMethod(request)) { 605 allowedMethods.addAll(mappingInfo.methodNames()); 606 } 607 break; 608 } 609 } 610 } 611 mappingInfo.sortMatchedPatterns(pathComparator); 612 } 613 else { 614 // No paths specified: parameter match sufficient. 615 match = mappingInfo.matches(request); 616 if (match && mappingInfo.getMethodCount() == 0 && mappingInfo.getParamCount() == 0 && 617 resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) { 618 match = false; 619 } 620 else { 621 if (!mappingInfo.matchesRequestMethod(request)) { 622 allowedMethods.addAll(mappingInfo.methodNames()); 623 } 624 } 625 } 626 if (match) { 627 Method oldMappedMethod = targetHandlerMethods.put(mappingInfo, handlerMethod); 628 if (oldMappedMethod != null && oldMappedMethod != handlerMethod) { 629 if (methodNameResolver != null && !mappingInfo.hasPatterns()) { 630 if (!oldMappedMethod.getName().equals(handlerMethod.getName())) { 631 if (resolvedMethodName == null) { 632 resolvedMethodName = methodNameResolver.getHandlerMethodName(request); 633 } 634 if (!resolvedMethodName.equals(oldMappedMethod.getName())) { 635 oldMappedMethod = null; 636 } 637 if (!resolvedMethodName.equals(handlerMethod.getName())) { 638 if (oldMappedMethod != null) { 639 targetHandlerMethods.put(mappingInfo, oldMappedMethod); 640 oldMappedMethod = null; 641 } 642 else { 643 targetHandlerMethods.remove(mappingInfo); 644 } 645 } 646 } 647 } 648 if (oldMappedMethod != null) { 649 throw new IllegalStateException( 650 "Ambiguous handler methods mapped for HTTP path '" + lookupPath + "': {" + 651 oldMappedMethod + ", " + handlerMethod + 652 "}. If you intend to handle the same path in multiple methods, then factor " + 653 "them out into a dedicated handler class with that path mapped at the type level!"); 654 } 655 } 656 } 657 } 658 if (!targetHandlerMethods.isEmpty()) { 659 List<RequestSpecificMappingInfo> matches = new ArrayList<RequestSpecificMappingInfo>(targetHandlerMethods.keySet()); 660 RequestSpecificMappingInfoComparator requestMappingInfoComparator = 661 new RequestSpecificMappingInfoComparator(pathComparator, request); 662 Collections.sort(matches, requestMappingInfoComparator); 663 RequestSpecificMappingInfo bestMappingMatch = matches.get(0); 664 String bestMatchedPath = bestMappingMatch.bestMatchedPattern(); 665 if (bestMatchedPath != null) { 666 extractHandlerMethodUriTemplates(bestMatchedPath, lookupPath, request); 667 } 668 return targetHandlerMethods.get(bestMappingMatch); 669 } 670 else { 671 if (!allowedMethods.isEmpty()) { 672 throw new HttpRequestMethodNotSupportedException(request.getMethod(), StringUtils.toStringArray(allowedMethods)); 673 } 674 throw new org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException( 675 lookupPath, request.getMethod(), request.getParameterMap()); 676 } 677 } 678 679 private boolean useTypeLevelMapping(HttpServletRequest request) { 680 if (!hasTypeLevelMapping() || ObjectUtils.isEmpty(getTypeLevelMapping().value())) { 681 return false; 682 } 683 Object value = request.getAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING); 684 return (value != null) ? (Boolean) value : Boolean.TRUE; 685 } 686 687 private boolean useSuffixPattern(HttpServletRequest request) { 688 Object value = request.getAttribute(DefaultAnnotationHandlerMapping.USE_DEFAULT_SUFFIX_PATTERN); 689 return (value != null) ? (Boolean) value : Boolean.TRUE; 690 } 691 692 /** 693 * Determines the combined pattern for the given methodLevelPattern and path. 694 * <p>Uses the following algorithm: 695 * <ol> 696 * <li>If there is a type-level mapping with path information, it is {@linkplain 697 * PathMatcher#combine(String, String) combined} with the method-level pattern.</li> 698 * <li>If there is a {@linkplain HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} 699 * in the request, it is combined with the method-level pattern.</li> 700 * <li>Otherwise, the method-level pattern is returned.</li> 701 * </ol> 702 */ 703 private String getCombinedPattern(String methodLevelPattern, String lookupPath, HttpServletRequest request) { 704 boolean useSuffixPattern = useSuffixPattern(request); 705 if (useTypeLevelMapping(request)) { 706 String[] typeLevelPatterns = getTypeLevelMapping().value(); 707 for (String typeLevelPattern : typeLevelPatterns) { 708 if (!typeLevelPattern.startsWith("/")) { 709 typeLevelPattern = "/" + typeLevelPattern; 710 } 711 String combinedPattern = pathMatcher.combine(typeLevelPattern, methodLevelPattern); 712 String matchingPattern = getMatchingPattern(combinedPattern, lookupPath, useSuffixPattern); 713 if (matchingPattern != null) { 714 return matchingPattern; 715 } 716 } 717 return null; 718 } 719 String bestMatchingPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); 720 if (StringUtils.hasText(bestMatchingPattern) && bestMatchingPattern.endsWith("*")) { 721 String combinedPattern = pathMatcher.combine(bestMatchingPattern, methodLevelPattern); 722 String matchingPattern = getMatchingPattern(combinedPattern, lookupPath, useSuffixPattern); 723 if (matchingPattern != null && !matchingPattern.equals(bestMatchingPattern)) { 724 return matchingPattern; 725 } 726 } 727 return getMatchingPattern(methodLevelPattern, lookupPath, useSuffixPattern); 728 } 729 730 private String getMatchingPattern(String pattern, String lookupPath, boolean useSuffixPattern) { 731 if (pattern.equals(lookupPath)) { 732 return pattern; 733 } 734 boolean hasSuffix = pattern.indexOf('.') != -1; 735 if (useSuffixPattern && !hasSuffix) { 736 String patternWithSuffix = pattern + ".*"; 737 if (pathMatcher.match(patternWithSuffix, lookupPath)) { 738 return patternWithSuffix; 739 } 740 } 741 if (pathMatcher.match(pattern, lookupPath)) { 742 return pattern; 743 } 744 boolean endsWithSlash = pattern.endsWith("/"); 745 if (useSuffixPattern && !endsWithSlash) { 746 String patternWithSlash = pattern + "/"; 747 if (pathMatcher.match(patternWithSlash, lookupPath)) { 748 return patternWithSlash; 749 } 750 } 751 return null; 752 } 753 754 @SuppressWarnings("unchecked") 755 private void extractHandlerMethodUriTemplates(String mappedPattern, String lookupPath, HttpServletRequest request) { 756 Map<String, String> variables = 757 (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); 758 int patternVariableCount = StringUtils.countOccurrencesOf(mappedPattern, "{"); 759 if ((variables == null || patternVariableCount != variables.size()) && pathMatcher.match(mappedPattern, lookupPath)) { 760 variables = pathMatcher.extractUriTemplateVariables(mappedPattern, lookupPath); 761 Map<String, String> decodedVariables = urlPathHelper.decodePathVariables(request, variables); 762 request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedVariables); 763 } 764 } 765 } 766 767 768 /** 769 * Servlet-specific subclass of {@code HandlerMethodInvoker}. 770 */ 771 @SuppressWarnings("deprecation") 772 private class ServletHandlerMethodInvoker extends org.springframework.web.bind.annotation.support.HandlerMethodInvoker { 773 774 private boolean responseArgumentUsed = false; 775 776 private ServletHandlerMethodInvoker(org.springframework.web.bind.annotation.support.HandlerMethodResolver resolver) { 777 super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer, 778 customArgumentResolvers, messageConverters); 779 } 780 781 @Override 782 protected void raiseMissingParameterException(String paramName, Class<?> paramType) throws Exception { 783 throw new MissingServletRequestParameterException(paramName, paramType.getSimpleName()); 784 } 785 786 @Override 787 protected void raiseSessionRequiredException(String message) throws Exception { 788 throw new HttpSessionRequiredException(message); 789 } 790 791 @Override 792 protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) 793 throws Exception { 794 795 return AnnotationMethodHandlerAdapter.this.createBinder( 796 webRequest.getNativeRequest(HttpServletRequest.class), target, objectName); 797 } 798 799 @Override 800 protected void doBind(WebDataBinder binder, NativeWebRequest webRequest) throws Exception { 801 ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; 802 servletBinder.bind(webRequest.getNativeRequest(ServletRequest.class)); 803 } 804 805 @Override 806 protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception { 807 HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); 808 return AnnotationMethodHandlerAdapter.this.createHttpInputMessage(servletRequest); 809 } 810 811 @Override 812 protected HttpOutputMessage createHttpOutputMessage(NativeWebRequest webRequest) throws Exception { 813 HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse(); 814 return AnnotationMethodHandlerAdapter.this.createHttpOutputMessage(servletResponse); 815 } 816 817 @Override 818 protected Object resolveDefaultValue(String value) { 819 if (beanFactory == null) { 820 return value; 821 } 822 String placeholdersResolved = beanFactory.resolveEmbeddedValue(value); 823 BeanExpressionResolver exprResolver = beanFactory.getBeanExpressionResolver(); 824 if (exprResolver == null) { 825 return value; 826 } 827 return exprResolver.evaluate(placeholdersResolved, expressionContext); 828 } 829 830 @Override 831 protected Object resolveCookieValue(String cookieName, Class<?> paramType, NativeWebRequest webRequest) 832 throws Exception { 833 834 HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); 835 Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName); 836 if (Cookie.class.isAssignableFrom(paramType)) { 837 return cookieValue; 838 } 839 else if (cookieValue != null) { 840 return urlPathHelper.decodeRequestString(servletRequest, cookieValue.getValue()); 841 } 842 else { 843 return null; 844 } 845 } 846 847 @Override 848 @SuppressWarnings({"unchecked"}) 849 protected String resolvePathVariable(String pathVarName, Class<?> paramType, NativeWebRequest webRequest) 850 throws Exception { 851 852 HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); 853 Map<String, String> uriTemplateVariables = 854 (Map<String, String>) servletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); 855 if (uriTemplateVariables == null || !uriTemplateVariables.containsKey(pathVarName)) { 856 throw new IllegalStateException( 857 "Could not find @PathVariable [" + pathVarName + "] in @RequestMapping"); 858 } 859 return uriTemplateVariables.get(pathVarName); 860 } 861 862 @Override 863 protected Object resolveStandardArgument(Class<?> parameterType, NativeWebRequest webRequest) throws Exception { 864 HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); 865 HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); 866 867 if (ServletRequest.class.isAssignableFrom(parameterType) || 868 MultipartRequest.class.isAssignableFrom(parameterType)) { 869 Object nativeRequest = webRequest.getNativeRequest(parameterType); 870 if (nativeRequest == null) { 871 throw new IllegalStateException( 872 "Current request is not of type [" + parameterType.getName() + "]: " + request); 873 } 874 return nativeRequest; 875 } 876 else if (ServletResponse.class.isAssignableFrom(parameterType)) { 877 this.responseArgumentUsed = true; 878 Object nativeResponse = webRequest.getNativeResponse(parameterType); 879 if (nativeResponse == null) { 880 throw new IllegalStateException( 881 "Current response is not of type [" + parameterType.getName() + "]: " + response); 882 } 883 return nativeResponse; 884 } 885 else if (HttpSession.class.isAssignableFrom(parameterType)) { 886 return request.getSession(); 887 } 888 else if (Principal.class.isAssignableFrom(parameterType)) { 889 return request.getUserPrincipal(); 890 } 891 else if (Locale.class == parameterType) { 892 return RequestContextUtils.getLocale(request); 893 } 894 else if (InputStream.class.isAssignableFrom(parameterType)) { 895 return request.getInputStream(); 896 } 897 else if (Reader.class.isAssignableFrom(parameterType)) { 898 return request.getReader(); 899 } 900 else if (OutputStream.class.isAssignableFrom(parameterType)) { 901 this.responseArgumentUsed = true; 902 return response.getOutputStream(); 903 } 904 else if (Writer.class.isAssignableFrom(parameterType)) { 905 this.responseArgumentUsed = true; 906 return response.getWriter(); 907 } 908 return super.resolveStandardArgument(parameterType, webRequest); 909 } 910 911 @SuppressWarnings("unchecked") 912 public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue, 913 ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception { 914 915 ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class); 916 if (responseStatus != null) { 917 HttpStatus statusCode = responseStatus.code(); 918 String reason = responseStatus.reason(); 919 if (!StringUtils.hasText(reason)) { 920 webRequest.getResponse().setStatus(statusCode.value()); 921 } 922 else { 923 webRequest.getResponse().sendError(statusCode.value(), reason); 924 } 925 926 // to be picked up by the RedirectView 927 webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, statusCode); 928 929 this.responseArgumentUsed = true; 930 } 931 932 // Invoke custom resolvers if present... 933 if (customModelAndViewResolvers != null) { 934 for (ModelAndViewResolver mavResolver : customModelAndViewResolvers) { 935 ModelAndView mav = mavResolver.resolveModelAndView( 936 handlerMethod, handlerType, returnValue, implicitModel, webRequest); 937 if (mav != ModelAndViewResolver.UNRESOLVED) { 938 return mav; 939 } 940 } 941 } 942 943 if (returnValue instanceof HttpEntity) { 944 handleHttpEntityResponse((HttpEntity<?>) returnValue, webRequest); 945 return null; 946 } 947 else if (AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class) != null) { 948 handleResponseBody(returnValue, webRequest); 949 return null; 950 } 951 else if (returnValue instanceof ModelAndView) { 952 ModelAndView mav = (ModelAndView) returnValue; 953 mav.getModelMap().mergeAttributes(implicitModel); 954 return mav; 955 } 956 else if (returnValue instanceof Model) { 957 return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap()); 958 } 959 else if (returnValue instanceof View) { 960 return new ModelAndView((View) returnValue).addAllObjects(implicitModel); 961 } 962 else if (AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class) != null) { 963 addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel); 964 return new ModelAndView().addAllObjects(implicitModel); 965 } 966 else if (returnValue instanceof Map) { 967 return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map<String, ?>) returnValue); 968 } 969 else if (returnValue instanceof String) { 970 return new ModelAndView((String) returnValue).addAllObjects(implicitModel); 971 } 972 else if (returnValue == null) { 973 // Either returned null or was 'void' return. 974 if (this.responseArgumentUsed || webRequest.isNotModified()) { 975 return null; 976 } 977 else { 978 // Assuming view name translation... 979 return new ModelAndView().addAllObjects(implicitModel); 980 } 981 } 982 else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) { 983 // Assume a single model attribute... 984 addReturnValueAsModelAttribute(handlerMethod, handlerType, returnValue, implicitModel); 985 return new ModelAndView().addAllObjects(implicitModel); 986 } 987 else { 988 throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); 989 } 990 } 991 992 private void handleResponseBody(Object returnValue, ServletWebRequest webRequest) throws Exception { 993 if (returnValue == null) { 994 return; 995 } 996 HttpInputMessage inputMessage = createHttpInputMessage(webRequest); 997 HttpOutputMessage outputMessage = createHttpOutputMessage(webRequest); 998 writeWithMessageConverters(returnValue, inputMessage, outputMessage); 999 } 1000 1001 private void handleHttpEntityResponse(HttpEntity<?> responseEntity, ServletWebRequest webRequest) throws Exception { 1002 if (responseEntity == null) { 1003 return; 1004 } 1005 HttpInputMessage inputMessage = createHttpInputMessage(webRequest); 1006 HttpOutputMessage outputMessage = createHttpOutputMessage(webRequest); 1007 if (responseEntity instanceof ResponseEntity && outputMessage instanceof ServerHttpResponse) { 1008 ((ServerHttpResponse) outputMessage).setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode()); 1009 } 1010 HttpHeaders entityHeaders = responseEntity.getHeaders(); 1011 if (!entityHeaders.isEmpty()) { 1012 outputMessage.getHeaders().putAll(entityHeaders); 1013 } 1014 Object body = responseEntity.getBody(); 1015 if (body != null) { 1016 writeWithMessageConverters(body, inputMessage, outputMessage); 1017 } 1018 else { 1019 // flush headers 1020 outputMessage.getBody(); 1021 } 1022 } 1023 1024 @SuppressWarnings({ "unchecked", "rawtypes" }) 1025 private void writeWithMessageConverters(Object returnValue, 1026 HttpInputMessage inputMessage, HttpOutputMessage outputMessage) 1027 throws IOException, HttpMediaTypeNotAcceptableException { 1028 1029 List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept(); 1030 if (acceptedMediaTypes.isEmpty()) { 1031 acceptedMediaTypes = Collections.singletonList(MediaType.ALL); 1032 } 1033 MediaType.sortByQualityValue(acceptedMediaTypes); 1034 Class<?> returnValueType = returnValue.getClass(); 1035 List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>(); 1036 if (getMessageConverters() != null) { 1037 for (MediaType acceptedMediaType : acceptedMediaTypes) { 1038 for (HttpMessageConverter messageConverter : getMessageConverters()) { 1039 if (messageConverter.canWrite(returnValueType, acceptedMediaType)) { 1040 messageConverter.write(returnValue, acceptedMediaType, outputMessage); 1041 if (logger.isDebugEnabled()) { 1042 MediaType contentType = outputMessage.getHeaders().getContentType(); 1043 if (contentType == null) { 1044 contentType = acceptedMediaType; 1045 } 1046 logger.debug("Written [" + returnValue + "] as \"" + contentType + 1047 "\" using [" + messageConverter + "]"); 1048 } 1049 this.responseArgumentUsed = true; 1050 return; 1051 } 1052 } 1053 } 1054 for (HttpMessageConverter messageConverter : messageConverters) { 1055 allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); 1056 } 1057 } 1058 throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes); 1059 } 1060 } 1061 1062 1063 /** 1064 * Holder for request mapping metadata. 1065 */ 1066 static class RequestMappingInfo { 1067 1068 private final String[] patterns; 1069 1070 private final RequestMethod[] methods; 1071 1072 private final String[] params; 1073 1074 private final String[] headers; 1075 1076 RequestMappingInfo(String[] patterns, RequestMethod[] methods, String[] params, String[] headers) { 1077 this.patterns = (patterns != null ? patterns : new String[0]); 1078 this.methods = (methods != null ? methods : new RequestMethod[0]); 1079 this.params = (params != null ? params : new String[0]); 1080 this.headers = (headers != null ? headers : new String[0]); 1081 } 1082 1083 public boolean hasPatterns() { 1084 return (this.patterns.length > 0); 1085 } 1086 1087 public String[] getPatterns() { 1088 return this.patterns; 1089 } 1090 1091 public int getMethodCount() { 1092 return this.methods.length; 1093 } 1094 1095 public int getParamCount() { 1096 return this.params.length; 1097 } 1098 1099 public int getHeaderCount() { 1100 return this.headers.length; 1101 } 1102 1103 public boolean matches(HttpServletRequest request) { 1104 return matchesRequestMethod(request) && matchesParameters(request) && matchesHeaders(request); 1105 } 1106 1107 public boolean matchesHeaders(HttpServletRequest request) { 1108 return ServletAnnotationMappingUtils.checkHeaders(this.headers, request); 1109 } 1110 1111 public boolean matchesParameters(HttpServletRequest request) { 1112 return ServletAnnotationMappingUtils.checkParameters(this.params, request); 1113 } 1114 1115 public boolean matchesRequestMethod(HttpServletRequest request) { 1116 return ServletAnnotationMappingUtils.checkRequestMethod(this.methods, request); 1117 } 1118 1119 public Set<String> methodNames() { 1120 Set<String> methodNames = new LinkedHashSet<String>(this.methods.length); 1121 for (RequestMethod method : this.methods) { 1122 methodNames.add(method.name()); 1123 } 1124 return methodNames; 1125 } 1126 1127 @Override 1128 public boolean equals(Object obj) { 1129 RequestMappingInfo other = (RequestMappingInfo) obj; 1130 return (Arrays.equals(this.patterns, other.patterns) && Arrays.equals(this.methods, other.methods) && 1131 Arrays.equals(this.params, other.params) && Arrays.equals(this.headers, other.headers)); 1132 } 1133 1134 @Override 1135 public int hashCode() { 1136 return (Arrays.hashCode(this.patterns) * 23 + Arrays.hashCode(this.methods) * 29 + 1137 Arrays.hashCode(this.params) * 31 + Arrays.hashCode(this.headers)); 1138 } 1139 1140 @Override 1141 public String toString() { 1142 StringBuilder builder = new StringBuilder(); 1143 builder.append(Arrays.asList(this.patterns)); 1144 if (this.methods.length > 0) { 1145 builder.append(','); 1146 builder.append(Arrays.asList(this.methods)); 1147 } 1148 if (this.headers.length > 0) { 1149 builder.append(','); 1150 builder.append(Arrays.asList(this.headers)); 1151 } 1152 if (this.params.length > 0) { 1153 builder.append(','); 1154 builder.append(Arrays.asList(this.params)); 1155 } 1156 return builder.toString(); 1157 } 1158 } 1159 1160 1161 /** 1162 * Subclass of {@link RequestMappingInfo} that holds request-specific data. 1163 */ 1164 static class RequestSpecificMappingInfo extends RequestMappingInfo { 1165 1166 private final List<String> matchedPatterns = new ArrayList<String>(); 1167 1168 RequestSpecificMappingInfo(String[] patterns, RequestMethod[] methods, String[] params, String[] headers) { 1169 super(patterns, methods, params, headers); 1170 } 1171 1172 RequestSpecificMappingInfo(RequestMappingInfo other) { 1173 super(other.patterns, other.methods, other.params, other.headers); 1174 } 1175 1176 public void addMatchedPattern(String matchedPattern) { 1177 matchedPatterns.add(matchedPattern); 1178 } 1179 1180 public void sortMatchedPatterns(Comparator<String> pathComparator) { 1181 Collections.sort(matchedPatterns, pathComparator); 1182 } 1183 1184 public String bestMatchedPattern() { 1185 return (!this.matchedPatterns.isEmpty() ? this.matchedPatterns.get(0) : null); 1186 } 1187 } 1188 1189 1190 /** 1191 * Comparator capable of sorting {@link RequestSpecificMappingInfo}s (RHIs) so that 1192 * sorting a list with this comparator will result in: 1193 * <ul> 1194 * <li>RHIs with {@linkplain AnnotationMethodHandlerAdapter.RequestSpecificMappingInfo#matchedPatterns better matched paths} 1195 * take precedence over those with a weaker match (as expressed by the {@linkplain PathMatcher#getPatternComparator(String) 1196 * path pattern comparator}.) Typically, this means that patterns without wild cards and uri templates 1197 * will be ordered before those without.</li> 1198 * <li>RHIs with one single {@linkplain RequestMappingInfo#methods request method} will be 1199 * ordered before those without a method, or with more than one method.</li> 1200 * <li>RHIs with more {@linkplain RequestMappingInfo#params request parameters} will be ordered 1201 * before those with less parameters</li> 1202 * </ol> 1203 */ 1204 static class RequestSpecificMappingInfoComparator implements Comparator<RequestSpecificMappingInfo> { 1205 1206 private final Comparator<String> pathComparator; 1207 1208 private final ServerHttpRequest request; 1209 1210 RequestSpecificMappingInfoComparator(Comparator<String> pathComparator, HttpServletRequest request) { 1211 this.pathComparator = pathComparator; 1212 this.request = new ServletServerHttpRequest(request); 1213 } 1214 1215 @Override 1216 public int compare(RequestSpecificMappingInfo info1, RequestSpecificMappingInfo info2) { 1217 int pathComparison = pathComparator.compare(info1.bestMatchedPattern(), info2.bestMatchedPattern()); 1218 if (pathComparison != 0) { 1219 return pathComparison; 1220 } 1221 int info1ParamCount = info1.getParamCount(); 1222 int info2ParamCount = info2.getParamCount(); 1223 if (info1ParamCount != info2ParamCount) { 1224 return info2ParamCount - info1ParamCount; 1225 } 1226 int info1HeaderCount = info1.getHeaderCount(); 1227 int info2HeaderCount = info2.getHeaderCount(); 1228 if (info1HeaderCount != info2HeaderCount) { 1229 return info2HeaderCount - info1HeaderCount; 1230 } 1231 int acceptComparison = compareAcceptHeaders(info1, info2); 1232 if (acceptComparison != 0) { 1233 return acceptComparison; 1234 } 1235 int info1MethodCount = info1.getMethodCount(); 1236 int info2MethodCount = info2.getMethodCount(); 1237 if (info1MethodCount == 0 && info2MethodCount > 0) { 1238 return 1; 1239 } 1240 else if (info2MethodCount == 0 && info1MethodCount > 0) { 1241 return -1; 1242 } 1243 else if (info1MethodCount == 1 & info2MethodCount > 1) { 1244 return -1; 1245 } 1246 else if (info2MethodCount == 1 & info1MethodCount > 1) { 1247 return 1; 1248 } 1249 return 0; 1250 } 1251 1252 private int compareAcceptHeaders(RequestMappingInfo info1, RequestMappingInfo info2) { 1253 List<MediaType> requestAccepts = request.getHeaders().getAccept(); 1254 MediaType.sortByQualityValue(requestAccepts); 1255 1256 List<MediaType> info1Accepts = getAcceptHeaderValue(info1); 1257 List<MediaType> info2Accepts = getAcceptHeaderValue(info2); 1258 1259 for (MediaType requestAccept : requestAccepts) { 1260 int pos1 = indexOfIncluded(info1Accepts, requestAccept); 1261 int pos2 = indexOfIncluded(info2Accepts, requestAccept); 1262 if (pos1 != pos2) { 1263 return pos2 - pos1; 1264 } 1265 } 1266 return 0; 1267 } 1268 1269 private int indexOfIncluded(List<MediaType> infoAccepts, MediaType requestAccept) { 1270 for (int i = 0; i < infoAccepts.size(); i++) { 1271 MediaType info1Accept = infoAccepts.get(i); 1272 if (requestAccept.includes(info1Accept)) { 1273 return i; 1274 } 1275 } 1276 return -1; 1277 } 1278 1279 private List<MediaType> getAcceptHeaderValue(RequestMappingInfo info) { 1280 for (String header : info.headers) { 1281 int separator = header.indexOf('='); 1282 if (separator != -1) { 1283 String key = header.substring(0, separator); 1284 String value = header.substring(separator + 1); 1285 if ("Accept".equalsIgnoreCase(key)) { 1286 return MediaType.parseMediaTypes(value); 1287 } 1288 } 1289 } 1290 return Collections.emptyList(); 1291 } 1292 } 1293 1294}