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:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user