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.annotation; 018 019import java.lang.reflect.AnnotatedElement; 020import java.lang.reflect.Method; 021import java.util.List; 022import java.util.Set; 023import javax.servlet.http.HttpServletRequest; 024 025import org.springframework.context.EmbeddedValueResolverAware; 026import org.springframework.core.annotation.AnnotatedElementUtils; 027import org.springframework.stereotype.Controller; 028import org.springframework.util.Assert; 029import org.springframework.util.CollectionUtils; 030import org.springframework.util.StringValueResolver; 031import org.springframework.web.accept.ContentNegotiationManager; 032import org.springframework.web.bind.annotation.CrossOrigin; 033import org.springframework.web.bind.annotation.RequestMapping; 034import org.springframework.web.bind.annotation.RequestMethod; 035import org.springframework.web.cors.CorsConfiguration; 036import org.springframework.web.method.HandlerMethod; 037import org.springframework.web.servlet.handler.MatchableHandlerMapping; 038import org.springframework.web.servlet.handler.RequestMatchResult; 039import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; 040import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; 041import org.springframework.web.servlet.mvc.condition.RequestCondition; 042import org.springframework.web.servlet.mvc.method.RequestMappingInfo; 043import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; 044 045/** 046 * Creates {@link RequestMappingInfo} instances from type and method-level 047 * {@link RequestMapping @RequestMapping} annotations in 048 * {@link Controller @Controller} classes. 049 * 050 * @author Arjen Poutsma 051 * @author Rossen Stoyanchev 052 * @author Sam Brannen 053 * @since 3.1 054 */ 055public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping 056 implements MatchableHandlerMapping, EmbeddedValueResolverAware { 057 058 private boolean useSuffixPatternMatch = true; 059 060 private boolean useRegisteredSuffixPatternMatch = false; 061 062 private boolean useTrailingSlashMatch = true; 063 064 private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); 065 066 private StringValueResolver embeddedValueResolver; 067 068 private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); 069 070 071 /** 072 * Whether to use suffix pattern match (".*") when matching patterns to 073 * requests. If enabled a method mapped to "/users" also matches to "/users.*". 074 * <p>The default value is {@code true}. 075 * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for 076 * more fine-grained control over specific suffixes to allow. 077 */ 078 public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { 079 this.useSuffixPatternMatch = useSuffixPatternMatch; 080 } 081 082 /** 083 * Whether suffix pattern matching should work only against path extensions 084 * explicitly registered with the {@link ContentNegotiationManager}. This 085 * is generally recommended to reduce ambiguity and to avoid issues such as 086 * when a "." appears in the path for other reasons. 087 * <p>By default this is set to "false". 088 */ 089 public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { 090 this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; 091 this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch); 092 } 093 094 /** 095 * Whether to match to URLs irrespective of the presence of a trailing slash. 096 * If enabled a method mapped to "/users" also matches to "/users/". 097 * <p>The default value is {@code true}. 098 */ 099 public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { 100 this.useTrailingSlashMatch = useTrailingSlashMatch; 101 } 102 103 /** 104 * Set the {@link ContentNegotiationManager} to use to determine requested media types. 105 * If not set, the default constructor is used. 106 */ 107 public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { 108 Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null"); 109 this.contentNegotiationManager = contentNegotiationManager; 110 } 111 112 /** 113 * Return the configured {@link ContentNegotiationManager}. 114 */ 115 public ContentNegotiationManager getContentNegotiationManager() { 116 return this.contentNegotiationManager; 117 } 118 119 @Override 120 public void setEmbeddedValueResolver(StringValueResolver resolver) { 121 this.embeddedValueResolver = resolver; 122 } 123 124 @Override 125 public void afterPropertiesSet() { 126 this.config = new RequestMappingInfo.BuilderConfiguration(); 127 this.config.setUrlPathHelper(getUrlPathHelper()); 128 this.config.setPathMatcher(getPathMatcher()); 129 this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); 130 this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); 131 this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); 132 this.config.setContentNegotiationManager(getContentNegotiationManager()); 133 134 super.afterPropertiesSet(); 135 } 136 137 138 /** 139 * Whether to use suffix pattern matching. 140 */ 141 public boolean useSuffixPatternMatch() { 142 return this.useSuffixPatternMatch; 143 } 144 145 /** 146 * Whether to use registered suffixes for pattern matching. 147 */ 148 public boolean useRegisteredSuffixPatternMatch() { 149 return this.useRegisteredSuffixPatternMatch; 150 } 151 152 /** 153 * Whether to match to URLs irrespective of the presence of a trailing slash. 154 */ 155 public boolean useTrailingSlashMatch() { 156 return this.useTrailingSlashMatch; 157 } 158 159 /** 160 * Return the file extensions to use for suffix pattern matching. 161 */ 162 public List<String> getFileExtensions() { 163 return this.config.getFileExtensions(); 164 } 165 166 167 /** 168 * {@inheritDoc} 169 * <p>Expects a handler to have either a type-level @{@link Controller} 170 * annotation or a type-level @{@link RequestMapping} annotation. 171 */ 172 @Override 173 protected boolean isHandler(Class<?> beanType) { 174 return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || 175 AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); 176 } 177 178 /** 179 * Uses method and type-level @{@link RequestMapping} annotations to create 180 * the RequestMappingInfo. 181 * @return the created RequestMappingInfo, or {@code null} if the method 182 * does not have a {@code @RequestMapping} annotation. 183 * @see #getCustomMethodCondition(Method) 184 * @see #getCustomTypeCondition(Class) 185 */ 186 @Override 187 protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { 188 RequestMappingInfo info = createRequestMappingInfo(method); 189 if (info != null) { 190 RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); 191 if (typeInfo != null) { 192 info = typeInfo.combine(info); 193 } 194 } 195 return info; 196 } 197 198 /** 199 * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, 200 * supplying the appropriate custom {@link RequestCondition} depending on whether 201 * the supplied {@code annotatedElement} is a class or method. 202 * @see #getCustomTypeCondition(Class) 203 * @see #getCustomMethodCondition(Method) 204 */ 205 private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { 206 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); 207 RequestCondition<?> condition = (element instanceof Class ? 208 getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); 209 return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); 210 } 211 212 /** 213 * Provide a custom type-level request condition. 214 * The custom {@link RequestCondition} can be of any type so long as the 215 * same condition type is returned from all calls to this method in order 216 * to ensure custom request conditions can be combined and compared. 217 * <p>Consider extending {@link AbstractRequestCondition} for custom 218 * condition types and using {@link CompositeRequestCondition} to provide 219 * multiple custom conditions. 220 * @param handlerType the handler type for which to create the condition 221 * @return the condition, or {@code null} 222 */ 223 protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { 224 return null; 225 } 226 227 /** 228 * Provide a custom method-level request condition. 229 * The custom {@link RequestCondition} can be of any type so long as the 230 * same condition type is returned from all calls to this method in order 231 * to ensure custom request conditions can be combined and compared. 232 * <p>Consider extending {@link AbstractRequestCondition} for custom 233 * condition types and using {@link CompositeRequestCondition} to provide 234 * multiple custom conditions. 235 * @param method the handler method for which to create the condition 236 * @return the condition, or {@code null} 237 */ 238 protected RequestCondition<?> getCustomMethodCondition(Method method) { 239 return null; 240 } 241 242 /** 243 * Create a {@link RequestMappingInfo} from the supplied 244 * {@link RequestMapping @RequestMapping} annotation, which is either 245 * a directly declared annotation, a meta-annotation, or the synthesized 246 * result of merging annotation attributes within an annotation hierarchy. 247 */ 248 protected RequestMappingInfo createRequestMappingInfo( 249 RequestMapping requestMapping, RequestCondition<?> customCondition) { 250 251 return RequestMappingInfo 252 .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) 253 .methods(requestMapping.method()) 254 .params(requestMapping.params()) 255 .headers(requestMapping.headers()) 256 .consumes(requestMapping.consumes()) 257 .produces(requestMapping.produces()) 258 .mappingName(requestMapping.name()) 259 .customCondition(customCondition) 260 .options(this.config) 261 .build(); 262 } 263 264 /** 265 * Resolve placeholder values in the given array of patterns. 266 * @return a new array with updated patterns 267 */ 268 protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) { 269 if (this.embeddedValueResolver == null) { 270 return patterns; 271 } 272 else { 273 String[] resolvedPatterns = new String[patterns.length]; 274 for (int i = 0; i < patterns.length; i++) { 275 resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]); 276 } 277 return resolvedPatterns; 278 } 279 } 280 281 @Override 282 public RequestMatchResult match(HttpServletRequest request, String pattern) { 283 RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build(); 284 RequestMappingInfo matchingInfo = info.getMatchingCondition(request); 285 if (matchingInfo == null) { 286 return null; 287 } 288 Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns(); 289 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); 290 return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); 291 } 292 293 @Override 294 protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { 295 HandlerMethod handlerMethod = createHandlerMethod(handler, method); 296 CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class); 297 CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class); 298 299 if (typeAnnotation == null && methodAnnotation == null) { 300 return null; 301 }302 303 CorsConfiguration config = new CorsConfiguration(); 304 updateCorsConfig(config, typeAnnotation); 305 updateCorsConfig(config, methodAnnotation); 306 307 if (CollectionUtils.isEmpty(config.getAllowedMethods())) { 308 for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { 309 config.addAllowedMethod(allowedMethod.name()); 310 } 311 } 312 return config.applyPermitDefaultValues(); 313 } 314 315 private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) { 316 if (annotation == null) { 317 return; 318 } 319 for (String origin : annotation.origins()) { 320 config.addAllowedOrigin(resolveCorsAnnotationValue(origin)); 321 } 322 for (RequestMethod method : annotation.methods()) { 323 config.addAllowedMethod(method.name()); 324 } 325 for (String header : annotation.allowedHeaders()) { 326 config.addAllowedHeader(resolveCorsAnnotationValue(header)); 327 } 328 for (String header : annotation.exposedHeaders()) { 329 config.addExposedHeader(resolveCorsAnnotationValue(header)); 330 } 331 332 String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials()); 333 if ("true".equalsIgnoreCase(allowCredentials)) { 334 config.setAllowCredentials(true); 335 } 336 else if ("false".equalsIgnoreCase(allowCredentials)) { 337 config.setAllowCredentials(false); 338 } 339 else if (!allowCredentials.isEmpty()) { 340 throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " + 341 "or an empty string (\"\"): current value is [" + allowCredentials + "]"); 342 } 343 344 if (annotation.maxAge() >= 0 && config.getMaxAge() == null) { 345 config.setMaxAge(annotation.maxAge()); 346 } 347 } 348 349 private String resolveCorsAnnotationValue(String value) { 350 return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); 351 } 352 353}