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

@ -41,7 +41,7 @@ import java.util.TreeMap;
* qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) * qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )
* </pre> * </pre>
* *
* Once created a {@link AcceptType} is <b>IMMUTABLE</b>. * Once created an {@link AcceptType} is <b>IMMUTABLE</b>.
*/ */
public class AcceptType { public class AcceptType {
@ -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> * @param format accept types, comma-separated, as specified for the HTTP header <code>Accept</code>
* @return a list of <code>AcceptType</code> objects * @return a list of <code>AcceptType</code> objects
* @throws IllegalArgumentException if input string is not parseable * @throws IllegalArgumentException if input string is not parseable
@ -142,6 +142,16 @@ public class AcceptType {
return result; 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() { public String getType() {
return type; return type;
} }

View File

@ -36,8 +36,7 @@ public class ContentNegotiator {
private ContentNegotiator() {} private ContentNegotiator() {}
private static List<ContentType> private static List<ContentType> getDefaultSupportedContentTypes(final Class<? extends Processor> processorClass) {
getDefaultSupportedContentTypes(final Class<? extends Processor> processorClass) {
List<ContentType> defaults = new ArrayList<ContentType>(); List<ContentType> defaults = new ArrayList<ContentType>();
if (processorClass == MetadataProcessor.class) { if (processorClass == MetadataProcessor.class) {
@ -75,36 +74,24 @@ public class ContentNegotiator {
ODataFormat.JSON.name().equalsIgnoreCase(formatString) ? ODataFormat.JSON : ODataFormat.JSON.name().equalsIgnoreCase(formatString) ? ODataFormat.JSON :
ODataFormat.XML.name().equalsIgnoreCase(formatString) ? ODataFormat.XML : ODataFormat.XML.name().equalsIgnoreCase(formatString) ? ODataFormat.XML :
ODataFormat.ATOM.name().equalsIgnoreCase(formatString) ? ODataFormat.ATOM : null; ODataFormat.ATOM.name().equalsIgnoreCase(formatString) ? ODataFormat.ATOM : null;
result = getSupportedContentType(format == null ? try {
ContentType.create(formatOption.getFormat()) : format.getContentType(ODataServiceVersion.V40), result = getAcceptedType(
AcceptType.fromContentType(format == null ?
ContentType.create(formatOption.getFormat()) : format.getContentType(ODataServiceVersion.V40)),
supportedContentTypes); supportedContentTypes);
} catch (final IllegalArgumentException e) {}
if (result == null) { if (result == null) {
throw new ContentNegotiatorException("Unsupported $format = " + formatString, throw new ContentNegotiatorException("Unsupported $format = " + formatString,
ContentNegotiatorException.MessageKeys.UNSUPPORTED_FORMAT_OPTION, formatString); ContentNegotiatorException.MessageKeys.UNSUPPORTED_FORMAT_OPTION, formatString);
} }
} else if (acceptHeaderValue != null) { } else if (acceptHeaderValue != null) {
final List<AcceptType> acceptedContentTypes = AcceptType.create(acceptHeaderValue); final List<AcceptType> acceptedContentTypes = AcceptType.create(acceptHeaderValue);
for (AcceptType acceptedType : acceptedContentTypes) { try {
for (final ContentType supportedContentType : supportedContentTypes) { result = getAcceptedType(acceptedContentTypes, supportedContentTypes);
ContentType contentType = supportedContentType; } catch (final IllegalArgumentException e) {
if (acceptedType.getParameters().containsKey("charset")) { throw new ContentNegotiatorException("charset in accept header not supported: " + acceptHeaderValue, e,
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); ContentNegotiatorException.MessageKeys.WRONG_CHARSET_IN_HEADER, HttpHeader.ACCEPT, acceptHeaderValue);
} }
}
if (acceptedType.matches(contentType)) {
result = contentType;
break;
}
}
if (result != null) {
break;
}
}
if (result == null) { if (result == null) {
throw new ContentNegotiatorException( throw new ContentNegotiatorException(
"unsupported accept content type: " + acceptedContentTypes + " != " + supportedContentTypes, "unsupported accept content type: " + acceptedContentTypes + " != " + supportedContentTypes,
@ -114,7 +101,7 @@ public class ContentNegotiator {
final ContentType requestedContentType = processorClass == MetadataProcessor.class ? final ContentType requestedContentType = processorClass == MetadataProcessor.class ?
ODataFormat.XML.getContentType(ODataServiceVersion.V40) : ODataFormat.XML.getContentType(ODataServiceVersion.V40) :
ODataFormat.JSON.getContentType(ODataServiceVersion.V40); ODataFormat.JSON.getContentType(ODataServiceVersion.V40);
result = getSupportedContentType(requestedContentType, supportedContentTypes); result = getAcceptedType(AcceptType.fromContentType(requestedContentType), supportedContentTypes);
if (result == null) { if (result == null) {
throw new ContentNegotiatorException( throw new ContentNegotiatorException(
"unsupported accept content type: " + requestedContentType + " != " + supportedContentTypes, "unsupported accept content type: " + requestedContentType + " != " + supportedContentTypes,
@ -125,11 +112,22 @@ public class ContentNegotiator {
return result; return result;
} }
private static ContentType getSupportedContentType(final ContentType requestedContentType, private static ContentType getAcceptedType(final List<AcceptType> acceptedContentTypes,
final List<ContentType> supportedContentTypes) { final List<ContentType> supportedContentTypes) {
for (final AcceptType acceptedType : acceptedContentTypes) {
for (final ContentType supportedContentType : supportedContentTypes) { for (final ContentType supportedContentType : supportedContentTypes) {
if (requestedContentType.isCompatible(supportedContentType)) { ContentType contentType = supportedContentType;
return 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; 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 = "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_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_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_JSONQ = "application/json;q=0.2";
static final private String ACCEPT_CASE_XML = "application/xml"; static final private String ACCEPT_CASE_XML = "application/xml";
static final private String ACCEPT_CASE_WILDCARD1 = "*/*"; static final private String ACCEPT_CASE_WILDCARD1 = "*/*";
@ -62,6 +63,7 @@ public class ContentNegotiatorTest {
{ ACCEPT_CASE_MIN, null, null, null }, { ACCEPT_CASE_MIN, null, null, null },
{ ACCEPT_CASE_MIN, "json", null, null }, { ACCEPT_CASE_MIN, "json", null, null },
{ ACCEPT_CASE_MIN, "json", ACCEPT_CASE_JSONQ, null }, { ACCEPT_CASE_MIN, "json", ACCEPT_CASE_JSONQ, null },
{ ACCEPT_CASE_NONE, ACCEPT_CASE_NONE, null, null },
{ "a/a", "a/a", null, "a/a" }, { "a/a", "a/a", null, "a/a" },
{ ACCEPT_CASE_MIN, null, ACCEPT_CASE_JSONQ, null }, { ACCEPT_CASE_MIN, null, ACCEPT_CASE_JSONQ, null },
{ ACCEPT_CASE_MIN, null, ACCEPT_CASE_WILDCARD1, 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", " 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" },
{ "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_MIN, "json", ACCEPT_CASE_MIN, null },
{ ACCEPT_CASE_FULL, null, ACCEPT_CASE_FULL, ACCEPT_CASE_FULL }, { 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 = { String[][] casesMetadata = {
@ -85,24 +89,28 @@ public class ContentNegotiatorTest {
{ ACCEPT_CASE_XML, null, ACCEPT_CASE_WILDCARD2, null }, { 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", " 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 = { String[][] casesFail = {
/* expected $format accept additional content types */ /* expected $format accept additional content types */
{ ACCEPT_CASE_XML, "xxx/yyy", null, null }, { null, "xxx/yyy", null, null },
{ "a/a", "a/a", null, "b/b" }, { null, "a/a", null, "b/b" },
{ ACCEPT_CASE_XML, null, ACCEPT_CASE_JSONQ, null }, { null, "a/a;x=y", null, "a/a;v=w" },
{ "application/json", null, ACCEPT_CASE_FULL, null }, // not yet supported { 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 //CHECKSTYLE:ON
//@formatter:on //@formatter:on
@Test @Test
public void testServiceDocumentSingleCase() throws Exception { public void testServiceDocumentSingleCase() throws Exception {
String[] useCase = { ACCEPT_CASE_MIN_UTF8, null, ACCEPT_CASE_MIN_UTF8, null }; testContentNegotiation(
new String[] { ACCEPT_CASE_MIN_UTF8, null, ACCEPT_CASE_MIN_UTF8, null },
testContentNegotiation(useCase, ServiceDocumentProcessor.class); ServiceDocumentProcessor.class);
} }
@Test @Test
@ -114,9 +122,12 @@ public class ContentNegotiatorTest {
@Test @Test
public void testMetadataSingleCase() throws Exception { 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 @Test
@ -127,11 +138,11 @@ public class ContentNegotiatorTest {
} }
@Test @Test
public void testMetadataFail() throws Exception { public void testEntityCollectionFail() throws Exception {
for (String[] useCase : casesFail) { for (String[] useCase : casesFail) {
try { try {
testContentNegotiation(useCase, MetadataProcessor.class); testContentNegotiation(useCase, EntityCollectionProcessor.class);
fail("Exception expected!"); fail("Exception expected for '" + useCase[1] + '|' + useCase[2] + '|' + useCase[3] + "'!");
} catch (final ContentNegotiatorException e) {} } catch (final ContentNegotiatorException e) {}
} }
} }
@ -157,8 +168,10 @@ public class ContentNegotiatorTest {
final ContentType requestedContentType = ContentNegotiator.doContentNegotiation(fo, request, p, processorClass); final ContentType requestedContentType = ContentNegotiator.doContentNegotiation(fo, request, p, processorClass);
assertNotNull(requestedContentType); assertNotNull(requestedContentType);
if (useCase[0] != null) {
assertEquals(ContentType.create(useCase[0]), requestedContentType); assertEquals(ContentType.create(useCase[0]), requestedContentType);
} }
}
private List<ContentType> createCustomContentTypes(final String contentTypeString) { private List<ContentType> createCustomContentTypes(final String contentTypeString) {