From db289495e546f09b7ad55426001b56f52e67c173 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 9 May 2012 16:29:22 -0400 Subject: [PATCH] Pretty print option for Jackson converter and view Jackson serialization supports pretty printing. Usually it's enabled by invoking ObjectMapper.configure(..), which is not convenient for apps with XML configuration. The Jackson HttpMessageConverter and View now both have a prettyPrint property. A second more serious issue is documented here: https://github.com/FasterXML/jackson-databind/issues/12 The workaround discussed at the above link has been implemented. Issue: SPR-7201 --- .../MappingJackson2HttpMessageConverter.java | 32 +++ .../MappingJacksonHttpMessageConverter.java | 34 ++- ...ppingJacksonHttpMessageConverterTests.java | 218 ++++++++++++++++++ ...pingJackson2HttpMessageConverterTests.java | 106 +++++++++ ...ppingJacksonHttpMessageConverterTests.java | 196 ++-------------- .../view/json/MappingJackson2JsonView.java | 33 +++ .../view/json/MappingJacksonJsonView.java | 40 +++- .../json/MappingJackson2JsonViewTest.java | 15 ++ .../view/json/MappingJacksonJsonViewTest.java | 16 +- 9 files changed, 510 insertions(+), 180 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/http/converter/json/AbstractMappingJacksonHttpMessageConverterTests.java create mode 100644 spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java index 343b02fe8f..a717ecbe29 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java @@ -31,8 +31,10 @@ import org.springframework.util.Assert; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} @@ -57,6 +59,8 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv private boolean prefixJson = false; + private Boolean prettyPrint; + /** * Construct a new {@code BindingJacksonHttpMessageConverter}. @@ -77,6 +81,13 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv public void setObjectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); + } } /** @@ -97,6 +108,20 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv this.prefixJson = prefixJson; } + /** + * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *
+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+	 * converter.setObjectMapper(mapper);
+	 * 
+ */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } + @Override public boolean canRead(Class clazz, MediaType mediaType) { @@ -135,6 +160,13 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); JsonGenerator jsonGenerator = this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + jsonGenerator.useDefaultPrettyPrinter(); + } + try { if (this.prefixJson) { jsonGenerator.writeRaw("{} && "); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java index 2eb6e62171..b26d8bc954 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java @@ -24,9 +24,9 @@ import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonProcessingException; import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; import org.codehaus.jackson.map.type.TypeFactory; import org.codehaus.jackson.type.JavaType; - import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -35,6 +35,8 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.Assert; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; + /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} * that can read and write JSON using Jackson's {@link ObjectMapper}. @@ -57,6 +59,8 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve private boolean prefixJson = false; + private Boolean prettyPrint; + /** * Construct a new {@code BindingJacksonHttpMessageConverter}. @@ -77,6 +81,13 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve public void setObjectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, this.prettyPrint); + } } /** @@ -97,6 +108,20 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve this.prefixJson = prefixJson; } + /** + * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *
+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
+	 * converter.setObjectMapper(mapper);
+	 * 
+ *

The default value is {@code false}. + */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } @Override public boolean canRead(Class clazz, MediaType mediaType) { @@ -135,6 +160,13 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); JsonGenerator jsonGenerator = this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) { + jsonGenerator.useDefaultPrettyPrinter(); + } + try { if (this.prefixJson) { jsonGenerator.writeRaw("{} && "); diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/AbstractMappingJacksonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/AbstractMappingJacksonHttpMessageConverterTests.java new file mode 100644 index 0000000000..bda5afc9df --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/json/AbstractMappingJacksonHttpMessageConverterTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.json; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; + +/** + * Base class for Jackson and Jackson 2 converter tests. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public abstract class AbstractMappingJacksonHttpMessageConverterTests> { + + private T converter; + + @Before + public void setup() { + this.converter = createConverter(); + } + + protected T getConverter() { + return this.converter; + } + + protected abstract T createConverter(); + + @Test + public void canRead() { + assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json"))); + assertTrue(converter.canRead(Map.class, new MediaType("application", "json"))); + } + + @Test + public void canWrite() { + assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json"))); + assertTrue(converter.canWrite(Map.class, new MediaType("application", "json"))); + } + + @Test + public void readTyped() throws IOException { + String body = + "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + } + + @Test + @SuppressWarnings("unchecked") + public void readUntyped() throws IOException { + String body = + "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + HashMap result = (HashMap) converter.read(HashMap.class, inputMessage); + assertEquals("Foo", result.get("string")); + assertEquals(42, result.get("number")); + assertEquals(42D, (Double) result.get("fraction"), 0D); + List array = new ArrayList(); + array.add("Foo"); + array.add("Bar"); + assertEquals(array, result.get("array")); + assertEquals(Boolean.TRUE, result.get("bool")); + assertEquals("AQI=", result.get("bytes")); + } + + @Test + public void write() throws IOException { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + MyBean body = new MyBean(); + body.setString("Foo"); + body.setNumber(42); + body.setFraction(42F); + body.setArray(new String[]{"Foo", "Bar"}); + body.setBool(true); + body.setBytes(new byte[]{0x1, 0x2}); + converter.write(body, null, outputMessage); + Charset utf8 = Charset.forName("UTF-8"); + String result = outputMessage.getBodyAsString(utf8); + assertTrue(result.contains("\"string\":\"Foo\"")); + assertTrue(result.contains("\"number\":42")); + assertTrue(result.contains("fraction\":42.0")); + assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]")); + assertTrue(result.contains("\"bool\":true")); + assertTrue(result.contains("\"bytes\":\"AQI=\"")); + assertEquals("Invalid content-type", new MediaType("application", "json", utf8), + outputMessage.getHeaders().getContentType()); + } + + @Test + public void writeUTF16() throws IOException { + Charset utf16 = Charset.forName("UTF-16BE"); + MediaType contentType = new MediaType("application", "json", utf16); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + String body = "H\u00e9llo W\u00f6rld"; + converter.write(body, contentType, outputMessage); + assertEquals("Invalid result", "\"" + body + "\"", outputMessage.getBodyAsString(utf16)); + assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType()); + } + + @Test(expected = HttpMessageNotReadableException.class) + public void readInvalidJson() throws IOException { + String body = "FooBar"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + converter.read(MyBean.class, inputMessage); + } + + @Test(expected = HttpMessageNotReadableException.class) + public void readValidJsonWithUnknownProperty() throws IOException { + String body = "{\"string\":\"string\",\"unknownProperty\":\"value\"}"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + converter.read(MyBean.class, inputMessage); + } + + public static class MyBean { + + private String string; + + private int number; + + private float fraction; + + private String[] array; + + private boolean bool; + + private byte[] bytes; + + public byte[] getBytes() { + return bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + + public float getFraction() { + return fraction; + } + + public void setFraction(float fraction) { + this.fraction = fraction; + } + + public String[] getArray() { + return array; + } + + public void setArray(String[] array) { + this.array = array; + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java new file mode 100644 index 0000000000..2b8409c650 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2012 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter.json; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Jackson 2.x converter tests. + * + * @author Rossen Stoyanchev + */ +public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJacksonHttpMessageConverterTests { + + @Override + protected MappingJackson2HttpMessageConverter createConverter() { + return new MappingJackson2HttpMessageConverter(); + } + + @Test + @SuppressWarnings("unchecked") + public void readGenerics() throws IOException { + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() { + + @Override + protected JavaType getJavaType(Class clazz) { + if (List.class.isAssignableFrom(clazz)) { + return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class); + } + else { + return super.getJavaType(clazz); + } + } + }; + String body = + "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + List results = (List) converter.read(List.class, inputMessage); + assertEquals(1, results.size()); + MyBean result = results.get(0); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + } + + @Test + public void prettyPrint() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + PrettyPrintBean bean = new PrettyPrintBean(); + bean.setName("Jason"); + + getConverter().setPrettyPrint(true); + getConverter().writeInternal(bean, outputMessage); + String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); + + assertEquals("{\n \"name\" : \"Jason\"\n}", result); + } + + + public static class PrettyPrintBean { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java index 3ba8dea96e..180fe10461 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java @@ -23,80 +23,31 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.codehaus.jackson.map.type.TypeFactory; import org.codehaus.jackson.type.JavaType; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; /** - * Jackson conversion tests parameterized with Jackson and Jackson 2 converters. + * Jackson 1.x converter tests. * - * @author Arjen Poutsma * @author Rossen Stoyanchev */ -@RunWith(Parameterized.class) -public class MappingJacksonHttpMessageConverterTests { +public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJacksonHttpMessageConverterTests { - private HttpMessageConverter converter; - - @Parameters - public static Collection handlerTypes() { - Object[][] array = new Object[2][1]; - - array[0] = new Object[] { new MappingJackson2HttpMessageConverter()}; - array[1] = new Object[] { new MappingJacksonHttpMessageConverter()}; - - return Arrays.asList(array); - } - - public MappingJacksonHttpMessageConverterTests(HttpMessageConverter converter) { - this.converter = converter; - } - - @Test - public void canRead() { - assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json"))); - assertTrue(converter.canRead(Map.class, new MediaType("application", "json"))); - } - - @Test - public void canWrite() { - assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json"))); - assertTrue(converter.canWrite(Map.class, new MediaType("application", "json"))); - } - - @Test - public void readTyped() throws IOException { - String body = - "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); - inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); - assertEquals("Foo", result.getString()); - assertEquals(42, result.getNumber()); - assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); - assertTrue(result.isBool()); - assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + @Override + protected MappingJacksonHttpMessageConverter createConverter() { + return new MappingJacksonHttpMessageConverter(); } @Test @SuppressWarnings("unchecked") public void readGenerics() throws IOException { - converter = new MappingJacksonHttpMessageConverter() { + MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter() { @Override protected JavaType getJavaType(Class clazz) { if (List.class.isAssignableFrom(clazz)) { @@ -124,134 +75,29 @@ public class MappingJacksonHttpMessageConverterTests { } @Test - @SuppressWarnings("unchecked") - public void readUntyped() throws IOException { - String body = - "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); - inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - HashMap result = (HashMap) converter.read(HashMap.class, inputMessage); - assertEquals("Foo", result.get("string")); - assertEquals(42, result.get("number")); - assertEquals(42D, (Double) result.get("fraction"), 0D); - List array = new ArrayList(); - array.add("Foo"); - array.add("Bar"); - assertEquals(array, result.get("array")); - assertEquals(Boolean.TRUE, result.get("bool")); - assertEquals("AQI=", result.get("bytes")); - } - - @Test - public void write() throws IOException { + public void prettyPrint() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - MyBean body = new MyBean(); - body.setString("Foo"); - body.setNumber(42); - body.setFraction(42F); - body.setArray(new String[]{"Foo", "Bar"}); - body.setBool(true); - body.setBytes(new byte[]{0x1, 0x2}); - converter.write(body, null, outputMessage); - Charset utf8 = Charset.forName("UTF-8"); - String result = outputMessage.getBodyAsString(utf8); - assertTrue(result.contains("\"string\":\"Foo\"")); - assertTrue(result.contains("\"number\":42")); - assertTrue(result.contains("fraction\":42.0")); - assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]")); - assertTrue(result.contains("\"bool\":true")); - assertTrue(result.contains("\"bytes\":\"AQI=\"")); - assertEquals("Invalid content-type", new MediaType("application", "json", utf8), - outputMessage.getHeaders().getContentType()); + PrettyPrintBean bean = new PrettyPrintBean(); + bean.setName("Jason"); + + getConverter().setPrettyPrint(true); + getConverter().writeInternal(bean, outputMessage); + String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); + + assertEquals("{\n \"name\" : \"Jason\"\n}", result); } - @Test - public void writeUTF16() throws IOException { - Charset utf16 = Charset.forName("UTF-16BE"); - MediaType contentType = new MediaType("application", "json", utf16); - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - String body = "H\u00e9llo W\u00f6rld"; - converter.write(body, contentType, outputMessage); - assertEquals("Invalid result", "\"" + body + "\"", outputMessage.getBodyAsString(utf16)); - assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType()); - } - @Test(expected = HttpMessageNotReadableException.class) - public void readInvalidJson() throws IOException { - String body = "FooBar"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); - inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - converter.read(MyBean.class, inputMessage); - } + public static class PrettyPrintBean { - @Test(expected = HttpMessageNotReadableException.class) - public void readValidJsonWithUnknownProperty() throws IOException { - String body = "{\"string\":\"string\",\"unknownProperty\":\"value\"}"; - MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); - inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - converter.read(MyBean.class, inputMessage); - } + private String name; - public static class MyBean { - - private String string; - - private int number; - - private float fraction; - - private String[] array; - - private boolean bool; - - private byte[] bytes; - - public byte[] getBytes() { - return bytes; + public String getName() { + return name; } - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - - public boolean isBool() { - return bool; - } - - public void setBool(boolean bool) { - this.bool = bool; - } - - public String getString() { - return string; - } - - public void setString(String string) { - this.string = string; - } - - public int getNumber() { - return number; - } - - public void setNumber(int number) { - this.number = number; - } - - public float getFraction() { - return fraction; - } - - public void setFraction(float fraction) { - this.fraction = fraction; - } - - public String[] getArray() { - return array; - } - - public void setArray(String[] array) { - this.array = array; + public void setName(String name) { + this.name = name; } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java index 4427b138d6..ea94f287c9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java @@ -25,7 +25,9 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -61,6 +63,8 @@ public class MappingJackson2JsonView extends AbstractView { private boolean prefixJson = false; + private Boolean prettyPrint; + private Set modelKeys; private boolean extractValueFromSingleKeyModel = false; @@ -89,6 +93,13 @@ public class MappingJackson2JsonView extends AbstractView { public void setObjectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "'objectMapper' must not be null"); this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); + } } /** @@ -112,6 +123,21 @@ public class MappingJackson2JsonView extends AbstractView { this.prefixJson = prefixJson; } + /** + * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *
+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+	 * converter.setObjectMapper(mapper);
+	 * 
+ *

The default value is {@code false}. + */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } + /** * Set the attribute in the model that should be rendered by this view. * When set, all other model attributes will be ignored. @@ -192,6 +218,13 @@ public class MappingJackson2JsonView extends AbstractView { Object value = filterModel(model); JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + generator.useDefaultPrettyPrinter(); + } + if (this.prefixJson) { generator.writeRaw("{} && "); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java index cdc59c24ef..c511a38bf5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java @@ -20,20 +20,23 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; import org.codehaus.jackson.map.SerializerFactory; - import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.validation.BindingResult; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.AbstractView; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; + /** * Spring MVC {@link View} that renders JSON content by serializing the model for the current request * using Jackson's {@link ObjectMapper}. @@ -62,12 +65,14 @@ public class MappingJacksonJsonView extends AbstractView { private boolean prefixJson = false; + private Boolean prettyPrint; + private Set modelKeys; private boolean extractValueFromSingleKeyModel = false; private boolean disableCaching = true; - + /** * Construct a new {@code JacksonJsonView}, setting the content type to {@code application/json}. @@ -90,6 +95,13 @@ public class MappingJacksonJsonView extends AbstractView { public void setObjectMapper(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "'objectMapper' must not be null"); this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, this.prettyPrint); + } } /** @@ -113,6 +125,21 @@ public class MappingJacksonJsonView extends AbstractView { this.prefixJson = prefixJson; } + /** + * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *

+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
+	 * converter.setObjectMapper(mapper);
+	 * 
+ *

The default value is {@code false}. + */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } + /** * Set the attribute in the model that should be rendered by this view. * When set, all other model attributes will be ignored. @@ -120,7 +147,7 @@ public class MappingJacksonJsonView extends AbstractView { public void setModelKey(String modelKey) { this.modelKeys = Collections.singleton(modelKey); } - + /** * Set the attributes in the model that should be rendered by this view. * When set, all other model attributes will be ignored. @@ -193,6 +220,13 @@ public class MappingJacksonJsonView extends AbstractView { Object value = filterModel(model); JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) { + generator.useDefaultPrettyPrinter(); + } + if (this.prefixJson) { generator.writeRaw("{} && "); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java index 93774c21ca..dad748d6d6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java @@ -37,6 +37,7 @@ import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.ScriptableObject; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; import com.fasterxml.jackson.core.JsonGenerator; @@ -143,6 +144,20 @@ public class MappingJackson2JsonViewTest { validateResult(); } + @Test + public void renderWithPrettyPrint() throws Exception { + + ModelMap model = new ModelMap("foo", new TestBeanSimple()); + + view.setPrettyPrint(true); + view.render(model, request, response); + + String result = response.getContentAsString(); + assertTrue("Pretty printing not applied:\n" + result, result.startsWith("{\n \"foo\" : {\n ")); + + validateResult(); + } + @Test public void renderSimpleBeanPrefixed() throws Exception { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java index eecc00d28d..9930c511d4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java @@ -38,7 +38,6 @@ import org.codehaus.jackson.map.SerializerFactory; import org.codehaus.jackson.map.SerializerProvider; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.ser.BeanSerializerFactory; - import org.junit.Before; import org.junit.Test; import org.mozilla.javascript.Context; @@ -46,6 +45,7 @@ import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.ScriptableObject; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; /** @@ -137,6 +137,20 @@ public class MappingJacksonJsonViewTest { validateResult(); } + @Test + public void renderWithPrettyPrint() throws Exception { + + ModelMap model = new ModelMap("foo", new TestBeanSimple()); + + view.setPrettyPrint(true); + view.render(model, request, response); + + String result = response.getContentAsString(); + assertTrue("Pretty printing not applied:\n" + result, result.startsWith("{\n \"foo\" : {\n ")); + + validateResult(); + } + @Test public void renderSimpleBeanPrefixed() throws Exception {