org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping Maven / Gradle / Ivy
/*
* Copyright 2002-2020 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
*
* https://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.lang.reflect.Parameter;
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.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
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.RequestBody;
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.ConsumesRequestCondition;
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.
*
* Deprecation Note:
In 5.2.4,
* {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch} and
* {@link #setUseRegisteredSuffixPatternMatch(boolean) useRegisteredSuffixPatternMatch}
* are deprecated in order to discourage use of path extensions for request
* mapping and for content negotiation (with similar deprecations in
* {@link org.springframework.web.accept.ContentNegotiationManagerFactoryBean
* ContentNegotiationManagerFactoryBean}). For further context, please read issue
* #24719.
*
* @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>> 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 {@code true}.
*
Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
* more fine-grained control over specific suffixes to allow.
* @deprecated as of 5.2.4. See class level comment about deprecation of
* path extension config options. As there is no replacement for this method,
* for the time being it's necessary to set it to {@code false}. In 5.3
* when {@code false} becomes the default, use of this property will no
* longer be necessary.
*/
@Deprecated
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.
*
By default this is set to "false".
* @deprecated as of 5.2.4. See class level comment about deprecation of
* path extension config options.
*/
@Deprecated
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 {@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 {@code @RequestMapping}
* method whose controller type is matched by the corresponding
* {@code Predicate}. The prefix for the first matching predicate is used.
*
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>> prefixes) {
this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes));
}
/**
* The configured path prefixes as a read-only, possibly empty map.
* @since 5.1
*/
public Map>> getPathPrefixes() {
return this.pathPrefixes;
}
/**
* 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 {@link ContentNegotiationManager}.
*/
public ContentNegotiationManager getContentNegotiationManager() {
return this.contentNegotiationManager;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(useSuffixPatternMatch());
this.config.setTrailingSlashMatch(useTrailingSlashMatch());
this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
this.config.setContentNegotiationManager(getContentNegotiationManager());
super.afterPropertiesSet();
}
/**
* Whether to use registered suffixes for pattern matching.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/
@Deprecated
public boolean useSuffixPatternMatch() {
return this.useSuffixPatternMatch;
}
/**
* Whether to use registered suffixes for pattern matching.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/
@Deprecated
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 file extensions to use for suffix pattern matching.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/
@Nullable
@Deprecated
@SuppressWarnings("deprecation")
public List getFileExtensions() {
return this.config.getFileExtensions();
}
/**
* {@inheritDoc}
* 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 @{@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).options(this.config).build().combine(info);
}
}
return info;
}
@Nullable
String getPathPrefix(Class handlerType) {
for (Map.Entry>> 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 {@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 {@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.
* 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 {@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.
*
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 {@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.
* @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 void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
super.registerMapping(mapping, handler, method);
updateConsumesCondition(mapping, method);
}
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
super.registerHandlerMethod(handler, method, mapping);
updateConsumesCondition(mapping, method);
}
private void updateConsumesCondition(RequestMappingInfo info, Method method) {
ConsumesRequestCondition condition = info.getConsumesCondition();
if (!condition.isEmpty()) {
for (Parameter parameter : method.getParameters()) {
MergedAnnotation annot = MergedAnnotations.from(parameter).get(RequestBody.class);
if (annot.isPresent()) {
condition.setBodyRequired(annot.getBoolean("required"));
break;
}
}
}
}
@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 patterns = matchingInfo.getPatternsCondition().getPatterns();
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request, LOOKUP_PATH);
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;
}
}
}