/*
 * Copyright 2002-2018 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.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import javax.servlet.http.HttpServletRequest;

import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
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 RequestMappingInfo instances from type and method-level @RequestMapping annotations in @Controller classes.
Author:Arjen Poutsma, Rossen Stoyanchev, Sam Brannen
Since:3.1
/** * 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 Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>(); private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); @Nullable 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.*".

The default value is true.

Also see setUseRegisteredSuffixPatternMatch(boolean) for more fine-grained control over specific suffixes to allow.

/** * 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 ContentNegotiationManager. This is generally recommended to reduce ambiguity and to avoid issues such as when a "." appears in the path for other reasons.

By default this is set to "false".

/** * 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/".

The default value is true.

/** * 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; }
Configure path prefixes to apply to controller methods.

Prefixes are used to enrich the mappings of every @RequestMapping method whose controller type is matched by the corresponding Predicate. The prefix for the first matching predicate is used.

Consider using HandlerTypePredicate to group controllers.

Params:
  • prefixes – a map with path prefixes as key
Since:5.1
/** * Configure path prefixes to apply to controller methods. * <p>Prefixes are used to enrich the mappings of every {@code @RequestMapping} * method whose controller type is matched by the corresponding * {@code Predicate}. The prefix for the first matching predicate is used. * <p>Consider using {@link org.springframework.web.method.HandlerTypePredicate * HandlerTypePredicate} to group controllers. * @param prefixes a map with path prefixes as key * @since 5.1 */
public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) { this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)); }
The configured path prefixes as a read-only, possibly empty map.
Since:5.1
/** * The configured path prefixes as a read-only, possibly empty map. * @since 5.1 */
public Map<String, Predicate<Class<?>>> getPathPrefixes() { return this.pathPrefixes; }
Set the ContentNegotiationManager to use to determine requested media types. If not set, the default constructor is used.
/** * 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; }
Return the configured ContentNegotiationManager.
/** * Return the configured {@link ContentNegotiationManager}. */
public ContentNegotiationManager getContentNegotiationManager() { return this.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.
/** * Whether to use suffix pattern matching. */
public boolean useSuffixPatternMatch() { return this.useSuffixPatternMatch; }
Whether to use registered suffixes for pattern matching.
/** * 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.
/** * Whether to match to URLs irrespective of the presence of a trailing slash. */
public boolean useTrailingSlashMatch() { return this.useTrailingSlashMatch; }
Return the file extensions to use for suffix pattern matching.
/** * Return the file extensions to use for suffix pattern matching. */
@Nullable public List<String> getFileExtensions() { return this.config.getFileExtensions(); }
{@inheritDoc}

Expects a handler to have either a type-level @Controller annotation or a type-level @RequestMapping annotation.

/** * {@inheritDoc} * <p>Expects a handler to have either a type-level @{@link Controller} * annotation or a type-level @{@link RequestMapping} annotation. */
@Override protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }
Uses method and type-level @RequestMapping annotations to create the RequestMappingInfo.
See Also:
Returns:the created RequestMappingInfo, or null if the method does not have a @RequestMapping annotation.
/** * 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 @Nullable 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); } String prefix = getPathPrefix(handlerType); if (prefix != null) { info = RequestMappingInfo.paths(prefix).build().combine(info); } } return info; } @Nullable String getPathPrefix(Class<?> handlerType) { for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) { if (entry.getValue().test(handlerType)) { String prefix = entry.getKey(); if (this.embeddedValueResolver != null) { prefix = this.embeddedValueResolver.resolveStringValue(prefix); } return prefix; } } return null; }
Delegates to createRequestMappingInfo(RequestMapping, RequestCondition<?>), supplying the appropriate custom RequestCondition depending on whether the supplied annotatedElement is a class or method.
See Also:
/** * 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) */
@Nullable 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 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.

Consider extending AbstractRequestCondition for custom condition types and using CompositeRequestCondition to provide multiple custom conditions.

Params:
  • handlerType – the handler type for which to create the condition
Returns:the condition, or 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} */
@Nullable protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) { return null; }
Provide a custom method-level request condition. The custom 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.

Consider extending AbstractRequestCondition for custom condition types and using CompositeRequestCondition to provide multiple custom conditions.

Params:
  • method – the handler method for which to create the condition
Returns:the condition, or 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} */
@Nullable protected RequestCondition<?> getCustomMethodCondition(Method method) { return null; }
Create a RequestMappingInfo from the supplied @RequestMapping annotation, which is either a directly declared annotation, a meta-annotation, or the synthesized result of merging annotation attributes within an annotation hierarchy.
/** * 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, @Nullable RequestCondition<?> customCondition) { RequestMappingInfo.Builder builder = RequestMappingInfo .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()); if (customCondition != null) { builder.customCondition(customCondition); } return builder.options(this.config).build(); }
Resolve placeholder values in the given array of patterns.
Returns:a new array with updated patterns
/** * 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, @Nullable 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) { if (this.embeddedValueResolver != null) { String resolved = this.embeddedValueResolver.resolveStringValue(value); return (resolved != null ? resolved : ""); } else { return value; } } }