Complete RequestMappingHandlerMapping
This commit adds RequestMappingInfoHandlerMapping and RequestMappingHandlerMapping with support equivalent to that in spring-webmvc.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user