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 4f1810b408a..34e10b9e9ea 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 @@ -25,7 +25,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; - import javax.servlet.http.HttpServletRequest; import org.springframework.core.annotation.AnnotationUtils; @@ -141,7 +140,9 @@ public class RequestMappingHandlerMapping extends AbstractHandlerMethodMappingPackage protected for testing purposes. */ RequestMappingInfo(Collection patterns, RequestMethod[] methods) { - this(patterns, RequestConditionFactory.parseMethods(methods), null, null, null); + this(patterns, RequestConditionFactory.parseMethods(methods), null, null, null, null); } /** @@ -75,12 +78,14 @@ public final class RequestMappingInfo { RequestMethodsRequestCondition methodsCondition, ParamsRequestCondition paramsCondition, HeadersRequestCondition headersCondition, - ConsumesRequestCondition consumesCondition) { + ConsumesRequestCondition consumesCondition, + ProducesRequestCondition producesCondition) { this.patterns = asUnmodifiableSet(prependLeadingSlash(patterns)); 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(); } private static Set prependLeadingSlash(Collection patterns) { @@ -106,40 +111,47 @@ public final class RequestMappingInfo { } /** - * Returns the patterns of this request key. + * Returns the patterns of this request mapping info. */ public Set getPatterns() { return patterns; } /** - * Returns the request method conditions of this request key. + * Returns the request method conditions of this request mapping info. */ public RequestMethodsRequestCondition getMethods() { return methodsCondition; } /** - * Returns the request parameters conditions of this request key. + * Returns the request parameters conditions of this request mapping info. */ public ParamsRequestCondition getParams() { return paramsCondition; } /** - * Returns the request headers conditions of this request key. + * Returns the request headers conditions of this request mapping info. */ public HeadersRequestCondition getHeaders() { return headersCondition; } /** - * Returns the request consumes conditions of this request key. + * Returns the request consumes conditions of this request mapping info. */ public ConsumesRequestCondition getConsumes() { return consumesCondition; } + /** + * Returns the request produces conditions of this request mapping info. + */ + public ProducesRequestCondition getProduces() { + return producesCondition; + } + /** * Combines this {@code RequestMappingInfo} with another as follows: *
    @@ -156,7 +168,7 @@ public final class RequestMappingInfo { *
* @param methodKey the key to combine with * @param pathMatcher to {@linkplain PathMatcher#combine(String, String) combine} the patterns - * @return a new request key containing conditions from both keys + * @return a new request mapping info containing conditions from both keys */ public RequestMappingInfo combine(RequestMappingInfo methodKey, PathMatcher pathMatcher) { Set patterns = combinePatterns(this.patterns, methodKey.patterns, pathMatcher); @@ -164,8 +176,9 @@ public final class RequestMappingInfo { 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); + return new RequestMappingInfo(patterns, methods, params, headers, consumes, produces); } private static Set combinePatterns(Collection typePatterns, @@ -203,23 +216,24 @@ public final class RequestMappingInfo { * @param lookupPath mapping lookup path within the current servlet mapping if applicable * @param request the current request * @param pathMatcher to check for matching patterns - * @return a new request key that contains all matching attributes, or {@code null} if not all conditions match + * @return a new request mapping info that contains all matching attributes, or {@code null} if not all conditions match */ public RequestMappingInfo getMatchingRequestMapping(String lookupPath, HttpServletRequest request, PathMatcher pathMatcher) { RequestMethodsRequestCondition matchingMethodCondition = methodsCondition.getMatchingCondition(request); ParamsRequestCondition matchingParamsCondition = paramsCondition.getMatchingCondition(request); HeadersRequestCondition matchingHeadersCondition = headersCondition.getMatchingCondition(request); ConsumesRequestCondition matchingConsumesCondition = consumesCondition.getMatchingCondition(request); + ProducesRequestCondition matchingProducesCondition = producesCondition.getMatchingCondition(request); if (matchingMethodCondition == null || matchingParamsCondition == null || matchingHeadersCondition == null || - matchingConsumesCondition == null) { + matchingConsumesCondition == null || matchingProducesCondition == null) { return null; } else { List matchingPatterns = getMatchingPatterns(lookupPath, pathMatcher); if (!matchingPatterns.isEmpty()) { return new RequestMappingInfo(matchingPatterns, matchingMethodCondition, matchingParamsCondition, - matchingHeadersCondition, matchingConsumesCondition); + matchingHeadersCondition, matchingConsumesCondition, matchingProducesCondition); } else { return null; @@ -271,7 +285,8 @@ public final class RequestMappingInfo { this.methodsCondition.equals(other.methodsCondition) && this.paramsCondition.equals(other.paramsCondition) && this.headersCondition.equals(other.headersCondition) && - this.consumesCondition.equals(other.consumesCondition)); + this.consumesCondition.equals(other.consumesCondition) && + this.producesCondition.equals(other.producesCondition)); } return false; } @@ -285,6 +300,7 @@ public final class RequestMappingInfo { result = 31 * result + paramsCondition.hashCode(); result = 31 * result + headersCondition.hashCode(); result = 31 * result + consumesCondition.hashCode(); + result = 31 * result + producesCondition.hashCode(); hash = result; } return result; @@ -298,6 +314,7 @@ public final class RequestMappingInfo { builder.append(",params=").append(paramsCondition); builder.append(",headers=").append(headersCondition); builder.append(",consumes=").append(consumesCondition); + builder.append(",produces=").append(producesCondition); builder.append('}'); return builder.toString(); } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ConsumesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ConsumesRequestCondition.java index 0244584f08a..adc78f560c2 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ConsumesRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ConsumesRequestCondition.java @@ -16,7 +16,6 @@ package org.springframework.web.servlet.mvc.method.condition; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -27,33 +26,23 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.springframework.http.MediaType; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** - * Represents a collection of consumes conditions, typically obtained from {@link - * org.springframework.web.bind.annotation.RequestMapping#consumes() @RequestMapping.consumes()}. + * Represents a collection of consumes conditions, typically obtained from {@link org.springframework.web.bind.annotation.RequestMapping#consumes() + * @RequestMapping.consumes()}. * * @author Arjen Poutsma - * @see RequestConditionFactory#parseHeaders(String...) + * @see RequestConditionFactory#parseConsumes(String...) + * @see RequestConditionFactory#parseConsumes(String[], String[]) * @since 3.1 */ public class ConsumesRequestCondition - extends LogicalDisjunctionRequestCondition + extends MediaTypesRequestCondition implements Comparable { - private final ConsumeRequestCondition mostSpecificCondition; - ConsumesRequestCondition(Collection conditions) { super(conditions); - Assert.notEmpty(conditions, "'conditions' must not be empty"); - mostSpecificCondition = getMostSpecificCondition(); - } - - private ConsumeRequestCondition getMostSpecificCondition() { - List conditions = new ArrayList(getConditions()); - Collections.sort(conditions); - return conditions.get(0); } ConsumesRequestCondition(String... consumes) { @@ -72,7 +61,7 @@ public class ConsumesRequestCondition } /** - * Creates an default set of consumes request conditions. + * Creates a default set of consumes request conditions. */ public ConsumesRequestCondition() { this(Collections.singleton(new ConsumeRequestCondition(MediaType.ALL, false))); @@ -101,8 +90,8 @@ public class ConsumesRequestCondition } /** - * Combines this collection of request condition with another. Returns {@code other}, unless it has the default - * value (i.e. */*). + * Combines this collection of request condition with another. Returns {@code other}, unless it has the default value + * (i.e. */*). * * @param other the condition to combine with */ @@ -122,79 +111,32 @@ public class ConsumesRequestCondition } public int compareTo(ConsumesRequestCondition other) { - return this.mostSpecificCondition.compareTo(other.mostSpecificCondition); + return this.getMostSpecificCondition().compareTo(other.getMostSpecificCondition()); } - private static MediaType getContentType(HttpServletRequest request) { - if (StringUtils.hasLength(request.getContentType())) { - return MediaType.parseMediaType(request.getContentType()); - } - else { - return MediaType.APPLICATION_OCTET_STREAM; - } - } - - static class ConsumeRequestCondition implements RequestCondition, Comparable { - - private final MediaType mediaType; - - private final boolean isNegated; + static class ConsumeRequestCondition extends MediaTypesRequestCondition.MediaTypeRequestCondition { ConsumeRequestCondition(String expression) { - if (expression.startsWith("!")) { - isNegated = true; - expression = expression.substring(1); + super(expression); + } + + ConsumeRequestCondition(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + @Override + protected boolean match(HttpServletRequest request, MediaType mediaType) { + MediaType contentType = getContentType(request); + return mediaType.includes(contentType); + } + + private MediaType getContentType(HttpServletRequest request) { + if (StringUtils.hasLength(request.getContentType())) { + return MediaType.parseMediaType(request.getContentType()); } else { - isNegated = false; + return MediaType.APPLICATION_OCTET_STREAM; } - this.mediaType = MediaType.parseMediaType(expression); - } - - ConsumeRequestCondition(MediaType mediaType, boolean isNegated) { - this.mediaType = mediaType; - this.isNegated = isNegated; - } - - public boolean match(HttpServletRequest request) { - MediaType contentType = getContentType(request); - boolean match = this.mediaType.includes(contentType); - return !isNegated ? match : !match; - } - - public int compareTo(ConsumeRequestCondition other) { - return MediaType.SPECIFICITY_COMPARATOR.compare(this.mediaType, other.mediaType); - } - - MediaType getMediaType() { - return mediaType; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj != null && obj instanceof ConsumeRequestCondition) { - ConsumeRequestCondition other = (ConsumeRequestCondition) 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(); } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/MediaTypesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/MediaTypesRequestCondition.java new file mode 100644 index 00000000000..3cdf81d0260 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/MediaTypesRequestCondition.java @@ -0,0 +1,125 @@ +/* + * 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.method.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; + +/** + * @author Arjen Poutsma + */ +class MediaTypesRequestCondition + extends LogicalDisjunctionRequestCondition { + + private final List sortedConditions; + + public MediaTypesRequestCondition(Collection conditions) { + super(conditions); + Assert.notEmpty(conditions, "'conditions' must not be empty"); + sortedConditions = new ArrayList(conditions); + Collections.sort(sortedConditions); + } + + private MediaTypeRequestCondition getMostSpecificCondition(Collection conditions) { + List conditionList = new ArrayList(conditions); + Collections.sort(conditionList); + return conditionList.get(0); + } + + protected MediaTypeRequestCondition getMostSpecificCondition() { + return sortedConditions.get(0); + } + + protected List getSortedConditions() { + return sortedConditions; + } + + /** + * @author Arjen Poutsma + */ + protected abstract static class MediaTypeRequestCondition + implements RequestCondition, Comparable { + + private final MediaType mediaType; + + private final boolean isNegated; + + MediaTypeRequestCondition(MediaType mediaType, boolean negated) { + this.mediaType = mediaType; + isNegated = negated; + } + + MediaTypeRequestCondition(String expression) { + if (expression.startsWith("!")) { + isNegated = true; + expression = expression.substring(1); + } + else { + isNegated = false; + } + this.mediaType = MediaType.parseMediaType(expression); + } + + public boolean match(HttpServletRequest request) { + boolean match = match(request, this.mediaType); + return !isNegated ? match : !match; + } + + protected abstract boolean match(HttpServletRequest request, MediaType mediaType); + + MediaType getMediaType() { + return mediaType; + } + + public int compareTo(MediaTypeRequestCondition 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())) { + MediaTypeRequestCondition other = (MediaTypeRequestCondition) 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(); + } + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ProducesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ProducesRequestCondition.java new file mode 100644 index 00000000000..d2475ae3c85 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/ProducesRequestCondition.java @@ -0,0 +1,173 @@ +/* + * 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.method.condition; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; + +/** + * Represents a collection of produces conditions, typically obtained from {@link + * org.springframework.web.bind.annotation.RequestMapping#produces() @RequestMapping.produces()}. + * + * @author Arjen Poutsma + * @see RequestConditionFactory#parseProduces(String...) + * @see RequestConditionFactory#parseProduces(String[], String[]) + * @since 3.1 + */ +public class ProducesRequestCondition + extends MediaTypesRequestCondition { + + ProducesRequestCondition(Collection conditions) { + super(conditions); + } + + + ProducesRequestCondition(String... consumes) { + this(parseConditions(Arrays.asList(consumes))); + } + + private static Set parseConditions(List consumes) { + if (consumes.isEmpty()) { + consumes = Collections.singletonList("*/*"); + } + Set conditions = new LinkedHashSet(consumes.size()); + for (String consume : consumes) { + conditions.add(new ProduceRequestCondition(consume)); + } + return conditions; + } + + /** + * Creates an default set of consumes request conditions. + */ + public ProducesRequestCondition() { + this(Collections.singleton(new ProduceRequestCondition(MediaType.ALL, false))); + } + + /** + * Returns a new {@code RequestCondition} that contains all conditions of this key that match the request. + * + * @param request the request + * @return a new request condition that contains all matching attributes, or {@code null} if not all conditions match + */ + public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) { + Set matchingConditions = new LinkedHashSet(getConditions()); + for (Iterator iterator = matchingConditions.iterator(); iterator.hasNext();) { + ProduceRequestCondition condition = iterator.next(); + if (!condition.match(request)) { + iterator.remove(); + } + } + if (matchingConditions.isEmpty()) { + return null; + } + else { + return new ProducesRequestCondition(matchingConditions); + } + } + + /** + * Combines this collection of request condition with another. Returns {@code other}, unless it has the default + * value (i.e. {@code */*}). + * + * @param other the condition to combine with + */ + public ProducesRequestCondition combine(ProducesRequestCondition other) { + return !other.hasDefaultValue() ? other : this; + } + + private boolean hasDefaultValue() { + Set conditions = getConditions(); + if (conditions.size() == 1) { + ProduceRequestCondition condition = conditions.iterator().next(); + return condition.getMediaType().equals(MediaType.ALL); + } + else { + return false; + } + } + + public int compareTo(ProducesRequestCondition other, List acceptedMediaTypes) { + for (MediaType acceptedMediaType : acceptedMediaTypes) { + int thisIndex = this.indexOfMediaType(acceptedMediaType); + int otherIndex = other.indexOfMediaType(acceptedMediaType); + if (thisIndex != otherIndex) { + return otherIndex - thisIndex; + } else if (thisIndex != -1 && otherIndex != -1) { + ProduceRequestCondition thisCondition = this.getSortedConditions().get(thisIndex); + ProduceRequestCondition otherCondition = other.getSortedConditions().get(otherIndex); + int result = thisCondition.compareTo(otherCondition); + if (result != 0) { + return result; + } + } + } + return 0; + } + + private int indexOfMediaType(MediaType mediaType) { + List sortedConditions = getSortedConditions(); + for (int i = 0; i < sortedConditions.size(); i++) { + ProduceRequestCondition condition = sortedConditions.get(i); + if (mediaType.includes(condition.getMediaType())) { + return i; + } + } + return -1; + } + + static class ProduceRequestCondition extends MediaTypesRequestCondition.MediaTypeRequestCondition { + + ProduceRequestCondition(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + ProduceRequestCondition(String expression) { + super(expression); + } + + @Override + protected boolean match(HttpServletRequest request, MediaType mediaType) { + List acceptedMediaTypes = getAccept(request); + for (MediaType acceptedMediaType : acceptedMediaTypes) { + if (mediaType.isCompatibleWith(acceptedMediaType)) { + return true; + } + } + return false; + } + + private List getAccept(HttpServletRequest request) { + String acceptHeader = request.getHeader("Accept"); + if (StringUtils.hasLength(acceptHeader)) { + return MediaType.parseMediaTypes(acceptHeader); + } + else { + return Collections.singletonList(MediaType.ALL); + } + } + } +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionFactory.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionFactory.java index 461d48dc21b..f67aa1fba31 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionFactory.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/condition/RequestConditionFactory.java @@ -99,6 +99,15 @@ public abstract class RequestConditionFactory { return new ConsumesRequestCondition(consumes); } + /** + * Parses the given consumes and {@code Content-Type} headers, and returns them as a single request condition.

Only + * {@code Content-Type} headers will be used, all other headers will be ignored. + * + * @param consumes the consumes + * @param headers the headers + * @return the request condition + * @see org.springframework.web.bind.annotation.RequestMapping#consumes() + */ public static ConsumesRequestCondition parseConsumes(String[] consumes, String[] headers) { List allConditions = parseContentTypeHeaders(headers); @@ -115,14 +124,65 @@ public abstract class RequestConditionFactory { } private static List parseContentTypeHeaders(String[] headers) { - List allConditions = + List conditions = new ArrayList(); HeadersRequestCondition headersCondition = new HeadersRequestCondition(headers); for (HeadersRequestCondition.HeaderRequestCondition headerCondition : headersCondition.getConditions()) { if (CONTENT_TYPE_HEADER.equalsIgnoreCase(headerCondition.name)) { List mediaTypes = MediaType.parseMediaTypes(headerCondition.value); for (MediaType mediaType : mediaTypes) { - allConditions.add(new ConsumesRequestCondition.ConsumeRequestCondition(mediaType, + conditions.add(new ConsumesRequestCondition.ConsumeRequestCondition(mediaType, + headerCondition.isNegated)); + } + } + } + return conditions; + } + + /** + * Parses the given produces, and returns them as a single request condition. + * + * @param produces the produces + * @return the request condition + * @see org.springframework.web.bind.annotation.RequestMapping#produces() + */ + public static ProducesRequestCondition parseProduces(String... produces) { + return new ProducesRequestCondition(produces); + } + + /** + * Parses the given produces and {@code Accept} headers, and returns them as a single request condition.

Only {@code + * Accept} headers will be used, all other headers will be ignored. + * + * @param produces the consumes + * @param headers the headers + * @return the request condition + * @see org.springframework.web.bind.annotation.RequestMapping#produces() + */ + public static ProducesRequestCondition parseProduces(String[] produces, String[] headers) { + + List allConditions = parseAcceptHeaders(headers); + + // ignore the default consumes() value if any accept headers have been set + boolean headersHasAccept = !allConditions.isEmpty(); + boolean producesHasDefaultValue = produces.length == 1 && produces[0].equals("*/*"); + if (!headersHasAccept || !producesHasDefaultValue) { + for (String produce : produces) { + allConditions.add(new ProducesRequestCondition.ProduceRequestCondition(produce)); + } + } + return new ProducesRequestCondition(allConditions); + } + + private static List parseAcceptHeaders(String[] headers) { + List allConditions = + new ArrayList(); + HeadersRequestCondition headersCondition = new HeadersRequestCondition(headers); + for (HeadersRequestCondition.HeaderRequestCondition headerCondition : headersCondition.getConditions()) { + if (ACCEPT_HEADER.equalsIgnoreCase(headerCondition.name)) { + List mediaTypes = MediaType.parseMediaTypes(headerCondition.value); + for (MediaType mediaType : mediaTypes) { + allConditions.add(new ProducesRequestCondition.ProduceRequestCondition(mediaType, headerCondition.isNegated)); } } @@ -130,4 +190,5 @@ public abstract class RequestConditionFactory { return allConditions; } + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoComparatorTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoComparatorTests.java index 68e31284383..dc05c3ca4e1 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoComparatorTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoComparatorTests.java @@ -21,7 +21,6 @@ import java.util.Comparator; import java.util.List; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -100,7 +99,7 @@ public class RequestMappingInfoComparatorTests { RequestMappingInfo empty = new RequestMappingInfo(null, null); RequestMappingInfo oneMethod = new RequestMappingInfo(null, new RequestMethod[] {RequestMethod.GET}); RequestMappingInfo oneMethodOneParam = - new RequestMappingInfo(null, RequestConditionFactory.parseMethods(RequestMethod.GET), RequestConditionFactory.parseParams("foo"), null, null); + new RequestMappingInfo(null, RequestConditionFactory.parseMethods(RequestMethod.GET), RequestConditionFactory.parseParams("foo"), null, null, null); List list = asList(empty, oneMethod, oneMethodOneParam); Collections.shuffle(list); Collections.sort(list, handlerMapping.getMappingComparator("", request)); @@ -111,16 +110,16 @@ public class RequestMappingInfoComparatorTests { } @Test - @Ignore // TODO : remove ignore - public void acceptHeaders() { - RequestMappingInfo html = new RequestMappingInfo(null, null, null, RequestConditionFactory.parseHeaders("accept=text/html"), null); - RequestMappingInfo xml = new RequestMappingInfo(null, null, null, RequestConditionFactory.parseHeaders("accept=application/xml"), null); + public void produces() { + RequestMappingInfo html = new RequestMappingInfo(null, null, null, null, null, RequestConditionFactory.parseProduces("text/html")); + RequestMappingInfo xml = new RequestMappingInfo(null, null, null, null, null, RequestConditionFactory.parseProduces("application/xml")); RequestMappingInfo none = new RequestMappingInfo(null, null); request.addHeader("Accept", "application/xml, text/html"); Comparator comparator = handlerMapping.getMappingComparator("", request); - assertTrue(comparator.compare(html, xml) > 0); + int result = comparator.compare(html, xml); + assertTrue("Invalid comparison result: " + result, result > 0); assertTrue(comparator.compare(xml, html) < 0); assertTrue(comparator.compare(xml, none) < 0); assertTrue(comparator.compare(none, xml) > 0); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoTests.java index 65e9bed971e..472f6a0f1eb 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingInfoTests.java @@ -181,13 +181,13 @@ public class RequestMappingInfoTests { RequestMappingInfo key = new RequestMappingInfo(asList("/foo"), null, RequestConditionFactory.parseParams("foo=bar"), null, - null); + null, null); RequestMappingInfo match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); assertNotNull(match); key = new RequestMappingInfo(singleton("/foo"), null, RequestConditionFactory.parseParams("foo!=bar"), null, - null); + null, null); match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); assertNull(match); @@ -202,13 +202,13 @@ public class RequestMappingInfoTests { RequestMappingInfo key = new RequestMappingInfo(singleton("/foo"), null, null, RequestConditionFactory.parseHeaders("foo=bar"), - null); + null, null); RequestMappingInfo match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); assertNotNull(match); key = new RequestMappingInfo(singleton("/foo"), null, null, RequestConditionFactory.parseHeaders("foo!=bar"), - null); + null, null); match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); assertNull(match); @@ -222,13 +222,33 @@ public class RequestMappingInfoTests { String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); RequestMappingInfo key = new RequestMappingInfo(singleton("/foo"), null, null, null, - RequestConditionFactory.parseConsumes("text/plain")); + RequestConditionFactory.parseConsumes("text/plain"), null); RequestMappingInfo match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); assertNotNull(match); key = new RequestMappingInfo(singleton("/foo"), null, null, null, - RequestConditionFactory.parseConsumes("application/xml")); + RequestConditionFactory.parseConsumes("application/xml"), null); + match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); + + assertNull(match); + } + + @Test + public void producesCondition() { + PathMatcher pathMatcher = new AntPathMatcher(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addHeader("Accept", "text/plain"); + String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); + + RequestMappingInfo key = new RequestMappingInfo(singleton("/foo"), null, null, null, + null, RequestConditionFactory.parseProduces("text/plain")); + RequestMappingInfo match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); + + assertNotNull(match); + + key = new RequestMappingInfo(singleton("/foo"), null, null, null, null, + RequestConditionFactory.parseProduces("application/xml")); match = key.getMatchingRequestMapping(lookupPath, request, pathMatcher); assertNull(match); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletHandlerMethodTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletHandlerMethodTests.java index 39a09143dad..843485e36d4 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletHandlerMethodTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletHandlerMethodTests.java @@ -16,14 +16,6 @@ package org.springframework.web.servlet.mvc.method.annotation; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - import java.beans.PropertyEditorSupport; import java.io.IOException; import java.io.Serializable; @@ -48,7 +40,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; - import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -60,8 +51,8 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlRootElement; -import org.junit.Ignore; import org.junit.Test; + import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; @@ -142,6 +133,8 @@ import org.springframework.web.servlet.mvc.method.annotation.support.ServletWebA import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver; +import static org.junit.Assert.*; + /** * The origin of this test fixture is {@link ServletHandlerMethodTests} with tests in this class adapted to run * against the HandlerMethod infrastructure rather than against the DefaultAnnotationHandlerMapping, the @@ -1033,8 +1026,6 @@ public class ServletHandlerMethodTests { assertEquals("non-pdf", response.getContentAsString()); } - // TODO: uncomment ignore - @Ignore @Test public void acceptHeaders() throws ServletException, IOException { initDispatcherServlet(AcceptHeadersController.class, null); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/condition/ProducesRequestConditionTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/condition/ProducesRequestConditionTests.java new file mode 100644 index 00000000000..32dbac627b1 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/condition/ProducesRequestConditionTests.java @@ -0,0 +1,232 @@ +/* + * 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.method.condition; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.junit.Assert.*; + +/** + * @author Arjen Poutsma + */ +public class ProducesRequestConditionTests { + + @Test + public void consumesMatch() { + RequestCondition condition = new ProducesRequestCondition("text/plain"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/plain"); + + assertTrue(condition.match(request)); + } + + @Test + public void negatedConsumesMatch() { + RequestCondition condition = new ProducesRequestCondition("!text/plain"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/plain"); + + assertFalse(condition.match(request)); + } + + @Test + public void consumesWildcardMatch() { + RequestCondition condition = new ProducesRequestCondition("text/*"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/plain"); + + assertTrue(condition.match(request)); + } + + @Test + public void consumesMultipleMatch() { + RequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/plain"); + + assertTrue(condition.match(request)); + } + + @Test + public void consumesSingleNoMatch() { + RequestCondition condition = new ProducesRequestCondition("text/plain"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "application/xml"); + + assertFalse(condition.match(request)); + } + + @Test + public void compareToSingle() { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/*"); + + List accept = Collections.singletonList(MediaType.TEXT_PLAIN); + + int result = condition1.compareTo(condition2, accept); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, accept); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultiple() { + ProducesRequestCondition condition1 = new ProducesRequestCondition("*/*", "text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/*", "text/plain;q=0.7"); + + List accept = Collections.singletonList(MediaType.TEXT_PLAIN); + + int result = condition1.compareTo(condition2, accept); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, accept); + assertTrue("Invalid comparison result: " + result, result > 0); + + condition1 = new ProducesRequestCondition("*/*"); + condition2 = new ProducesRequestCondition("text/*"); + + accept = Collections.singletonList(new MediaType("text", "*")); + + result = condition1.compareTo(condition2, accept); + assertTrue("Invalid comparison result: " + result, result > 0); + + result = condition2.compareTo(condition1, accept); + assertTrue("Invalid comparison result: " + result, result < 0); + } + + @Test + public void compareToMultipleAccept() { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/*", "text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/*", "application/xml"); + + List accept = Arrays.asList(MediaType.TEXT_PLAIN, MediaType.APPLICATION_XML); + + int result = condition1.compareTo(condition2, accept); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, accept); + assertTrue("Invalid comparison result: " + result, result > 0); + + accept = Arrays.asList(MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN); + + result = condition1.compareTo(condition2, accept); + assertTrue("Invalid comparison result: " + result, result > 0); + + result = condition2.compareTo(condition1, accept); + assertTrue("Invalid comparison result: " + result, result < 0); + } + + @Test + public void combine() { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/xml"); + + ProducesRequestCondition result = condition1.combine(condition2); + assertEquals(condition2, result); + } + + @Test + public void combineWithDefault() { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("*/*"); + + ProducesRequestCondition result = condition1.combine(condition2); + assertEquals(condition1, result); + } + + @Test + public void parseConsumesAndHeaders() { + String[] consumes = new String[] {"text/plain"}; + String[] headers = new String[]{"foo=bar", "accept=application/xml,application/pdf"}; + ProducesRequestCondition condition = RequestConditionFactory.parseProduces(consumes, headers); + + assertConditions(condition, "text/plain", "application/xml", "application/pdf"); + } + + @Test + public void parseConsumesDefault() { + String[] consumes = new String[] {"*/*"}; + String[] headers = new String[0]; + ProducesRequestCondition condition = RequestConditionFactory.parseProduces(consumes, headers); + + assertConditions(condition, "*/*"); + } + @Test + public void parseConsumesDefaultAndHeaders() { + String[] consumes = new String[] {"*/*"}; + String[] headers = new String[]{"foo=bar", "accept=text/plain"}; + ProducesRequestCondition condition = RequestConditionFactory.parseProduces(consumes, headers); + + assertConditions(condition, "text/plain"); + } + + @Test + public void getMatchingCondition() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "text/plain"); + + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); + + ProducesRequestCondition result = condition.getMatchingCondition(request); + assertConditions(result, "text/plain"); + + condition = new ProducesRequestCondition("application/xml"); + + result = condition.getMatchingCondition(request); + assertNull(result); + } + + private void assertConditions(ProducesRequestCondition condition, String... expected) { + Set conditions = condition.getConditions(); + assertEquals("Invalid amount of conditions", conditions.size(), expected.length); + for (String s : expected) { + boolean found = false; + for (ProducesRequestCondition.ProduceRequestCondition requestCondition : conditions) { + String conditionMediaType = requestCondition.getMediaType().toString(); + if (conditionMediaType.equals(s)) { + found = true; + break; + + } + } + if (!found) { + fail("Condition [" + s + "] not found"); + } + } + + + + } + + + + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 67221d683fe..206d52b1f1e 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -311,4 +311,17 @@ public @interface RequestMapping { */ String[] consumes() default "*/*"; + /** + * The producible media types of the mapped request, narrowing the primary mapping. + *

The format is a sequence of media types ("text/plain", "application/*), + * with a request only mapped if the {@code Accept} matches one of these media types. + * Expressions can be negated by using the "!" operator, as in "!text/plain", which matches + * all requests with a {@code Accept} other than "text/plain". + *

Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings override + * this consumes restriction. + * @see org.springframework.http.MediaType + */ + String[] produces() default "*/*"; + }