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

@@ -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;
}
}
}