Skip to content

Commit 6665634

Browse files
committed
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
1 parent 92bd240 commit 6665634

17 files changed

+1421
-431
lines changed

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ configure(allprojects) { project ->
4848
ext.tomcatVersion = "8.0.9"
4949
ext.xstreamVersion = "1.4.7"
5050
ext.protobufVersion = "2.5.0"
51+
ext.woodstoxVersion = "4.1.6"
5152

5253
ext.gradleScriptDir = "${rootProject.projectDir}/gradle"
5354

@@ -326,7 +327,7 @@ project("spring-core") {
326327
optional("log4j:log4j:1.2.17")
327328
testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}")
328329
testCompile("xmlunit:xmlunit:1.5")
329-
testCompile("org.codehaus.woodstox:wstx-asl:3.2.7") {
330+
testCompile("org.codehaus.woodstox:woodstox-core-asl:${woodstoxVersion}") {
330331
exclude group: "stax", module: "stax-api"
331332
}
332333
}
@@ -665,6 +666,7 @@ project("spring-web") {
665666
optional("org.apache.httpcomponents:httpclient:4.3.4")
666667
optional("org.apache.httpcomponents:httpasyncclient:4.0.1")
667668
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
669+
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}")
668670
optional("com.google.code.gson:gson:${gsonVersion}")
669671
optional("com.rometools:rome:1.5.0")
670672
optional("org.eclipse.jetty:jetty-servlet:${jettyVersion}") {
@@ -824,6 +826,7 @@ project("spring-webmvc") {
824826
exclude group: "xml-apis", module: "xml-apis"
825827
}
826828
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
829+
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}")
827830
optional("com.rometools:rome:1.5.0")
828831
optional("javax.el:javax.el-api:2.2.5")
829832
optional("org.apache.tiles:tiles-api:${tiles3Version}")
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter.json;
18+
19+
import java.io.IOException;
20+
import java.lang.reflect.Type;
21+
import java.nio.charset.Charset;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
24+
import com.fasterxml.jackson.core.JsonEncoding;
25+
import com.fasterxml.jackson.core.JsonGenerator;
26+
import com.fasterxml.jackson.core.JsonProcessingException;
27+
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
28+
import com.fasterxml.jackson.databind.JavaType;
29+
import com.fasterxml.jackson.databind.ObjectMapper;
30+
import com.fasterxml.jackson.databind.SerializationFeature;
31+
32+
import org.springframework.http.HttpInputMessage;
33+
import org.springframework.http.HttpOutputMessage;
34+
import org.springframework.http.MediaType;
35+
import org.springframework.http.converter.AbstractHttpMessageConverter;
36+
import org.springframework.http.converter.GenericHttpMessageConverter;
37+
import org.springframework.http.converter.HttpMessageConverter;
38+
import org.springframework.http.converter.HttpMessageNotReadableException;
39+
import org.springframework.http.converter.HttpMessageNotWritableException;
40+
import org.springframework.util.Assert;
41+
import org.springframework.util.ClassUtils;
42+
43+
/**
44+
* Abstract base class for Jackson based and content type independent
45+
* {@link HttpMessageConverter} implementations.
46+
*
47+
* <p>Compatible with Jackson 2.1 and higher.
48+
*
49+
* @author Arjen Poutsma
50+
* @author Keith Donald
51+
* @author Rossen Stoyanchev
52+
* @author Juergen Hoeller
53+
* @author Sebastien Deleuze
54+
* @since 4.1
55+
*/
56+
public abstract class AbstractJackson2HttpMessageConverter extends
57+
AbstractHttpMessageConverter<Object> implements GenericHttpMessageConverter<Object> {
58+
59+
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
60+
61+
// Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference
62+
private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class,
63+
"canDeserialize", JavaType.class, AtomicReference.class);
64+
65+
66+
protected ObjectMapper objectMapper;
67+
68+
private Boolean prettyPrint;
69+
70+
71+
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
72+
this.objectMapper = objectMapper;
73+
}
74+
75+
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
76+
super(supportedMediaType);
77+
this.objectMapper = objectMapper;
78+
}
79+
80+
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
81+
super(supportedMediaTypes);
82+
this.objectMapper = objectMapper;
83+
}
84+
85+
/**
86+
* Set the {@code ObjectMapper} for this view.
87+
* If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
88+
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further
89+
* control of the JSON serialization process. For example, an extended
90+
* {@link com.fasterxml.jackson.databind.ser.SerializerFactory}
91+
* can be configured that provides custom serializers for specific types.
92+
* The other option for refining the serialization process is to use Jackson's
93+
* provided annotations on the types to be serialized, in which case a
94+
* custom-configured ObjectMapper is unnecessary.
95+
*/
96+
public void setObjectMapper(ObjectMapper objectMapper) {
97+
Assert.notNull(objectMapper, "ObjectMapper must not be null");
98+
this.objectMapper = objectMapper;
99+
configurePrettyPrint();
100+
}
101+
102+
/**
103+
* Return the underlying {@code ObjectMapper} for this view.
104+
*/
105+
public ObjectMapper getObjectMapper() {
106+
return this.objectMapper;
107+
}
108+
109+
/**
110+
* Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
111+
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
112+
* <pre class="code">
113+
* ObjectMapper mapper = new ObjectMapper();
114+
* mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
115+
* converter.setObjectMapper(mapper);
116+
* </pre>
117+
*/
118+
public void setPrettyPrint(boolean prettyPrint) {
119+
this.prettyPrint = prettyPrint;
120+
configurePrettyPrint();
121+
}
122+
123+
private void configurePrettyPrint() {
124+
if (this.prettyPrint != null) {
125+
this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
126+
}
127+
}
128+
129+
@Override
130+
public boolean canRead(Class<?> clazz, MediaType mediaType) {
131+
return canRead(clazz, null, mediaType);
132+
}
133+
134+
@Override
135+
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
136+
JavaType javaType = getJavaType(type, contextClass);
137+
if (!jackson23Available || !logger.isWarnEnabled()) {
138+
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
139+
}
140+
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
141+
if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) {
142+
return true;
143+
}
144+
Throwable cause = causeRef.get();
145+
if (cause != null) {
146+
String msg = "Failed to evaluate deserialization for type " + javaType;
147+
if (logger.isDebugEnabled()) {
148+
logger.warn(msg, cause);
149+
}
150+
else {
151+
logger.warn(msg + ": " + cause);
152+
}
153+
}
154+
return false;
155+
}
156+
157+
@Override
158+
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
159+
if (!jackson23Available || !logger.isWarnEnabled()) {
160+
return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
161+
}
162+
AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
163+
if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) {
164+
return true;
165+
}
166+
Throwable cause = causeRef.get();
167+
if (cause != null) {
168+
String msg = "Failed to evaluate serialization for type [" + clazz + "]";
169+
if (logger.isDebugEnabled()) {
170+
logger.warn(msg, cause);
171+
}
172+
else {
173+
logger.warn(msg + ": " + cause);
174+
}
175+
}
176+
return false;
177+
}
178+
179+
@Override
180+
protected boolean supports(Class<?> clazz) {
181+
// should not be called, since we override canRead/Write instead
182+
throw new UnsupportedOperationException();
183+
}
184+
185+
@Override
186+
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
187+
throws IOException, HttpMessageNotReadableException {
188+
189+
JavaType javaType = getJavaType(clazz, null);
190+
return readJavaType(javaType, inputMessage);
191+
}
192+
193+
@Override
194+
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
195+
throws IOException, HttpMessageNotReadableException {
196+
197+
JavaType javaType = getJavaType(type, contextClass);
198+
return readJavaType(javaType, inputMessage);
199+
}
200+
201+
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
202+
try {
203+
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
204+
}
205+
catch (IOException ex) {
206+
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
207+
}
208+
}
209+
210+
@Override
211+
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
212+
throws IOException, HttpMessageNotWritableException {
213+
214+
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
215+
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
216+
try {
217+
writePrefix(generator, object);
218+
Class<?> serializationView = null;
219+
Object value = object;
220+
if (value instanceof MappingJacksonValue) {
221+
MappingJacksonValue container = (MappingJacksonValue) object;
222+
value = container.getValue();
223+
serializationView = container.getSerializationView();
224+
}
225+
if (serializationView != null) {
226+
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
227+
}
228+
else {
229+
this.objectMapper.writeValue(generator, value);
230+
}
231+
writeSuffix(generator, object);
232+
generator.flush();
233+
234+
}
235+
catch (JsonProcessingException ex) {
236+
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
237+
}
238+
}
239+
240+
/**
241+
* Write a prefix before the main content.
242+
* @param generator the generator to use for writing content.
243+
* @param object the object to write to the output message.
244+
*/
245+
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
246+
247+
}
248+
249+
/**
250+
* Write a suffix after the main content.
251+
* @param generator the generator to use for writing content.
252+
* @param object the object to write to the output message.
253+
*/
254+
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
255+
256+
}
257+
258+
/**
259+
* Return the Jackson {@link JavaType} for the specified type and context class.
260+
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
261+
* but this can be overridden in subclasses, to allow for custom generic collection handling.
262+
* For instance:
263+
* <pre class="code">
264+
* protected JavaType getJavaType(Type type) {
265+
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
266+
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
267+
* } else {
268+
* return super.getJavaType(type);
269+
* }
270+
* }
271+
* </pre>
272+
* @param type the type to return the java type for
273+
* @param contextClass a context class for the target type, for example a class
274+
* in which the target type appears in a method signature, can be {@code null}
275+
* signature, can be {@code null}
276+
* @return the java type
277+
*/
278+
protected JavaType getJavaType(Type type, Class<?> contextClass) {
279+
return this.objectMapper.getTypeFactory().constructType(type, contextClass);
280+
}
281+
282+
/**
283+
* Determine the JSON encoding to use for the given content type.
284+
* @param contentType the media type as requested by the caller
285+
* @return the JSON encoding to use (never {@code null})
286+
*/
287+
protected JsonEncoding getJsonEncoding(MediaType contentType) {
288+
if (contentType != null && contentType.getCharSet() != null) {
289+
Charset charset = contentType.getCharSet();
290+
for (JsonEncoding encoding : JsonEncoding.values()) {
291+
if (charset.name().equals(encoding.getJavaName())) {
292+
return encoding;
293+
}
294+
}
295+
}
296+
return JsonEncoding.UTF8;
297+
}
298+
299+
@Override
300+
protected MediaType getDefaultContentType(Object object) throws IOException {
301+
if (object instanceof MappingJacksonValue) {
302+
object = ((MappingJacksonValue) object).getValue();
303+
}
304+
return super.getDefaultContentType(object);
305+
}
306+
307+
@Override
308+
protected Long getContentLength(Object object, MediaType contentType) throws IOException {
309+
if (object instanceof MappingJacksonValue) {
310+
object = ((MappingJacksonValue) object).getValue();
311+
}
312+
return super.getContentLength(object, contentType);
313+
}
314+
315+
}

spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
* to enable or disable Jackson features from within XML configuration.
5252
*
5353
* <p>Example usage with
54-
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}:
54+
* {@link MappingJackson2HttpMessageConverter}:
5555
*
5656
* <pre class="code">
5757
* &lt;bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
@@ -486,7 +486,8 @@ public ObjectMapper getObject() {
486486

487487
@Override
488488
public Class<?> getObjectType() {
489-
return ObjectMapper.class;
489+
Assert.notNull(this.objectMapper, "ObjectMapper must not be null");
490+
return this.objectMapper.getClass();
490491
}
491492

492493
@Override

0 commit comments

Comments
 (0)