From 113976f2d9eb076e0f38e343629f23bea95155b9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 12 May 2017 12:31:18 +0100 Subject: [PATCH] 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 --- .../payload/AbstractFieldsSnippet.java | 35 ++++++++++++------ .../restdocs/payload/JsonContentHandler.java | 3 +- .../restdocs/payload/XmlContentHandler.java | 3 +- .../payload/JsonContentHandlerTests.java | 6 ++++ .../RequestFieldsSnippetFailureTests.java | 26 ++++++++++---- .../payload/RequestFieldsSnippetTests.java | 34 ++++++++++++------ .../ResponseFieldsSnippetFailureTests.java | 15 ++++++-- .../payload/ResponseFieldsSnippetTests.java | 36 ++++++++++++------- .../payload/XmlContentHandlerTests.java | 13 ++++++- 9 files changed, 126 insertions(+), 45 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 8c83ab97..1a2364bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -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; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 82819a72..02eded55 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -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 diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index f5a466ff..45c08846 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -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 diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java index b5f74523..349ea04a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java @@ -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()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 80c9cdf1..bddf8801 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -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.emptyList()) .document(this.operationBuilder.request("http://localhost") - .content("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) + .content("5") + .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("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) + .content("5") + .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("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) + .content("5") + .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.emptyList()) + .document(this.operationBuilder.request("http://localhost") + .content("Some plain text") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) .build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index aa351b40..d1c56f3b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -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("5charlie") - .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("5charlie") + .header(HttpHeaders.CONTENT_TYPE, contentType.toString()) .build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 92c8da49..bf322669 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -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("foo") .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.emptyList()) + .document(this.operationBuilder.response().content("Some plain text") + .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 5b3438fb..a7f2f047 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -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("5charlie") - .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("5charlie") + .header(HttpHeaders.CONTENT_TYPE, contentType.toString()) + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java index 5c9f1b6e..064ed972 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java @@ -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("5").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()); }