Support documenting fields in XML payloads with any content type

Previously, the document fields in an XML payload, the request or
response had to have an application/xml content type. This prevented
documenting standard XML content types such as text/xml and
application/rss+xml as well as payloads with custom XML content types.

This commit updates the logic that sets up the ContentHandler to
first attempt to parse the content as JSON. If that fails it then
parses it as XML. If that fails an exception is thrown. This allows
any JSON or XML content, irrespective of the actual content type, to
be documented.

Closes gh-393
This commit is contained in:
Andy Wilkinson
2017-05-12 12:31:18 +01:00
parent 88db798b25
commit 113976f2d9
9 changed files with 126 additions and 45 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -219,17 +219,32 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
}
private ContentHandler getContentHandler(byte[] content, MediaType contentType) {
try {
if (contentType != null
&& MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
return new XmlContentHandler(content);
}
else {
return new JsonContentHandler(content);
ContentHandler contentHandler = createJsonContentHandler(content);
if (contentHandler == null) {
contentHandler = createXmlContentHandler(content);
if (contentHandler == null) {
throw new PayloadHandlingException("Cannot handle " + contentType
+ " content as it could not be parsed as JSON or XML");
}
}
catch (IOException ex) {
throw new ModelCreationException(ex);
return contentHandler;
}
private ContentHandler createJsonContentHandler(byte[] content) {
try {
return new JsonContentHandler(content);
}
catch (Exception ex) {
return null;
}
}
private ContentHandler createXmlContentHandler(byte[] content) {
try {
return new XmlContentHandler(content);
}
catch (Exception ex) {
return null;
}
}

View File

@@ -41,8 +41,9 @@ class JsonContentHandler implements ContentHandler {
private final byte[] rawContent;
JsonContentHandler(byte[] content) throws IOException {
JsonContentHandler(byte[] content) {
this.rawContent = content;
readContent();
}
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2017 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.
@@ -62,6 +62,7 @@ class XmlContentHandler implements ContentHandler {
throw new IllegalStateException("Failed to create document builder", ex);
}
this.rawContent = rawContent;
readPayload();
}
@Override

View File

@@ -51,4 +51,10 @@ public class JsonContentHandlerTests {
assertThat((JsonFieldType) fieldType, is(equalTo(JsonFieldType.STRING)));
}
@Test
public void failsFastWithNonJsonContent() {
this.thrown.expect(PayloadHandlingException.class);
new JsonContentHandler("Non-JSON content".getBytes());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -135,8 +135,8 @@ public class RequestFieldsSnippetFailureTests {
startsWith("The following parts of the payload were not documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor>emptyList())
.document(this.operationBuilder.request("http://localhost")
.content("<a><b>5</b></a>").header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.content("<a><b>5</b></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@@ -159,8 +159,8 @@ public class RequestFieldsSnippetFailureTests {
this.thrown.expect(FieldTypeRequiredException.class);
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
.document(this.operationBuilder.request("http://localhost")
.content("<a>5</a>").header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.content("<a>5</a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@@ -187,8 +187,20 @@ public class RequestFieldsSnippetFailureTests {
+ " in the payload: [a/b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
.document(this.operationBuilder.request("http://localhost")
.content("<a><c>5</c></a>").header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.content("<a><c>5</c></a>")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
public void unsupportedContent() throws IOException {
this.thrown.expect(PayloadHandlingException.class);
this.thrown.expectMessage(equalTo("Cannot handle text/plain content as it could"
+ " not be parsed as JSON or XML"));
new RequestFieldsSnippet(Collections.<FieldDescriptor>emptyList())
.document(this.operationBuilder.request("http://localhost")
.content("Some plain text")
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.build());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -264,21 +264,33 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
}
@Test
public void xmlRequestFields() throws IOException {
public void applicationXmlRequestFields() throws IOException {
xmlRequestFields(MediaType.APPLICATION_XML);
}
@Test
public void textXmlRequestFields() throws IOException {
xmlRequestFields(MediaType.TEXT_XML);
}
@Test
public void customXmlRequestFields() throws IOException {
xmlRequestFields(MediaType.parseMediaType("application/vnd.com.example+xml"));
}
private void xmlRequestFields(MediaType contentType) throws IOException {
this.snippets.expectRequestFields()
.withContents(tableWithHeader("Path", "Type", "Description")
.row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two")
.row("`a`", "`a`", "three"));
new RequestFieldsSnippet(
Arrays.asList(fieldWithPath("a/b").description("one").type("b"),
fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a")))
.document(
this.operationBuilder.request("http://localhost")
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
new RequestFieldsSnippet(Arrays.asList(
fieldWithPath("a/b").description("one").type("b"),
fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a")))
.document(this.operationBuilder.request("http://localhost")
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE, contentType.toString())
.build());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -108,7 +108,7 @@ public class ResponseFieldsSnippetFailureTests {
.content("<a>foo</a>")
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.build());
.build());
}
@Test
@@ -162,4 +162,15 @@ public class ResponseFieldsSnippetFailureTests {
.build());
}
@Test
public void unsupportedContent() throws IOException {
this.thrown.expect(PayloadHandlingException.class);
this.thrown.expectMessage(equalTo("Cannot handle text/plain content as it could"
+ " not be parsed as JSON or XML"));
new ResponseFieldsSnippet(Collections.<FieldDescriptor>emptyList())
.document(this.operationBuilder.response().content("Some plain text")
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.build());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -238,21 +238,33 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests {
}
@Test
public void xmlResponseFields() throws IOException {
public void applicationXmlResponseFields() throws IOException {
xmlResponseFields(MediaType.APPLICATION_XML);
}
@Test
public void textXmlResponseFields() throws IOException {
xmlResponseFields(MediaType.TEXT_XML);
}
@Test
public void customXmlResponseFields() throws IOException {
xmlResponseFields(MediaType.parseMediaType("application/vnd.com.example+xml"));
}
private void xmlResponseFields(MediaType contentType) throws IOException {
this.snippets.expectResponseFields()
.withContents(tableWithHeader("Path", "Type", "Description")
.row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two")
.row("`a`", "`a`", "three"));
new ResponseFieldsSnippet(
Arrays.asList(fieldWithPath("a/b").description("one").type("b"),
fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a")))
.document(
this.operationBuilder.response()
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.build());
new ResponseFieldsSnippet(Arrays.asList(
fieldWithPath("a/b").description("one").type("b"),
fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a")))
.document(this.operationBuilder.response()
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE, contentType.toString())
.build());
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-2017 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.
@@ -18,7 +18,9 @@ package org.springframework.restdocs.payload;
import java.util.Arrays;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
@@ -34,6 +36,9 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.subsecti
*/
public class XmlContentHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void topLevelElementCanBeDocumented() {
String undocumentedContent = createHandler("<a>5</a>").getUndocumentedContent(
@@ -84,6 +89,12 @@ public class XmlContentHandlerTests {
assertThat(undocumentedContent, is(nullValue()));
}
@Test
public void failsFastWithNonXmlContent() {
this.thrown.expect(PayloadHandlingException.class);
createHandler("non-XML content");
}
private XmlContentHandler createHandler(String xml) {
return new XmlContentHandler(xml.getBytes());
}