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 {