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:
parent
00ff018b39
commit
0460a5eceb
|
|
@ -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);
|
||||||
}
|
|
||||||
}
|
|
||||||
int thisIndex = this.indexOfMediaType(acceptedMediaType);
|
|
||||||
int otherIndex = other.indexOfMediaType(acceptedMediaType);
|
|
||||||
if (thisIndex != otherIndex) {
|
|
||||||
return otherIndex - thisIndex;
|
|
||||||
} else if (thisIndex != -1 && otherIndex != -1) {
|
|
||||||
ProduceMediaTypeExpression thisExpr = this.expressions.get(thisIndex);
|
|
||||||
ProduceMediaTypeExpression otherExpr = other.expressions.get(otherIndex);
|
|
||||||
int result = thisExpr.compareTo(otherExpr);
|
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
thisIndex = this.indexOfIncludedMediaType(acceptedMediaType);
|
||||||
}
|
otherIndex = other.indexOfIncludedMediaType(acceptedMediaType);
|
||||||
|
result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex);
|
||||||
if (acceptedMediaTypes.isEmpty()) {
|
if (result != 0) {
|
||||||
if (isOneEmptyButNotBoth(other)) {
|
return result;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,33 +175,54 @@ public class ProducesRequestConditionTests {
|
||||||
assertTrue("Invalid comparison result: " + result, result < 0);
|
assertTrue("Invalid comparison result: " + result, result < 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SPR-8536
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void compareToEmptyCondition() {
|
public void compareToMediaTypeAll() {
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
request.addHeader("Accept", "*/*");
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue