better handling of parameters in server content negotiation

Change-Id: Ie2771940b3afea6efb3dc84f8322bc60069052d1

Signed-off-by: Christian Amend <chrisam@apache.org>
This commit is contained in:
Klaus Straubinger 2014-09-25 13:45:39 +02:00 committed by Christian Amend
parent 788036db25
commit 7e0b013cef
3 changed files with 70 additions and 49 deletions

View File

@ -27,7 +27,7 @@ import java.util.TreeMap;
/**
* Internally used {@link AcceptType} for OData library.
*
*
* See RFC 7231, chapter 5.3.2:
* <pre>
* Accept = #( media-range [ accept-params ] )
@ -41,7 +41,7 @@ import java.util.TreeMap;
* qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )
* </pre>
*
* Once created a {@link AcceptType} is <b>IMMUTABLE</b>.
* Once created an {@link AcceptType} is <b>IMMUTABLE</b>.
*/
public class AcceptType {
@ -82,7 +82,7 @@ public class AcceptType {
if (TypeUtil.MEDIA_TYPE_WILDCARD.equals(this.type) && !TypeUtil.MEDIA_TYPE_WILDCARD.equals(subtype)) {
throw new IllegalArgumentException("Illegal combination of WILDCARD type with NONE WILDCARD subtype.");
}
final String q = parameters.get(TypeUtil.PARAMETER_Q);
if (q == null) {
quality = 1F;
@ -124,7 +124,7 @@ public class AcceptType {
}
/**
* Create a list of {@link AcceptType} objects based on given input string (<code>format</code>).
* Creates a list of {@link AcceptType} objects based on given input string (<code>format</code>).
* @param format accept types, comma-separated, as specified for the HTTP header <code>Accept</code>
* @return a list of <code>AcceptType</code> objects
* @throws IllegalArgumentException if input string is not parseable
@ -142,6 +142,16 @@ public class AcceptType {
return result;
}
/**
* Creates a list of {@link AcceptType} objects based on given content type.
* @param contentType the content type
* @return an immutable one-element list of <code>AcceptType</code> objects that matches only the given content type
*/
public static List<AcceptType> fromContentType(final ContentType contentType) {
return Collections.singletonList(new AcceptType(
contentType.getType(), contentType.getSubtype(), contentType.getParameters(), 1F));
}
public String getType() {
return type;
}

View File

@ -36,8 +36,7 @@ public class ContentNegotiator {
private ContentNegotiator() {}
private static List<ContentType>
getDefaultSupportedContentTypes(final Class<? extends Processor> processorClass) {
private static List<ContentType> getDefaultSupportedContentTypes(final Class<? extends Processor> processorClass) {
List<ContentType> defaults = new ArrayList<ContentType>();
if (processorClass == MetadataProcessor.class) {
@ -75,35 +74,23 @@ public class ContentNegotiator {
ODataFormat.JSON.name().equalsIgnoreCase(formatString) ? ODataFormat.JSON :
ODataFormat.XML.name().equalsIgnoreCase(formatString) ? ODataFormat.XML :
ODataFormat.ATOM.name().equalsIgnoreCase(formatString) ? ODataFormat.ATOM : null;
result = getSupportedContentType(format == null ?
ContentType.create(formatOption.getFormat()) : format.getContentType(ODataServiceVersion.V40),
supportedContentTypes);
try {
result = getAcceptedType(
AcceptType.fromContentType(format == null ?
ContentType.create(formatOption.getFormat()) : format.getContentType(ODataServiceVersion.V40)),
supportedContentTypes);
} catch (final IllegalArgumentException e) {}
if (result == null) {
throw new ContentNegotiatorException("Unsupported $format = " + formatString,
ContentNegotiatorException.MessageKeys.UNSUPPORTED_FORMAT_OPTION, formatString);
}
} else if (acceptHeaderValue != null) {
final List<AcceptType> acceptedContentTypes = AcceptType.create(acceptHeaderValue);
for (AcceptType acceptedType : acceptedContentTypes) {
for (final ContentType supportedContentType : supportedContentTypes) {
ContentType contentType = supportedContentType;
if (acceptedType.getParameters().containsKey("charset")) {
final String value = acceptedType.getParameters().get("charset");
if ("utf8".equalsIgnoreCase(value) || "utf-8".equalsIgnoreCase(value)) {
contentType = ContentType.create(contentType, ContentType.PARAMETER_CHARSET_UTF8);
} else {
throw new ContentNegotiatorException("charset in accept header not supported: " + acceptHeaderValue,
ContentNegotiatorException.MessageKeys.WRONG_CHARSET_IN_HEADER, HttpHeader.ACCEPT, acceptHeaderValue);
}
}
if (acceptedType.matches(contentType)) {
result = contentType;
break;
}
}
if (result != null) {
break;
}
try {
result = getAcceptedType(acceptedContentTypes, supportedContentTypes);
} catch (final IllegalArgumentException e) {
throw new ContentNegotiatorException("charset in accept header not supported: " + acceptHeaderValue, e,
ContentNegotiatorException.MessageKeys.WRONG_CHARSET_IN_HEADER, HttpHeader.ACCEPT, acceptHeaderValue);
}
if (result == null) {
throw new ContentNegotiatorException(
@ -114,7 +101,7 @@ public class ContentNegotiator {
final ContentType requestedContentType = processorClass == MetadataProcessor.class ?
ODataFormat.XML.getContentType(ODataServiceVersion.V40) :
ODataFormat.JSON.getContentType(ODataServiceVersion.V40);
result = getSupportedContentType(requestedContentType, supportedContentTypes);
result = getAcceptedType(AcceptType.fromContentType(requestedContentType), supportedContentTypes);
if (result == null) {
throw new ContentNegotiatorException(
"unsupported accept content type: " + requestedContentType + " != " + supportedContentTypes,
@ -125,11 +112,22 @@ public class ContentNegotiator {
return result;
}
private static ContentType getSupportedContentType(final ContentType requestedContentType,
private static ContentType getAcceptedType(final List<AcceptType> acceptedContentTypes,
final List<ContentType> supportedContentTypes) {
for (final ContentType supportedContentType : supportedContentTypes) {
if (requestedContentType.isCompatible(supportedContentType)) {
return supportedContentType;
for (final AcceptType acceptedType : acceptedContentTypes) {
for (final ContentType supportedContentType : supportedContentTypes) {
ContentType contentType = supportedContentType;
if (acceptedType.getParameters().containsKey("charset")) {
final String value = acceptedType.getParameters().get("charset");
if ("utf8".equalsIgnoreCase(value) || "utf-8".equalsIgnoreCase(value)) {
contentType = ContentType.create(contentType, ContentType.PARAMETER_CHARSET_UTF8);
} else {
throw new IllegalArgumentException("charset not supported: " + acceptedType);
}
}
if (acceptedType.matches(contentType)) {
return contentType;
}
}
}
return null;

View File

@ -49,6 +49,7 @@ public class ContentNegotiatorTest {
static final private String ACCEPT_CASE_MIN = "application/json;odata.metadata=minimal";
static final private String ACCEPT_CASE_MIN_UTF8 = "application/json;charset=UTF-8;odata.metadata=minimal";
static final private String ACCEPT_CASE_FULL = "application/json;odata.metadata=full";
static final private String ACCEPT_CASE_NONE = "application/json;odata.metadata=none";
static final private String ACCEPT_CASE_JSONQ = "application/json;q=0.2";
static final private String ACCEPT_CASE_XML = "application/xml";
static final private String ACCEPT_CASE_WILDCARD1 = "*/*";
@ -62,6 +63,7 @@ public class ContentNegotiatorTest {
{ ACCEPT_CASE_MIN, null, null, null },
{ ACCEPT_CASE_MIN, "json", null, null },
{ ACCEPT_CASE_MIN, "json", ACCEPT_CASE_JSONQ, null },
{ ACCEPT_CASE_NONE, ACCEPT_CASE_NONE, null, null },
{ "a/a", "a/a", null, "a/a" },
{ ACCEPT_CASE_MIN, null, ACCEPT_CASE_JSONQ, null },
{ ACCEPT_CASE_MIN, null, ACCEPT_CASE_WILDCARD1, null },
@ -69,9 +71,11 @@ public class ContentNegotiatorTest {
{ "a/a", "a/a", null, "a/a,b/b" },
{ "a/a", " a/a ", null, " a/a , b/b " },
{ "a/a;x=y", "a/a", ACCEPT_CASE_WILDCARD1, "a/a;x=y" },
{ "a/a;v=w;x=y", null, "a/a;x=y", "a/a;b=c,a/a;v=w;x=y" },
{ "a/a;v=w;x=y", "a/a;x=y", null, "a/a;b=c,a/a;v=w;x=y" },
{ ACCEPT_CASE_MIN, "json", ACCEPT_CASE_MIN, null },
{ ACCEPT_CASE_FULL, null, ACCEPT_CASE_FULL, ACCEPT_CASE_FULL },
{ ACCEPT_CASE_MIN_UTF8, null, ACCEPT_CASE_MIN_UTF8, null },
{ ACCEPT_CASE_MIN_UTF8, null, ACCEPT_CASE_MIN_UTF8, null }
};
String[][] casesMetadata = {
@ -85,24 +89,28 @@ public class ContentNegotiatorTest {
{ ACCEPT_CASE_XML, null, ACCEPT_CASE_WILDCARD2, null },
{ "a/a", "a/a", null, "a/a,b/b" },
{ "a/a", " a/a ", null, " a/a , b/b " },
{ "a/a;x=y", "a/a", ACCEPT_CASE_WILDCARD1, "a/a;x=y" },
{ "a/a;x=y", "a/a", ACCEPT_CASE_WILDCARD1, "a/a;x=y" }
};
String[][] casesFail = {
/* expected $format accept additional content types */
{ ACCEPT_CASE_XML, "xxx/yyy", null, null },
{ "a/a", "a/a", null, "b/b" },
{ ACCEPT_CASE_XML, null, ACCEPT_CASE_JSONQ, null },
{ "application/json", null, ACCEPT_CASE_FULL, null }, // not yet supported
{ null, "xxx/yyy", null, null },
{ null, "a/a", null, "b/b" },
{ null, "a/a;x=y", null, "a/a;v=w" },
{ null, null, "a/a;x=y", "a/a;v=w" },
{ null, "atom", null, null }, // not yet supported
{ null, null, ACCEPT_CASE_FULL, null }, // not yet supported
{ null, "a/b;charset=ISO-8859-1", null, "a/b" },
{ null, null, "a/b;charset=ISO-8859-1", "a/b" }
};
//CHECKSTYLE:ON
//@formatter:on
@Test
public void testServiceDocumentSingleCase() throws Exception {
String[] useCase = { ACCEPT_CASE_MIN_UTF8, null, ACCEPT_CASE_MIN_UTF8, null };
testContentNegotiation(useCase, ServiceDocumentProcessor.class);
testContentNegotiation(
new String[] { ACCEPT_CASE_MIN_UTF8, null, ACCEPT_CASE_MIN_UTF8, null },
ServiceDocumentProcessor.class);
}
@Test
@ -114,9 +122,12 @@ public class ContentNegotiatorTest {
@Test
public void testMetadataSingleCase() throws Exception {
String[] useCase = { ACCEPT_CASE_XML, null, null, null };
testContentNegotiation(new String[] { ACCEPT_CASE_XML, null, null, null }, MetadataProcessor.class);
}
testContentNegotiation(useCase, MetadataProcessor.class);
@Test(expected = ContentNegotiatorException.class)
public void testMetadataJsonFail() throws Exception {
testContentNegotiation(new String[] { null, "json", null, null }, MetadataProcessor.class);
}
@Test
@ -127,11 +138,11 @@ public class ContentNegotiatorTest {
}
@Test
public void testMetadataFail() throws Exception {
public void testEntityCollectionFail() throws Exception {
for (String[] useCase : casesFail) {
try {
testContentNegotiation(useCase, MetadataProcessor.class);
fail("Exception expected!");
testContentNegotiation(useCase, EntityCollectionProcessor.class);
fail("Exception expected for '" + useCase[1] + '|' + useCase[2] + '|' + useCase[3] + "'!");
} catch (final ContentNegotiatorException e) {}
}
}
@ -157,7 +168,9 @@ public class ContentNegotiatorTest {
final ContentType requestedContentType = ContentNegotiator.doContentNegotiation(fo, request, p, processorClass);
assertNotNull(requestedContentType);
assertEquals(ContentType.create(useCase[0]), requestedContentType);
if (useCase[0] != null) {
assertEquals(ContentType.create(useCase[0]), requestedContentType);
}
}
private List<ContentType> createCustomContentTypes(final String contentTypeString) {