diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index 3839553300..4d03ab3cfa 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -16,9 +16,13 @@ package org.springframework.web.servlet.mvc.method; +import java.util.List; + import javax.servlet.http.HttpServletRequest; +import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; @@ -29,6 +33,7 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.condition.RequestConditionHolder; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; +import org.springframework.web.util.UrlPathHelper; /** * Encapsulates the following request mapping conditions: @@ -330,4 +335,285 @@ public final class RequestMappingInfo implements RequestConditionBy default this is not set. + */ + Builder headers(String... headers); + + /** + * Set the consumes conditions. + */ + Builder consumes(String... consumes); + + /** + * Set the produces conditions. + */ + Builder produces(String... produces); + + /** + * Set the mapping name. + */ + Builder mappingName(String name); + + /** + * Set a custom conditions to use. + */ + Builder customCondition(RequestCondition condition); + + /** + * Provide additional configuration needed for request mapping purposes. + */ + Builder options(BuilderConfiguration options); + + /** + * Build the RequestMappingInfo. + */ + RequestMappingInfo build(); + } + + private static class DefaultBuilder implements Builder { + + private String[] paths; + + private RequestMethod[] methods; + + private String[] params; + + private String[] headers; + + private String[] consumes; + + private String[] produces; + + private String mappingName; + + private RequestCondition customCondition; + + private BuilderConfiguration options = new BuilderConfiguration(); + + + public DefaultBuilder(String... paths) { + this.paths = paths; + } + + + @Override + public Builder paths(String... paths) { + this.paths = paths; + return this; + } + + @Override + public DefaultBuilder methods(RequestMethod... methods) { + this.methods = methods; + return this; + } + + @Override + public DefaultBuilder params(String... params) { + this.params = params; + return this; + } + + @Override + public DefaultBuilder headers(String... headers) { + this.headers = headers; + return this; + } + + @Override + public DefaultBuilder consumes(String... consumes) { + this.consumes = consumes; + return this; + } + + @Override + public DefaultBuilder produces(String... produces) { + this.produces = produces; + return this; + } + + @Override + public DefaultBuilder mappingName(String name) { + this.mappingName = name; + return this; + } + + @Override + public DefaultBuilder customCondition(RequestCondition condition) { + this.customCondition = condition; + return this; + } + + @Override + public Builder options(BuilderConfiguration options) { + this.options = options; + return this; + } + + @Override + public RequestMappingInfo build() { + + ContentNegotiationManager manager = this.options.getContentNegotiationManager(); + + PatternsRequestCondition patternsCondition = new PatternsRequestCondition( + this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(), + this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(), + this.options.getFileExtensions()); + + return new RequestMappingInfo(this.mappingName, patternsCondition, + new RequestMethodsRequestCondition(methods), + new ParamsRequestCondition(this.params), + new HeadersRequestCondition(this.headers), + new ConsumesRequestCondition(this.consumes, this.headers), + new ProducesRequestCondition(this.produces, this.headers, manager), + this.customCondition); + } + } + + /** + * Container for configuration options used for request mapping purposes. + * Such configuration is required to create RequestMappingInfo instances but + * is typically used across all RequestMappingInfo instances. + * + * @see Builder#options + * @since 4.2 + */ + public static class BuilderConfiguration { + + private UrlPathHelper urlPathHelper; + + private PathMatcher pathMatcher; + + private boolean trailingSlashMatch = true; + + private boolean suffixPatternMatch = true; + + private boolean registeredSuffixPatternMatch = false; + + private ContentNegotiationManager contentNegotiationManager; + + + /** + * Set a custom UrlPathHelper to use for the PatternsRequestCondition. + *

By default this is not set. + */ + public void setPathHelper(UrlPathHelper pathHelper) { + this.urlPathHelper = pathHelper; + } + + public UrlPathHelper getUrlPathHelper() { + return this.urlPathHelper; + } + + /** + * Set a custom PathMatcher to use for the PatternsRequestCondition. + *

By default this is not set. + */ + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + /** + * Whether to apply trailing slash matching in PatternsRequestCondition. + *

By default this is set to 'true'. + */ + public void setTrailingSlashMatch(boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + } + + public boolean useTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + /** + * Whether to apply suffix pattern matching in PatternsRequestCondition. + *

By default this is set to 'true'. + * @see #setRegisteredSuffixPatternMatch(boolean) + */ + public void setSuffixPatternMatch(boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + } + + public boolean useSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + /** + * Whether suffix pattern matching should be restricted to registered + * file extensions only. Setting this property also sets + * suffixPatternMatch=true and requires that a + * {@link #setContentNegotiationManager} is also configured in order to + * obtain the registered file extensions. + */ + public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch); + } + + public boolean useRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + /** + * Return the file extensions to use for suffix pattern matching. If + * {@code registeredSuffixPatternMatch=true}, the extensions are obtained + * from the configured {@code contentNegotiationManager}. + */ + public List getFileExtensions() { + if (useRegisteredSuffixPatternMatch() && getContentNegotiationManager() != null) { + return this.contentNegotiationManager.getAllFileExtensions(); + } + return null; + } + + /** + * Set the ContentNegotiationManager to use for the ProducesRequestCondition. + *

By default this is not set. + */ + public void setContentNegotiationManager(ContentNegotiationManager manager) { + this.contentNegotiationManager = manager; + } + + public ContentNegotiationManager getContentNegotiationManager() { + return this.contentNegotiationManager; + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 3e66d60055..8275fa85b7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -18,7 +18,6 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.List; import org.springframework.context.EmbeddedValueResolverAware; @@ -38,14 +37,8 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; 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.HeadersRequestCondition; import org.springframework.web.servlet.mvc.condition.NameValueExpression; -import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; -import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; -import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; @@ -69,10 +62,10 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); - private final List fileExtensions = new ArrayList(); - private StringValueResolver embeddedValueResolver; + private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); + /** * Whether to use suffix pattern match (".*") when matching patterns to @@ -130,13 +123,17 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi @Override public void afterPropertiesSet() { - if (this.useRegisteredSuffixPatternMatch) { - this.fileExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); - } - super.afterPropertiesSet(); - } + this.config = new RequestMappingInfo.BuilderConfiguration(); + this.config.setPathHelper(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. @@ -170,7 +167,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * Return the file extensions to use for suffix pattern matching. */ public List getFileExtensions() { - return this.fileExtensions; + return this.config.getFileExtensions(); } @@ -215,6 +212,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @param handlerType the handler type for which to create the condition * @return the condition, or {@code null} */ + @SuppressWarnings("unused") protected RequestCondition getCustomTypeCondition(Class handlerType) { return null; } @@ -230,6 +228,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @param method the handler method for which to create the condition * @return the condition, or {@code null} */ + @SuppressWarnings("unused") protected RequestCondition getCustomMethodCondition(Method method) { return null; } @@ -238,6 +237,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * Transitional method used to invoke one of two createRequestMappingInfo * variants one of which is deprecated. */ + @SuppressWarnings("deprecation") private RequestMappingInfo createRequestMappingInfo(AnnotatedElement annotatedElement) { RequestMapping annotation; AnnotationAttributes attributes; @@ -272,6 +272,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * {@link #createRequestMappingInfo(AnnotationAttributes, RequestCondition)}. */ @Deprecated + @SuppressWarnings("unused") protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition customCondition) { @@ -287,31 +288,20 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi protected RequestMappingInfo createRequestMappingInfo(AnnotationAttributes attributes, RequestCondition customCondition) { - String mappingName = attributes.getString("name"); - String[] paths = attributes.getStringArray("path"); paths = ObjectUtils.isEmpty(paths) ? attributes.getStringArray("value") : paths; - PatternsRequestCondition patternsCondition = new PatternsRequestCondition( - resolveEmbeddedValuesInPatterns(paths), getUrlPathHelper(), getPathMatcher(), - this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions); - - RequestMethod[] methods = (RequestMethod[]) attributes.get("method"); - RequestMethodsRequestCondition methodsCondition = new RequestMethodsRequestCondition(methods); - - String[] params = attributes.getStringArray("params"); - ParamsRequestCondition paramsCondition = new ParamsRequestCondition(params); - - String[] headers = attributes.getStringArray("headers"); - String[] consumes = attributes.getStringArray("consumes"); - String[] produces = attributes.getStringArray("produces"); - - HeadersRequestCondition headersCondition = new HeadersRequestCondition(headers); - ConsumesRequestCondition consumesCondition = new ConsumesRequestCondition(consumes, headers); - ProducesRequestCondition producesCondition = new ProducesRequestCondition(produces, - headers, this.contentNegotiationManager); - - return new RequestMappingInfo(mappingName, patternsCondition, methodsCondition, paramsCondition, - headersCondition, consumesCondition, producesCondition, customCondition); + paths = resolveEmbeddedValuesInPatterns(paths); + + return RequestMappingInfo.paths(paths) + .methods((RequestMethod[]) attributes.get("method")) + .params(attributes.getStringArray("params")) + .headers(attributes.getStringArray("headers")) + .consumes(attributes.getStringArray("consumes")) + .produces(attributes.getStringArray("produces")) + .mappingName(attributes.getString("name")) + .customCondition(customCondition) + .options(this.config) + .build(); } /** @@ -342,8 +332,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } CorsConfiguration config = new CorsConfiguration(); - applyAnnotation(config, typeAnnotation); - applyAnnotation(config, methodAnnotation); + updateCorsConfig(config, typeAnnotation); + updateCorsConfig(config, methodAnnotation); if (CollectionUtils.isEmpty(config.getAllowedMethods())) { for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { @@ -360,7 +350,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return config; } - private void applyAnnotation(CorsConfiguration config, CrossOrigin annotation) { + private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) { if (annotation == null) { return; }