/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; /** * Creates {@link RequestMappingInfo} instances from type and method-level * {@link RequestMapping @RequestMapping} annotations in * {@link Controller @Controller} classes. * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Sam Brannen * @since 3.1 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware { private boolean useSuffixPatternMatch = true; private boolean useRegisteredSuffixPatternMatch = false; private boolean useTrailingSlashMatch = true; private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); private StringValueResolver embeddedValueResolver; private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); /** * Whether to use suffix pattern match (".*") when matching patterns to * requests. If enabled a method mapped to "/users" also matches to "/users.*". * <p>The default value is {@code true}. * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for * more fine-grained control over specific suffixes to allow. */ public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { this.useSuffixPatternMatch = useSuffixPatternMatch; } /** * Whether suffix pattern matching should work only against path extensions * explicitly registered with the {@link ContentNegotiationManager}. This * is generally recommended to reduce ambiguity and to avoid issues such as * when a "." appears in the path for other reasons. * <p>By default this is set to "false". */ public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch); } /** * Whether to match to URLs irrespective of the presence of a trailing slash. * If enabled a method mapped to "/users" also matches to "/users/". * <p>The default value is {@code true}. */ public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { this.useTrailingSlashMatch = useTrailingSlashMatch; } /** * Set the {@link ContentNegotiationManager} to use to determine requested media types. * If not set, the default constructor is used. */ public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null"); this.contentNegotiationManager = contentNegotiationManager; } @Override public void setEmbeddedValueResolver(StringValueResolver resolver) { this.embeddedValueResolver = resolver; } @Override public void afterPropertiesSet() { this.config = new RequestMappingInfo.BuilderConfiguration(); this.config.setUrlPathHelper(getUrlPathHelper()); this.config.setPathMatcher(getPathMatcher()); this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); this.config.setContentNegotiationManager(getContentNegotiationManager()); super.afterPropertiesSet(); } /** * Whether to use suffix pattern matching. */ public boolean useSuffixPatternMatch() { return this.useSuffixPatternMatch; } /** * Whether to use registered suffixes for pattern matching. */ public boolean useRegisteredSuffixPatternMatch() { return this.useRegisteredSuffixPatternMatch; } /** * Whether to match to URLs irrespective of the presence of a trailing slash. */ public boolean useTrailingSlashMatch() { return this.useTrailingSlashMatch; } /** * Return the configured {@link ContentNegotiationManager}. */ public ContentNegotiationManager getContentNegotiationManager() { return this.contentNegotiationManager; } /** * Return the file extensions to use for suffix pattern matching. */ public List<String> getFileExtensions() { return this.config.getFileExtensions(); } /** * {@inheritDoc} * Expects a handler to have a type-level @{@link Controller} annotation. */ @Override protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); } /** * Uses method and type-level @{@link RequestMapping} annotations to create * the RequestMappingInfo. * @return the created RequestMappingInfo, or {@code null} if the method * does not have a {@code @RequestMapping} annotation. * @see #getCustomMethodCondition(Method) * @see #getCustomTypeCondition(Class) */ @Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null) { info = typeInfo.combine(info); } } return info; } /** * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, * supplying the appropriate custom {@link RequestCondition} depending on whether * the supplied {@code annotatedElement} is a class or method. * @see #getCustomTypeCondition(Class) * @see #getCustomMethodCondition(Method) */ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); } /** * Provide a custom type-level request condition. * The custom {@link RequestCondition} can be of any type so long as the * same condition type is returned from all calls to this method in order * to ensure custom request conditions can be combined and compared. * <p>Consider extending {@link AbstractRequestCondition} for custom * condition types and using {@link CompositeRequestCondition} to provide * multiple custom conditions. * @param handlerType the handler type for which to create the condition * @return the condition, or {@code null} */ protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { return null; } /** * Provide a custom method-level request condition. * The custom {@link RequestCondition} can be of any type so long as the * same condition type is returned from all calls to this method in order * to ensure custom request conditions can be combined and compared. * <p>Consider extending {@link AbstractRequestCondition} for custom * condition types and using {@link CompositeRequestCondition} to provide * multiple custom conditions. * @param method the handler method for which to create the condition * @return the condition, or {@code null} */ protected RequestCondition<?> getCustomMethodCondition(Method method) { return null; } /** * Create a {@link RequestMappingInfo} from the supplied * {@link RequestMapping @RequestMapping} annotation, which is either * a directly declared annotation, a meta-annotation, or the synthesized * result of merging annotation attributes within an annotation hierarchy. */ protected RequestMappingInfo createRequestMappingInfo( RequestMapping requestMapping, RequestCondition<?> customCondition) { return RequestMappingInfo .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()) .customCondition(customCondition) .options(this.config) .build(); } /** * Resolve placeholder values in the given array of patterns. * @return a new array with updated patterns */ protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) { if (this.embeddedValueResolver == null) { return patterns; } else { String[] resolvedPatterns = new String[patterns.length]; for (int i = 0; i < patterns.length; i++) { resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]); } return resolvedPatterns; } } @Override public RequestMatchResult match(HttpServletRequest request, String pattern) { RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build(); RequestMappingInfo matchingInfo = info.getMatchingCondition(request); if (matchingInfo == null) { return null; } Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns(); String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); } @Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { HandlerMethod handlerMethod = createHandlerMethod(handler, method); Class<?> beanType = handlerMethod.getBeanType(); CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class); CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class); if (typeAnnotation == null && methodAnnotation == null) { return null; } CorsConfiguration config = new CorsConfiguration(); updateCorsConfig(config, typeAnnotation); updateCorsConfig(config, methodAnnotation); if (CollectionUtils.isEmpty(config.getAllowedMethods())) { for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { config.addAllowedMethod(allowedMethod.name()); } } return config.applyPermitDefaultValues(); } private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) { if (annotation == null) { return; } for (String origin : annotation.origins()) { config.addAllowedOrigin(resolveCorsAnnotationValue(origin)); } for (RequestMethod method : annotation.methods()) { config.addAllowedMethod(method.name()); } for (String header : annotation.allowedHeaders()) { config.addAllowedHeader(resolveCorsAnnotationValue(header)); } for (String header : annotation.exposedHeaders()) { config.addExposedHeader(resolveCorsAnnotationValue(header)); } String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials()); if ("true".equalsIgnoreCase(allowCredentials)) { config.setAllowCredentials(true); } else if ("false".equalsIgnoreCase(allowCredentials)) { config.setAllowCredentials(false); } else if (!allowCredentials.isEmpty()) { throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " + "or an empty string (\"\"): current value is [" + allowCredentials + "]"); } if (annotation.maxAge() >= 0 && config.getMaxAge() == null) { config.setMaxAge(annotation.maxAge()); } } private String resolveCorsAnnotationValue(String value) { return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); } }