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

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