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