From 4826cae0641f07ae38f3e08fd99a2fa3abc71cac Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 17 Jun 2011 23:19:14 +0000 Subject: [PATCH] SPR-7812 Add CustomRequestCondition --- .../handler/AbstractHandlerMethodMapping.java | 10 +- .../mvc/method/RequestMappingInfo.java | 200 ++++++++++++------ .../RequestMappingInfoHandlerMapping.java | 112 ++++------ .../RequestMappingHandlerMapping.java | 26 ++- ...ort.java => AbstractRequestCondition.java} | 17 +- .../condition/ConsumesRequestCondition.java | 20 +- .../condition/CustomRequestCondition.java | 132 ++++++++++++ .../condition/HeadersRequestCondition.java | 8 +- .../condition/ParamsRequestCondition.java | 8 +- .../condition/PatternsRequestCondition.java | 10 +- .../condition/ProducesRequestCondition.java | 10 +- .../method/condition/RequestCondition.java | 12 +- .../RequestMethodsRequestCondition.java | 10 +- .../handler/HandlerMethodMappingTests.java | 9 +- .../RequestMappingInfoComparatorTests.java | 26 +-- .../mvc/method/RequestMappingInfoTests.java | 38 ++-- 16 files changed, 408 insertions(+), 240 deletions(-) rename org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/{RequestConditionSupport.java => AbstractRequestCondition.java} (77%) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/CustomRequestCondition.java diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 554fc456eaa..1fec5d58f11 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -257,14 +257,14 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap List matches = new ArrayList(); for (T mapping : mappings) { - T match = getMatchingMapping(mapping, lookupPath, request); + T match = getMatchingMapping(mapping, request); if (match != null) { matches.add(new Match(match, handlerMethods.get(mapping))); } } if (!matches.isEmpty()) { - Comparator comparator = new MatchComparator(getMappingComparator(lookupPath, request)); + Comparator comparator = new MatchComparator(getMappingComparator(request)); Collections.sort(matches, comparator); if (logger.isTraceEnabled()) { @@ -309,20 +309,18 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap * will contain only 1). * * @param mapping the mapping to get a match for - * @param lookupPath mapping lookup path within the current servlet mapping if applicable * @param request the current HTTP servlet request * @return a matching mapping, or {@code null} if the given mapping does not match the request */ - protected abstract T getMatchingMapping(T mapping, String lookupPath, HttpServletRequest request); + protected abstract T getMatchingMapping(T mapping, HttpServletRequest request); /** * Returns a comparator to sort request mappings with. The returned comparator should sort 'better' matches higher. * - * @param lookupPath mapping lookup path within the current servlet mapping if applicable * @param request the current HTTP servlet request * @return the comparator */ - protected abstract Comparator getMappingComparator(String lookupPath, HttpServletRequest request); + protected abstract Comparator getMappingComparator(HttpServletRequest request); /** * Invoked when no match was found. Default implementation returns {@code null}. diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index 883fcd0b682..ca35d1ca44e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -18,17 +18,29 @@ package org.springframework.web.servlet.mvc.method; import javax.servlet.http.HttpServletRequest; -import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.mvc.method.condition.ConsumesRequestCondition; +import org.springframework.web.servlet.mvc.method.condition.CustomRequestCondition; import org.springframework.web.servlet.mvc.method.condition.HeadersRequestCondition; import org.springframework.web.servlet.mvc.method.condition.ParamsRequestCondition; import org.springframework.web.servlet.mvc.method.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.method.condition.ProducesRequestCondition; +import org.springframework.web.servlet.mvc.method.condition.RequestCondition; import org.springframework.web.servlet.mvc.method.condition.RequestMethodsRequestCondition; /** - * Contains request mapping conditions to be matched to a given request. + * A RequestMapingInfo encapsulates and operates on the following request mapping conditions: + *
    + *
  • {@link PatternsRequestCondition}
  • + *
  • {@link RequestMethodsRequestCondition}
  • + *
  • {@link ParamsRequestCondition}
  • + *
  • {@link HeadersRequestCondition}
  • + *
  • {@link ConsumesRequestCondition}
  • + *
  • {@link ProducesRequestCondition}
  • + *
+ * + * Optionally a custom request condition may also be provided by wrapping it in an instance + * of {@link CustomRequestCondition}. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -47,129 +59,182 @@ public final class RequestMappingInfo { private final ConsumesRequestCondition consumesCondition; private final ProducesRequestCondition producesCondition; + + private CustomRequestCondition customCondition = new CustomRequestCondition(); private int hash; /** * Creates a new {@code RequestMappingInfo} instance. */ - public RequestMappingInfo(PatternsRequestCondition patternsCondition, - RequestMethodsRequestCondition methodsCondition, - ParamsRequestCondition paramsCondition, - HeadersRequestCondition headersCondition, - ConsumesRequestCondition consumesCondition, - ProducesRequestCondition producesCondition) { - this.patternsCondition = patternsCondition != null ? patternsCondition : new PatternsRequestCondition(); - this.methodsCondition = methodsCondition != null ? methodsCondition : new RequestMethodsRequestCondition(); - this.paramsCondition = paramsCondition != null ? paramsCondition : new ParamsRequestCondition(); - this.headersCondition = headersCondition != null ? headersCondition : new HeadersRequestCondition(); - this.consumesCondition = consumesCondition != null ? consumesCondition : new ConsumesRequestCondition(); - this.producesCondition = producesCondition != null ? producesCondition : new ProducesRequestCondition(); + public RequestMappingInfo(PatternsRequestCondition patterns, + RequestMethodsRequestCondition methods, + ParamsRequestCondition params, + HeadersRequestCondition headers, + ConsumesRequestCondition consumes, + ProducesRequestCondition produces) { + this(patterns, methods, params, headers, consumes, produces, null); } /** - * Package protected, used for testing. + * Creates a new {@code RequestMappingInfo} instance also providing a custom {@link RequestCondition}. + */ + public RequestMappingInfo(PatternsRequestCondition patterns, + RequestMethodsRequestCondition methods, + ParamsRequestCondition params, + HeadersRequestCondition headers, + ConsumesRequestCondition consumes, + ProducesRequestCondition produces, + CustomRequestCondition custom) { + this.patternsCondition = patterns != null ? patterns : new PatternsRequestCondition(); + this.methodsCondition = methods != null ? methods : new RequestMethodsRequestCondition(); + this.paramsCondition = params != null ? params : new ParamsRequestCondition(); + this.headersCondition = headers != null ? headers : new HeadersRequestCondition(); + this.consumesCondition = consumes != null ? consumes : new ConsumesRequestCondition(); + this.producesCondition = produces != null ? produces : new ProducesRequestCondition(); + this.customCondition = custom != null ? custom : new CustomRequestCondition(); + } + + /** + * Package protected constructor for tests. */ RequestMappingInfo(String[] patterns, RequestMethod... methods) { this(new PatternsRequestCondition(patterns), new RequestMethodsRequestCondition(methods), null, null, null, null); } /** - * Returns the patterns of this request mapping info. + * Returns the URL patterns of this request mapping info. */ public PatternsRequestCondition getPatternsCondition() { return patternsCondition; } /** - * Returns the request method condition of this request mapping info. + * Returns the HTTP request methods of this {@link RequestMappingInfo}. */ public RequestMethodsRequestCondition getMethodsCondition() { return methodsCondition; } /** - * Returns the request parameters condition of this request mapping info. + * Returns the "parameters" condition of this {@link RequestMappingInfo}. */ public ParamsRequestCondition getParamsCondition() { return paramsCondition; } /** - * Returns the request headers condition of this request mapping info. + * Returns the "headers" condition of this {@link RequestMappingInfo}. */ public HeadersRequestCondition getHeadersCondition() { return headersCondition; } /** - * Returns the request consumes condition of this request mapping info. + * Returns the "consumes" condition of this {@link RequestMappingInfo}. */ public ConsumesRequestCondition getConsumesCondition() { return consumesCondition; } /** - * Returns the request produces condition of this request mapping info. + * Returns the "produces" condition of this {@link RequestMappingInfo}. */ public ProducesRequestCondition getProducesCondition() { return producesCondition; } /** - * Combines this {@code RequestMappingInfo} with another as follows: - *
    - *
  • URL patterns: - *
      - *
    • If both have patterns combine them according to the rules of the given {@link PathMatcher} - *
    • If either contains patterns, but not both, use the available pattern - *
    • If neither contains patterns use "" - *
    - *
  • HTTP methods are combined as union of all HTTP methods listed in both keys. - *
  • Request parameters are combined as per {@link ParamsRequestCondition#combine(ParamsRequestCondition)}. - *
  • Request headers are combined as per {@link HeadersRequestCondition#combine(HeadersRequestCondition)}. - *
  • Consumes are combined as per {@link ConsumesRequestCondition#combine(ConsumesRequestCondition)}. - *
- * @param methodKey the key to combine with - * @return a new request mapping info containing conditions from both keys + * Sets a custom request condition. */ - public RequestMappingInfo combine(RequestMappingInfo methodKey) { - PatternsRequestCondition patterns = this.patternsCondition.combine(methodKey.patternsCondition); - RequestMethodsRequestCondition methods = this.methodsCondition.combine(methodKey.methodsCondition); - ParamsRequestCondition params = this.paramsCondition.combine(methodKey.paramsCondition); - HeadersRequestCondition headers = this.headersCondition.combine(methodKey.headersCondition); - ConsumesRequestCondition consumes = this.consumesCondition.combine(methodKey.consumesCondition); - ProducesRequestCondition produces = this.producesCondition.combine(methodKey.producesCondition); - - return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces); + public void setCustomCondition(CustomRequestCondition customCondition) { + this.customCondition = customCondition; + } + + /** + * Combines "this" request mapping info (i.e. the current instance) with another request mapping info instance. + *

Example: combine type- and method-level request mappings. + * @return a new request mapping info instance; never {@code null} + */ + public RequestMappingInfo combine(RequestMappingInfo other) { + PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition); + RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition); + ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition); + HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition); + ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition); + ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition); + CustomRequestCondition custom = this.customCondition.combine(other.customCondition); + + return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces, custom); } /** - * Returns a new {@code RequestMappingInfo} with conditions relevant to the current request. - * For example the list of URL path patterns is trimmed to contain the patterns that match the URL. - * @param request the current request - * @return a new request mapping info that contains all matching attributes, or {@code null} if not all conditions match + * Checks if all conditions in this request mapping info match the provided request and returns + * a potentially new request mapping info with conditions tailored to the current request. + *

For example the returned instance may contain the subset of URL patterns that match to + * the current request, sorted with best matching patterns on top. + * @return a new instance in case all conditions match; or {@code null} otherwise */ - public RequestMappingInfo getMatchingRequestMapping(HttpServletRequest request) { - RequestMethodsRequestCondition matchingMethod = methodsCondition.getMatchingCondition(request); - ParamsRequestCondition matchingParams = paramsCondition.getMatchingCondition(request); - HeadersRequestCondition matchingHeaders = headersCondition.getMatchingCondition(request); - ConsumesRequestCondition matchingConsumes = consumesCondition.getMatchingCondition(request); - ProducesRequestCondition matchingProduces = producesCondition.getMatchingCondition(request); - - if (matchingMethod == null || matchingParams == null || matchingHeaders == null || - matchingConsumes == null || matchingProduces == null) { + public RequestMappingInfo getMatchingRequestMappingInfo(HttpServletRequest request) { + RequestMethodsRequestCondition methods = methodsCondition.getMatchingCondition(request); + ParamsRequestCondition params = paramsCondition.getMatchingCondition(request); + HeadersRequestCondition headers = headersCondition.getMatchingCondition(request); + ConsumesRequestCondition consumes = consumesCondition.getMatchingCondition(request); + ProducesRequestCondition produces = producesCondition.getMatchingCondition(request); + + if (methods == null || params == null || headers == null || consumes == null || produces == null) { return null; } - PatternsRequestCondition matchingPatterns = patternsCondition.getMatchingCondition(request); - if (matchingPatterns != null) { - return new RequestMappingInfo(matchingPatterns, matchingMethod, - matchingParams, matchingHeaders, matchingConsumes, - matchingProduces); + PatternsRequestCondition patterns = patternsCondition.getMatchingCondition(request); + if (patterns == null) { + return null; } - - return null; + + CustomRequestCondition custom = customCondition.getMatchingCondition(request); + if (custom == null) { + return null; + } + + return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces, custom); + } + + /** + * Compares "this" info (i.e. the current instance) with another info in the context of a request. + *

Note: it is assumed both instances have been obtained via + * {@link #getMatchingRequestMappingInfo(HttpServletRequest)} to ensure they have conditions with + * content relevant to current request. + */ + public int compareTo(RequestMappingInfo other, HttpServletRequest request) { + int result = patternsCondition.compareTo(other.getPatternsCondition(), request); + if (result != 0) { + return result; + } + result = paramsCondition.compareTo(other.getParamsCondition(), request); + if (result != 0) { + return result; + } + result = headersCondition.compareTo(other.getHeadersCondition(), request); + if (result != 0) { + return result; + } + result = consumesCondition.compareTo(other.getConsumesCondition(), request); + if (result != 0) { + return result; + } + result = producesCondition.compareTo(other.getProducesCondition(), request); + if (result != 0) { + return result; + } + result = methodsCondition.compareTo(other.getMethodsCondition(), request); + if (result != 0) { + return result; + } + result = customCondition.compareTo(other.customCondition, request); + if (result != 0) { + return result; + } + return 0; } @Override @@ -184,7 +249,8 @@ public final class RequestMappingInfo { this.paramsCondition.equals(other.paramsCondition) && this.headersCondition.equals(other.headersCondition) && this.consumesCondition.equals(other.consumesCondition) && - this.producesCondition.equals(other.producesCondition)); + this.producesCondition.equals(other.producesCondition) && + this.customCondition.equals(other.customCondition)); } return false; } @@ -199,6 +265,7 @@ public final class RequestMappingInfo { result = 31 * result + headersCondition.hashCode(); result = 31 * result + consumesCondition.hashCode(); result = 31 * result + producesCondition.hashCode(); + result = 31 * result + customCondition.hashCode(); hash = result; } return result; @@ -213,6 +280,7 @@ public final class RequestMappingInfo { builder.append(",headers=").append(headersCondition); builder.append(",consumes=").append(consumesCondition); builder.append(",produces=").append(producesCondition); + builder.append(",custom=").append(customCondition); builder.append('}'); return builder.toString(); } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index 97fa811fff0..bbf1acea62e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -27,7 +27,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.springframework.http.MediaType; -import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -53,34 +52,47 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe while (mappings.size() > 1) { RequestMappingInfo mapping = mappings.remove(0); for (RequestMappingInfo otherMapping : mappings) { - // further validate mapping conditions + // TODO: further validate mapping conditions } } } + /** + * Get the URL paths associated with this {@link RequestMappingInfo}. + */ @Override protected Set getMappingPaths(RequestMappingInfo mapping) { - return mapping.getPatternsCondition().getPatterns(); + Set paths = new HashSet(); + for (String pattern : mapping.getPatternsCondition().getPatterns()) { + if (!getPathMatcher().isPattern(pattern)) { + paths.add(pattern); + } + } + return paths; } /** - * Returns a new {@link RequestMappingInfo} with attributes matching to the current request or {@code null}. - * - * @see RequestMappingInfo#getMatchingRequestMapping(String, HttpServletRequest, PathMatcher) + * Checks if the given RequestMappingInfo matches the current request and returns a potentially new + * RequestMappingInfo instances tailored to the current request, for example containing the subset + * of URL patterns or media types that match the request. + * + * @returns a RequestMappingInfo instance in case of a match; or {@code null} in case of no match. */ @Override - protected RequestMappingInfo getMatchingMapping(RequestMappingInfo mapping, - String lookupPath, - HttpServletRequest request) { - return mapping.getMatchingRequestMapping(request); + protected RequestMappingInfo getMatchingMapping(RequestMappingInfo mapping, HttpServletRequest request) { + return mapping.getMatchingRequestMappingInfo(request); } /** - * Returns a {@link Comparator} that can be used to sort and select the best matching {@link RequestMappingInfo}. + * Returns a {@link Comparator} for sorting {@link RequestMappingInfo} in the context of the given request. */ @Override - protected Comparator getMappingComparator(String lookupPath, HttpServletRequest request) { - return new RequestMappingInfoComparator(request); + protected Comparator getMappingComparator(final HttpServletRequest request) { + return new Comparator() { + public int compare(RequestMappingInfo info, RequestMappingInfo otherInfo) { + return info.compareTo(otherInfo, request); + } + }; } /** @@ -106,30 +118,33 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe /** * Iterates all {@link RequestMappingInfo}s looking for mappings that match by URL but not by HTTP method. * - * @throws HttpRequestMethodNotSupportedException if there are matches by URL but not by HTTP method + * @throws HttpRequestMethodNotSupportedException + * if there are matches by URL but not by HTTP method + * @throws HttpMediaTypeNotAcceptableException + * if there are matches by URL but the consumable media types don't match the 'Content-Type' header + * @throws HttpMediaTypeNotAcceptableException + * if there are matches by URL but the producible media types don't match the 'Accept' header */ @Override - protected HandlerMethod handleNoMatch(Set requestMappingInfos, - String lookupPath, + protected HandlerMethod handleNoMatch(Set requestMappingInfos, + String lookupPath, HttpServletRequest request) throws ServletException { Set allowedMethods = new HashSet(6); Set consumableMediaTypes = new HashSet(); Set producibleMediaTypes = new HashSet(); for (RequestMappingInfo info : requestMappingInfos) { - for (String pattern : info.getPatternsCondition().getPatterns()) { - if (getPathMatcher().match(pattern, lookupPath)) { - if (info.getMethodsCondition().getMatchingCondition(request) == null) { - for (RequestMethod method : info.getMethodsCondition().getMethods()) { - allowedMethods.add(method.name()); - } - } - if (info.getConsumesCondition().getMatchingCondition(request) == null) { - consumableMediaTypes.addAll(info.getConsumesCondition().getMediaTypes()); - } - if (info.getProducesCondition().getMatchingCondition(request) == null) { - producibleMediaTypes.addAll(info.getProducesCondition().getMediaTypes()); + if (info.getPatternsCondition().getMatchingCondition(request) != null) { + if (info.getMethodsCondition().getMatchingCondition(request) == null) { + for (RequestMethod method : info.getMethodsCondition().getMethods()) { + allowedMethods.add(method.name()); } } + if (info.getConsumesCondition().getMatchingCondition(request) == null) { + consumableMediaTypes.addAll(info.getConsumesCondition().getMediaTypes()); + } + if (info.getProducesCondition().getMatchingCondition(request) == null) { + producibleMediaTypes.addAll(info.getProducesCondition().getMediaTypes()); + } } } if (!allowedMethods.isEmpty()) { @@ -150,45 +165,4 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } } - /** - * A comparator for {@link RequestMappingInfo}s. Effective comparison can only be done in the context - * of a specific request. For example only a subset of URL patterns may apply to the current request. - */ - private class RequestMappingInfoComparator implements Comparator { - - private final HttpServletRequest request; - - public RequestMappingInfoComparator(HttpServletRequest request) { - this.request = request; - } - - public int compare(RequestMappingInfo mapping, RequestMappingInfo otherMapping) { - int result = mapping.getPatternsCondition().compareTo(otherMapping.getPatternsCondition(), request); - if (result != 0) { - return result; - } - result = mapping.getParamsCondition().compareTo(otherMapping.getParamsCondition(), request); - if (result != 0) { - return result; - } - result = mapping.getHeadersCondition().compareTo(otherMapping.getHeadersCondition(), request); - if (result != 0) { - return result; - } - result = mapping.getConsumesCondition().compareTo(otherMapping.getConsumesCondition(), request); - if (result != 0) { - return result; - } - result = mapping.getProducesCondition().compareTo(otherMapping.getProducesCondition(), request); - if (result != 0) { - return result; - } - result = mapping.getMethodsCondition().compareTo(otherMapping.getMethodsCondition(), request); - if (result != 0) { - return result; - } - return 0; - } - } - } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 23afb1cf64e..532be9e9c3d 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -37,7 +37,7 @@ import org.springframework.web.servlet.mvc.method.condition.RequestMethodsReques * * @author Arjen Poutsma * @author Rossen Stoyanchev - * @since 3.1.0 + * @since 3.1 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping { @@ -62,20 +62,18 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi */ @Override protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { - RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (annotation != null) { - RequestMappingInfo methodMapping = createFromRequestMapping(annotation); - RequestMapping typeAnnot = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); - if (typeAnnot != null) { - RequestMappingInfo typeMapping = createFromRequestMapping(typeAnnot); - return typeMapping.combine(methodMapping); - } - else { - return methodMapping; - } + RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (methodAnnotation == null) { + return null; + } + RequestMappingInfo methodInfo = createFromRequestMapping(methodAnnotation); + RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); + if (typeAnnotation != null) { + RequestMappingInfo typeInfo = createFromRequestMapping(typeAnnotation); + return typeInfo.combine(methodInfo); } else { - return null; + return methodInfo; } } @@ -88,5 +86,5 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), new ProducesRequestCondition(annotation.produces(), annotation.headers())); } - + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionSupport.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/AbstractRequestCondition.java similarity index 77% rename from org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionSupport.java rename to org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/AbstractRequestCondition.java index 7d2faad65aa..93266c87c27 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionSupport.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/AbstractRequestCondition.java @@ -25,11 +25,11 @@ import java.util.Iterator; * @author Rossen Stoyanchev * @since 3.1 */ -abstract class RequestConditionSupport> implements RequestCondition { +abstract class AbstractRequestCondition> implements RequestCondition { /** - * Returns the individual expressions a request condition is composed of such as - * URL patterns, HTTP request methods, parameter expressions, etc. + * Returns the discrete expressions a request condition is composed of such as URL patterns, + * HTTP request methods, parameter expressions, etc. */ protected abstract Collection getContent(); @@ -39,7 +39,7 @@ abstract class RequestConditionSupport other = (RequestConditionSupport) o; + AbstractRequestCondition other = (AbstractRequestCondition) o; return getContent().equals(other.getContent()); } return false; @@ -57,12 +57,7 @@ abstract class RequestConditionSupport { +public class ConsumesRequestCondition extends AbstractRequestCondition { private final List expressions; /** * Creates a {@link ConsumesRequestCondition} with the given consumable media type expressions. - * @param consumes the expressions to parse; if 0 the condition matches to every request + * @param consumes the expressions to parse; if 0, the condition matches to every request */ public ConsumesRequestCondition(String... consumes) { this(consumes, null); @@ -60,8 +60,8 @@ public class ConsumesRequestCondition extends RequestConditionSupportExample: method-level "consumes" overrides type-level "consumes" condition. */ @@ -133,13 +133,13 @@ public class ConsumesRequestCondition extends RequestConditionSupport { + + @SuppressWarnings("rawtypes") + private final RequestCondition customCondition; + + /** + * Creates a {@link CustomRequestCondition} that wraps the given {@link RequestCondition} instance. + * @param requestCondition the custom request condition to wrap + */ + public CustomRequestCondition(RequestCondition requestCondition) { + this.customCondition = requestCondition; + } + + /** + * Creates an empty {@link CustomRequestCondition}. + */ + public CustomRequestCondition() { + this(null); + } + + public RequestCondition getRequestCondition() { + return customCondition; + } + + @Override + protected Collection getContent() { + return customCondition != null ? Collections.singleton(customCondition) : Collections.emptyList(); + } + + @Override + protected String getToStringInfix() { + return ""; + } + + /** + * Delegates the operation to the wrapped custom request conditions. May also return "this" instance + * if the "other" does not contain a custom request condition and vice versa. + */ + @SuppressWarnings("unchecked") + public CustomRequestCondition combine(CustomRequestCondition other) { + if (customCondition == null && other.customCondition == null) { + return this; + } + else if (customCondition == null) { + return other; + } + else if (other.customCondition == null) { + return this; + } + else { + assertCompatible(other); + RequestCondition combined = (RequestCondition) customCondition.combine(other.customCondition); + return new CustomRequestCondition(combined); + } + } + + private void assertCompatible(CustomRequestCondition other) { + if (customCondition != null && other.customCondition != null) { + Class clazz = customCondition.getClass(); + Class otherClazz = other.customCondition.getClass(); + if (!clazz.equals(otherClazz)) { + throw new ClassCastException("Incompatible custom request conditions: " + clazz + ", " + otherClazz); + } + } + } + + /** + * Delegates the operation to the wrapped custom request condition; or otherwise returns the same + * instance if there is no custom request condition. + */ + public CustomRequestCondition getMatchingCondition(HttpServletRequest request) { + if (customCondition == null) { + return this; + } + RequestCondition match = (RequestCondition) customCondition.getMatchingCondition(request); + return new CustomRequestCondition(match); + } + + /** + * Delegates the operation to the wrapped custom request conditions after checking for the presence + * custom request conditions and asserting type safety. The presence of a custom request condition + * in one instance but not the other will cause it to be selected, and vice versa. + */ + @SuppressWarnings("unchecked") + public int compareTo(CustomRequestCondition other, HttpServletRequest request) { + if (customCondition == null && other.customCondition == null) { + return 0; + } + else if (customCondition == null) { + return 1; + } + else if (other.customCondition == null) { + return -1; + } + else { + assertCompatible(other); + return customCondition.compareTo(other.customCondition, request); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/HeadersRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/HeadersRequestCondition.java index 559e0e1ceb9..81a45e0c7f0 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/HeadersRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/HeadersRequestCondition.java @@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; * @author Rossen Stoyanchev * @since 3.1 */ -public class HeadersRequestCondition extends RequestConditionSupport { +public class HeadersRequestCondition extends AbstractRequestCondition { private final Set expressions; @@ -50,7 +50,7 @@ public class HeadersRequestCondition extends RequestConditionSupport { +public class ParamsRequestCondition extends AbstractRequestCondition { private final Set expressions; /** * Create a {@link ParamsRequestCondition} with the given param expressions. * - * @param params 0 or more param expressions; if 0 the condition will match to every request. + * @param params 0 or more param expressions; if 0, the condition will match to every request. */ public ParamsRequestCondition(String... params) { this(parseExpressions(params)); @@ -69,8 +69,8 @@ public class ParamsRequestCondition extends RequestConditionSupport { +public class PatternsRequestCondition extends AbstractRequestCondition { private final Set patterns; @@ -62,7 +62,7 @@ public class PatternsRequestCondition extends RequestConditionSupport getPatterns() { return patterns; } @@ -107,8 +107,8 @@ public class PatternsRequestCondition extends RequestConditionSupport { +public class ProducesRequestCondition extends AbstractRequestCondition { private final List expressions; @@ -60,8 +60,8 @@ public class ProducesRequestCondition extends RequestConditionSupportRequest conditions can be combined (e.g. type + method-level conditions), matched to a request, * or compared to each other to determine if one matches the request better. * + * @param The type of objects that this RequestCondition can be compared to and combined with. + * * @author Rossen Stoyanchev * @author Arjen Poutsma * @since 3.1 */ -public interface RequestCondition> { +public interface RequestCondition { /** * Defines the rules for combining "this" condition (i.e. the current instance) with another condition. @@ -36,7 +38,7 @@ public interface RequestCondition> { * * @returns a request condition instance that is the result of combining the two condition instances. */ - This combine(This other); + T combine(T other); /** * Checks if this condition matches the provided request and returns a potentially new request condition @@ -45,13 +47,13 @@ public interface RequestCondition> { * * @return a condition instance in case of a match; or {@code null} if there is no match. */ - This getMatchingCondition(HttpServletRequest request); + T getMatchingCondition(HttpServletRequest request); /** * Compares "this" condition (i.e. the current instance) with another condition in the context of a request. - *

Note: it is assumed instances have been obtained via {@link #getMatchingCondition(HttpServletRequest)} + *

Note: it is assumed both instances have been obtained via {@link #getMatchingCondition(HttpServletRequest)} * to ensure they have content relevant to current request only. */ - int compareTo(This other, HttpServletRequest request); + int compareTo(T other, HttpServletRequest request); } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestMethodsRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestMethodsRequestCondition.java index 8be1f398b09..c64936542ca 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestMethodsRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestMethodsRequestCondition.java @@ -36,13 +36,13 @@ import org.springframework.web.bind.annotation.RequestMethod; * @author Rossen Stoyanchev * @since 3.1 */ -public class RequestMethodsRequestCondition extends RequestConditionSupport { +public class RequestMethodsRequestCondition extends AbstractRequestCondition { private final Set methods; /** - * Create a {@link RequestMethodsRequestCondition} with the given {@link RequestMethod}s. - * @param requestMethods 0 or more HTTP request methods; if 0 the condition will match to every request. + * Create a {@link RequestMethodsRequestCondition} with the given request methods. + * @param requestMethods 0 or more HTTP request methods; if, 0 the condition will match to every request. */ public RequestMethodsRequestCondition(RequestMethod... requestMethods) { this(asList(requestMethods)); @@ -72,8 +72,8 @@ public class RequestMethodsRequestCondition extends RequestConditionSupport { + private UrlPathHelper pathHelper = new UrlPathHelper(); + private PathMatcher pathMatcher = new AntPathMatcher(); @Override - protected String getMatchingMapping(String pattern, String lookupPath, HttpServletRequest request) { + protected String getMatchingMapping(String pattern, HttpServletRequest request) { + String lookupPath = pathHelper.getLookupPathForRequest(request); return pathMatcher.match(pattern, lookupPath) ? pattern : null; } @@ -103,7 +107,8 @@ public class HandlerMethodMappingTests { } @Override - protected Comparator getMappingComparator(String lookupPath, HttpServletRequest request) { + protected Comparator getMappingComparator(HttpServletRequest request) { + String lookupPath = pathHelper.getLookupPathForRequest(request); return pathMatcher.getPatternComparator(lookupPath); } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoComparatorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoComparatorTests.java index 90619c719b5..bf4c26524d2 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoComparatorTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoComparatorTests.java @@ -34,7 +34,6 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl import org.springframework.web.servlet.mvc.method.condition.ParamsRequestCondition; import org.springframework.web.servlet.mvc.method.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.method.condition.RequestMethodsRequestCondition; -import org.springframework.web.util.UrlPathHelper; /** * Test fixture with {@link RequestMappingHandlerMapping} testing its {@link RequestMappingInfo} comparator. @@ -57,8 +56,7 @@ public class RequestMappingInfoComparatorTests { @Test public void moreSpecificPatternWins() { request.setRequestURI("/foo"); - String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); - Comparator comparator = handlerMapping.getMappingComparator(lookupPath, request); + Comparator comparator = handlerMapping.getMappingComparator(request); RequestMappingInfo key1 = new RequestMappingInfo(new String[]{"/fo*"}); RequestMappingInfo key2 = new RequestMappingInfo(new String[]{"/foo"}); @@ -68,8 +66,7 @@ public class RequestMappingInfoComparatorTests { @Test public void equalPatterns() { request.setRequestURI("/foo"); - String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); - Comparator comparator = handlerMapping.getMappingComparator(lookupPath, request); + Comparator comparator = handlerMapping.getMappingComparator(request); RequestMappingInfo key1 = new RequestMappingInfo(new String[]{"/foo*"}); RequestMappingInfo key2 = new RequestMappingInfo(new String[]{"/foo*"}); @@ -79,20 +76,19 @@ public class RequestMappingInfoComparatorTests { @Test public void greaterNumberOfMatchingPatternsWins() throws Exception { request.setRequestURI("/foo.html"); - String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); RequestMappingInfo key1 = new RequestMappingInfo(new String[]{"/foo", "*.jpeg"}); RequestMappingInfo key2 = new RequestMappingInfo(new String[]{"/foo", "*.html"}); - RequestMappingInfo match1 = handlerMapping.getMatchingMapping(key1, lookupPath, request); - RequestMappingInfo match2 = handlerMapping.getMatchingMapping(key2, lookupPath, request); + RequestMappingInfo match1 = handlerMapping.getMatchingMapping(key1, request); + RequestMappingInfo match2 = handlerMapping.getMatchingMapping(key2, request); List matches = asList(match1, match2); - Collections.sort(matches, handlerMapping.getMappingComparator(lookupPath, request)); + Collections.sort(matches, handlerMapping.getMappingComparator(request)); assertSame(match2.getPatternsCondition(), matches.get(0).getPatternsCondition()); } @Test public void oneMethodWinsOverNone() { - Comparator comparator = handlerMapping.getMappingComparator("", request); + Comparator comparator = handlerMapping.getMappingComparator(request); RequestMappingInfo key1 = new RequestMappingInfo(null); RequestMappingInfo key2 = new RequestMappingInfo(null, new RequestMethod[] {RequestMethod.GET}); @@ -108,7 +104,7 @@ public class RequestMappingInfoComparatorTests { new ParamsRequestCondition("foo"), null, null, null); List list = asList(empty, oneMethod, oneMethodOneParam); Collections.shuffle(list); - Collections.sort(list, handlerMapping.getMappingComparator("", request)); + Collections.sort(list, handlerMapping.getMappingComparator(request)); assertEquals(oneMethodOneParam, list.get(0)); assertEquals(oneMethod, list.get(1)); @@ -122,7 +118,7 @@ public class RequestMappingInfoComparatorTests { RequestMappingInfo none = new RequestMappingInfo(null); request.addHeader("Accept", "application/xml, text/html"); - Comparator comparator = handlerMapping.getMappingComparator("", request); + Comparator comparator = handlerMapping.getMappingComparator(request); int result = comparator.compare(html, xml); assertTrue("Invalid comparison result: " + result, result > 0); @@ -134,14 +130,14 @@ public class RequestMappingInfoComparatorTests { request = new MockHttpServletRequest(); request.addHeader("Accept", "application/xml, text/*"); - comparator = handlerMapping.getMappingComparator("", request); + comparator = handlerMapping.getMappingComparator(request); assertTrue(comparator.compare(html, xml) > 0); assertTrue(comparator.compare(xml, html) < 0); request = new MockHttpServletRequest(); request.addHeader("Accept", "application/pdf"); - comparator = handlerMapping.getMappingComparator("", request); + comparator = handlerMapping.getMappingComparator(request); assertTrue(comparator.compare(html, xml) == 0); assertTrue(comparator.compare(xml, html) == 0); @@ -149,7 +145,7 @@ public class RequestMappingInfoComparatorTests { // See SPR-7000 request = new MockHttpServletRequest(); request.addHeader("Accept", "text/html;q=0.9,application/xml"); - comparator = handlerMapping.getMappingComparator("", request); + comparator = handlerMapping.getMappingComparator(request); assertTrue(comparator.compare(html, xml) > 0); assertTrue(comparator.compare(xml, html) < 0); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java index d566c887229..3b01e2d97e5 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoTests.java @@ -87,34 +87,34 @@ public class RequestMappingInfoTests { @Test public void matchPatternsToRequest() { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - RequestMappingInfo match = createFromPatterns("/foo").getMatchingRequestMapping(request); + RequestMappingInfo match = createFromPatterns("/foo").getMatchingRequestMappingInfo(request); assertNotNull(match); request = new MockHttpServletRequest("GET", "/foo/bar"); - match = createFromPatterns("/foo/*").getMatchingRequestMapping(request); + match = createFromPatterns("/foo/*").getMatchingRequestMappingInfo(request); assertNotNull("Pattern match", match); request = new MockHttpServletRequest("GET", "/foo.html"); - match = createFromPatterns("/foo").getMatchingRequestMapping(request); + match = createFromPatterns("/foo").getMatchingRequestMappingInfo(request); assertNotNull("Implicit match by extension", match); assertEquals("Contains matched pattern", "/foo.*", match.getPatternsCondition().getPatterns().iterator().next()); request = new MockHttpServletRequest("GET", "/foo/"); - match = createFromPatterns("/foo").getMatchingRequestMapping(request); + match = createFromPatterns("/foo").getMatchingRequestMappingInfo(request); assertNotNull("Implicit match by trailing slash", match); assertEquals("Contains matched pattern", "/foo/", match.getPatternsCondition().getPatterns().iterator().next()); request = new MockHttpServletRequest("GET", "/foo.html"); - match = createFromPatterns("/foo.jpg").getMatchingRequestMapping(request); + match = createFromPatterns("/foo.jpg").getMatchingRequestMappingInfo(request); assertNull("Implicit match ignored if pattern has extension", match); request = new MockHttpServletRequest("GET", "/foo.html"); - match = createFromPatterns("/foo.jpg").getMatchingRequestMapping(request); + match = createFromPatterns("/foo.jpg").getMatchingRequestMappingInfo(request); assertNull("Implicit match ignored on pattern with trailing slash", match); } @@ -124,17 +124,17 @@ public class RequestMappingInfoTests { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); RequestMappingInfo key = createFromPatterns("/foo"); - RequestMappingInfo match = createFromPatterns("/foo").getMatchingRequestMapping(request); + RequestMappingInfo match = createFromPatterns("/foo").getMatchingRequestMappingInfo(request); assertNotNull("No method matches any method", match); key = new RequestMappingInfo(new String[]{"/foo"}, GET); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); assertNotNull("Exact match", match); key = new RequestMappingInfo(new String[]{"/foo"}, POST); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); assertNull("No match", match); } @@ -144,13 +144,13 @@ public class RequestMappingInfoTests { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); RequestMappingInfo key = new RequestMappingInfo(new String[] {"/foo*", "/bar"}, GET, POST); - RequestMappingInfo match = key.getMatchingRequestMapping(request); + RequestMappingInfo match = key.getMatchingRequestMappingInfo(request); RequestMappingInfo expected = new RequestMappingInfo(new String[] {"/foo*"}, GET); assertEquals("Matching RequestKey contains matched patterns and methods only", expected, match); key = createFromPatterns("/**", "/foo*", "/foo"); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); expected = createFromPatterns("/foo", "/foo*", "/**"); assertEquals("Matched patterns are sorted with best match at the top", expected, match); @@ -165,14 +165,14 @@ public class RequestMappingInfoTests { new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, new ParamsRequestCondition("foo=bar"), null, null, null); - RequestMappingInfo match = key.getMatchingRequestMapping(request); + RequestMappingInfo match = key.getMatchingRequestMappingInfo(request); assertNotNull(match); key = new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, new ParamsRequestCondition("foo!=bar"), null, null, null); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); assertNull(match); } @@ -186,14 +186,14 @@ public class RequestMappingInfoTests { new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, null, new HeadersRequestCondition("foo=bar"), null, null); - RequestMappingInfo match = key.getMatchingRequestMapping(request); + RequestMappingInfo match = key.getMatchingRequestMappingInfo(request); assertNotNull(match); key = new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, null, new HeadersRequestCondition("foo!=bar"), null, null); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); assertNull(match); } @@ -207,14 +207,14 @@ public class RequestMappingInfoTests { new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, null, null, new ConsumesRequestCondition("text/plain"), null); - RequestMappingInfo match = key.getMatchingRequestMapping(request); + RequestMappingInfo match = key.getMatchingRequestMappingInfo(request); assertNotNull(match); key = new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, null, null, new ConsumesRequestCondition("application/xml"), null); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); assertNull(match); } @@ -228,14 +228,14 @@ public class RequestMappingInfoTests { new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, null, null, null, new ProducesRequestCondition("text/plain")); - RequestMappingInfo match = key.getMatchingRequestMapping(request); + RequestMappingInfo match = key.getMatchingRequestMappingInfo(request); assertNotNull(match); key = new RequestMappingInfo( new PatternsRequestCondition("/foo"), null, null, null, null, new ProducesRequestCondition("application/xml")); - match = key.getMatchingRequestMapping(request); + match = key.getMatchingRequestMappingInfo(request); assertNull(match); }