Complete RequestMappingHandlerMapping

This commit adds RequestMappingInfoHandlerMapping and
RequestMappingHandlerMapping with support equivalent to that in
spring-webmvc.
This commit is contained in:
Rossen Stoyanchev
2016-04-18 13:02:50 -04:00
parent 0e5a892bad
commit 0f44fedd19
6 changed files with 1375 additions and 189 deletions

View File

@@ -29,6 +29,45 @@ import org.springframework.web.server.ServerWebExchange;
*/
public interface HandlerMapping {
/**
* Name of the {@link ServerWebExchange} attribute that contains the
* best matching pattern within the handler mapping.
* <p>Note: This attribute is not required to be supported by all
* HandlerMapping implementations. URL-based HandlerMappings will
* typically support it, but handlers should not necessarily expect
* this request attribute to be present in all scenarios.
*/
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
/**
* Name of the {@link ServerWebExchange} attribute that contains the URI
* templates map, mapping variable names to values.
* <p>Note: This attribute is not required to be supported by all
* HandlerMapping implementations. URL-based HandlerMappings will
* typically support it, but handlers should not necessarily expect
* this request attribute to be present in all scenarios.
*/
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
/**
* Name of the {@link ServerWebExchange} attribute that contains a map with
* URI matrix variables.
* <p>Note: This attribute is not required to be supported by all
* HandlerMapping implementations and may also not be present depending on
* whether the HandlerMapping is configured to keep matrix variable content
* in the request URI.
*/
String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";
/**
* Name of the {@link ServerWebExchange} attribute that contains the set of
* producible MediaTypes applicable to the mapped handler.
* <p>Note: This attribute is not required to be supported by all
* HandlerMapping implementations. Handlers should not necessarily expect
* this request attribute to be present in all scenarios.
*/
String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";
/**
* Return a handler for this request.
* @param exchange current server exchange

View File

@@ -0,0 +1,336 @@
/*
* 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.reactive.result.method;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.NameValueExpression;
import org.springframework.web.reactive.result.condition.ParamsRequestCondition;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.WebUtils;
/**
* Abstract base class for classes for which {@link RequestMappingInfo} defines
* the mapping between a request and a handler method.
*
* @author Rossen Stoyanchev
*/
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
static {
try {
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
}
catch (NoSuchMethodException ex) {
// Should never happen
throw new IllegalStateException("No handler for HTTP OPTIONS", ex);
}
}
/**
* Get the URL path patterns associated with this {@link RequestMappingInfo}.
*/
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
return info.getPatternsCondition().getPatterns();
}
/**
* Check if the given RequestMappingInfo matches the current request and
* return a (potentially new) instance with conditions that match the
* current request -- for example with a subset of URL patterns.
* @return an info in case of a match; or {@code null} otherwise.
*/
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, ServerWebExchange exchange) {
return info.getMatchingCondition(exchange);
}
/**
* Provide a Comparator to sort RequestMappingInfos matched to a request.
*/
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(final ServerWebExchange exchange) {
return (info1, info2) -> info1.compareTo(info2, exchange);
}
/**
* Expose URI template variables, matrix variables, and producible media types in the request.
* @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
* @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
* @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
*/
@Override
protected void handleMatch(RequestMappingInfo info, String lookupPath, ServerWebExchange exchange) {
super.handleMatch(info, lookupPath, exchange);
String bestPattern;
Map<String, String> uriVariables;
Map<String, String> decodedUriVariables;
Set<String> patterns = info.getPatternsCondition().getPatterns();
if (patterns.isEmpty()) {
bestPattern = lookupPath;
uriVariables = Collections.emptyMap();
decodedUriVariables = Collections.emptyMap();
}
else {
bestPattern = patterns.iterator().next();
uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
decodedUriVariables = getPathHelper().decodePathVariables(exchange, uriVariables);
}
exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(exchange, uriVariables);
exchange.getAttributes().put(MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
}
}
private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
ServerWebExchange exchange, Map<String, String> uriVariables) {
Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<>();
for (Entry<String, String> uriVar : uriVariables.entrySet()) {
String uriVarValue = uriVar.getValue();
int equalsIndex = uriVarValue.indexOf('=');
if (equalsIndex == -1) {
continue;
}
String matrixVariables;
int semicolonIndex = uriVarValue.indexOf(';');
if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) {
matrixVariables = uriVarValue;
}
else {
matrixVariables = uriVarValue.substring(semicolonIndex + 1);
uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex));
}
MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
result.put(uriVar.getKey(), getPathHelper().decodeMatrixVariables(exchange, vars));
}
return result;
}
/**
* Iterate all RequestMappingInfos once again, look if any match by URL at
* least and raise exceptions accordingly.
* @throws HttpRequestMethodNotSupportedException if there are matches by URL
* but not by HTTP method
* @throws HttpMediaTypeNotAcceptableException if there are matches by URL
* but not by consumable/producible media types
*/
@Override
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
String lookupPath, ServerWebExchange exchange) throws Exception {
Set<String> allowedMethods = new LinkedHashSet<>(4);
Set<RequestMappingInfo> patternMatches = new HashSet<>();
Set<RequestMappingInfo> patternAndMethodMatches = new HashSet<>();
for (RequestMappingInfo info : requestMappingInfos) {
if (info.getPatternsCondition().getMatchingCondition(exchange) != null) {
patternMatches.add(info);
if (info.getMethodsCondition().getMatchingCondition(exchange) != null) {
patternAndMethodMatches.add(info);
}
else {
for (RequestMethod method : info.getMethodsCondition().getMethods()) {
allowedMethods.add(method.name());
}
}
}
}
ServerHttpRequest request = exchange.getRequest();
if (patternMatches.isEmpty()) {
return null;
}
else if (patternAndMethodMatches.isEmpty()) {
HttpMethod httpMethod = request.getMethod();
if (HttpMethod.OPTIONS.matches(httpMethod.name())) {
HttpOptionsHandler handler = new HttpOptionsHandler(allowedMethods);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
else if (!allowedMethods.isEmpty()) {
throw new HttpRequestMethodNotSupportedException(httpMethod.name(), allowedMethods);
}
}
Set<MediaType> consumableMediaTypes;
Set<MediaType> producibleMediaTypes;
List<String[]> paramConditions;
if (patternAndMethodMatches.isEmpty()) {
consumableMediaTypes = getConsumableMediaTypes(exchange, patternMatches);
producibleMediaTypes = getProducibleMediaTypes(exchange, patternMatches);
paramConditions = getRequestParams(exchange, patternMatches);
}
else {
consumableMediaTypes = getConsumableMediaTypes(exchange, patternAndMethodMatches);
producibleMediaTypes = getProducibleMediaTypes(exchange, patternAndMethodMatches);
paramConditions = getRequestParams(exchange, patternAndMethodMatches);
}
if (!consumableMediaTypes.isEmpty()) {
MediaType contentType;
try {
contentType = request.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(consumableMediaTypes));
}
else if (!producibleMediaTypes.isEmpty()) {
throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(producibleMediaTypes));
}
else {
if (!CollectionUtils.isEmpty(paramConditions)) {
Map<String, String[]> params = request.getQueryParams().entrySet().stream()
.collect(Collectors.toMap(Entry::getKey,
entry -> entry.getValue().toArray(new String[entry.getValue().size()]))
);
throw new UnsatisfiedServletRequestParameterException(paramConditions, params);
}
else {
return null;
}
}
}
private Set<MediaType> getConsumableMediaTypes(ServerWebExchange exchange,
Set<RequestMappingInfo> partialMatches) {
Set<MediaType> result = new HashSet<>();
for (RequestMappingInfo partialMatch : partialMatches) {
if (partialMatch.getConsumesCondition().getMatchingCondition(exchange) == null) {
result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes());
}
}
return result;
}
private Set<MediaType> getProducibleMediaTypes(ServerWebExchange exchange,
Set<RequestMappingInfo> partialMatches) {
Set<MediaType> result = new HashSet<>();
for (RequestMappingInfo partialMatch : partialMatches) {
if (partialMatch.getProducesCondition().getMatchingCondition(exchange) == null) {
result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes());
}
}
return result;
}
private List<String[]> getRequestParams(ServerWebExchange exchange,
Set<RequestMappingInfo> partialMatches) {
List<String[]> result = new ArrayList<>();
for (RequestMappingInfo partialMatch : partialMatches) {
ParamsRequestCondition condition = partialMatch.getParamsCondition();
Set<NameValueExpression<String>> expressions = condition.getExpressions();
if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(exchange) == null) {
int i = 0;
String[] array = new String[expressions.size()];
for (NameValueExpression<String> expression : expressions) {
array[i++] = expression.toString();
}
result.add(array);
}
}
return result;
}
/**
* Default handler for HTTP OPTIONS.
*/
private static class HttpOptionsHandler {
private final HttpHeaders headers = new HttpHeaders();
public HttpOptionsHandler(Set<String> declaredMethods) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
}
private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) {
Set<HttpMethod> result = new LinkedHashSet<HttpMethod>(declaredMethods.size());
if (declaredMethods.isEmpty()) {
for (HttpMethod method : HttpMethod.values()) {
if (!HttpMethod.TRACE.equals(method)) {
result.add(method);
}
}
}
else {
boolean hasHead = declaredMethods.contains("HEAD");
for (String method : declaredMethods) {
result.add(HttpMethod.valueOf(method));
if (!hasHead && "GET".equals(method)) {
result.add(HttpMethod.HEAD);
}
}
}
return result;
}
public HttpHeaders handle() {
return this.headers;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -16,154 +16,260 @@
package org.springframework.web.reactive.result.method.annotation;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
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.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.reactive.accept.ContentTypeResolver;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.result.condition.RequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
/**
* An extension of {@link RequestMappingInfoHandlerMapping} that creates
* {@link RequestMappingInfo} instances from class-level and method-level
* {@link RequestMapping @RequestMapping} annotations.
*
* @author Rossen Stoyanchev
*/
public class RequestMappingHandlerMapping implements HandlerMapping,
ApplicationContextAware, InitializingBean {
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements EmbeddedValueResolverAware {
private static final Log logger = LogFactory.getLog(RequestMappingHandlerMapping.class);
private boolean useSuffixPatternMatch = true;
private boolean useRegisteredSuffixPatternMatch = true;
private boolean useTrailingSlashMatch = true;
private ContentTypeResolver contentTypeResolver = new HeaderContentTypeResolver();
private StringValueResolver embeddedValueResolver;
private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
private final Map<RequestMappingInfo, HandlerMethod> methodMap = new TreeMap<>();
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
/**
* Whether to use suffix pattern matching. If enabled a method mapped to
* "/path" also matches to "/path.*".
* <p>The default value is {@code true}.
* <p><strong>Note:</strong> when using suffix pattern matching it's usually
* preferable to be explicit about what is and isn't an extension so rather
* than setting this property consider using
* {@link #setUseRegisteredSuffixPatternMatch} instead.
*/
public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
this.useSuffixPatternMatch = useSuffixPatternMatch;
}
@Override
public void afterPropertiesSet() throws Exception {
this.applicationContext.getBeansOfType(Object.class).values().forEach(this::detectHandlerMethods);
/**
* Whether suffix pattern matching should work only against path extensions
* explicitly registered with the configured {@link ContentTypeResolver}. 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 "true".
*/
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
}
protected void detectHandlerMethods(final Object bean) {
final Class<?> beanType = bean.getClass();
if (AnnotationUtils.findAnnotation(beanType, Controller.class) != null) {
HandlerMethodSelector.selectMethods(beanType, method -> {
RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
if (annotation != null && annotation.value().length > 0) {
String path = annotation.value()[0];
RequestMethod[] methods = annotation.method();
HandlerMethod handlerMethod = new HandlerMethod(bean, method);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + path + "\" onto " + handlerMethod);
}
RequestMappingInfo info = new RequestMappingInfo(path, methods);
if (this.methodMap.containsKey(info)) {
throw new IllegalStateException("Duplicate mapping found for " + info);
}
methodMap.put(info, handlerMethod);
}
return false;
});
}
/**
* 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 setContentTypeResolver(ContentTypeResolver contentTypeResolver) {
Assert.notNull(contentTypeResolver, "'ContentTypeResolver' must not be null");
this.contentTypeResolver = contentTypeResolver;
}
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
return Flux.create(subscriber -> {
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : this.methodMap.entrySet()) {
RequestMappingInfo info = entry.getKey();
if (info.matchesRequest(exchange.getRequest())) {
HandlerMethod handlerMethod = entry.getValue();
if (logger.isDebugEnabled()) {
logger.debug("Mapped " + exchange.getRequest().getMethod() + " " +
exchange.getRequest().getURI().getPath() + " to [" + handlerMethod + "]");
}
subscriber.onNext(handlerMethod);
break;
}
}
subscriber.onComplete();
}).next();
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setPathHelper(getPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentTypeResolver(getContentTypeResolver());
super.afterPropertiesSet();
}
private static class RequestMappingInfo implements Comparable {
/**
* Whether to use suffix pattern matching.
*/
public boolean useSuffixPatternMatch() {
return this.useSuffixPatternMatch;
}
private String path;
/**
* Whether to use registered suffixes for pattern matching.
*/
public boolean useRegisteredSuffixPatternMatch() {
return this.useRegisteredSuffixPatternMatch;
}
private Set<RequestMethod> methods;
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
*/
public boolean useTrailingSlashMatch() {
return this.useTrailingSlashMatch;
}
/**
* Return the configured {@link ContentTypeResolver}.
*/
public ContentTypeResolver getContentTypeResolver() {
return this.contentTypeResolver;
}
/**
* Return the file extensions to use for suffix pattern matching.
*/
public List<String> getFileExtensions() {
return this.config.getFileExtensions();
}
public RequestMappingInfo(String path, RequestMethod... methods) {
this(path, asList(methods));
}
/**
* {@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));
}
private static List<RequestMethod> asList(RequestMethod... requestMethods) {
return (requestMethods != null ?
Arrays.asList(requestMethods) : Collections.<RequestMethod>emptyList());
}
public RequestMappingInfo(String path, Collection<RequestMethod> methods) {
this.path = path;
this.methods = new TreeSet<>(methods);
}
public String getPath() {
return this.path;
}
public Set<RequestMethod> getMethods() {
return this.methods;
}
public boolean matchesRequest(ServerHttpRequest request) {
String httpMethod = request.getMethod().name();
return request.getURI().getPath().equals(getPath()) &&
(getMethods().isEmpty() || getMethods().contains(RequestMethod.valueOf(httpMethod)));
}
@Override
public int compareTo(Object o) {
RequestMappingInfo other = (RequestMappingInfo) o;
if (!this.path.equals(other.getPath())) {
return -1;
/**
* 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);
}
if (this.methods.isEmpty() && !other.methods.isEmpty()) {
return 1;
}
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 org.springframework.web.reactive.result.condition.AbstractRequestCondition
* AbstractRequestCondition} for custom condition types and using
* {@link org.springframework.web.reactive.result.condition.CompositeRequestCondition
* CompositeRequestCondition} to provide multiple custom conditions.
* @param handlerType the handler type for which to create the condition
* @return the condition, or {@code null}
*/
@SuppressWarnings("UnusedParameters")
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 org.springframework.web.reactive.result.condition.AbstractRequestCondition
* AbstractRequestCondition} for custom condition types and using
* {@link org.springframework.web.reactive.result.condition.CompositeRequestCondition
* CompositeRequestCondition} to provide multiple custom conditions.
* @param method the handler method for which to create the condition
* @return the condition, or {@code null}
*/
@SuppressWarnings("UnusedParameters")
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]);
}
if (!this.methods.isEmpty() && other.methods.isEmpty()) {
return -1;
}
if (this.methods.equals(other.methods)) {
return 0;
}
return -1;
return resolvedPatterns;
}
}

View File

@@ -16,7 +16,11 @@
package org.springframework.web.util;
import java.io.UnsupportedEncodingException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
/**
@@ -49,11 +53,12 @@ public class HttpRequestPathHelper {
public String getLookupPathForRequest(ServerWebExchange exchange) {
String path = exchange.getRequest().getURI().getPath();
return (this.shouldUrlDecode() ? decode(path) : path);
String path = exchange.getRequest().getURI().getRawPath();
return (this.shouldUrlDecode() ? decode(exchange, path) : path);
}
private String decode(String path) {
private String decode(ServerWebExchange exchange, String path) {
// TODO: look up request encoding?
try {
return UriUtils.decode(path, "UTF-8");
}
@@ -63,4 +68,48 @@ public class HttpRequestPathHelper {
}
}
/**
* Decode the given URI path variables unless {@link #setUrlDecode(boolean)}
* is set to {@code true} in which case it is assumed the URL path from
* which the variables were extracted is already decoded through a call to
* {@link #getLookupPathForRequest(ServerWebExchange)}.
* @param exchange current exchange
* @param vars URI variables extracted from the URL path
* @return the same Map or a new Map instance
*/
public Map<String, String> decodePathVariables(ServerWebExchange exchange, Map<String, String> vars) {
if (this.urlDecode) {
return vars;
}
Map<String, String> decodedVars = new LinkedHashMap<>(vars.size());
for (Map.Entry<String, String> entry : vars.entrySet()) {
decodedVars.put(entry.getKey(), decode(exchange, entry.getValue()));
}
return decodedVars;
}
/**
* Decode the given matrix variables unless {@link #setUrlDecode(boolean)}
* is set to {@code true} in which case it is assumed the URL path from
* which the variables were extracted is already decoded through a call to
* {@link #getLookupPathForRequest(ServerWebExchange)}.
* @param exchange current exchange
* @param vars URI variables extracted from the URL path
* @return the same Map or a new Map instance
*/
public MultiValueMap<String, String> decodeMatrixVariables(ServerWebExchange exchange,
MultiValueMap<String, String> vars) {
if (this.urlDecode) {
return vars;
}
MultiValueMap<String, String> decodedVars = new LinkedMultiValueMap<>(vars.size());
for (String key : vars.keySet()) {
for (String value : vars.get(key)) {
decodedVars.add(key, decode(exchange, value));
}
}
return decodedVars;
}
}

View File

@@ -0,0 +1,535 @@
/*
* 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.reactive.result.method;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.core.test.TestSubscriber;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.ModelMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
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.method.HandlerMethod;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.result.method.RequestMappingInfo.BuilderConfiguration;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.WebSessionManager;
import org.springframework.web.util.HttpRequestPathHelper;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link RequestMappingInfoHandlerMapping}.
*
* @author Rossen Stoyanchev
*/
public class RequestMappingInfoHandlerMappingTests {
private TestRequestMappingInfoHandlerMapping handlerMapping;
private HandlerMethod fooMethod;
private HandlerMethod fooParamMethod;
private HandlerMethod barMethod;
private HandlerMethod emptyMethod;
@Before
public void setUp() throws Exception {
TestController testController = new TestController();
this.fooMethod = new HandlerMethod(testController, "foo");
this.fooParamMethod = new HandlerMethod(testController, "fooParam");
this.barMethod = new HandlerMethod(testController, "bar");
this.emptyMethod = new HandlerMethod(testController, "empty");
this.handlerMapping = new TestRequestMappingInfoHandlerMapping();
this.handlerMapping.registerHandler(testController);
}
@Test
public void getMappingPathPatterns() throws Exception {
String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"};
RequestMappingInfo info = RequestMappingInfo.paths(patterns).build();
Set<String> actual = this.handlerMapping.getMappingPathPatterns(info);
assertEquals(new HashSet<>(Arrays.asList(patterns)), actual);
}
@Test
public void getHandlerDirectMatch() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/foo");
HandlerMethod handlerMethod = getHandler(exchange);
assertEquals(this.fooMethod.getMethod(), handlerMethod.getMethod());
}
@Test
public void getHandlerGlobMatch() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/bar");
HandlerMethod handlerMethod = getHandler(exchange);
assertEquals(this.barMethod.getMethod(), handlerMethod.getMethod());
}
@Test
public void getHandlerEmptyPathMatch() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "");
HandlerMethod handlerMethod = getHandler(exchange);
assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
exchange = createExchange(HttpMethod.GET, "/");
handlerMethod = getHandler(exchange);
assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
}
@Test
public void getHandlerBestMatch() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/foo");
exchange.getRequest().getQueryParams().add("p", "anything");
HandlerMethod handlerMethod = getHandler(exchange);
assertEquals(this.fooParamMethod.getMethod(), handlerMethod.getMethod());
}
@Test
public void getHandlerRequestMethodNotAllowed() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.POST, "/bar");
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
assertError(mono, HttpRequestMethodNotSupportedException.class,
ex -> assertArrayEquals(new String[]{"GET", "HEAD"}, ex.getSupportedMethods()));
}
// SPR-9603
@Test
public void getHandlerRequestMethodMatchFalsePositive() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/users");
exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
this.handlerMapping.registerHandler(new UserController());
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
TestSubscriber<Object> subscriber = new TestSubscriber<>();
mono.subscribeWith(subscriber);
subscriber.assertError(HttpMediaTypeNotAcceptableException.class);
}
// SPR-8462
@Test
public void getHandlerMediaTypeNotSupported() throws Exception {
testHttpMediaTypeNotSupportedException("/person/1");
testHttpMediaTypeNotSupportedException("/person/1/");
testHttpMediaTypeNotSupportedException("/person/1.json");
}
@Test
public void getHandlerHttpOptions() throws Exception {
testHttpOptions("/foo", "GET,HEAD");
testHttpOptions("/person/1", "PUT");
testHttpOptions("/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS");
testHttpOptions("/something", "PUT,POST");
}
@Test
public void getHandlerTestInvalidContentType() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.PUT, "/person/1");
exchange.getRequest().getHeaders().add("Content-Type", "bogus");
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
assertError(mono, HttpMediaTypeNotSupportedException.class,
ex -> assertEquals("Invalid mime type \"bogus\": does not contain '/'", ex.getMessage()));
}
// SPR-8462
@Test
public void getHandlerMediaTypeNotAccepted() throws Exception {
testHttpMediaTypeNotAcceptableException("/persons");
testHttpMediaTypeNotAcceptableException("/persons/");
testHttpMediaTypeNotAcceptableException("/persons.json");
}
// SPR-12854
@Test
public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/params");
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
assertError(mono, UnsatisfiedServletRequestParameterException.class, ex -> {
List<String[]> groups = ex.getParamConditionGroups();
assertEquals(2, groups.size());
assertThat(Arrays.asList("foo=bar", "bar=baz"),
containsInAnyOrder(groups.get(0)[0], groups.get(1)[0]));
});
}
@Test
public void getHandlerProducibleMediaTypesAttribute() throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/content");
exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
getHandler(exchange);
String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
assertEquals(Collections.singleton(MediaType.APPLICATION_XML), exchange.getAttributes().get(name));
exchange = createExchange(HttpMethod.GET, "/content");
exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
getHandler(exchange);
assertNull("Negated expression shouldn't be listed as producible type",
exchange.getAttributes().get(name));
}
@SuppressWarnings("unchecked")
@Test
public void handleMatchUriTemplateVariables() throws Exception {
RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/{path2}").build();
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2");
String lookupPath = exchange.getRequest().getURI().getPath();
this.handlerMapping.handleMatch(key, lookupPath, exchange);
String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
Map<String, String> uriVariables = (Map<String, String>) exchange.getAttributes().get(name);
assertNotNull(uriVariables);
assertEquals("1", uriVariables.get("path1"));
assertEquals("2", uriVariables.get("path2"));
}
// SPR-9098
@Test
public void handleMatchUriTemplateVariablesDecode() throws Exception {
RequestMappingInfo key = RequestMappingInfo.paths("/{group}/{identifier}").build();
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/group/a%2Fb");
HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
pathHelper.setUrlDecode(false);
String lookupPath = pathHelper.getLookupPathForRequest(exchange);
this.handlerMapping.setPathHelper(pathHelper);
this.handlerMapping.handleMatch(key, lookupPath, exchange);
String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
@SuppressWarnings("unchecked")
Map<String, String> uriVariables = (Map<String, String>) exchange.getAttributes().get(name);
assertNotNull(uriVariables);
assertEquals("group", uriVariables.get("group"));
assertEquals("a/b", uriVariables.get("identifier"));
}
@Test
public void handleMatchBestMatchingPatternAttribute() throws Exception {
RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/2", "/**").build();
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2");
this.handlerMapping.handleMatch(key, "/1/2", exchange);
assertEquals("/{path1}/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
}
@Test
public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() throws Exception {
RequestMappingInfo key = RequestMappingInfo.paths().build();
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/1/2");
this.handlerMapping.handleMatch(key, "/1/2", exchange);
assertEquals("/1/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
}
@Test
public void handleMatchMatrixVariables() throws Exception {
ServerWebExchange exchange;
MultiValueMap<String, String> matrixVariables;
Map<String, String> uriVariables;
exchange = createExchange(HttpMethod.GET, "/");
handleMatch(exchange, "/{cars}", "/cars;colors=red,blue,green;year=2012");
matrixVariables = getMatrixVariables(exchange, "cars");
uriVariables = getUriTemplateVariables(exchange);
assertNotNull(matrixVariables);
assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors"));
assertEquals("2012", matrixVariables.getFirst("year"));
assertEquals("cars", uriVariables.get("cars"));
exchange = createExchange(HttpMethod.GET, "/");
handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars;colors=red,blue,green;year=2012");
matrixVariables = getMatrixVariables(exchange, "params");
uriVariables = getUriTemplateVariables(exchange);
assertNotNull(matrixVariables);
assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors"));
assertEquals("2012", matrixVariables.getFirst("year"));
assertEquals("cars", uriVariables.get("cars"));
assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params"));
exchange = createExchange(HttpMethod.GET, "/");
handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars");
matrixVariables = getMatrixVariables(exchange, "params");
uriVariables = getUriTemplateVariables(exchange);
assertNull(matrixVariables);
assertEquals("cars", uriVariables.get("cars"));
assertEquals("", uriVariables.get("params"));
}
@Test
public void handleMatchMatrixVariablesDecoding() throws Exception {
HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper();
urlPathHelper.setUrlDecode(false);
this.handlerMapping.setPathHelper(urlPathHelper );
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/");
handleMatch(exchange, "/path{filter}", "/path;mvar=a%2fb");
MultiValueMap<String, String> matrixVariables = getMatrixVariables(exchange, "filter");
Map<String, String> uriVariables = getUriTemplateVariables(exchange);
assertNotNull(matrixVariables);
assertEquals(Collections.singletonList("a/b"), matrixVariables.get("mvar"));
assertEquals(";mvar=a/b", uriVariables.get("filter"));
}
private ServerWebExchange createExchange(HttpMethod method, String url) throws URISyntaxException {
ServerHttpRequest request = new MockServerHttpRequest(method, new URI(url));
WebSessionManager sessionManager = mock(WebSessionManager.class);
return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager);
}
@SuppressWarnings("unchecked")
private <T> void assertError(Mono<Object> mono, final Class<T> exceptionClass, final Consumer<T> consumer) {
TestSubscriber<Object> subscriber = new TestSubscriber<>();
mono.subscribeWith(subscriber);
subscriber.assertErrorWith(ex -> {
assertEquals(exceptionClass, ex.getClass());
consumer.accept((T) ex);
});
}
@SuppressWarnings("ConstantConditions")
private HandlerMethod getHandler(ServerWebExchange exchange) throws Exception {
Mono<Object> handler = this.handlerMapping.getHandler(exchange);
return (HandlerMethod) handler.get();
}
private void testHttpMediaTypeNotSupportedException(String url) throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.PUT, url);
exchange.getRequest().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
assertError(mono, HttpMediaTypeNotSupportedException.class, ex ->
assertEquals("Invalid supported consumable media types",
Collections.singletonList(new MediaType("application", "xml")),
ex.getSupportedMediaTypes()));
}
private void testHttpOptions(String requestURI, String allowHeader) throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.OPTIONS, requestURI);
HandlerMethod handlerMethod = getHandler(exchange);
ModelMap model = new ExtendedModelMap();
Mono<HandlerResult> mono = new InvocableHandlerMethod(handlerMethod).invokeForRequest(exchange, model);
HandlerResult result = mono.get();
assertNotNull(result);
Optional<Object> value = result.getReturnValue();
assertTrue(value.isPresent());
assertEquals(HttpHeaders.class, value.get().getClass());
assertEquals(allowHeader, ((HttpHeaders) value.get()).getFirst("Allow"));
}
private void testHttpMediaTypeNotAcceptableException(String url) throws Exception {
ServerWebExchange exchange = createExchange(HttpMethod.GET, url);
exchange.getRequest().getHeaders().setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
assertError(mono, HttpMediaTypeNotAcceptableException.class, ex ->
assertEquals("Invalid supported producible media types",
Collections.singletonList(new MediaType("application", "xml")),
ex.getSupportedMediaTypes()));
}
private void handleMatch(ServerWebExchange exchange, String pattern, String lookupPath) {
RequestMappingInfo info = RequestMappingInfo.paths(pattern).build();
this.handlerMapping.handleMatch(info, lookupPath, exchange);
}
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> getMatrixVariables(ServerWebExchange exchange, String uriVarName) {
String attrName = HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE;
return ((Map<String, MultiValueMap<String, String>>) exchange.getAttributes().get(attrName)).get(uriVarName);
}
@SuppressWarnings("unchecked")
private Map<String, String> getUriTemplateVariables(ServerWebExchange exchange) {
String attrName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
return (Map<String, String>) exchange.getAttributes().get(attrName);
}
@SuppressWarnings("unused")
@Controller
private static class TestController {
@RequestMapping(value = "/foo", method = RequestMethod.GET)
public void foo() {
}
@RequestMapping(value = "/foo", method = RequestMethod.GET, params="p")
public void fooParam() {
}
@RequestMapping(value = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD })
public void bar() {
}
@RequestMapping(value = "")
public void empty() {
}
@RequestMapping(value = "/person/{id}", method = RequestMethod.PUT, consumes="application/xml")
public void consumes(@RequestBody String text) {
}
@RequestMapping(value = "/persons", produces="application/xml")
public String produces() {
return "";
}
@RequestMapping(value = "/params", params="foo=bar")
public String param() {
return "";
}
@RequestMapping(value = "/params", params="bar=baz")
public String param2() {
return "";
}
@RequestMapping(value = "/content", produces="application/xml")
public String xmlContent() {
return "";
}
@RequestMapping(value = "/content", produces="!application/xml")
public String nonXmlContent() {
return "";
}
@RequestMapping(value = "/something", method = RequestMethod.OPTIONS)
public HttpHeaders fooOptions() {
HttpHeaders headers = new HttpHeaders();
headers.add("Allow", "PUT,POST");
return headers;
}
}
@SuppressWarnings("unused")
@Controller
private static class UserController {
@RequestMapping(value = "/users", method = RequestMethod.GET, produces = "application/json")
public void getUser() {
}
@RequestMapping(value = "/users", method = RequestMethod.PUT)
public void saveUser() {
}
}
private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping {
public void registerHandler(Object handler) {
super.detectHandlerMethods(handler);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
if (annotation != null) {
BuilderConfiguration options = new BuilderConfiguration();
options.setPathHelper(getPathHelper());
options.setPathMatcher(getPathMatcher());
options.setSuffixPatternMatch(true);
options.setTrailingSlashMatch(true);
return RequestMappingInfo.paths(annotation.value()).methods(annotation.method())
.params(annotation.params()).headers(annotation.headers())
.consumes(annotation.consumes()).produces(annotation.produces())
.options(options).build();
}
else {
return null;
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -16,111 +16,232 @@
package org.springframework.web.reactive.result.method.annotation;
import java.net.URI;
import java.util.List;
import java.util.stream.StreamSupport;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.session.WebSessionManager;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.reactive.accept.FileExtensionContentTypeResolver;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* @author Sebastien Deleuze
* Unit tests for {@link RequestMappingHandlerMapping}.
*
* @author Rossen Stoyanchev
*/
public class RequestMappingHandlerMappingTests {
private RequestMappingHandlerMapping mapping;
private final StaticWebApplicationContext wac = new StaticWebApplicationContext();
private final RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping();
@Before
public void setup() {
StaticApplicationContext wac = new StaticApplicationContext();
wac.registerSingleton("handlerMapping", RequestMappingHandlerMapping.class);
wac.registerSingleton("controller", TestController.class);
wac.refresh();
this.mapping = (RequestMappingHandlerMapping)wac.getBean("handlerMapping");
public void setUp() throws Exception {
this.handlerMapping.setApplicationContext(wac);
}
@Test
public void path() throws Exception {
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("boo"));
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager sessionManager = mock(WebSessionManager.class);
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager);
Publisher<?> handlerPublisher = this.mapping.getHandler(exchange);
HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher);
assertEquals(TestController.class.getMethod("boo"), handlerMethod.getMethod());
public void useRegisteredSuffixPatternMatch() {
assertTrue(this.handlerMapping.useSuffixPatternMatch());
assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch());
FileExtensionContentTypeResolver contentTypeResolver = mock(FileExtensionContentTypeResolver.class);
when(contentTypeResolver.getAllFileExtensions()).thenReturn(Collections.singletonList("json"));
this.handlerMapping.setContentTypeResolver(contentTypeResolver);
this.handlerMapping.afterPropertiesSet();
assertTrue(this.handlerMapping.useSuffixPatternMatch());
assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch());
assertEquals(Collections.singletonList("json"), this.handlerMapping.getFileExtensions());
}
@Test
public void method() throws Exception {
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.POST, new URI("foo"));
MockServerHttpResponse response = new MockServerHttpResponse();
WebSessionManager sessionManager = mock(WebSessionManager.class);
ServerWebExchange exchange = new DefaultServerWebExchange(request, response, sessionManager);
Publisher<?> handlerPublisher = this.mapping.getHandler(exchange);
HandlerMethod handlerMethod = toHandlerMethod(handlerPublisher);
assertEquals(TestController.class.getMethod("postFoo"), handlerMethod.getMethod());
public void useRegisteredSuffixPatternMatchInitialization() {
FileExtensionContentTypeResolver contentTypeResolver = mock(FileExtensionContentTypeResolver.class);
when(contentTypeResolver.getAllFileExtensions()).thenReturn(Collections.singletonList("json"));
request = new MockServerHttpRequest(HttpMethod.GET, new URI("foo"));
exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager);
handlerPublisher = this.mapping.getHandler(exchange);
handlerMethod = toHandlerMethod(handlerPublisher);
assertEquals(TestController.class.getMethod("getFoo"), handlerMethod.getMethod());
final Set<String> actualExtensions = new HashSet<>();
RequestMappingHandlerMapping localHandlerMapping = new RequestMappingHandlerMapping() {
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
actualExtensions.addAll(getFileExtensions());
return super.getMappingForMethod(method, handlerType);
}
};
this.wac.registerSingleton("testController", ComposedAnnotationController.class);
this.wac.refresh();
localHandlerMapping.setContentTypeResolver(contentTypeResolver);
localHandlerMapping.setUseRegisteredSuffixPatternMatch(true);
localHandlerMapping.setApplicationContext(this.wac);
localHandlerMapping.afterPropertiesSet();
assertEquals(Collections.singleton("json"), actualExtensions);
}
private HandlerMethod toHandlerMethod(Publisher<?> handlerPublisher) throws InterruptedException {
assertNotNull(handlerPublisher);
List<?> handlerList = StreamSupport.stream(Flux.from(handlerPublisher).toIterable().spliterator(), false).collect(toList());
assertEquals(1, handlerList.size());
return (HandlerMethod) handlerList.get(0);
@Test
public void useSuffixPatternMatch() {
assertTrue(this.handlerMapping.useSuffixPatternMatch());
assertTrue(this.handlerMapping.useRegisteredSuffixPatternMatch());
this.handlerMapping.setUseSuffixPatternMatch(false);
assertFalse(this.handlerMapping.useSuffixPatternMatch());
this.handlerMapping.setUseRegisteredSuffixPatternMatch(false);
assertFalse("'false' registeredSuffixPatternMatch shouldn't impact suffixPatternMatch",
this.handlerMapping.useSuffixPatternMatch());
this.handlerMapping.setUseRegisteredSuffixPatternMatch(true);
assertTrue("'true' registeredSuffixPatternMatch should enable suffixPatternMatch",
this.handlerMapping.useSuffixPatternMatch());
}
@Test
public void resolveEmbeddedValuesInPatterns() {
this.handlerMapping.setEmbeddedValueResolver(
value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value
);
String[] patterns = new String[] { "/foo", "/${pattern}/bar" };
String[] result = this.handlerMapping.resolveEmbeddedValuesInPatterns(patterns);
assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result);
}
@Test
public void resolveRequestMappingViaComposedAnnotation() throws Exception {
RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST);
assertEquals(MediaType.APPLICATION_JSON_VALUE,
info.getConsumesCondition().getConsumableMediaTypes().iterator().next().toString());
assertEquals(MediaType.APPLICATION_JSON_VALUE,
info.getProducesCondition().getProducibleMediaTypes().iterator().next().toString());
}
@Test
public void getMapping() throws Exception {
assertComposedAnnotationMapping(RequestMethod.GET);
}
@Test
public void postMapping() throws Exception {
assertComposedAnnotationMapping(RequestMethod.POST);
}
@Test
public void putMapping() throws Exception {
assertComposedAnnotationMapping(RequestMethod.PUT);
}
@Test
public void deleteMapping() throws Exception {
assertComposedAnnotationMapping(RequestMethod.DELETE);
}
@Test
public void patchMapping() throws Exception {
assertComposedAnnotationMapping(RequestMethod.PATCH);
}
private RequestMappingInfo assertComposedAnnotationMapping(RequestMethod requestMethod) throws Exception {
String methodName = requestMethod.name().toLowerCase();
String path = "/" + methodName;
return assertComposedAnnotationMapping(methodName, path, requestMethod);
}
private RequestMappingInfo assertComposedAnnotationMapping(String methodName, String path,
RequestMethod requestMethod) throws Exception {
Class<?> clazz = ComposedAnnotationController.class;
Method method = clazz.getMethod(methodName);
RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, clazz);
assertNotNull(info);
Set<String> paths = info.getPatternsCondition().getPatterns();
assertEquals(1, paths.size());
assertEquals(path, paths.iterator().next());
Set<RequestMethod> methods = info.getMethodsCondition().getMethods();
assertEquals(1, methods.size());
assertEquals(requestMethod, methods.iterator().next());
return info;
}
@Controller
@SuppressWarnings("unused")
private static class TestController {
@Controller @SuppressWarnings("unused")
static class ComposedAnnotationController {
@RequestMapping(path = "foo", method = RequestMethod.POST)
public String postFoo() {
return "postFoo";
@RequestMapping
public void handle() {
}
@RequestMapping(path = "foo", method = RequestMethod.GET)
public String getFoo() {
return "getFoo";
@PostJson("/postJson")
public void postJson() {
}
@RequestMapping("bar")
public String bar() {
return "bar";
@GetMapping("/get")
public void get() {
}
@RequestMapping("boo")
public String boo() {
return "boo";
@PostMapping("/post")
public void post() {
}
@PutMapping("/put")
public void put() {
}
@DeleteMapping("/delete")
public void delete() {
}
@PatchMapping("/patch")
public void patch() {
}
}
@RequestMapping(method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface PostJson {
@AliasFor(annotation = RequestMapping.class, attribute = "path") @SuppressWarnings("unused")
String[] value() default {};
}
}