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 {