diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java
index 9dd0ccb072..6253cb8020 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerMapping.java
@@ -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.
+ *
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.
+ *
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.
+ *
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.
+ *
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
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java
new file mode 100644
index 0000000000..7e29549416
--- /dev/null
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMapping.java
@@ -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 {
+
+ 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 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 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 uriVariables;
+ Map decodedUriVariables;
+
+ Set 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> matrixVars = extractMatrixVariables(exchange, uriVariables);
+ exchange.getAttributes().put(MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
+
+ if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
+ Set mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
+ exchange.getAttributes().put(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
+ }
+ }
+
+ private Map> extractMatrixVariables(
+ ServerWebExchange exchange, Map uriVariables) {
+
+ Map> result = new LinkedHashMap<>();
+ for (Entry 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 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 requestMappingInfos,
+ String lookupPath, ServerWebExchange exchange) throws Exception {
+
+ Set allowedMethods = new LinkedHashSet<>(4);
+
+ Set patternMatches = new HashSet<>();
+ Set 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 consumableMediaTypes;
+ Set producibleMediaTypes;
+ List 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 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 getConsumableMediaTypes(ServerWebExchange exchange,
+ Set partialMatches) {
+
+ Set result = new HashSet<>();
+ for (RequestMappingInfo partialMatch : partialMatches) {
+ if (partialMatch.getConsumesCondition().getMatchingCondition(exchange) == null) {
+ result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes());
+ }
+ }
+ return result;
+ }
+
+ private Set getProducibleMediaTypes(ServerWebExchange exchange,
+ Set partialMatches) {
+
+ Set result = new HashSet<>();
+ for (RequestMappingInfo partialMatch : partialMatches) {
+ if (partialMatch.getProducesCondition().getMatchingCondition(exchange) == null) {
+ result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes());
+ }
+ }
+ return result;
+ }
+
+ private List getRequestParams(ServerWebExchange exchange,
+ Set partialMatches) {
+
+ List result = new ArrayList<>();
+ for (RequestMappingInfo partialMatch : partialMatches) {
+ ParamsRequestCondition condition = partialMatch.getParamsCondition();
+ Set> expressions = condition.getExpressions();
+ if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(exchange) == null) {
+ int i = 0;
+ String[] array = new String[expressions.size()];
+ for (NameValueExpression 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 declaredMethods) {
+ this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
+ }
+
+ private static Set initAllowedHttpMethods(Set declaredMethods) {
+ Set result = new LinkedHashSet(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;
+ }
+ }
+
+}
diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java
index e6861bd8c2..96ec3b6c6e 100644
--- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java
+++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java
@@ -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 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.*".
+ *
The default value is {@code true}.
+ *
Note: 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.
+ *
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/".
+ *
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