001/* 002 * Copyright 2002-2018 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.method; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.LinkedHashMap; 024import java.util.LinkedHashSet; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import javax.servlet.ServletException; 029import javax.servlet.http.HttpServletRequest; 030 031import org.springframework.http.HttpHeaders; 032import org.springframework.http.HttpMethod; 033import org.springframework.http.InvalidMediaTypeException; 034import org.springframework.http.MediaType; 035import org.springframework.util.CollectionUtils; 036import org.springframework.util.MultiValueMap; 037import org.springframework.util.StringUtils; 038import org.springframework.web.HttpMediaTypeNotAcceptableException; 039import org.springframework.web.HttpMediaTypeNotSupportedException; 040import org.springframework.web.HttpRequestMethodNotSupportedException; 041import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; 042import org.springframework.web.bind.annotation.RequestMethod; 043import org.springframework.web.method.HandlerMethod; 044import org.springframework.web.servlet.HandlerMapping; 045import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; 046import org.springframework.web.servlet.mvc.condition.NameValueExpression; 047import org.springframework.web.util.WebUtils; 048 049/** 050 * Abstract base class for classes for which {@link RequestMappingInfo} defines 051 * the mapping between a request and a handler method. 052 * 053 * @author Arjen Poutsma 054 * @author Rossen Stoyanchev 055 * @since 3.1 056 */ 057public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> { 058 059 private static final Method HTTP_OPTIONS_HANDLE_METHOD; 060 061 static { 062 try { 063 HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle"); 064 } 065 catch (NoSuchMethodException ex) { 066 // Should never happen 067 throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS", ex); 068 } 069 } 070 071 072 protected RequestMappingInfoHandlerMapping() { 073 setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy()); 074 } 075 076 077 /** 078 * Get the URL path patterns associated with this {@link RequestMappingInfo}. 079 */ 080 @Override 081 protected Set<String> getMappingPathPatterns(RequestMappingInfo info) { 082 return info.getPatternsCondition().getPatterns(); 083 } 084 085 /** 086 * Check if the given RequestMappingInfo matches the current request and 087 * return a (potentially new) instance with conditions that match the 088 * current request -- for example with a subset of URL patterns. 089 * @return an info in case of a match; or {@code null} otherwise. 090 */ 091 @Override 092 protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) { 093 return info.getMatchingCondition(request); 094 } 095 096 /** 097 * Provide a Comparator to sort RequestMappingInfos matched to a request. 098 */ 099 @Override 100 protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) { 101 return new Comparator<RequestMappingInfo>() { 102 @Override 103 public int compare(RequestMappingInfo info1, RequestMappingInfo info2) { 104 return info1.compareTo(info2, request); 105 } 106 }; 107 } 108 109 /** 110 * Expose URI template variables, matrix variables, and producible media types in the request. 111 * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE 112 * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE 113 * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE 114 */ 115 @Override 116 protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) { 117 super.handleMatch(info, lookupPath, request); 118 119 String bestPattern; 120 Map<String, String> uriVariables; 121 Map<String, String> decodedUriVariables; 122 123 Set<String> patterns = info.getPatternsCondition().getPatterns(); 124 if (patterns.isEmpty()) { 125 bestPattern = lookupPath; 126 uriVariables = Collections.emptyMap(); 127 decodedUriVariables = Collections.emptyMap(); 128 } 129 else { 130 bestPattern = patterns.iterator().next(); 131 uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); 132 decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables); 133 } 134 135 request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); 136 request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables); 137 138 if (isMatrixVariableContentAvailable()) { 139 Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables); 140 request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars); 141 } 142 143 if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { 144 Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); 145 request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); 146 } 147 } 148 149 private boolean isMatrixVariableContentAvailable() { 150 return !getUrlPathHelper().shouldRemoveSemicolonContent(); 151 } 152 153 private Map<String, MultiValueMap<String, String>> extractMatrixVariables( 154 HttpServletRequest request, Map<String, String> uriVariables) { 155 156 Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<String, MultiValueMap<String, String>>(); 157 for (Map.Entry<String, String> uriVar : uriVariables.entrySet()) { 158 String uriVarValue = uriVar.getValue(); 159 160 int equalsIndex = uriVarValue.indexOf('='); 161 if (equalsIndex == -1) { 162 continue; 163 } 164 165 String matrixVariables; 166 167 int semicolonIndex = uriVarValue.indexOf(';'); 168 if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) { 169 matrixVariables = uriVarValue; 170 } 171 else { 172 matrixVariables = uriVarValue.substring(semicolonIndex + 1); 173 uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex)); 174 } 175 176 MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables); 177 result.put(uriVar.getKey(), getUrlPathHelper().decodeMatrixVariables(request, vars)); 178 } 179 return result; 180 } 181 182 /** 183 * Iterate all RequestMappingInfo's once again, look if any match by URL at 184 * least and raise exceptions according to what doesn't match. 185 * @throws HttpRequestMethodNotSupportedException if there are matches by URL 186 * but not by HTTP method 187 * @throws HttpMediaTypeNotAcceptableException if there are matches by URL 188 * but not by consumable/producible media types 189 */ 190 @Override 191 protected HandlerMethod handleNoMatch( 192 Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException { 193 194 PartialMatchHelper helper = new PartialMatchHelper(infos, request); 195 if (helper.isEmpty()) { 196 return null; 197 } 198 199 if (helper.hasMethodsMismatch()) { 200 Set<String> methods = helper.getAllowedMethods(); 201 if (HttpMethod.OPTIONS.matches(request.getMethod())) { 202 HttpOptionsHandler handler = new HttpOptionsHandler(methods); 203 return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); 204 } 205 throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods); 206 } 207 208 if (helper.hasConsumesMismatch()) { 209 Set<MediaType> mediaTypes = helper.getConsumableMediaTypes(); 210 MediaType contentType = null; 211 if (StringUtils.hasLength(request.getContentType())) { 212 try { 213 contentType = MediaType.parseMediaType(request.getContentType()); 214 } 215 catch (InvalidMediaTypeException ex) { 216 throw new HttpMediaTypeNotSupportedException(ex.getMessage()); 217 } 218 } 219 throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(mediaTypes)); 220 } 221 222 if (helper.hasProducesMismatch()) { 223 Set<MediaType> mediaTypes = helper.getProducibleMediaTypes(); 224 throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(mediaTypes)); 225 } 226 227 if (helper.hasParamsMismatch()) { 228 List<String[]> conditions = helper.getParamConditions(); 229 throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap()); 230 } 231 232 return null; 233 } 234 235 236 /** 237 * Aggregate all partial matches and expose methods checking across them. 238 */ 239 private static class PartialMatchHelper { 240 241 private final List<PartialMatch> partialMatches = new ArrayList<PartialMatch>(); 242 243 public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) { 244 for (RequestMappingInfo info : infos) { 245 if (info.getPatternsCondition().getMatchingCondition(request) != null) { 246 this.partialMatches.add(new PartialMatch(info, request)); 247 } 248 } 249 } 250 251 /** 252 * Whether there any partial matches. 253 */ 254 public boolean isEmpty() { 255 return this.partialMatches.isEmpty(); 256 } 257 258 /** 259 * Any partial matches for "methods"? 260 */ 261 public boolean hasMethodsMismatch() { 262 for (PartialMatch match : this.partialMatches) { 263 if (match.hasMethodsMatch()) { 264 return false; 265 } 266 } 267 return true; 268 } 269 270 /** 271 * Any partial matches for "methods" and "consumes"? 272 */ 273 public boolean hasConsumesMismatch() { 274 for (PartialMatch match : this.partialMatches) { 275 if (match.hasConsumesMatch()) { 276 return false; 277 } 278 } 279 return true; 280 } 281 282 /** 283 * Any partial matches for "methods", "consumes", and "produces"? 284 */ 285 public boolean hasProducesMismatch() { 286 for (PartialMatch match : this.partialMatches) { 287 if (match.hasProducesMatch()) { 288 return false; 289 } 290 } 291 return true; 292 } 293 294 /** 295 * Any partial matches for "methods", "consumes", "produces", and "params"? 296 */ 297 public boolean hasParamsMismatch() { 298 for (PartialMatch match : this.partialMatches) { 299 if (match.hasParamsMatch()) { 300 return false; 301 } 302 } 303 return true; 304 } 305 306 /** 307 * Return declared HTTP methods. 308 */ 309 public Set<String> getAllowedMethods() { 310 Set<String> result = new LinkedHashSet<String>(); 311 for (PartialMatch match : this.partialMatches) { 312 for (RequestMethod method : match.getInfo().getMethodsCondition().getMethods()) { 313 result.add(method.name()); 314 } 315 } 316 return result; 317 } 318 319 /** 320 * Return declared "consumable" types but only among those that also 321 * match the "methods" condition. 322 */ 323 public Set<MediaType> getConsumableMediaTypes() { 324 Set<MediaType> result = new LinkedHashSet<MediaType>(); 325 for (PartialMatch match : this.partialMatches) { 326 if (match.hasMethodsMatch()) { 327 result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes()); 328 } 329 } 330 return result; 331 } 332 333 /** 334 * Return declared "producible" types but only among those that also 335 * match the "methods" and "consumes" conditions. 336 */ 337 public Set<MediaType> getProducibleMediaTypes() { 338 Set<MediaType> result = new LinkedHashSet<MediaType>(); 339 for (PartialMatch match : this.partialMatches) { 340 if (match.hasConsumesMatch()) { 341 result.addAll(match.getInfo().getProducesCondition().getProducibleMediaTypes()); 342 } 343 } 344 return result; 345 } 346 347 /** 348 * Return declared "params" conditions but only among those that also 349 * match the "methods", "consumes", and "params" conditions. 350 */ 351 public List<String[]> getParamConditions() { 352 List<String[]> result = new ArrayList<String[]>(); 353 for (PartialMatch match : this.partialMatches) { 354 if (match.hasProducesMatch()) { 355 Set<NameValueExpression<String>> set = match.getInfo().getParamsCondition().getExpressions(); 356 if (!CollectionUtils.isEmpty(set)) { 357 int i = 0; 358 String[] array = new String[set.size()]; 359 for (NameValueExpression<String> expression : set) { 360 array[i++] = expression.toString(); 361 } 362 result.add(array); 363 } 364 } 365 } 366 return result; 367 } 368 369 370 /** 371 * Container for a RequestMappingInfo that matches the URL path at least. 372 */ 373 private static class PartialMatch { 374 375 private final RequestMappingInfo info; 376 377 private final boolean methodsMatch; 378 379 private final boolean consumesMatch; 380 381 private final boolean producesMatch; 382 383 private final boolean paramsMatch; 384 385 /** 386 * @param info RequestMappingInfo that matches the URL path. 387 * @param request the current request 388 */ 389 public PartialMatch(RequestMappingInfo info, HttpServletRequest request) { 390 this.info = info; 391 this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null); 392 this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null); 393 this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null); 394 this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null); 395 } 396 397 public RequestMappingInfo getInfo() { 398 return this.info; 399 } 400 401 public boolean hasMethodsMatch() { 402 return this.methodsMatch; 403 } 404 405 public boolean hasConsumesMatch() { 406 return (hasMethodsMatch() && this.consumesMatch); 407 } 408 409 public boolean hasProducesMatch() { 410 return (hasConsumesMatch() && this.producesMatch); 411 } 412 413 public boolean hasParamsMatch() { 414 return (hasProducesMatch() && this.paramsMatch); 415 } 416 417 @Override 418 public String toString() { 419 return this.info.toString(); 420 } 421 } 422 } 423 424 425 /** 426 * Default handler for HTTP OPTIONS. 427 */ 428 private static class HttpOptionsHandler { 429 430 private final HttpHeaders headers = new HttpHeaders(); 431 432 public HttpOptionsHandler(Set<String> declaredMethods) { 433 this.headers.setAllow(initAllowedHttpMethods(declaredMethods)); 434 } 435 436 private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) { 437 Set<HttpMethod> result = new LinkedHashSet<HttpMethod>(declaredMethods.size()); 438 if (declaredMethods.isEmpty()) { 439 for (HttpMethod method : HttpMethod.values()) { 440 if (method != HttpMethod.TRACE) { 441 result.add(method); 442 } 443 } 444 } 445 else { 446 for (String method : declaredMethods) { 447 HttpMethod httpMethod = HttpMethod.valueOf(method); 448 result.add(httpMethod); 449 if (httpMethod == HttpMethod.GET) { 450 result.add(HttpMethod.HEAD); 451 } 452 } 453 } 454 return result; 455 } 456 457 public HttpHeaders handle() { 458 return this.headers; 459 } 460 } 461 462}