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.
This commit is contained in:
Rossen Stoyanchev 2011-09-23 18:20:27 +00:00
parent fb526f534a
commit fe7e2a7f54
12 changed files with 307 additions and 127 deletions

View File

@ -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<AbstractMediaTypeExpression>, 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();
}
}

View File

@ -27,7 +27,7 @@ import javax.servlet.http.HttpServletRequest;
* @author Arjen Poutsma
* @since 3.1
*/
abstract class AbstractNameValueExpression<T> {
abstract class AbstractNameValueExpression<T> implements NameValueExpression<T> {
protected final String name;
@ -49,6 +49,18 @@ abstract class AbstractNameValueExpression<T> {
}
}
public String getName() {
return this.name;
}
public T getValue() {
return this.value;
}
public boolean isNegated() {
return this.isNegated;
}
protected abstract T parseValue(String valueExpression);
public final boolean match(HttpServletRequest request) {

View File

@ -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<Con
/**
* Creates a new instance from 0 or more "consumes" expressions.
* @param consumes expressions with the syntax described in {@link RequestMapping#consumes()}
* if 0 expressions are provided, the condition will match to every request
* @param consumes expressions with the syntax described in
* {@link RequestMapping#consumes()}; if 0 expressions are provided,
* the condition will match to every request.
*/
public ConsumesRequestCondition(String... consumes) {
this(consumes, null);
}
/**
* Creates a new instance with "consumes" and "header" expressions. "Header" expressions
* where the header name is not 'Content-Type' or have no header value defined are ignored.
* If 0 expressions are provided in total, the condition will match to every request
* @param consumes expressions with the syntax described in {@link RequestMapping#consumes()}
* @param headers expressions with the syntax described in {@link RequestMapping#headers()}
* Creates a new instance with "consumes" and "header" expressions.
* "Header" expressions where the header name is not 'Content-Type' or have
* no header value defined are ignored. If 0 expressions are provided in
* total, the condition will match to every request
* @param consumes as described in {@link RequestMapping#consumes()}
* @param headers as described in {@link RequestMapping#headers()}
*/
public ConsumesRequestCondition(String[] consumes, String[] headers) {
this(parseExpressions(consumes, headers));
@ -95,12 +98,21 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
}
/**
* Returns the media types for this condition.
* Return the contained MediaType expressions.
*/
public Set<MediaType> getMediaTypes() {
public Set<MediaTypeExpression> getExpressions() {
return new LinkedHashSet<MediaTypeExpression>(this.expressions);
}
/**
* Returns the media types for this condition excluding negated expressions.
*/
public Set<MediaType> getConsumableMediaTypes() {
Set<MediaType> result = new LinkedHashSet<MediaType>();
for (ConsumeMediaTypeExpression expression : expressions) {
result.add(expression.getMediaType());
for (ConsumeMediaTypeExpression expression : this.expressions) {
if (!expression.isNegated()) {
result.add(expression.getMediaType());
}
}
return result;
}
@ -109,12 +121,12 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
* Whether the condition has any media type expressions.
*/
public boolean isEmpty() {
return expressions.isEmpty();
return this.expressions.isEmpty();
}
@Override
protected Collection<ConsumeMediaTypeExpression> getContent() {
return expressions;
return this.expressions;
}
@Override
@ -133,8 +145,8 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
/**
* Checks if any of the contained media type expressions match the given
* request 'Accept' header and returns an instance that is guaranteed to
* contain matching expressions only. The match is performed via
* request 'Content-Type' header and returns an instance that is guaranteed
* to contain matching expressions only. The match is performed via
* {@link MediaType#includes(MediaType)}.
*
* @param request the current request
@ -170,24 +182,24 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
* the matching consumable media type expression only or is otherwise empty.
*/
public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) {
if (expressions.isEmpty() && other.expressions.isEmpty()) {
if (this.expressions.isEmpty() && other.expressions.isEmpty()) {
return 0;
}
else if (expressions.isEmpty()) {
else if (this.expressions.isEmpty()) {
return 1;
}
else if (other.expressions.isEmpty()) {
return -1;
}
else {
return expressions.get(0).compareTo(other.expressions.get(0));
return this.expressions.get(0).compareTo(other.expressions.get(0));
}
}
/**
* Parses and matches a single media type expression to a request's 'Content-Type' header.
*/
static class ConsumeMediaTypeExpression extends MediaTypeExpression {
static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression {
ConsumeMediaTypeExpression(String expression) {
super(expression);
@ -199,11 +211,9 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con
@Override
protected boolean matchMediaType(HttpServletRequest request) {
MediaType contentType = StringUtils.hasLength(request.getContentType()) ?
MediaType.parseMediaType(request.getContentType()) :
MediaType.APPLICATION_OCTET_STREAM ;
return getMediaType().includes(contentType);
}
}

View File

@ -70,9 +70,16 @@ public final class HeadersRequestCondition extends AbstractRequestCondition<Head
return expressions;
}
/**
* Return the contained request header expressions.
*/
public Set<NameValueExpression<String>> getExpressions() {
return new LinkedHashSet<NameValueExpression<String>>(this.expressions);
}
@Override
protected Collection<HeaderExpression> getContent() {
return expressions;
return this.expressions;
}
@Override

View File

@ -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()}.
* 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 Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*
* @see RequestMapping#consumes()
* @see RequestMapping#produces()
*/
abstract class MediaTypeExpression implements Comparable<MediaTypeExpression> {
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();
}

View File

@ -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<T> {
String getName();
T getValue();
boolean isNegated();
}

View File

@ -61,6 +61,13 @@ public final class ParamsRequestCondition extends AbstractRequestCondition<Param
return expressions;
}
/**
* Return the contained request parameter expressions.
*/
public Set<NameValueExpression<String>> getExpressions() {
return new LinkedHashSet<NameValueExpression<String>>(this.expressions);
}
@Override
protected Collection<ParamExpression> getContent() {
return expressions;

View File

@ -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<Pro
}
/**
* Returns the media types for this condition.
* Return the contained "produces" expressions.
*/
public Set<MediaType> getMediaTypes() {
public Set<MediaTypeExpression> getExpressions() {
return new LinkedHashSet<MediaTypeExpression>(this.expressions);
}
/**
* Return the contained producible media types excluding negated expressions.
*/
public Set<MediaType> getProducibleMediaTypes() {
Set<MediaType> result = new LinkedHashSet<MediaType>();
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<Pro
* Whether the condition has any media type expressions.
*/
public boolean isEmpty() {
return expressions.isEmpty();
return this.expressions.isEmpty();
}
@Override
protected List<ProduceMediaTypeExpression> getContent() {
return this.expressions.isEmpty() ? DEFAULT_EXPRESSIONS : this.expressions;
return this.expressions;
}
@Override
@ -166,7 +176,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
* <li>Get 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)}.
* <li>If a lower index is found, the "produces" condition wins.
* <li>If a lower index is found, the condition at that index wins.
* <li>If both indexes are equal, the media types at the index are
* compared further with {@link MediaType#SPECIFICITY_COMPARATOR}.
* </ol>
@ -209,8 +219,8 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
}
private int indexOfEqualMediaType(MediaType mediaType) {
for (int i = 0; i < getContent().size(); i++) {
if (mediaType.equals(getContent().get(i).getMediaType())) {
for (int i = 0; i < getExpressionsToCompare().size(); i++) {
if (mediaType.equals(getExpressionsToCompare().get(i).getMediaType())) {
return i;
}
}
@ -218,8 +228,8 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
}
private int indexOfIncludedMediaType(MediaType mediaType) {
for (int i = 0; i < getContent().size(); i++) {
if (mediaType.includes(getContent().get(i).getMediaType())) {
for (int i = 0; i < getExpressionsToCompare().size(); i++) {
if (mediaType.includes(getExpressionsToCompare().get(i).getMediaType())) {
return i;
}
}
@ -233,21 +243,30 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
result = index2 - index1;
}
else if (index1 != -1 && index2 != -1) {
ProduceMediaTypeExpression expr1 = condition1.getContent().get(index1);
ProduceMediaTypeExpression expr2 = condition2.getContent().get(index2);
ProduceMediaTypeExpression expr1 = condition1.getExpressionsToCompare().get(index1);
ProduceMediaTypeExpression expr2 = condition2.getExpressionsToCompare().get(index2);
result = expr1.compareTo(expr2);
result = (result != 0) ? result : expr1.getMediaType().compareTo(expr2.getMediaType());
}
return result;
}
private static final List<ProduceMediaTypeExpression> 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<ProduceMediaTypeExpression> getExpressionsToCompare() {
return this.expressions.isEmpty() ? DEFAULT_EXPRESSION_LIST : this.expressions;
}
private static final List<ProduceMediaTypeExpression> 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<Pro
}
return false;
}
}
}

View File

@ -91,8 +91,8 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
Map<String, String> uriTemplateVariables = getPathMatcher().extractUriTemplateVariables(pattern, lookupPath);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
if (!info.getProducesCondition().isEmpty()) {
Set<MediaType> mediaTypes = info.getProducesCondition().getMediaTypes();
if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
Set<MediaType> 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());
}
}
}

View File

@ -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/*");

View File

@ -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");
}

View File

@ -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 {