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.annotation; 018 019import java.lang.reflect.AnnotatedElement; 020import java.lang.reflect.Method; 021import java.lang.reflect.Parameter; 022import java.util.Collections; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.function.Predicate; 028 029import javax.servlet.http.HttpServletRequest; 030 031import org.springframework.context.EmbeddedValueResolverAware; 032import org.springframework.core.annotation.AnnotatedElementUtils; 033import org.springframework.core.annotation.MergedAnnotation; 034import org.springframework.core.annotation.MergedAnnotations; 035import org.springframework.lang.Nullable; 036import org.springframework.stereotype.Controller; 037import org.springframework.util.Assert; 038import org.springframework.util.CollectionUtils; 039import org.springframework.util.StringValueResolver; 040import org.springframework.web.accept.ContentNegotiationManager; 041import org.springframework.web.bind.annotation.CrossOrigin; 042import org.springframework.web.bind.annotation.RequestBody; 043import org.springframework.web.bind.annotation.RequestMapping; 044import org.springframework.web.bind.annotation.RequestMethod; 045import org.springframework.web.cors.CorsConfiguration; 046import org.springframework.web.method.HandlerMethod; 047import org.springframework.web.servlet.handler.MatchableHandlerMapping; 048import org.springframework.web.servlet.handler.RequestMatchResult; 049import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; 050import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; 051import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; 052import org.springframework.web.servlet.mvc.condition.RequestCondition; 053import org.springframework.web.servlet.mvc.method.RequestMappingInfo; 054import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; 055 056/** 057 * Creates {@link RequestMappingInfo} instances from type and method-level 058 * {@link RequestMapping @RequestMapping} annotations in 059 * {@link Controller @Controller} classes. 060 * 061 * <p><strong>Deprecation Note:</strong></p> In 5.2.4, 062 * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch} and 063 * {@link #setUseRegisteredSuffixPatternMatch(boolean) useRegisteredSuffixPatternMatch} 064 * are deprecated in order to discourage use of path extensions for request 065 * mapping and for content negotiation (with similar deprecations in 066 * {@link org.springframework.web.accept.ContentNegotiationManagerFactoryBean 067 * ContentNegotiationManagerFactoryBean}). For further context, please read issue 068 * <a href="https://github.com/spring-projects/spring-framework/issues/24179">#24719</a>. 069 * 070 * @author Arjen Poutsma 071 * @author Rossen Stoyanchev 072 * @author Sam Brannen 073 * @since 3.1 074 */ 075public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping 076 implements MatchableHandlerMapping, EmbeddedValueResolverAware { 077 078 private boolean useSuffixPatternMatch = true; 079 080 private boolean useRegisteredSuffixPatternMatch = false; 081 082 private boolean useTrailingSlashMatch = true; 083 084 private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>(); 085 086 private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); 087 088 @Nullable 089 private StringValueResolver embeddedValueResolver; 090 091 private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); 092 093 094 /** 095 * Whether to use suffix pattern match (".*") when matching patterns to 096 * requests. If enabled a method mapped to "/users" also matches to "/users.*". 097 * <p>The default value is {@code true}. 098 * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for 099 * more fine-grained control over specific suffixes to allow. 100 * @deprecated as of 5.2.4. See class level comment about deprecation of 101 * path extension config options. As there is no replacement for this method, 102 * for the time being it's necessary to set it to {@code false}. In 5.3 103 * when {@code false} becomes the default, use of this property will no 104 * longer be necessary. 105 */ 106 @Deprecated 107 public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { 108 this.useSuffixPatternMatch = useSuffixPatternMatch; 109 } 110 111 /** 112 * Whether suffix pattern matching should work only against path extensions 113 * explicitly registered with the {@link ContentNegotiationManager}. This 114 * is generally recommended to reduce ambiguity and to avoid issues such as 115 * when a "." appears in the path for other reasons. 116 * <p>By default this is set to "false". 117 * @deprecated as of 5.2.4. See class level comment about deprecation of 118 * path extension config options. 119 */ 120 @Deprecated 121 public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { 122 this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; 123 this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch); 124 } 125 126 /** 127 * Whether to match to URLs irrespective of the presence of a trailing slash. 128 * If enabled a method mapped to "/users" also matches to "/users/". 129 * <p>The default value is {@code true}. 130 */ 131 public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { 132 this.useTrailingSlashMatch = useTrailingSlashMatch; 133 } 134 135 /** 136 * Configure path prefixes to apply to controller methods. 137 * <p>Prefixes are used to enrich the mappings of every {@code @RequestMapping} 138 * method whose controller type is matched by the corresponding 139 * {@code Predicate}. The prefix for the first matching predicate is used. 140 * <p>Consider using {@link org.springframework.web.method.HandlerTypePredicate 141 * HandlerTypePredicate} to group controllers. 142 * @param prefixes a map with path prefixes as key 143 * @since 5.1 144 */ 145 public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) { 146 this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)); 147 } 148 149 /** 150 * The configured path prefixes as a read-only, possibly empty map. 151 * @since 5.1 152 */ 153 public Map<String, Predicate<Class<?>>> getPathPrefixes() { 154 return this.pathPrefixes; 155 } 156 157 /** 158 * Set the {@link ContentNegotiationManager} to use to determine requested media types. 159 * If not set, the default constructor is used. 160 */ 161 public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { 162 Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null"); 163 this.contentNegotiationManager = contentNegotiationManager; 164 } 165 166 /** 167 * Return the configured {@link ContentNegotiationManager}. 168 */ 169 public ContentNegotiationManager getContentNegotiationManager() { 170 return this.contentNegotiationManager; 171 } 172 173 @Override 174 public void setEmbeddedValueResolver(StringValueResolver resolver) { 175 this.embeddedValueResolver = resolver; 176 } 177 178 @Override 179 @SuppressWarnings("deprecation") 180 public void afterPropertiesSet() { 181 this.config = new RequestMappingInfo.BuilderConfiguration(); 182 this.config.setUrlPathHelper(getUrlPathHelper()); 183 this.config.setPathMatcher(getPathMatcher()); 184 this.config.setSuffixPatternMatch(useSuffixPatternMatch()); 185 this.config.setTrailingSlashMatch(useTrailingSlashMatch()); 186 this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch()); 187 this.config.setContentNegotiationManager(getContentNegotiationManager()); 188 189 super.afterPropertiesSet(); 190 } 191 192 193 /** 194 * Whether to use registered suffixes for pattern matching. 195 * @deprecated as of 5.2.4. See class-level note on the deprecation of path 196 * extension config options. 197 */ 198 @Deprecated 199 public boolean useSuffixPatternMatch() { 200 return this.useSuffixPatternMatch; 201 } 202 203 /** 204 * Whether to use registered suffixes for pattern matching. 205 * @deprecated as of 5.2.4. See class-level note on the deprecation of path 206 * extension config options. 207 */ 208 @Deprecated 209 public boolean useRegisteredSuffixPatternMatch() { 210 return this.useRegisteredSuffixPatternMatch; 211 } 212 213 /** 214 * Whether to match to URLs irrespective of the presence of a trailing slash. 215 */ 216 public boolean useTrailingSlashMatch() { 217 return this.useTrailingSlashMatch; 218 } 219 220 /** 221 * Return the file extensions to use for suffix pattern matching. 222 * @deprecated as of 5.2.4. See class-level note on the deprecation of path 223 * extension config options. 224 */ 225 @Nullable 226 @Deprecated 227 @SuppressWarnings("deprecation") 228 public List<String> getFileExtensions() { 229 return this.config.getFileExtensions(); 230 } 231 232 233 /** 234 * {@inheritDoc} 235 * <p>Expects a handler to have either a type-level @{@link Controller} 236 * annotation or a type-level @{@link RequestMapping} annotation. 237 */ 238 @Override 239 protected boolean isHandler(Class<?> beanType) { 240 return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || 241 AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); 242 } 243 244 /** 245 * Uses method and type-level @{@link RequestMapping} annotations to create 246 * the RequestMappingInfo. 247 * @return the created RequestMappingInfo, or {@code null} if the method 248 * does not have a {@code @RequestMapping} annotation. 249 * @see #getCustomMethodCondition(Method) 250 * @see #getCustomTypeCondition(Class) 251 */ 252 @Override 253 @Nullable 254 protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { 255 RequestMappingInfo info = createRequestMappingInfo(method); 256 if (info != null) { 257 RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); 258 if (typeInfo != null) { 259 info = typeInfo.combine(info); 260 } 261 String prefix = getPathPrefix(handlerType); 262 if (prefix != null) { 263 info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info); 264 } 265 } 266 return info; 267 } 268 269 @Nullable 270 String getPathPrefix(Class<?> handlerType) { 271 for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) { 272 if (entry.getValue().test(handlerType)) { 273 String prefix = entry.getKey(); 274 if (this.embeddedValueResolver != null) { 275 prefix = this.embeddedValueResolver.resolveStringValue(prefix); 276 } 277 return prefix; 278 } 279 } 280 return null; 281 } 282 283 /** 284 * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, 285 * supplying the appropriate custom {@link RequestCondition} depending on whether 286 * the supplied {@code annotatedElement} is a class or method. 287 * @see #getCustomTypeCondition(Class) 288 * @see #getCustomMethodCondition(Method) 289 */ 290 @Nullable 291 private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { 292 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); 293 RequestCondition<?> condition = (element instanceof Class ? 294 getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); 295 return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); 296 } 297 298 /** 299 * Provide a custom type-level request condition. 300 * The custom {@link RequestCondition} can be of any type so long as the 301 * same condition type is returned from all calls to this method in order 302 * to ensure custom request conditions can be combined and compared. 303 * <p>Consider extending {@link AbstractRequestCondition} for custom 304 * condition types and using {@link CompositeRequestCondition} to provide 305 * multiple custom conditions. 306 * @param handlerType the handler type for which to create the condition 307 * @return the condition, or {@code null} 308 */ 309 @Nullable 310 protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { 311 return null; 312 } 313 314 /** 315 * Provide a custom method-level request condition. 316 * The custom {@link RequestCondition} can be of any type so long as the 317 * same condition type is returned from all calls to this method in order 318 * to ensure custom request conditions can be combined and compared. 319 * <p>Consider extending {@link AbstractRequestCondition} for custom 320 * condition types and using {@link CompositeRequestCondition} to provide 321 * multiple custom conditions. 322 * @param method the handler method for which to create the condition 323 * @return the condition, or {@code null} 324 */ 325 @Nullable 326 protected RequestCondition<?> getCustomMethodCondition(Method method) { 327 return null; 328 } 329 330 /** 331 * Create a {@link RequestMappingInfo} from the supplied 332 * {@link RequestMapping @RequestMapping} annotation, which is either 333 * a directly declared annotation, a meta-annotation, or the synthesized 334 * result of merging annotation attributes within an annotation hierarchy. 335 */ 336 protected RequestMappingInfo createRequestMappingInfo( 337 RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) { 338 339 RequestMappingInfo.Builder builder = RequestMappingInfo 340 .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) 341 .methods(requestMapping.method()) 342 .params(requestMapping.params()) 343 .headers(requestMapping.headers()) 344 .consumes(requestMapping.consumes()) 345 .produces(requestMapping.produces()) 346 .mappingName(requestMapping.name()); 347 if (customCondition != null) { 348 builder.customCondition(customCondition); 349 } 350 return builder.options(this.config).build(); 351 } 352 353 /** 354 * Resolve placeholder values in the given array of patterns. 355 * @return a new array with updated patterns 356 */ 357 protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) { 358 if (this.embeddedValueResolver == null) { 359 return patterns; 360 } 361 else { 362 String[] resolvedPatterns = new String[patterns.length]; 363 for (int i = 0; i < patterns.length; i++) { 364 resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]); 365 } 366 return resolvedPatterns; 367 } 368 } 369 370 @Override 371 public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) { 372 super.registerMapping(mapping, handler, method); 373 updateConsumesCondition(mapping, method); 374 } 375 376 @Override 377 protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { 378 super.registerHandlerMethod(handler, method, mapping); 379 updateConsumesCondition(mapping, method); 380 } 381 382 private void updateConsumesCondition(RequestMappingInfo info, Method method) { 383 ConsumesRequestCondition condition = info.getConsumesCondition(); 384 if (!condition.isEmpty()) { 385 for (Parameter parameter : method.getParameters()) { 386 MergedAnnotation<RequestBody> annot = MergedAnnotations.from(parameter).get(RequestBody.class); 387 if (annot.isPresent()) { 388 condition.setBodyRequired(annot.getBoolean("required")); 389 break; 390 } 391 } 392 } 393 } 394 395 @Override 396 public RequestMatchResult match(HttpServletRequest request, String pattern) { 397 RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build(); 398 RequestMappingInfo matchingInfo = info.getMatchingCondition(request); 399 if (matchingInfo == null) { 400 return null; 401 } 402 Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns(); 403 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request, LOOKUP_PATH); 404 return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); 405 } 406 407 @Override 408 protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { 409 HandlerMethod handlerMethod = createHandlerMethod(handler, method); 410 Class<?> beanType = handlerMethod.getBeanType(); 411 CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class); 412 CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class); 413 414 if (typeAnnotation == null && methodAnnotation == null) { 415 return null; 416 } 417 418 CorsConfiguration config = new CorsConfiguration(); 419 updateCorsConfig(config, typeAnnotation); 420 updateCorsConfig(config, methodAnnotation); 421 422 if (CollectionUtils.isEmpty(config.getAllowedMethods())) { 423 for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { 424 config.addAllowedMethod(allowedMethod.name()); 425 } 426 } 427 return config.applyPermitDefaultValues(); 428 } 429 430 private void updateCorsConfig(CorsConfiguration config, @Nullable CrossOrigin annotation) { 431 if (annotation == null) { 432 return; 433 } 434 for (String origin : annotation.origins()) { 435 config.addAllowedOrigin(resolveCorsAnnotationValue(origin)); 436 } 437 for (RequestMethod method : annotation.methods()) { 438 config.addAllowedMethod(method.name()); 439 } 440 for (String header : annotation.allowedHeaders()) { 441 config.addAllowedHeader(resolveCorsAnnotationValue(header)); 442 } 443 for (String header : annotation.exposedHeaders()) { 444 config.addExposedHeader(resolveCorsAnnotationValue(header)); 445 } 446 447 String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials()); 448 if ("true".equalsIgnoreCase(allowCredentials)) { 449 config.setAllowCredentials(true); 450 } 451 else if ("false".equalsIgnoreCase(allowCredentials)) { 452 config.setAllowCredentials(false); 453 } 454 else if (!allowCredentials.isEmpty()) { 455 throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " + 456 "or an empty string (\"\"): current value is [" + allowCredentials + "]"); 457 } 458 459 if (annotation.maxAge() >= 0 && config.getMaxAge() == null) { 460 config.setMaxAge(annotation.maxAge()); 461 } 462 } 463 464 private String resolveCorsAnnotationValue(String value) { 465 if (this.embeddedValueResolver != null) { 466 String resolved = this.embeddedValueResolver.resolveStringValue(value); 467 return (resolved != null ? resolved : ""); 468 } 469 else { 470 return value; 471 } 472 } 473 474}