Support Jackson based XML serialization/deserialization

This commit adds support for XML serialization/deserialization based on
the jackson-dataformat-xml extension. When using @EnableWebMvc or
<mvc:annotation-driven/>, Jackson will be used  by default instead of JAXB2
if jackson-dataformat-xml classes are found in the classpath.

This commit introduces MappingJackson2XmlHttpMessageConverter and
MappingJackson2XmlView classes, and common parts between JSON
and XML processing have been moved to AbstractJackson2HttpMessageConverter
and AbstractJackson2View classes.

MappingJackson2XmlView supports serialization of a single object. If the model
contains multiple entries, MappingJackson2XmlView.setModelKey() should be
used to specify the entry to serialize.

Pretty print works in XML, but tests are not included since a Woodstox dependency
is needed, and it is better to continue testing spring-web and spring-webmvc
against JAXB2.

Issue: SPR-11785
This commit is contained in:
Sebastien Deleuze
2014-08-06 18:19:20 +02:00
parent 92bd240474
commit 6665634675
17 changed files with 1423 additions and 433 deletions

View File

@@ -48,6 +48,7 @@ configure(allprojects) { project ->
ext.tomcatVersion = "8.0.9"
ext.xstreamVersion = "1.4.7"
ext.protobufVersion = "2.5.0"
ext.woodstoxVersion = "4.1.6"
ext.gradleScriptDir = "${rootProject.projectDir}/gradle"
@@ -326,7 +327,7 @@ project("spring-core") {
optional("log4j:log4j:1.2.17")
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
testCompile("xmlunit:xmlunit:1.5")
testCompile("org.codehaus.woodstox:wstx-asl:3.2.7") {
testCompile("org.codehaus.woodstox:woodstox-core-asl:${woodstoxVersion}") {
exclude group: "stax", module: "stax-api"
}
}
@@ -665,6 +666,7 @@ project("spring-web") {
optional("org.apache.httpcomponents:httpclient:4.3.4")
optional("org.apache.httpcomponents:httpasyncclient:4.0.1")
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}")
optional("com.google.code.gson:gson:${gsonVersion}")
optional("com.rometools:rome:1.5.0")
optional("org.eclipse.jetty:jetty-servlet:${jettyVersion}") {
@@ -824,6 +826,7 @@ project("spring-webmvc") {
exclude group: "xml-apis", module: "xml-apis"
}
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}")
optional("com.rometools:rome:1.5.0")
optional("javax.el:javax.el-api:2.2.5")
optional("org.apache.tiles:tiles-api:${tiles3Version}")

View File

@@ -0,0 +1,315 @@
/*
* Copyright 2002-2014 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 java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
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;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Abstract base class for Jackson based and content type independent
* {@link HttpMessageConverter} implementations.
*
* <p>Compatible with Jackson 2.1 and higher.
*
* @author Arjen Poutsma
* @author Keith Donald
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 4.1
*/
public abstract class AbstractJackson2HttpMessageConverter extends
AbstractHttpMessageConverter<Object> implements GenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
// Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference
private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class,
"canDeserialize", JavaType.class, AtomicReference.class);
protected ObjectMapper objectMapper;
private Boolean prettyPrint;
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
super(supportedMediaType);
this.objectMapper = objectMapper;
}
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
super(supportedMediaTypes);
this.objectMapper = objectMapper;
}
/**
* Set the {@code ObjectMapper} for this view.
* If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further
* control of the JSON serialization process. For example, an extended
* {@link com.fasterxml.jackson.databind.ser.SerializerFactory}
* can be configured that provides custom serializers for specific types.
* The other option for refining the serialization process is to use Jackson's
* provided annotations on the types to be serialized, in which case a
* custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
configurePrettyPrint();
}
/**
* Return the underlying {@code ObjectMapper} for this view.
*/
public ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
* <pre class="code">
* ObjectMapper mapper = new ObjectMapper();
* mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
* converter.setObjectMapper(mapper);
* </pre>
*/
public void setPrettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
configurePrettyPrint();
}
private void configurePrettyPrint() {
if (this.prettyPrint != null) {
this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
}
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return canRead(clazz, null, mediaType);
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
JavaType javaType = getJavaType(type, contextClass);
if (!jackson23Available || !logger.isWarnEnabled()) {
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
String msg = "Failed to evaluate deserialization for type " + javaType;
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
else {
logger.warn(msg + ": " + cause);
}
}
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!jackson23Available || !logger.isWarnEnabled()) {
return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
String msg = "Failed to evaluate serialization for type [" + clazz + "]";
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
else {
logger.warn(msg + ": " + cause);
}
}
return false;
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write instead
throw new UnsupportedOperationException();
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(clazz, null);
return readJavaType(javaType, inputMessage);
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage);
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
try {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
Class<?> serializationView = null;
Object value = object;
if (value instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
}
if (serializationView != null) {
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
}
else {
this.objectMapper.writeValue(generator, value);
}
writeSuffix(generator, object);
generator.flush();
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
}
}
/**
* Write a prefix before the main content.
* @param generator the generator to use for writing content.
* @param object the object to write to the output message.
*/
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
}
/**
* Write a suffix after the main content.
* @param generator the generator to use for writing content.
* @param object the object to write to the output message.
*/
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
}
/**
* Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
* <pre class="code">
* protected JavaType getJavaType(Type type) {
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(type);
* }
* }
* </pre>
* @param type the type to return the java type for
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature, can be {@code null}
* signature, can be {@code null}
* @return the java type
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
return this.objectMapper.getTypeFactory().constructType(type, contextClass);
}
/**
* Determine the JSON encoding to use for the given content type.
* @param contentType the media type as requested by the caller
* @return the JSON encoding to use (never {@code null})
*/
protected JsonEncoding getJsonEncoding(MediaType contentType) {
if (contentType != null && contentType.getCharSet() != null) {
Charset charset = contentType.getCharSet();
for (JsonEncoding encoding : JsonEncoding.values()) {
if (charset.name().equals(encoding.getJavaName())) {
return encoding;
}
}
}
return JsonEncoding.UTF8;
}
@Override
protected MediaType getDefaultContentType(Object object) throws IOException {
if (object instanceof MappingJacksonValue) {
object = ((MappingJacksonValue) object).getValue();
}
return super.getDefaultContentType(object);
}
@Override
protected Long getContentLength(Object object, MediaType contentType) throws IOException {
if (object instanceof MappingJacksonValue) {
object = ((MappingJacksonValue) object).getValue();
}
return super.getContentLength(object, contentType);
}
}

View File

@@ -51,7 +51,7 @@ import org.springframework.util.ClassUtils;
* to enable or disable Jackson features from within XML configuration.
*
* <p>Example usage with
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}:
* {@link MappingJackson2HttpMessageConverter}:
*
* <pre class="code">
* &lt;bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
@@ -486,7 +486,8 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
@Override
public Class<?> getObjectType() {
return ObjectMapper.class;
Assert.notNull(this.objectMapper, "ObjectMapper must not be null");
return this.objectMapper.getClass();
}
@Override

View File

@@ -17,27 +17,11 @@
package org.springframework.http.converter.json;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
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;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that
@@ -54,58 +38,22 @@ import org.springframework.util.ClassUtils;
* @author Keith Donald
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.1.2
*/
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
// Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference
private static final boolean jackson23Available =
ClassUtils.hasMethod(ObjectMapper.class, "canDeserialize", JavaType.class, AtomicReference.class);
private ObjectMapper objectMapper = new ObjectMapper();
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
private String jsonPrefix;
private Boolean prettyPrint;
/**
* Construct a new {@code MappingJackson2HttpMessageConverter}.
*/
public MappingJackson2HttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET),
super(new ObjectMapper(), new MediaType("application", "json", DEFAULT_CHARSET),
new MediaType("application", "*+json", DEFAULT_CHARSET));
}
/**
* Set the {@code ObjectMapper} for this view.
* If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further
* control of the JSON serialization process. For example, an extended
* {@link com.fasterxml.jackson.databind.ser.SerializerFactory}
* can be configured that provides custom serializers for specific types.
* The other option for refining the serialization process is to use Jackson's
* provided annotations on the types to be serialized, in which case a
* custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
configurePrettyPrint();
}
/**
* Return the underlying {@code ObjectMapper} for this view.
*/
public ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Specify a custom prefix to use for this view's JSON output.
* Default is none.
@@ -127,199 +75,29 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
this.jsonPrefix = (prefixJson ? "{} && " : null);
}
/**
* Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
* <pre class="code">
* ObjectMapper mapper = new ObjectMapper();
* mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
* converter.setObjectMapper(mapper);
* </pre>
*/
public void setPrettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
configurePrettyPrint();
}
private void configurePrettyPrint() {
if (this.prettyPrint != null) {
this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
}
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return canRead(clazz, null, mediaType);
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
JavaType javaType = getJavaType(type, contextClass);
if (!jackson23Available || !logger.isWarnEnabled()) {
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
if (this.jsonPrefix != null) {
generator.writeRaw(this.jsonPrefix);
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
String msg = "Failed to evaluate deserialization for type " + javaType;
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
else {
logger.warn(msg + ": " + cause);
}
}
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!jackson23Available || !logger.isWarnEnabled()) {
return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
}
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) {
return true;
}
Throwable cause = causeRef.get();
if (cause != null) {
String msg = "Failed to evaluate serialization for type [" + clazz + "]";
if (logger.isDebugEnabled()) {
logger.warn(msg, cause);
}
else {
logger.warn(msg + ": " + cause);
}
}
return false;
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write instead
throw new UnsupportedOperationException();
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(clazz, null);
return readJavaType(javaType, inputMessage);
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage);
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
try {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
if (this.jsonPrefix != null) {
generator.writeRaw(this.jsonPrefix);
}
Class<?> serializationView = null;
String jsonpFunction = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
object = container.getValue();
serializationView = container.getSerializationView();
jsonpFunction = container.getJsonpFunction();
}
if (jsonpFunction != null) {
generator.writeRaw(jsonpFunction + "(");
}
if (serializationView != null) {
this.objectMapper.writerWithView(serializationView).writeValue(generator, object);
}
else {
this.objectMapper.writeValue(generator, object);
}
if (jsonpFunction != null) {
generator.writeRaw(");");
generator.flush();
}
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
/**
* Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
* <pre class="code">
* protected JavaType getJavaType(Type type) {
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(type);
* }
* }
* </pre>
* @param type the type to return the java type for
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature, can be {@code null}
* signature, can be {@code null}
* @return the java type
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
return this.objectMapper.getTypeFactory().constructType(type, contextClass);
}
/**
* Determine the JSON encoding to use for the given content type.
* @param contentType the media type as requested by the caller
* @return the JSON encoding to use (never {@code null})
*/
protected JsonEncoding getJsonEncoding(MediaType contentType) {
if (contentType != null && contentType.getCharSet() != null) {
Charset charset = contentType.getCharSet();
for (JsonEncoding encoding : JsonEncoding.values()) {
if (charset.name().equals(encoding.getJavaName())) {
return encoding;
}
}
}
return JsonEncoding.UTF8;
}
@Override
protected MediaType getDefaultContentType(Object object) throws IOException {
String jsonpFunction = null;
if (object instanceof MappingJacksonValue) {
object = ((MappingJacksonValue) object).getValue();
jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction();
}
if (jsonpFunction != null) {
generator.writeRaw(jsonpFunction + "(");
}
return super.getDefaultContentType(object);
}
@Override
protected Long getContentLength(Object object, MediaType contentType) throws IOException {
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
String jsonpFunction = null;
if (object instanceof MappingJacksonValue) {
object = ((MappingJacksonValue) object).getValue();
jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction();
}
if (jsonpFunction != null) {
generator.writeRaw(");");
}
return super.getContentLength(object, contentType);
}
}

View File

@@ -18,9 +18,8 @@ package org.springframework.http.converter.json;
/**
* A simple holder for the POJO to serialize via
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
* MappingJackson2HttpMessageConverter} along with further serialization
* instructions to be passed in to the converter.
* {@link MappingJackson2HttpMessageConverter} along with further
* serialization instructions to be passed in to the converter.
*
* <p>On the server side this wrapper is added with a
* {@code ResponseBodyInterceptor} after content negotiation selects the

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2014 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.xml;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
/**
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that
* can read and write XML using <a href="https://github.com/FasterXML/jackson-dataformat-xml">Jackson 2.x extension component for
* reading and writing XML encoded data</a>.
*
* @author Sebastien Deleuze
* @since 4.1
*/
public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public MappingJackson2XmlHttpMessageConverter() {
super(new XmlMapper(),
new MediaType("application", "xml", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET));
}
}

View File

@@ -45,6 +45,7 @@ import com.fasterxml.jackson.databind.ser.Serializers;
import com.fasterxml.jackson.databind.ser.std.ClassSerializer;
import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import com.fasterxml.jackson.databind.type.SimpleType;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.junit.Before;
import org.junit.Test;
@@ -70,7 +71,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSettersWithNullValues() {
public void settersWithNullValues() {
// Should not crash:
factory.setSerializers((JsonSerializer<?>[]) null);
factory.setSerializersByType(null);
@@ -80,13 +81,13 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test(expected = FatalBeanException.class)
public void testUnknownFeature() {
public void unknownFeature() {
this.factory.setFeaturesToEnable(Boolean.TRUE);
this.factory.afterPropertiesSet();
}
@Test
public void testBooleanSetters() {
public void booleanSetters() {
this.factory.setAutoDetectFields(false);
this.factory.setAutoDetectGettersSetters(false);
this.factory.setDefaultViewInclusion(false);
@@ -107,7 +108,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSetNotNullSerializationInclusion() {
public void setNotNullSerializationInclusion() {
factory.afterPropertiesSet();
assertTrue(factory.getObject().getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.ALWAYS);
@@ -117,7 +118,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSetNotDefaultSerializationInclusion() {
public void setNotDefaultSerializationInclusion() {
factory.afterPropertiesSet();
assertTrue(factory.getObject().getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.ALWAYS);
@@ -127,7 +128,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSetNotEmptySerializationInclusion() {
public void setNotEmptySerializationInclusion() {
factory.afterPropertiesSet();
assertTrue(factory.getObject().getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.ALWAYS);
@@ -137,7 +138,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testDateTimeFormatSetter() {
public void dateTimeFormatSetter() {
SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
this.factory.setDateFormat(dateFormat);
@@ -148,7 +149,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSimpleDateFormatStringSetter() {
public void simpleDateFormatStringSetter() {
SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
this.factory.setSimpleDateFormat(DATE_FORMAT);
@@ -159,7 +160,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSetModules() {
public void setModules() {
NumberSerializer serializer1 = new NumberSerializer();
SimpleModule module = new SimpleModule();
module.addSerializer(Integer.class, serializer1);
@@ -173,7 +174,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testSimpleSetup() {
public void simpleSetup() {
this.factory.afterPropertiesSet();
assertNotNull(this.factory.getObject());
@@ -190,7 +191,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testPropertyNamingStrategy() {
public void propertyNamingStrategy() {
PropertyNamingStrategy strategy = new PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy();
this.factory.setPropertyNamingStrategy(strategy);
this.factory.afterPropertiesSet();
@@ -200,18 +201,17 @@ public class Jackson2ObjectMapperFactoryBeanTests {
}
@Test
public void testCompleteSetup() {
public void completeSetup() {
NopAnnotationIntrospector annotationIntrospector = NopAnnotationIntrospector.instance;
ObjectMapper objectMapper = new ObjectMapper();
factory.setObjectMapper(objectMapper);
assertTrue(this.factory.isSingleton());
assertEquals(ObjectMapper.class, this.factory.getObjectType());
Map<Class<?>, JsonDeserializer<?>> deserializers = new HashMap<Class<?>, JsonDeserializer<?>>();
deserializers.put(Date.class, new DateDeserializer());
factory.setObjectMapper(objectMapper);
JsonSerializer<Class<?>> serializer1 = new ClassSerializer();
JsonSerializer<Number> serializer2 = new NumberSerializer();
@@ -261,4 +261,14 @@ public class Jackson2ObjectMapperFactoryBeanTests {
assertTrue(objectMapper.getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.NON_NULL);
}
@Test
public void xmlMapper() {
this.factory.setObjectMapper(new XmlMapper());
this.factory.afterPropertiesSet();
assertNotNull(this.factory.getObject());
assertTrue(this.factory.isSingleton());
assertEquals(XmlMapper.class, this.factory.getObjectType());
}
}

View File

@@ -0,0 +1,241 @@
/*
* Copyright 2002-2014 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.xml;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import com.fasterxml.jackson.annotation.JsonView;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.MappingJacksonValue;
/**
* Jackson 2.x XML converter tests.
*
* @author Sebastien Deleuze
*/
public class MappingJackson2XmlHttpMessageConverterTests {
private final MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
@Test
public void canRead() {
assertTrue(converter.canRead(MyBean.class, new MediaType("application", "xml")));
}
@Test
public void canWrite() {
assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "xml")));
}
@Test
public void read() throws IOException {
String body =
"<MyBean><string>Foo</string><number>42</number><fraction>42.0</fraction><array><array>Foo</array><array>Bar</array></array><bool>true</bool><bytes>AQI=</bytes></MyBean>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
inputMessage.getHeaders().setContentType(new MediaType("application", "xml"));
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
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</string>"));
assertTrue(result.contains("<number>42</number>"));
assertTrue(result.contains("<fraction>42.0</fraction>"));
assertTrue(result.contains("<array><array>Foo</array><array>Bar</array></array>"));
assertTrue(result.contains("<bool>true</bool>"));
assertTrue(result.contains("<bytes>AQI=</bytes>"));
assertEquals("Invalid content-type", new MediaType("application", "xml", utf8),
outputMessage.getHeaders().getContentType());
}
@Test(expected = HttpMessageNotReadableException.class)
public void readInvalidXml() throws IOException {
String body = "FooBar";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
inputMessage.getHeaders().setContentType(new MediaType("application", "xml"));
converter.read(MyBean.class, inputMessage);
}
@Test(expected = HttpMessageNotReadableException.class)
public void readValidXmlWithUnknownProperty() throws IOException {
String body = "<MyBean><string>string</string><unknownProperty>value</unknownProperty></MyBean>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
inputMessage.getHeaders().setContentType(new MediaType("application", "xml"));
converter.read(MyBean.class, inputMessage);
}
@Test
public void jsonView() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
JacksonViewBean bean = new JacksonViewBean();
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("without");
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
jacksonValue.setSerializationView(MyJacksonView1.class);
this.writeInternal(jacksonValue, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
assertThat(result, containsString("<withView1>with</withView1>"));
assertThat(result, containsString("<withoutView>without</withoutView>"));
assertThat(result, not(containsString("<withView2>with</withView2>")));
}
private void writeInternal(Object object, HttpOutputMessage outputMessage)
throws NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Method method = AbstractJackson2HttpMessageConverter.class.getDeclaredMethod(
"writeInternal", Object.class, HttpOutputMessage.class);
method.setAccessible(true);
method.invoke(this.converter, object, outputMessage);
}
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;
}
}
private interface MyJacksonView1 {};
private interface MyJacksonView2 {};
private static class JacksonViewBean {
@JsonView(MyJacksonView1.class)
private String withView1;
@JsonView(MyJacksonView2.class)
private String withView2;
private String withoutView;
public String getWithView1() {
return withView1;
}
public void setWithView1(String withView1) {
this.withView1 = withView1;
}
public String getWithView2() {
return withView2;
}
public void setWithView2(String withView2) {
this.withView2 = withView2;
}
public String getWithoutView() {
return withoutView;
}
public void setWithoutView(String withoutView) {
this.withoutView = withoutView;
}
}
}

View File

@@ -47,6 +47,7 @@ import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -159,6 +160,9 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
private static final boolean jackson2XmlPresent =
ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
@@ -401,7 +405,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
props.put("atom", MediaType.APPLICATION_ATOM_XML_VALUE);
props.put("rss", "application/rss+xml");
}
if (jaxb2Present) {
if (jaxb2Present || jackson2XmlPresent) {
props.put("xml", MediaType.APPLICATION_XML_VALUE);
}
if (jackson2Present || gsonPresent) {
@@ -528,7 +532,10 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
messageConverters.add(createConverterDefinition(AtomFeedHttpMessageConverter.class, source));
messageConverters.add(createConverterDefinition(RssChannelHttpMessageConverter.class, source));
}
if (jaxb2Present) {
if(jackson2XmlPresent) {
messageConverters.add(createConverterDefinition(MappingJackson2XmlHttpMessageConverter.class, source));
}
else if (jaxb2Present) {
messageConverters.add(createConverterDefinition(Jaxb2RootElementHttpMessageConverter.class, source));
}
if (jackson2Present) {

View File

@@ -48,6 +48,7 @@ import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
@@ -171,6 +172,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebMvcConfigurationSupport.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebMvcConfigurationSupport.class.getClassLoader());
private static final boolean jackson2XmlPresent =
ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", WebMvcConfigurationSupport.class.getClassLoader());
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", WebMvcConfigurationSupport.class.getClassLoader());
@@ -302,7 +306,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
map.put("atom", MediaType.APPLICATION_ATOM_XML);
map.put("rss", MediaType.valueOf("application/rss+xml"));
}
if (jaxb2Present) {
if (jaxb2Present || jackson2XmlPresent) {
map.put("xml", MediaType.APPLICATION_XML);
}
if (jackson2Present || gsonPresent) {
@@ -653,7 +657,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jaxb2Present) {
if(jackson2XmlPresent) {
messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {

View File

@@ -29,8 +29,8 @@ import javax.servlet.http.HttpServletRequest;
/**
* A convenient base class for a {@code ResponseBodyAdvice} to instruct the
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
* MappingJackson2HttpMessageConverter} to serialize with JSONP formatting.
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
* to serialize with JSONP formatting.
*
* <p>Sub-classes must specify the query parameter name(s) to check for the name
* of the JSONP callback function.

View File

@@ -27,8 +27,7 @@ import org.springframework.http.server.ServerHttpResponse;
/**
* A convenient base class for {@code ResponseBodyAdvice} implementations
* that customize the response before JSON serialization with
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
* MappingJackson2HttpMessageConverter}.
* {@link MappingJackson2HttpMessageConverter}.
*
* @author Rossen Stoyanchev
* @since 4.1

View File

@@ -0,0 +1,246 @@
/*
* Copyright 2002-2014 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.web.servlet.view.json;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.Assert;
import org.springframework.web.servlet.view.AbstractView;
/**
* Abstract base class for Jackson based and content type independent
* {@link AbstractView} implementations.
*
* <p>Compatible with Jackson 2.1 and higher.
*
* @author Jeremy Grelle
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 4.1
*/
public abstract class AbstractJackson2View extends AbstractView {
private ObjectMapper objectMapper;
private JsonEncoding encoding = JsonEncoding.UTF8;
private Boolean prettyPrint;
private boolean disableCaching = true;
protected boolean updateContentLength = false;
protected AbstractJackson2View(ObjectMapper objectMapper, String contentType) {
this.objectMapper = objectMapper;
setContentType(contentType);
setExposePathVariables(false);
}
/**
* Set the {@code ObjectMapper} for this view.
* If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} will be used.
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control of
* the JSON serialization process. The other option is to use Jackson's provided annotations
* on the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
configurePrettyPrint();
}
/**
* Return the {@code ObjectMapper} for this view.
*/
public final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Set the {@code JsonEncoding} for this view.
* By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used.
*/
public void setEncoding(JsonEncoding encoding) {
Assert.notNull(encoding, "'encoding' must not be null");
this.encoding = encoding;
}
/**
* Return the {@code JsonEncoding} for this view.
*/
public final JsonEncoding getEncoding() {
return this.encoding;
}
/**
* Whether to use the default pretty printer when writing JSON.
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
* <pre class="code">
* ObjectMapper mapper = new ObjectMapper();
* mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
* </pre>
* <p>The default value is {@code false}.
*/
public void setPrettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
configurePrettyPrint();
}
private void configurePrettyPrint() {
if (this.prettyPrint != null) {
this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
}
}
/**
* Set the attribute in the model that should be rendered by this view.
* When set, all other model attributes will be ignored.
*/
public abstract void setModelKey(String modelKey);
/**
* Disables caching of the generated JSON.
* <p>Default is {@code true}, which will prevent the client from caching the generated JSON.
*/
public void setDisableCaching(boolean disableCaching) {
this.disableCaching = disableCaching;
}
/**
* Whether to update the 'Content-Length' header of the response. When set to
* {@code true}, the response is buffered in order to determine the content
* length and set the 'Content-Length' header of the response.
* <p>The default setting is {@code false}.
*/
public void setUpdateContentLength(boolean updateContentLength) {
this.updateContentLength = updateContentLength;
}
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
setResponseContentType(request, response);
response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) {
response.addHeader("Pragma", "no-cache");
response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
response.addDateHeader("Expires", 1L);
}
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream());
Object value = filterAndWrapModel(model, request);
writeContent(stream, value);
if (this.updateContentLength) {
writeToResponse(response, (ByteArrayOutputStream) stream);
}
}
/**
* Filter and optionally wrap the model in {@link MappingJacksonValue} container.
* @param model the model, as passed on to {@link #renderMergedOutputModel}
* @param request current HTTP request
* @return the wrapped or unwrapped value to be rendered
*/
protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) {
Object value = filterModel(model);
Class<?> serializationView = (Class<?>) model.get(JsonView.class.getName());
if (serializationView != null) {
MappingJacksonValue container = new MappingJacksonValue(value);
container.setSerializationView(serializationView);
value = container;
}
return value;
}
/**
* Filter out undesired attributes from the given model.
* The return value can be either another {@link Map} or a single value object.
* @param model the model, as passed on to {@link #renderMergedOutputModel}
* @return the value to be rendered
*/
protected abstract Object filterModel(Map<String, Object> model);
/**
* Write the actual JSON content to the stream.
* @param stream the output stream to use
* @param object the value to be rendered, as returned from {@link #filterModel}
* @throws IOException if writing failed
*/
protected void writeContent(OutputStream stream, Object object)
throws IOException {
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding);
writePrefix(generator, object);
Class<?> serializationView = null;
Object value = object;
if (value instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) value;
value = container.getValue();
serializationView = container.getSerializationView();
}
if (serializationView != null) {
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
}
else {
this.objectMapper.writeValue(generator, value);
}
writeSuffix(generator, object);
generator.flush();
}
/**
* Write a prefix before the main content.
* @param generator the generator to use for writing content.
* @param object the object to write to the output message.
*/
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
}
/**
* Write a suffix after the main content.
* @param generator the generator to use for writing content.
* @param object the object to write to the output message.
*/
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
}
}

View File

@@ -16,9 +16,7 @@
package org.springframework.web.servlet.view.json;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -29,18 +27,14 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
/**
* Spring MVC {@link View} that renders JSON content by serializing the model for the current request
@@ -59,7 +53,7 @@ import org.springframework.web.servlet.view.AbstractView;
* @author Sebastien Deleuze
* @since 3.1.2
*/
public class MappingJackson2JsonView extends AbstractView {
public class MappingJackson2JsonView extends AbstractJackson2View {
/**
* Default content type: "application/json".
@@ -72,23 +66,12 @@ public class MappingJackson2JsonView extends AbstractView {
*/
public static final String DEFAULT_JSONP_CONTENT_TYPE = "application/javascript";
private ObjectMapper objectMapper = new ObjectMapper();
private JsonEncoding encoding = JsonEncoding.UTF8;
private String jsonPrefix;
private Boolean prettyPrint;
private Set<String> modelKeys;
private boolean extractValueFromSingleKeyModel = false;
private boolean disableCaching = true;
private boolean updateContentLength = false;
private Set<String> jsonpParameterNames = new LinkedHashSet<String>(Arrays.asList("jsonp", "callback"));
@@ -96,45 +79,7 @@ public class MappingJackson2JsonView extends AbstractView {
* Construct a new {@code MappingJackson2JsonView}, setting the content type to {@code application/json}.
*/
public MappingJackson2JsonView() {
setContentType(DEFAULT_CONTENT_TYPE);
setExposePathVariables(false);
}
/**
* Set the {@code ObjectMapper} for this view.
* If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} will be used.
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control of
* the JSON serialization process. The other option is to use Jackson's provided annotations
* on the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
configurePrettyPrint();
}
/**
* Return the {@code ObjectMapper} for this view.
*/
public final ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Set the {@code JsonEncoding} for this view.
* By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used.
*/
public void setEncoding(JsonEncoding encoding) {
Assert.notNull(encoding, "'encoding' must not be null");
this.encoding = encoding;
}
/**
* Return the {@code JsonEncoding} for this view.
*/
public final JsonEncoding getEncoding() {
return this.encoding;
super(new ObjectMapper(), DEFAULT_CONTENT_TYPE);
}
/**
@@ -160,29 +105,9 @@ public class MappingJackson2JsonView extends AbstractView {
}
/**
* Whether to use the default pretty printer when writing JSON.
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
* <pre class="code">
* ObjectMapper mapper = new ObjectMapper();
* mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
* </pre>
* <p>The default value is {@code false}.
*/
public void setPrettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
configurePrettyPrint();
}
private void configurePrettyPrint() {
if (this.prettyPrint != null) {
this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
}
}
/**
* Set the attribute in the model that should be rendered by this view.
* When set, all other model attributes will be ignored.
* {@inheritDoc}
*/
@Override
public void setModelKey(String modelKey) {
this.modelKeys = Collections.singleton(modelKey);
}
@@ -232,24 +157,6 @@ public class MappingJackson2JsonView extends AbstractView {
this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel;
}
/**
* Disables caching of the generated JSON.
* <p>Default is {@code true}, which will prevent the client from caching the generated JSON.
*/
public void setDisableCaching(boolean disableCaching) {
this.disableCaching = disableCaching;
}
/**
* Whether to update the 'Content-Length' header of the response. When set to
* {@code true}, the response is buffered in order to determine the content
* length and set the 'Content-Length' header of the response.
* <p>The default setting is {@code false}.
*/
public void setUpdateContentLength(boolean updateContentLength) {
this.updateContentLength = updateContentLength;
}
/**
* Set JSONP request parameter names. Each time a request has one of those
* parameters, the resulting JSON will be wrapped into a function named as
@@ -262,40 +169,6 @@ public class MappingJackson2JsonView extends AbstractView {
this.jsonpParameterNames = jsonpParameterNames;
}
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
setResponseContentType(request, response);
response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) {
response.addHeader("Pragma", "no-cache");
response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
response.addDateHeader("Expires", 1L);
}
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream());
Class<?> serializationView = (Class<?>) model.get(JsonView.class.getName());
String jsonpParameterValue = getJsonpParameterValue(request);
Object value = filterModel(model);
if (serializationView != null || jsonpParameterValue != null) {
MappingJacksonValue container = new MappingJacksonValue(value);
container.setSerializationView(serializationView);
container.setJsonpFunction(jsonpParameterValue);
value = container;
}
writeContent(stream, value, this.jsonPrefix);
if (this.updateContentLength) {
writeToResponse(response, (ByteArrayOutputStream) stream);
}
}
private String getJsonpParameterValue(HttpServletRequest request) {
if (this.jsonpParameterNames != null) {
for (String name : this.jsonpParameterNames) {
@@ -316,6 +189,7 @@ public class MappingJackson2JsonView extends AbstractView {
* @param model the model, as passed on to {@link #renderMergedOutputModel}
* @return the value to be rendered
*/
@Override
protected Object filterModel(Map<String, Object> model) {
Map<String, Object> result = new HashMap<String, Object>(model.size());
Set<String> modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet());
@@ -328,41 +202,44 @@ public class MappingJackson2JsonView extends AbstractView {
return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result);
}
/**
* Write the actual JSON content to the stream.
* @param stream the output stream to use
* @param value the value to be rendered, as returned from {@link #filterModel}
* @param jsonPrefix the prefix for this view's JSON output
* (as indicated through {@link #setJsonPrefix}/{@link #setPrefixJson})
* @throws IOException if writing failed
*/
protected void writeContent(OutputStream stream, Object value, String jsonPrefix)
throws IOException {
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding);
if (jsonPrefix != null) {
generator.writeRaw(jsonPrefix);
@Override
protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) {
Object value = super.filterAndWrapModel(model, request);
String jsonpParameterValue = getJsonpParameterValue(request);
if (jsonpParameterValue != null) {
if(value instanceof MappingJacksonValue) {
((MappingJacksonValue) value).setJsonpFunction(jsonpParameterValue);
} else {
MappingJacksonValue container = new MappingJacksonValue(value);
container.setJsonpFunction(jsonpParameterValue);
value = container;
}
}
return value;
}
@Override
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
if (this.jsonPrefix != null) {
generator.writeRaw(this.jsonPrefix);
}
Class<?> serializationView = null;
String jsonpFunction = null;
if (value instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) value;
value = container.getValue();
serializationView = container.getSerializationView();
jsonpFunction = container.getJsonpFunction();
if (object instanceof MappingJacksonValue) {
jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction();
}
if (jsonpFunction != null) {
generator.writeRaw(jsonpFunction + "(" );
}
if (serializationView != null) {
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
}
else {
this.objectMapper.writeValue(generator, value);
}
@Override
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
String jsonpFunction = null;
if (object instanceof MappingJacksonValue) {
jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction();
}
if (jsonpFunction != null) {
generator.writeRaw(");");
generator.flush();
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2002-2014 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.web.servlet.view.xml;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.json.AbstractJackson2View;
/**
* Spring MVC {@link View} that renders XML content by serializing the model for the current request
* using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link XmlMapper}.
*
* <p>The Object to be serialized is supplied as a parameter in the model. The first serializable
* entry is used. Users can either specify a specific entry in the model via the
* {@link #setModelKey(String) sourceKey} property.
*
* <p>Compatible with Jackson 2.1 and higher.
*
* @author Sebastien Deleuze
* @since 4.1
*/
public class MappingJackson2XmlView extends AbstractJackson2View {
public static final String DEFAULT_CONTENT_TYPE = "application/xml";
private String modelKey;
public MappingJackson2XmlView() {
super(new XmlMapper(), DEFAULT_CONTENT_TYPE);
}
/**
* {@inheritDoc}
*/
@Override
public void setModelKey(String modelKey) {
this.modelKey = modelKey;
}
/**
* Filter out undesired attributes from the given model.
* The return value can be either another {@link Map} or a single value object.
* @param model the model, as passed on to {@link #renderMergedOutputModel}
* @return the value to be rendered
*/
@Override
protected Object filterModel(Map<String, Object> model) {
Object value = null;
if (this.modelKey != null) {
value = model.get(this.modelKey);
if (value == null) {
throw new IllegalStateException(
"Model contains no object with key [" + this.modelKey + "]");
}
} else {
for (Map.Entry<String, Object> entry : model.entrySet()) {
if (!(entry.getValue() instanceof BindingResult) &&
!entry.getKey().equals(JsonView.class.getName())) {
if(value != null) {
throw new IllegalStateException("Model contains more than one object to render, only one is supported");
}
value = entry.getValue();
}
}
}
return value;
}
}

View File

@@ -0,0 +1,347 @@
/*
* Copyright 2002-2014 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.web.servlet.view.xml;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ScriptableObject;
import org.springframework.http.MediaType;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
/**
* @author Sebastien Deleuze
*/
public class MappingJackson2XmlViewTests {
private MappingJackson2XmlView view;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private Context jsContext;
private ScriptableObject jsScope;
@Before
public void setUp() {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
jsContext = ContextFactory.getGlobal().enterContext();
jsScope = jsContext.initStandardObjects();
view = new MappingJackson2XmlView();
}
@Test
public void isExposePathVars() {
assertEquals("Must not expose path variables", false, view.isExposePathVariables());
}
@Test
public void renderSimpleMap() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", mock(BindingResult.class, "binding_result"));
model.put("foo", "bar");
view.setUpdateContentLength(true);
view.render(model, request, response);
assertEquals("no-cache", response.getHeader("Pragma"));
assertEquals("no-cache, no-store, max-age=0", response.getHeader("Cache-Control"));
assertNotNull(response.getHeader("Expires"));
assertEquals(MappingJackson2XmlView.DEFAULT_CONTENT_TYPE, response.getContentType());
String jsonResult = response.getContentAsString();
assertTrue(jsonResult.length() > 0);
assertEquals(jsonResult.length(), response.getContentLength());
validateResult();
}
@Test
public void renderWithSelectedContentType() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "bar");
view.render(model, request, response);
assertEquals("application/xml", response.getContentType());
request.setAttribute(View.SELECTED_CONTENT_TYPE, new MediaType("application", "vnd.example-v2+xml"));
view.render(model, request, response);
assertEquals("application/vnd.example-v2+xml", response.getContentType());
}
@Test
public void renderCaching() throws Exception {
view.setDisableCaching(false);
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", mock(BindingResult.class, "binding_result"));
model.put("foo", "bar");
view.render(model, request, response);
assertNull(response.getHeader("Pragma"));
assertNull(response.getHeader("Cache-Control"));
assertNull(response.getHeader("Expires"));
}
@Test
public void renderSimpleBean() throws Exception {
Object bean = new TestBeanSimple();
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", mock(BindingResult.class, "binding_result"));
model.put("foo", bean);
view.setUpdateContentLength(true);
view.render(model, request, response);
assertTrue(response.getContentAsString().length() > 0);
assertEquals(response.getContentAsString().length(), response.getContentLength());
validateResult();
}
@Test
public void renderWithCustomSerializerLocatedByAnnotation() throws Exception {
Object bean = new TestBeanSimpleAnnotated();
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", bean);
view.render(model, request, response);
assertTrue(response.getContentAsString().length() > 0);
assertTrue(response.getContentAsString().contains("<testBeanSimple>custom</testBeanSimple>"));
validateResult();
}
@Test
public void renderWithCustomSerializerLocatedByFactory() throws Exception {
SerializerFactory factory = new DelegatingSerializerFactory(null);
XmlMapper mapper = new XmlMapper();
mapper.setSerializerFactory(factory);
view.setObjectMapper(mapper);
Object bean = new TestBeanSimple();
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", bean);
view.render(model, request, response);
String result = response.getContentAsString();
assertTrue(result.length() > 0);
assertTrue(result.contains("custom</testBeanSimple>"));
validateResult();
}
@Test
public void renderOnlySpecifiedModelKey() throws Exception {
view.setModelKey("bar");
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "foo");
model.put("bar", "bar");
model.put("baz", "baz");
view.render(model, request, response);
String result = response.getContentAsString();
assertTrue(result.length() > 0);
assertFalse(result.contains("foo"));
assertTrue(result.contains("bar"));
assertFalse(result.contains("baz"));
validateResult();
}
@Test(expected = IllegalStateException.class)
public void renderModelWithMultipleKeys() throws Exception {
Map<String, Object> model = new TreeMap<String, Object>();
model.put("foo", "foo");
model.put("bar", "bar");
view.render(model, request, response);
fail();
}
@Test
public void renderSimpleBeanWithJsonView() throws Exception {
Object bean = new TestBeanSimple();
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", mock(BindingResult.class, "binding_result"));
model.put("foo", bean);
model.put(JsonView.class.getName(), MyJacksonView1.class);
view.setUpdateContentLength(true);
view.render(model, request, response);
String content = response.getContentAsString();
assertTrue(content.length() > 0);
assertEquals(content.length(), response.getContentLength());
assertTrue(content.contains("foo"));
assertFalse(content.contains("boo"));
assertFalse(content.contains(JsonView.class.getName()));
}
private void validateResult() throws Exception {
Object xmlResult =
jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "XML Stream", 1, null);
assertNotNull("XML Result did not eval as valid JavaScript", xmlResult);
assertEquals("application/xml", response.getContentType());
}
public interface MyJacksonView1 {
}
public interface MyJacksonView2 {
}
@SuppressWarnings("unused")
public static class TestBeanSimple {
@JsonView(MyJacksonView1.class)
private String property1 = "foo";
private boolean test = false;
@JsonView(MyJacksonView2.class)
private String property2 = "boo";
private TestChildBean child = new TestChildBean();
public String getProperty1() {
return property1;
}
public boolean getTest() {
return test;
}
public String getProperty2() {
return property2;
}
public Date getNow() {
return new Date();
}
public TestChildBean getChild() {
return child;
}
}
@JsonSerialize(using=TestBeanSimpleSerializer.class)
public static class TestBeanSimpleAnnotated extends TestBeanSimple {
}
public static class TestChildBean {
private String value = "bar";
private String baz = null;
private TestBeanSimple parent = null;
public String getValue() {
return value;
}
public String getBaz() {
return baz;
}
public TestBeanSimple getParent() {
return parent;
}
public void setParent(TestBeanSimple parent) {
this.parent = parent;
}
}
public static class TestBeanSimpleSerializer extends JsonSerializer<Object> {
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeFieldName("testBeanSimple");
jgen.writeString("custom");
jgen.writeEndObject();
}
}
@SuppressWarnings("serial")
public static class DelegatingSerializerFactory extends BeanSerializerFactory {
protected DelegatingSerializerFactory(SerializerFactoryConfig config) {
super(config);
}
@Override
public JsonSerializer<Object> createSerializer(SerializerProvider prov, JavaType type) throws JsonMappingException {
if (type.getRawClass() == TestBeanSimple.class) {
return new TestBeanSimpleSerializer();
}
else {
return super.createSerializer(prov, type);
}
}
}
}

View File

@@ -31616,7 +31616,7 @@ response. However, it stops short of writing any error content to the body of th
response while your application may need to add developer-friendly content to every
error response for example when providing a REST API. You can prepare a `ModelAndView`
and render error content through view resolution -- i.e. by configuring a
`ContentNegotiatingViewResolver`, `MappingJacksonJsonView`, and so on. However, you may
`ContentNegotiatingViewResolver`, `MappingJackson2JsonView`, and so on. However, you may
prefer to use `@ExceptionHandler` methods instead.
If you prefer to write error content via `@ExceptionHandler` methods you can extend
@@ -32192,9 +32192,12 @@ This is the complete list of HttpMessageConverters set up by mvc:annotation-driv
.. `FormHttpMessageConverter` converts form data to/from a `MultiValueMap<String,
String>`.
.. `Jaxb2RootElementHttpMessageConverter` converts Java objects to/from XML -- added if
JAXB2 is present on the classpath.
.. `MappingJackson2HttpMessageConverter` (or `MappingJacksonHttpMessageConverter`)
converts to/from JSON -- added if Jackson 2 (or Jackson) is present on the classpath.
JAXB2 is present and Jackson 2 XML extension is not present on the classpath.
.. `MappingJackson2HttpMessageConverter` converts to/from JSON -- added if Jackson 2
is present on the classpath.
.. `MappingJackson2XmlHttpMessageConverter` converts to/from XML -- added if
https://github.com/FasterXML/jackson-dataformat-xml[Jackson 2 XML extension] is present
on the classpath.
.. `AtomFeedHttpMessageConverter` converts Atom feeds -- added if Rome is present on the
classpath.
.. `RssChannelHttpMessageConverter` converts RSS feeds -- added if Rome is present on
@@ -35255,10 +35258,10 @@ https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-sp
[[view-xml-marshalling]]
=== XML Marshalling View
The `MarhsallingView` uses an XML `Marshaller` defined in the `org.springframework.oxm`
The `MarshallingView` uses an XML `Marshaller` defined in the `org.springframework.oxm`
package to render the response content as XML. The object to be marshalled can be set
explicitly using `MarhsallingView`'s `modelKey` bean property. Alternatively, the view
will iterate over all model properties and marshal only those types that are supported
will iterate over all model properties and marshal the first type that is supported
by the `Marshaller`. For more information on the functionality in the
`org.springframework.oxm` package refer to the chapter <<oxm,Marshalling XML using O/X
Mappers>>.
@@ -35268,8 +35271,7 @@ Mappers>>.
[[view-json-mapping]]
=== JSON Mapping View
The `MappingJackson2JsonView` (or `MappingJacksonJsonView` depending on the the Jackson
version you have) uses the Jackson library's `ObjectMapper` to render the response
The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` to render the response
content as JSON. By default, the entire contents of the model map (with the exception of
framework-specific classes) will be encoded as JSON. For cases where the contents of the
map need to be filtered, users may specify a specific set of model attributes to encode
@@ -35285,6 +35287,23 @@ serializers/deserializers need to be provided for specific types.
[[view-xml-mapping]]
=== XML Mapping View
The `MappingJackson2XmlView` uses the
https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML extension]'s `XmlMapper`
to render the response content as XML. If the model contains multiples entries, the
object to be serialized should be set explicitly using `MappingJackson2XmlView`'s
`modelKey` bean property. If the model contains a single entry, it will be serialized
automatically.
XML mapping can be customized as needed through the use of JAXB or Jackson's provided
annotations. When further control is needed, a custom `XmlMapper` can be injected
through the `ObjectMapper` property for cases where custom XML
serializers/deserializers need to be provided for specific types.
[[web-integration]]
== Integrating with other web frameworks
@@ -40459,7 +40478,7 @@ can be injected via constructor or bean properties. By default this converter su
[[rest-mapping-json-converter]]
===== MappingJackson2HttpMessageConverter (or MappingJacksonHttpMessageConverter with Jackson 1.x)
===== MappingJackson2HttpMessageConverter
An `HttpMessageConverter` implementation that can read and write JSON using Jackson's
`ObjectMapper`. JSON mapping can be customized as needed through the use of Jackson's
provided annotations. When further control is needed, a custom `ObjectMapper` can be
@@ -40468,6 +40487,17 @@ serializers/deserializers need to be provided for specific types. By default thi
converter supports ( `application/json`).
[[rest-mapping-xml-converter]]
===== MappingJackson2XmlHttpMessageConverter
An `HttpMessageConverter` implementation that can read and write XML using
https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's
`XmlMapper`. XML mapping can be customized as needed through the use of JAXB
or Jackson's provided annotations. When further control is needed, a custom `XmlMapper`
can be injected through the `ObjectMapper` property for cases where custom XML
serializers/deserializers need to be provided for specific types. By default this
converter supports ( `application/xml`).
[[rest-source-converter]]
===== SourceHttpMessageConverter
An `HttpMessageConverter` implementation that can read and write