From fe7e2a7f54c49efd99de2236dfdddf2639c2b354 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 23 Sep 2011 18:20:27 +0000 Subject: [PATCH] Improvements to the way the content of RequestConditions is exposed. RequestCondition types keep individual expression types (e.g. the discrete header or param expressions) package private. Although the implementation of these types should remain private, there is no reason not to provide access to the underlying expression data -- e.g. for creating a REST endpoint documentation tool, or if you want to know which of the "consumes"/"produces" media types are negated. This change ensures that all RequestCondition types have a public getter that makes available the basic expression data. --- .../AbstractMediaTypeExpression.java | 100 ++++++++++++++++++ .../AbstractNameValueExpression.java | 14 ++- .../condition/ConsumesRequestCondition.java | 64 ++++++----- .../condition/HeadersRequestCondition.java | 9 +- .../mvc/condition/MediaTypeExpression.java | 77 ++------------ .../mvc/condition/NameValueExpression.java | 40 +++++++ .../mvc/condition/ParamsRequestCondition.java | 7 ++ .../condition/ProducesRequestCondition.java | 54 ++++++---- .../RequestMappingInfoHandlerMapping.java | 8 +- .../ConsumesRequestConditionTests.java | 7 ++ .../ProducesRequestConditionTests.java | 26 +++-- ...RequestMappingInfoHandlerMappingTests.java | 28 +++++ 12 files changed, 307 insertions(+), 127 deletions(-) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/NameValueExpression.java diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java new file mode 100644 index 00000000000..933a98c5b35 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2011 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.servlet.mvc.condition; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Supports media type expressions as described in: + * {@link RequestMapping#consumes()} and {@link RequestMapping#produces()}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + */ +abstract class AbstractMediaTypeExpression implements Comparable, MediaTypeExpression { + + private final MediaType mediaType; + + private final boolean isNegated; + + AbstractMediaTypeExpression(String expression) { + if (expression.startsWith("!")) { + isNegated = true; + expression = expression.substring(1); + } + else { + isNegated = false; + } + this.mediaType = MediaType.parseMediaType(expression); + } + + AbstractMediaTypeExpression(MediaType mediaType, boolean negated) { + this.mediaType = mediaType; + isNegated = negated; + } + + public MediaType getMediaType() { + return mediaType; + } + + public boolean isNegated() { + return isNegated; + } + + public final boolean match(HttpServletRequest request) { + boolean match = matchMediaType(request); + return !isNegated ? match : !match; + } + + protected abstract boolean matchMediaType(HttpServletRequest request); + + public int compareTo(AbstractMediaTypeExpression other) { + return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && getClass().equals(obj.getClass())) { + AbstractMediaTypeExpression other = (AbstractMediaTypeExpression) obj; + return (this.mediaType.equals(other.mediaType)) && (this.isNegated == other.isNegated); + } + return false; + } + + @Override + public int hashCode() { + return mediaType.hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (isNegated) { + builder.append('!'); + } + builder.append(mediaType.toString()); + return builder.toString(); + } + +} \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractNameValueExpression.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractNameValueExpression.java index a8555d2790f..b6ebb6d717b 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractNameValueExpression.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractNameValueExpression.java @@ -27,7 +27,7 @@ import javax.servlet.http.HttpServletRequest; * @author Arjen Poutsma * @since 3.1 */ -abstract class AbstractNameValueExpression { +abstract class AbstractNameValueExpression implements NameValueExpression { protected final String name; @@ -48,6 +48,18 @@ abstract class AbstractNameValueExpression { this.value = parseValue(expression.substring(separator + 1)); } } + + public String getName() { + return this.name; + } + + public T getValue() { + return this.value; + } + + public boolean isNegated() { + return this.isNegated; + } protected abstract T parseValue(String valueExpression); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java index a61e015e9cf..a98b7f18ecc 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java @@ -32,11 +32,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression; /** - * A logical disjunction (' || ') request condition to match a request's 'Content-Type' - * header to a list of media type expressions. Two kinds of media type expressions are - * supported, which are described in {@link RequestMapping#consumes()} and - * {@link RequestMapping#headers()} where the header name is 'Content-Type'. - * Regardless of which syntax is used, the semantics are the same. + * A logical disjunction (' || ') request condition to match a request's + * 'Content-Type' header to a list of media type expressions. Two kinds of + * media type expressions are supported, which are described in + * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()} + * where the header name is 'Content-Type'. Regardless of which syntax is + * used, the semantics are the same. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -48,19 +49,21 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition getMediaTypes() { + public Set getExpressions() { + return new LinkedHashSet(this.expressions); + } + + /** + * Returns the media types for this condition excluding negated expressions. + */ + public Set getConsumableMediaTypes() { Set result = new LinkedHashSet(); - for (ConsumeMediaTypeExpression expression : expressions) { - result.add(expression.getMediaType()); + for (ConsumeMediaTypeExpression expression : this.expressions) { + if (!expression.isNegated()) { + result.add(expression.getMediaType()); + } } return result; } - + /** * Whether the condition has any media type expressions. */ public boolean isEmpty() { - return expressions.isEmpty(); + return this.expressions.isEmpty(); } @Override protected Collection getContent() { - return expressions; + return this.expressions; } @Override @@ -133,8 +145,8 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition> getExpressions() { + return new LinkedHashSet>(this.expressions); + } + @Override protected Collection getContent() { - return expressions; + return this.expressions; } @Override diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/MediaTypeExpression.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/MediaTypeExpression.java index bc68537d8a9..660f59481a8 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/MediaTypeExpression.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/MediaTypeExpression.java @@ -16,81 +16,24 @@ package org.springframework.web.servlet.mvc.condition; -import javax.servlet.http.HttpServletRequest; - import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; /** - * Supports media type expressions as described in: - * {@link RequestMapping#consumes()} and {@link RequestMapping#produces()}. - * - * @author Arjen Poutsma + * A contract for media type expressions (e.g. "text/plain", "!text/plain") as + * defined in the {@code @RequestMapping} annotation for "consumes" and + * "produces" conditions. + * * @author Rossen Stoyanchev * @since 3.1 + * + * @see RequestMapping#consumes() + * @see RequestMapping#produces() */ -abstract class MediaTypeExpression implements Comparable { +public interface MediaTypeExpression { - private final MediaType mediaType; + MediaType getMediaType(); - private final boolean isNegated; - - MediaTypeExpression(String expression) { - if (expression.startsWith("!")) { - isNegated = true; - expression = expression.substring(1); - } - else { - isNegated = false; - } - this.mediaType = MediaType.parseMediaType(expression); - } - - MediaTypeExpression(MediaType mediaType, boolean negated) { - this.mediaType = mediaType; - isNegated = negated; - } - - public final boolean match(HttpServletRequest request) { - boolean match = matchMediaType(request); - return !isNegated ? match : !match; - } - - protected abstract boolean matchMediaType(HttpServletRequest request); - - MediaType getMediaType() { - return mediaType; - } - - public int compareTo(MediaTypeExpression other) { - return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType()); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj != null && getClass().equals(obj.getClass())) { - MediaTypeExpression other = (MediaTypeExpression) obj; - return (this.mediaType.equals(other.mediaType)) && (this.isNegated == other.isNegated); - } - return false; - } - - @Override - public int hashCode() { - return mediaType.hashCode(); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (isNegated) { - builder.append('!'); - } - builder.append(mediaType.toString()); - return builder.toString(); - } + boolean isNegated(); } \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/NameValueExpression.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/NameValueExpression.java new file mode 100644 index 00000000000..670aae0ee92 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/NameValueExpression.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2011 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.servlet.mvc.condition; + +import org.springframework.web.bind.annotation.RequestMapping; + + +/** + * A contract for {@code "name!=value"} style expression used to specify request + * parameters and request header conditions in {@code @RequestMapping}. + * + * @author Rossen Stoyanchev + * @since 3.1 + * + * @see RequestMapping#params() + * @see RequestMapping#headers() + */ +public interface NameValueExpression { + + String getName(); + + T getValue(); + + boolean isNegated(); + +} \ No newline at end of file diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java index 253333667c2..46e74843c56 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java @@ -61,6 +61,13 @@ public final class ParamsRequestCondition extends AbstractRequestCondition> getExpressions() { + return new LinkedHashSet>(this.expressions); + } + @Override protected Collection getContent() { return expressions; diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java index ea03f6bc75e..0e46ffe4e1e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.mvc.condition; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -95,12 +96,21 @@ public final class ProducesRequestCondition extends AbstractRequestCondition getMediaTypes() { + public Set getExpressions() { + return new LinkedHashSet(this.expressions); + } + + /** + * Return the contained producible media types excluding negated expressions. + */ + public Set getProducibleMediaTypes() { Set result = new LinkedHashSet(); - for (ProduceMediaTypeExpression expression : getContent()) { - result.add(expression.getMediaType()); + for (ProduceMediaTypeExpression expression : this.expressions) { + if (!expression.isNegated()) { + result.add(expression.getMediaType()); + } } return result; } @@ -109,12 +119,12 @@ public final class ProducesRequestCondition extends AbstractRequestCondition getContent() { - return this.expressions.isEmpty() ? DEFAULT_EXPRESSIONS : this.expressions; + return this.expressions; } @Override @@ -166,7 +176,7 @@ public final class ProducesRequestCondition extends AbstractRequestConditionGet the lowest index of matching media types from each "produces" * condition first matching with {@link MediaType#equals(Object)} and * then with {@link MediaType#includes(MediaType)}. - *
  • If a lower index is found, the "produces" condition wins. + *
  • If a lower index is found, the condition at that index wins. *
  • If both indexes are equal, the media types at the index are * compared further with {@link MediaType#SPECIFICITY_COMPARATOR}. * @@ -207,10 +217,10 @@ public final class ProducesRequestCondition extends AbstractRequestCondition DEFAULT_EXPRESSIONS = - Collections.singletonList(new ProduceMediaTypeExpression("*/*")); + /** + * Return the contained "produces" expressions or if that's empty, a list + * with a {@code MediaType_ALL} expression. + */ + private List getExpressionsToCompare() { + return this.expressions.isEmpty() ? DEFAULT_EXPRESSION_LIST : this.expressions; + } + + private static final List DEFAULT_EXPRESSION_LIST = + Arrays.asList(new ProduceMediaTypeExpression("*/*")); + /** * Parses and matches a single media type expression to a request's 'Accept' header. */ - static class ProduceMediaTypeExpression extends MediaTypeExpression { + static class ProduceMediaTypeExpression extends AbstractMediaTypeExpression { ProduceMediaTypeExpression(MediaType mediaType, boolean negated) { super(mediaType, negated); @@ -267,7 +286,6 @@ public final class ProducesRequestCondition extends AbstractRequestCondition uriTemplateVariables = getPathMatcher().extractUriTemplateVariables(pattern, lookupPath); request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables); - if (!info.getProducesCondition().isEmpty()) { - Set mediaTypes = info.getProducesCondition().getMediaTypes(); + if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { + Set mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes); } } @@ -123,10 +123,10 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } } if (info.getConsumesCondition().getMatchingCondition(request) == null) { - consumableMediaTypes.addAll(info.getConsumesCondition().getMediaTypes()); + consumableMediaTypes.addAll(info.getConsumesCondition().getConsumableMediaTypes()); } if (info.getProducesCondition().getMatchingCondition(request) == null) { - producibleMediaTypes.addAll(info.getProducesCondition().getMediaTypes()); + producibleMediaTypes.addAll(info.getProducesCondition().getProducibleMediaTypes()); } } } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestConditionTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestConditionTests.java index d9c5b4f6247..2f10963f077 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestConditionTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestConditionTests.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Collection; +import java.util.Collections; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -54,6 +55,12 @@ public class ConsumesRequestConditionTests { assertNull(condition.getMatchingCondition(request)); } + @Test + public void getConsumableMediaTypesNegatedExpression() { + ConsumesRequestCondition condition = new ConsumesRequestCondition("!application/xml"); + assertEquals(Collections.emptySet(), condition.getConsumableMediaTypes()); + } + @Test public void consumesWildcardMatch() { ConsumesRequestCondition condition = new ConsumesRequestCondition("text/*"); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java index f9f7da667ff..e2e0bb0b5c8 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Collection; +import java.util.Collections; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -30,11 +31,12 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition.Pr /** * @author Arjen Poutsma + * @author Rossen Stoyanchev */ public class ProducesRequestConditionTests { - + @Test - public void consumesMatch() { + public void producesMatch() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -44,7 +46,7 @@ public class ProducesRequestConditionTests { } @Test - public void negatedConsumesMatch() { + public void negatedProducesMatch() { ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -54,7 +56,13 @@ public class ProducesRequestConditionTests { } @Test - public void consumesWildcardMatch() { + public void getProducibleMediaTypesNegatedExpression() { + ProducesRequestCondition condition = new ProducesRequestCondition("!application/xml"); + assertEquals(Collections.emptySet(), condition.getProducibleMediaTypes()); + } + + @Test + public void producesWildcardMatch() { ProducesRequestCondition condition = new ProducesRequestCondition("text/*"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -64,7 +72,7 @@ public class ProducesRequestConditionTests { } @Test - public void consumesMultipleMatch() { + public void producesMultipleMatch() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -74,7 +82,7 @@ public class ProducesRequestConditionTests { } @Test - public void consumesSingleNoMatch() { + public void producesSingleNoMatch() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -244,10 +252,10 @@ public class ProducesRequestConditionTests { } @Test - public void parseConsumesAndHeaders() { - String[] consumes = new String[] {"text/plain"}; + public void parseProducesAndHeaders() { + String[] produces = new String[] {"text/plain"}; String[] headers = new String[]{"foo=bar", "accept=application/xml,application/pdf"}; - ProducesRequestCondition condition = new ProducesRequestCondition(consumes, headers); + ProducesRequestCondition condition = new ProducesRequestCondition(produces, headers); assertConditions(condition, "text/plain", "application/xml", "application/pdf"); } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index da38423d6e6..6fd4d5a984a 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -25,6 +25,7 @@ import static org.junit.Assert.fail; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -213,6 +214,23 @@ public class RequestMappingInfoHandlerMappingTests { assertEquals("/1/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } + @Test + public void producibleMediaTypesAttribute() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/content"); + request.addHeader("Accept", "application/xml"); + this.mapping.getHandler(request); + + assertEquals(Collections.singleton(MediaType.APPLICATION_XML), + request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)); + + request = new MockHttpServletRequest("GET", "/content"); + request.addHeader("Accept", "application/json"); + this.mapping.getHandler(request); + + assertNull("Negated expression should not be listed as a producible type", + request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)); + } + @Test public void mappedInterceptors() throws Exception { String path = "/foo"; @@ -261,6 +279,16 @@ public class RequestMappingInfoHandlerMappingTests { public String produces() { return ""; } + + @RequestMapping(value = "/content", produces="application/xml") + public String xmlContent() { + return ""; + } + + @RequestMapping(value = "/content", produces="!application/xml") + public String nonXmlContent() { + return ""; + } } private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping {