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