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