SPR-8536 Add exact matches when comparing 'produces' conditions (e.g. given Accept: text/*, method with text/* is preferred over text/plain). Also pick a media type alphabetically when two 'produces' condition media types match equally (e.g. given Accept: text/* method with text/plain is chosen over text/xhtml)

This commit is contained in:
Rossen Stoyanchev 2011-08-25 13:09:14 +00:00
parent 00ff018b39
commit 0460a5eceb
3 changed files with 97 additions and 67 deletions

View File

@ -99,7 +99,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
*/ */
public Set<MediaType> getMediaTypes() { public Set<MediaType> getMediaTypes() {
Set<MediaType> result = new LinkedHashSet<MediaType>(); Set<MediaType> result = new LinkedHashSet<MediaType>();
for (ProduceMediaTypeExpression expression : expressions) { for (ProduceMediaTypeExpression expression : getContent()) {
result.add(expression.getMediaType()); result.add(expression.getMediaType());
} }
return result; return result;
@ -113,8 +113,8 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
} }
@Override @Override
protected Collection<ProduceMediaTypeExpression> getContent() { protected List<ProduceMediaTypeExpression> getContent() {
return expressions; return this.expressions.isEmpty() ? DEFAULT_EXPRESSIONS : this.expressions;
} }
@Override @Override
@ -158,73 +158,91 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
} }
/** /**
* Compares this and another Produces condition as follows: * Compares this and another "produces" condition as follows:
* *
* <ol> * <ol>
* <li>Sorts the request 'Accept' header media types by quality value via * <li>Sort 'Accept' header media types by quality value via
* {@link MediaType#sortByQualityValue(List)} and iterates over the sorted types. * {@link MediaType#sortByQualityValue(List)} and iterate the list.
* <li>Compares the sorted request media types against the media types of each * <li>Get the lowest index of matching media types from each "produces"
* Produces condition via {@link MediaType#includes(MediaType)}. * condition first matching with {@link MediaType#equals(Object)} and
* <li>A "produces" condition with a matching media type listed earlier wins. * then with {@link MediaType#includes(MediaType)}.
* <li>If both conditions have a matching media type at the same index, the * <li>If a lower index is found, the "produces" condition wins.
* media types are further compared by specificity and quality. * <li>If both indexes are equal, the media types at the index are
* compared further with {@link MediaType#SPECIFICITY_COMPARATOR}.
* </ol> * </ol>
* *
* <p>If a request media type is {@link MediaType#ALL} or if there is no 'Accept'
* header, and therefore both conditions match, preference is given to one
* Produces condition if it is empty and the other one is not.
*
* <p>It is assumed that both instances have been obtained via * <p>It is assumed that both instances have been obtained via
* {@link #getMatchingCondition(HttpServletRequest)} and each instance * {@link #getMatchingCondition(HttpServletRequest)} and each instance
* contains the matching producible media type expression only or * contains the matching producible media type expression only or
* is otherwise empty. * is otherwise empty.
*/ */
public int compareTo(ProducesRequestCondition other, HttpServletRequest request) { public int compareTo(ProducesRequestCondition other, HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept"); List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request);
List<MediaType> acceptedMediaTypes = MediaType.parseMediaTypes(acceptHeader);
MediaType.sortByQualityValue(acceptedMediaTypes); MediaType.sortByQualityValue(acceptedMediaTypes);
for (MediaType acceptedMediaType : acceptedMediaTypes) { for (MediaType acceptedMediaType : acceptedMediaTypes) {
if (acceptedMediaType.equals(MediaType.ALL)) { int thisIndex = this.indexOfEqualMediaType(acceptedMediaType);
if (isOneEmptyButNotBoth(other)) { int otherIndex = other.indexOfEqualMediaType(acceptedMediaType);
return this.isEmpty() ? -1 : 1; int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
} if (result != 0) {
return result;
} }
int thisIndex = this.indexOfMediaType(acceptedMediaType); thisIndex = this.indexOfIncludedMediaType(acceptedMediaType);
int otherIndex = other.indexOfMediaType(acceptedMediaType); otherIndex = other.indexOfIncludedMediaType(acceptedMediaType);
if (thisIndex != otherIndex) { result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
return otherIndex - thisIndex; if (result != 0) {
} else if (thisIndex != -1 && otherIndex != -1) { return result;
ProduceMediaTypeExpression thisExpr = this.expressions.get(thisIndex);
ProduceMediaTypeExpression otherExpr = other.expressions.get(otherIndex);
int result = thisExpr.compareTo(otherExpr);
if (result != 0) {
return result;
}
}
}
if (acceptedMediaTypes.isEmpty()) {
if (isOneEmptyButNotBoth(other)) {
return this.isEmpty() ? -1 : 1;
} }
} }
return 0; return 0;
} }
private int indexOfMediaType(MediaType mediaType) { private static List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) {
for (int i = 0; i < expressions.size(); i++) { String acceptHeader = request.getHeader("Accept");
if (mediaType.includes(expressions.get(i).getMediaType())) { if (StringUtils.hasLength(acceptHeader)) {
return MediaType.parseMediaTypes(acceptHeader);
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
private int indexOfEqualMediaType(MediaType mediaType) {
for (int i = 0; i < getContent().size(); i++) {
if (mediaType.equals(getContent().get(i).getMediaType())) {
return i; return i;
} }
} }
return -1; return -1;
} }
private boolean isOneEmptyButNotBoth(ProducesRequestCondition other) { private int indexOfIncludedMediaType(MediaType mediaType) {
return ((this.isEmpty() || other.isEmpty()) && (this.expressions.size() != other.expressions.size())); for (int i = 0; i < getContent().size(); i++) {
if (mediaType.includes(getContent().get(i).getMediaType())) {
return i;
}
}
return -1;
} }
private static int compareMatchingMediaTypes(ProducesRequestCondition condition1, int index1,
ProducesRequestCondition condition2, int index2) {
int result = 0;
if (index1 != index2) {
result = index2 - index1;
}
else if (index1 != -1 && index2 != -1) {
ProduceMediaTypeExpression expr1 = condition1.getContent().get(index1);
ProduceMediaTypeExpression expr2 = condition2.getContent().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("*/*"));
/** /**
* Parses and matches a single media type expression to a request's 'Accept' header. * Parses and matches a single media type expression to a request's 'Accept' header.
@ -250,15 +268,6 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
return false; return false;
} }
private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) {
String acceptHeader = request.getHeader("Accept");
if (StringUtils.hasLength(acceptHeader)) {
return MediaType.parseMediaTypes(acceptHeader);
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
} }
} }

View File

@ -175,33 +175,54 @@ public class ProducesRequestConditionTests {
assertTrue("Invalid comparison result: " + result, result < 0); assertTrue("Invalid comparison result: " + result, result < 0);
} }
@Test // SPR-8536
public void compareToEmptyCondition() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Accept", "*/*");
@Test
public void compareToMediaTypeAll() {
MockHttpServletRequest request = new MockHttpServletRequest();
ProducesRequestCondition condition1 = new ProducesRequestCondition(); ProducesRequestCondition condition1 = new ProducesRequestCondition();
ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json"); ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json");
int result = condition1.compareTo(condition2, request); assertTrue("Should have picked '*/*' condition as an exact match",
assertTrue("Invalid comparison result: " + result, result < 0); condition1.compareTo(condition2, request) < 0);
assertTrue("Should have picked '*/*' condition as an exact match",
condition2.compareTo(condition1, request) > 0);
result = condition2.compareTo(condition1, request); condition1 = new ProducesRequestCondition("*/*");
assertTrue("Invalid comparison result: " + result, result > 0); condition2 = new ProducesRequestCondition("application/json");
assertTrue(condition1.compareTo(condition2, request) < 0);
assertTrue(condition2.compareTo(condition1, request) > 0);
request.addHeader("Accept", "*/*");
condition1 = new ProducesRequestCondition();
condition2 = new ProducesRequestCondition("application/json");
assertTrue(condition1.compareTo(condition2, request) < 0);
assertTrue(condition2.compareTo(condition1, request) > 0);
condition1 = new ProducesRequestCondition("*/*");
condition2 = new ProducesRequestCondition("application/json");
assertTrue(condition1.compareTo(condition2, request) < 0);
assertTrue(condition2.compareTo(condition1, request) > 0);
} }
@Test @Test
public void compareToWithoutAcceptHeader() { public void compareToEqualMatch() {
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Accept", "text/*");
ProducesRequestCondition condition1 = new ProducesRequestCondition(); ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain");
ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json"); ProducesRequestCondition condition2 = new ProducesRequestCondition("text/xhtml");
int result = condition1.compareTo(condition2, request); int result = condition1.compareTo(condition2, request);
assertTrue("Invalid comparison result: " + result, result < 0); assertTrue("Should have used MediaType.equals(Object) to break the match", result < 0);
result = condition2.compareTo(condition1, request); result = condition2.compareTo(condition1, request);
assertTrue("Invalid comparison result: " + result, result > 0); assertTrue("Should have used MediaType.equals(Object) to break the match", result > 0);
} }
@Test @Test

View File

@ -51,8 +51,8 @@ public class RequestMappingInfoTests {
assertEquals(0, info.getPatternsCondition().getPatterns().size()); assertEquals(0, info.getPatternsCondition().getPatterns().size());
assertEquals(0, info.getMethodsCondition().getMethods().size()); assertEquals(0, info.getMethodsCondition().getMethods().size());
assertEquals(0, info.getConsumesCondition().getMediaTypes().size()); assertEquals(true, info.getConsumesCondition().isEmpty());
assertEquals(0, info.getProducesCondition().getMediaTypes().size()); assertEquals(true, info.getProducesCondition().isEmpty());
assertNotNull(info.getParamsCondition()); assertNotNull(info.getParamsCondition());
assertNotNull(info.getHeadersCondition()); assertNotNull(info.getHeadersCondition());
assertNull(info.getCustomCondition()); assertNull(info.getCustomCondition());