diff --git a/build.gradle b/build.gradle index de08f29005..5350a7dce1 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ configure(allprojects) { project -> ext.tomcatVersion = "8.0.9" ext.xstreamVersion = "1.4.7" ext.protobufVersion = "2.5.0" + ext.woodstoxVersion = "4.1.6" ext.gradleScriptDir = "${rootProject.projectDir}/gradle" @@ -326,7 +327,7 @@ project("spring-core") { optional("log4j:log4j:1.2.17") testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") testCompile("xmlunit:xmlunit:1.5") - testCompile("org.codehaus.woodstox:wstx-asl:3.2.7") { + testCompile("org.codehaus.woodstox:woodstox-core-asl:${woodstoxVersion}") { exclude group: "stax", module: "stax-api" } } @@ -665,6 +666,7 @@ project("spring-web") { optional("org.apache.httpcomponents:httpclient:4.3.4") optional("org.apache.httpcomponents:httpasyncclient:4.0.1") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}") optional("com.google.code.gson:gson:${gsonVersion}") optional("com.rometools:rome:1.5.0") optional("org.eclipse.jetty:jetty-servlet:${jettyVersion}") { @@ -824,6 +826,7 @@ project("spring-webmvc") { exclude group: "xml-apis", module: "xml-apis" } optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}") optional("com.rometools:rome:1.5.0") optional("javax.el:javax.el-api:2.2.5") optional("org.apache.tiles:tiles-api:${tiles3Version}") diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java new file mode 100644 index 0000000000..56d3760c96 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -0,0 +1,315 @@ +/* + * 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.json; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for Jackson based and content type independent + * {@link HttpMessageConverter} implementations. + * + *

Compatible with Jackson 2.1 and higher. + * + * @author Arjen Poutsma + * @author Keith Donald + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 4.1 + */ +public abstract class AbstractJackson2HttpMessageConverter extends + AbstractHttpMessageConverter implements GenericHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + // Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference + private static final boolean jackson23Available = ClassUtils.hasMethod(ObjectMapper.class, + "canDeserialize", JavaType.class, AtomicReference.class); + + + protected ObjectMapper objectMapper; + + private Boolean prettyPrint; + + + protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) { + super(supportedMediaType); + this.objectMapper = objectMapper; + } + + protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) { + super(supportedMediaTypes); + this.objectMapper = objectMapper; + } + + /** + * Set the {@code ObjectMapper} for this view. + * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. + *

Setting a custom-configured {@code ObjectMapper} is one way to take further + * control of the JSON serialization process. For example, an extended + * {@link com.fasterxml.jackson.databind.ser.SerializerFactory} + * can be configured that provides custom serializers for specific types. + * The other option for refining the serialization process is to use Jackson's + * provided annotations on the types to be serialized, in which case a + * custom-configured ObjectMapper is unnecessary. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + /** + * Return the underlying {@code ObjectMapper} for this view. + */ + public ObjectMapper getObjectMapper() { + return this.objectMapper; + } + + /** + * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *

+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+	 * converter.setObjectMapper(mapper);
+	 * 
+ */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); + } + } + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return canRead(clazz, null, mediaType); + } + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + JavaType javaType = getJavaType(type, contextClass); + if (!jackson23Available || !logger.isWarnEnabled()) { + return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); + } + AtomicReference causeRef = new AtomicReference(); + if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) { + return true; + } + Throwable cause = causeRef.get(); + if (cause != null) { + String msg = "Failed to evaluate deserialization for type " + javaType; + if (logger.isDebugEnabled()) { + logger.warn(msg, cause); + } + else { + logger.warn(msg + ": " + cause); + } + } + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + if (!jackson23Available || !logger.isWarnEnabled()) { + return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType)); + } + AtomicReference causeRef = new AtomicReference(); + if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) { + return true; + } + Throwable cause = causeRef.get(); + if (cause != null) { + String msg = "Failed to evaluate serialization for type [" + clazz + "]"; + if (logger.isDebugEnabled()) { + logger.warn(msg, cause); + } + else { + logger.warn(msg + ": " + cause); + } + } + return false; + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canRead/Write instead + throw new UnsupportedOperationException(); + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + JavaType javaType = getJavaType(clazz, null); + return readJavaType(javaType, inputMessage); + } + + @Override + public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + JavaType javaType = getJavaType(type, contextClass); + return readJavaType(javaType, inputMessage); + } + + private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { + try { + return this.objectMapper.readValue(inputMessage.getBody(), javaType); + } + catch (IOException ex) { + throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); + } + } + + @Override + protected void writeInternal(Object object, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); + JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); + try { + writePrefix(generator, object); + Class serializationView = null; + Object value = object; + if (value instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) object; + value = container.getValue(); + serializationView = container.getSerializationView(); + } + if (serializationView != null) { + this.objectMapper.writerWithView(serializationView).writeValue(generator, value); + } + else { + this.objectMapper.writeValue(generator, value); + } + writeSuffix(generator, object); + generator.flush(); + + } + catch (JsonProcessingException ex) { + throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex); + } + } + + /** + * Write a prefix before the main content. + * @param generator the generator to use for writing content. + * @param object the object to write to the output message. + */ + protected void writePrefix(JsonGenerator generator, Object object) throws IOException { + + } + + /** + * Write a suffix after the main content. + * @param generator the generator to use for writing content. + * @param object the object to write to the output message. + */ + protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { + + } + + /** + * Return the Jackson {@link JavaType} for the specified type and context class. + *

The default implementation returns {@code typeFactory.constructType(type, contextClass)}, + * but this can be overridden in subclasses, to allow for custom generic collection handling. + * For instance: + *

+	 * protected JavaType getJavaType(Type type) {
+	 *   if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
+	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
+	 *   } else {
+	 *     return super.getJavaType(type);
+	 *   }
+	 * }
+	 * 
+ * @param type the type to return the java type for + * @param contextClass a context class for the target type, for example a class + * in which the target type appears in a method signature, can be {@code null} + * signature, can be {@code null} + * @return the java type + */ + protected JavaType getJavaType(Type type, Class contextClass) { + return this.objectMapper.getTypeFactory().constructType(type, contextClass); + } + + /** + * Determine the JSON encoding to use for the given content type. + * @param contentType the media type as requested by the caller + * @return the JSON encoding to use (never {@code null}) + */ + protected JsonEncoding getJsonEncoding(MediaType contentType) { + if (contentType != null && contentType.getCharSet() != null) { + Charset charset = contentType.getCharSet(); + for (JsonEncoding encoding : JsonEncoding.values()) { + if (charset.name().equals(encoding.getJavaName())) { + return encoding; + } + } + } + return JsonEncoding.UTF8; + } + + @Override + protected MediaType getDefaultContentType(Object object) throws IOException { + if (object instanceof MappingJacksonValue) { + object = ((MappingJacksonValue) object).getValue(); + } + return super.getDefaultContentType(object); + } + + @Override + protected Long getContentLength(Object object, MediaType contentType) throws IOException { + if (object instanceof MappingJacksonValue) { + object = ((MappingJacksonValue) object).getValue(); + } + return super.getContentLength(object, contentType); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java index 19abe082e6..3ef14ce92c 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java @@ -51,7 +51,7 @@ import org.springframework.util.ClassUtils; * to enable or disable Jackson features from within XML configuration. * *

Example usage with - * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}: + * {@link MappingJackson2HttpMessageConverter}: * *

  * <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
@@ -486,7 +486,8 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean getObjectType() {
-		return ObjectMapper.class;
+		Assert.notNull(this.objectMapper, "ObjectMapper must not be null");
+		return this.objectMapper.getClass();
 	}
 
 	@Override
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
index ec8f942171..98e588d215 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
@@ -17,27 +17,11 @@
 package org.springframework.http.converter.json;
 
 import java.io.IOException;
-import java.lang.reflect.Type;
-import java.nio.charset.Charset;
-import java.util.concurrent.atomic.AtomicReference;
 
-import com.fasterxml.jackson.core.JsonEncoding;
 import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
-import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
 
-import org.springframework.http.HttpInputMessage;
-import org.springframework.http.HttpOutputMessage;
 import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.GenericHttpMessageConverter;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
 
 /**
  * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that
@@ -54,58 +38,22 @@ import org.springframework.util.ClassUtils;
  * @author Keith Donald
  * @author Rossen Stoyanchev
  * @author Juergen Hoeller
+ * @author Sebastien Deleuze
  * @since 3.1.2
  */
-public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter
-		implements GenericHttpMessageConverter {
-
-	public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-	// Check for Jackson 2.3's overloaded canDeserialize/canSerialize variants with cause reference
-	private static final boolean jackson23Available =
-			ClassUtils.hasMethod(ObjectMapper.class, "canDeserialize", JavaType.class, AtomicReference.class);
-
-
-	private ObjectMapper objectMapper = new ObjectMapper();
+public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
 
 	private String jsonPrefix;
 
-	private Boolean prettyPrint;
-
 
 	/**
 	 * Construct a new {@code MappingJackson2HttpMessageConverter}.
 	 */
 	public MappingJackson2HttpMessageConverter() {
-		super(new MediaType("application", "json", DEFAULT_CHARSET),
+		super(new ObjectMapper(), new MediaType("application", "json", DEFAULT_CHARSET),
 				new MediaType("application", "*+json", DEFAULT_CHARSET));
 	}
 
-
-	/**
-	 * Set the {@code ObjectMapper} for this view.
-	 * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
-	 * 

Setting a custom-configured {@code ObjectMapper} is one way to take further - * control of the JSON serialization process. For example, an extended - * {@link com.fasterxml.jackson.databind.ser.SerializerFactory} - * can be configured that provides custom serializers for specific types. - * The other option for refining the serialization process is to use Jackson's - * provided annotations on the types to be serialized, in which case a - * custom-configured ObjectMapper is unnecessary. - */ - public void setObjectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - this.objectMapper = objectMapper; - configurePrettyPrint(); - } - - /** - * Return the underlying {@code ObjectMapper} for this view. - */ - public ObjectMapper getObjectMapper() { - return this.objectMapper; - } - /** * Specify a custom prefix to use for this view's JSON output. * Default is none. @@ -127,199 +75,29 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv this.jsonPrefix = (prefixJson ? "{} && " : null); } - /** - * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. - * This is a shortcut for setting up an {@code ObjectMapper} as follows: - *

-	 * ObjectMapper mapper = new ObjectMapper();
-	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
-	 * converter.setObjectMapper(mapper);
-	 * 
- */ - public void setPrettyPrint(boolean prettyPrint) { - this.prettyPrint = prettyPrint; - configurePrettyPrint(); - } - - private void configurePrettyPrint() { - if (this.prettyPrint != null) { - this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); - } - } - @Override - public boolean canRead(Class clazz, MediaType mediaType) { - return canRead(clazz, null, mediaType); - } - - @Override - public boolean canRead(Type type, Class contextClass, MediaType mediaType) { - JavaType javaType = getJavaType(type, contextClass); - if (!jackson23Available || !logger.isWarnEnabled()) { - return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); + protected void writePrefix(JsonGenerator generator, Object object) throws IOException { + if (this.jsonPrefix != null) { + generator.writeRaw(this.jsonPrefix); } - AtomicReference causeRef = new AtomicReference(); - if (this.objectMapper.canDeserialize(javaType, causeRef) && canRead(mediaType)) { - return true; - } - Throwable cause = causeRef.get(); - if (cause != null) { - String msg = "Failed to evaluate deserialization for type " + javaType; - if (logger.isDebugEnabled()) { - logger.warn(msg, cause); - } - else { - logger.warn(msg + ": " + cause); - } - } - return false; - } - - @Override - public boolean canWrite(Class clazz, MediaType mediaType) { - if (!jackson23Available || !logger.isWarnEnabled()) { - return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType)); - } - AtomicReference causeRef = new AtomicReference(); - if (this.objectMapper.canSerialize(clazz, causeRef) && canWrite(mediaType)) { - return true; - } - Throwable cause = causeRef.get(); - if (cause != null) { - String msg = "Failed to evaluate serialization for type [" + clazz + "]"; - if (logger.isDebugEnabled()) { - logger.warn(msg, cause); - } - else { - logger.warn(msg + ": " + cause); - } - } - return false; - } - - @Override - protected boolean supports(Class clazz) { - // should not be called, since we override canRead/Write instead - throw new UnsupportedOperationException(); - } - - @Override - protected Object readInternal(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - - JavaType javaType = getJavaType(clazz, null); - return readJavaType(javaType, inputMessage); - } - - @Override - public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - - JavaType javaType = getJavaType(type, contextClass); - return readJavaType(javaType, inputMessage); - } - - private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { - try { - return this.objectMapper.readValue(inputMessage.getBody(), javaType); - } - catch (IOException ex) { - throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); - } - } - - @Override - protected void writeInternal(Object object, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); - JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); - try { - if (this.jsonPrefix != null) { - generator.writeRaw(this.jsonPrefix); - } - Class serializationView = null; - String jsonpFunction = null; - if (object instanceof MappingJacksonValue) { - MappingJacksonValue container = (MappingJacksonValue) object; - object = container.getValue(); - serializationView = container.getSerializationView(); - jsonpFunction = container.getJsonpFunction(); - } - if (jsonpFunction != null) { - generator.writeRaw(jsonpFunction + "("); - } - if (serializationView != null) { - this.objectMapper.writerWithView(serializationView).writeValue(generator, object); - } - else { - this.objectMapper.writeValue(generator, object); - } - if (jsonpFunction != null) { - generator.writeRaw(");"); - generator.flush(); - } - } - catch (JsonProcessingException ex) { - throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); - } - } - - /** - * Return the Jackson {@link JavaType} for the specified type and context class. - *

The default implementation returns {@code typeFactory.constructType(type, contextClass)}, - * but this can be overridden in subclasses, to allow for custom generic collection handling. - * For instance: - *

-	 * protected JavaType getJavaType(Type type) {
-	 *   if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
-	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
-	 *   } else {
-	 *     return super.getJavaType(type);
-	 *   }
-	 * }
-	 * 
- * @param type the type to return the java type for - * @param contextClass a context class for the target type, for example a class - * in which the target type appears in a method signature, can be {@code null} - * signature, can be {@code null} - * @return the java type - */ - protected JavaType getJavaType(Type type, Class contextClass) { - return this.objectMapper.getTypeFactory().constructType(type, contextClass); - } - - /** - * Determine the JSON encoding to use for the given content type. - * @param contentType the media type as requested by the caller - * @return the JSON encoding to use (never {@code null}) - */ - protected JsonEncoding getJsonEncoding(MediaType contentType) { - if (contentType != null && contentType.getCharSet() != null) { - Charset charset = contentType.getCharSet(); - for (JsonEncoding encoding : JsonEncoding.values()) { - if (charset.name().equals(encoding.getJavaName())) { - return encoding; - } - } - } - return JsonEncoding.UTF8; - } - - @Override - protected MediaType getDefaultContentType(Object object) throws IOException { + String jsonpFunction = null; if (object instanceof MappingJacksonValue) { - object = ((MappingJacksonValue) object).getValue(); + jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction(); + } + if (jsonpFunction != null) { + generator.writeRaw(jsonpFunction + "("); } - return super.getDefaultContentType(object); } @Override - protected Long getContentLength(Object object, MediaType contentType) throws IOException { + protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { + String jsonpFunction = null; if (object instanceof MappingJacksonValue) { - object = ((MappingJacksonValue) object).getValue(); + jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction(); + } + if (jsonpFunction != null) { + generator.writeRaw(");"); } - return super.getContentLength(object, contentType); } } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java index c184ac310a..e421b99ec7 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValue.java @@ -18,9 +18,8 @@ package org.springframework.http.converter.json; /** * A simple holder for the POJO to serialize via - * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter - * MappingJackson2HttpMessageConverter} along with further serialization - * instructions to be passed in to the converter. + * {@link MappingJackson2HttpMessageConverter} along with further + * serialization instructions to be passed in to the converter. * *

On the server side this wrapper is added with a * {@code ResponseBodyInterceptor} after content negotiation selects the diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java new file mode 100644 index 0000000000..c2908bb40d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java @@ -0,0 +1,40 @@ +/* + * 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 com.fasterxml.jackson.dataformat.xml.XmlMapper; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that + * can read and write XML using Jackson 2.x extension component for + * reading and writing XML encoded data. + * + * @author Sebastien Deleuze + * @since 4.1 + */ +public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + public MappingJackson2XmlHttpMessageConverter() { + super(new XmlMapper(), + new MediaType("application", "xml", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET)); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java index eced2ab722..33361de491 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java @@ -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, JsonDeserializer> deserializers = new HashMap, JsonDeserializer>(); deserializers.put(Date.class, new DateDeserializer()); - factory.setObjectMapper(objectMapper); - JsonSerializer> serializer1 = new ClassSerializer(); JsonSerializer 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()); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java new file mode 100644 index 0000000000..a289f31363 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java @@ -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 = + "Foo4242.0FooBartrueAQI="; + 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("Foo")); + assertTrue(result.contains("42")); + assertTrue(result.contains("42.0")); + assertTrue(result.contains("FooBar")); + assertTrue(result.contains("true")); + assertTrue(result.contains("AQI=")); + 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 = "stringvalue"; + 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("with")); + assertThat(result, containsString("without")); + assertThat(result, not(containsString("with"))); + } + + 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; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index fbb81ad0b4..104a73b53b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -47,6 +47,7 @@ import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -159,6 +160,9 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + private static final boolean jackson2XmlPresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); @@ -401,7 +405,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { props.put("atom", MediaType.APPLICATION_ATOM_XML_VALUE); props.put("rss", "application/rss+xml"); } - if (jaxb2Present) { + if (jaxb2Present || jackson2XmlPresent) { props.put("xml", MediaType.APPLICATION_XML_VALUE); } if (jackson2Present || gsonPresent) { @@ -528,7 +532,10 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { messageConverters.add(createConverterDefinition(AtomFeedHttpMessageConverter.class, source)); messageConverters.add(createConverterDefinition(RssChannelHttpMessageConverter.class, source)); } - if (jaxb2Present) { + if(jackson2XmlPresent) { + messageConverters.add(createConverterDefinition(MappingJackson2XmlHttpMessageConverter.class, source)); + } + else if (jaxb2Present) { messageConverters.add(createConverterDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); } if (jackson2Present) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 05d48330de..d9d49ddff7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -48,6 +48,7 @@ import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.AntPathMatcher; import org.springframework.util.ClassUtils; @@ -171,6 +172,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebMvcConfigurationSupport.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebMvcConfigurationSupport.class.getClassLoader()); + private static final boolean jackson2XmlPresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", WebMvcConfigurationSupport.class.getClassLoader()); + private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", WebMvcConfigurationSupport.class.getClassLoader()); @@ -302,7 +306,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv map.put("atom", MediaType.APPLICATION_ATOM_XML); map.put("rss", MediaType.valueOf("application/rss+xml")); } - if (jaxb2Present) { + if (jaxb2Present || jackson2XmlPresent) { map.put("xml", MediaType.APPLICATION_XML); } if (jackson2Present || gsonPresent) { @@ -653,7 +657,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } - if (jaxb2Present) { + if(jackson2XmlPresent) { + messageConverters.add(new MappingJackson2XmlHttpMessageConverter()); + } + else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java index 765d93d05a..7dc43b4021 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java @@ -29,8 +29,8 @@ import javax.servlet.http.HttpServletRequest; /** * A convenient base class for a {@code ResponseBodyAdvice} to instruct the - * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter - * MappingJackson2HttpMessageConverter} to serialize with JSONP formatting. + * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter} + * to serialize with JSONP formatting. * *

Sub-classes must specify the query parameter name(s) to check for the name * of the JSONP callback function. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java index a37c743e2a..dd87024c59 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMappingJacksonResponseBodyAdvice.java @@ -27,8 +27,7 @@ import org.springframework.http.server.ServerHttpResponse; /** * A convenient base class for {@code ResponseBodyAdvice} implementations * that customize the response before JSON serialization with - * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter - * MappingJackson2HttpMessageConverter}. + * {@link MappingJackson2HttpMessageConverter}. * * @author Rossen Stoyanchev * @since 4.1 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java new file mode 100644 index 0000000000..9adf410643 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java @@ -0,0 +1,246 @@ +/* + * 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.json; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import org.springframework.http.converter.json.MappingJacksonValue; +import org.springframework.util.Assert; +import org.springframework.web.servlet.view.AbstractView; + +/** + * Abstract base class for Jackson based and content type independent + * {@link AbstractView} implementations. + * + *

Compatible with Jackson 2.1 and higher. + * + * @author Jeremy Grelle + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @since 4.1 + */ +public abstract class AbstractJackson2View extends AbstractView { + + private ObjectMapper objectMapper; + + private JsonEncoding encoding = JsonEncoding.UTF8; + + private Boolean prettyPrint; + + private boolean disableCaching = true; + + protected boolean updateContentLength = false; + + + protected AbstractJackson2View(ObjectMapper objectMapper, String contentType) { + this.objectMapper = objectMapper; + setContentType(contentType); + setExposePathVariables(false); + } + + /** + * Set the {@code ObjectMapper} for this view. + * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} will be used. + *

Setting a custom-configured {@code ObjectMapper} is one way to take further control of + * the JSON serialization process. The other option is to use Jackson's provided annotations + * on the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "'objectMapper' must not be null"); + this.objectMapper = objectMapper; + configurePrettyPrint(); + } + + /** + * Return the {@code ObjectMapper} for this view. + */ + public final ObjectMapper getObjectMapper() { + return this.objectMapper; + } + + /** + * Set the {@code JsonEncoding} for this view. + * By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used. + */ + public void setEncoding(JsonEncoding encoding) { + Assert.notNull(encoding, "'encoding' must not be null"); + this.encoding = encoding; + } + + /** + * Return the {@code JsonEncoding} for this view. + */ + public final JsonEncoding getEncoding() { + return this.encoding; + } + + /** + * Whether to use the default pretty printer when writing JSON. + * This is a shortcut for setting up an {@code ObjectMapper} as follows: + *

+	 * ObjectMapper mapper = new ObjectMapper();
+	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+	 * 
+ *

The default value is {@code false}. + */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + configurePrettyPrint(); + } + + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); + } + } + + /** + * Set the attribute in the model that should be rendered by this view. + * When set, all other model attributes will be ignored. + */ + public abstract void setModelKey(String modelKey); + + /** + * Disables caching of the generated JSON. + *

Default is {@code true}, which will prevent the client from caching the generated JSON. + */ + public void setDisableCaching(boolean disableCaching) { + this.disableCaching = disableCaching; + } + + /** + * Whether to update the 'Content-Length' header of the response. When set to + * {@code true}, the response is buffered in order to determine the content + * length and set the 'Content-Length' header of the response. + *

The default setting is {@code false}. + */ + public void setUpdateContentLength(boolean updateContentLength) { + this.updateContentLength = updateContentLength; + } + + @Override + protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { + setResponseContentType(request, response); + response.setCharacterEncoding(this.encoding.getJavaName()); + if (this.disableCaching) { + response.addHeader("Pragma", "no-cache"); + response.addHeader("Cache-Control", "no-cache, no-store, max-age=0"); + response.addDateHeader("Expires", 1L); + } + } + + @Override + protected void renderMergedOutputModel(Map model, HttpServletRequest request, + HttpServletResponse response) throws Exception { + + OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream()); + Object value = filterAndWrapModel(model, request); + + writeContent(stream, value); + if (this.updateContentLength) { + writeToResponse(response, (ByteArrayOutputStream) stream); + } + } + + /** + * Filter and optionally wrap the model in {@link MappingJacksonValue} container. + * @param model the model, as passed on to {@link #renderMergedOutputModel} + * @param request current HTTP request + * @return the wrapped or unwrapped value to be rendered + */ + protected Object filterAndWrapModel(Map model, HttpServletRequest request) { + Object value = filterModel(model); + Class serializationView = (Class) model.get(JsonView.class.getName()); + if (serializationView != null) { + MappingJacksonValue container = new MappingJacksonValue(value); + container.setSerializationView(serializationView); + value = container; + } + return value; + } + + /** + * Filter out undesired attributes from the given model. + * The return value can be either another {@link Map} or a single value object. + * @param model the model, as passed on to {@link #renderMergedOutputModel} + * @return the value to be rendered + */ + protected abstract Object filterModel(Map model); + + /** + * Write the actual JSON content to the stream. + * @param stream the output stream to use + * @param object the value to be rendered, as returned from {@link #filterModel} + * @throws IOException if writing failed + */ + protected void writeContent(OutputStream stream, Object object) + throws IOException { + + JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding); + + writePrefix(generator, object); + Class serializationView = null; + Object value = object; + + if (value instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) value; + value = container.getValue(); + serializationView = container.getSerializationView(); + } + if (serializationView != null) { + this.objectMapper.writerWithView(serializationView).writeValue(generator, value); + } + else { + this.objectMapper.writeValue(generator, value); + } + writeSuffix(generator, object); + generator.flush(); + } + + /** + * Write a prefix before the main content. + * @param generator the generator to use for writing content. + * @param object the object to write to the output message. + */ + protected void writePrefix(JsonGenerator generator, Object object) throws IOException { + + } + + /** + * Write a suffix after the main content. + * @param generator the generator to use for writing content. + * @param object the object to write to the output message. + */ + protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { + + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java index 557d26777e..3e51bc6dfc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java @@ -16,9 +16,7 @@ package org.springframework.web.servlet.view.json; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -29,18 +27,14 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.http.converter.json.MappingJacksonValue; -import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.web.servlet.View; -import org.springframework.web.servlet.view.AbstractView; /** * Spring MVC {@link View} that renders JSON content by serializing the model for the current request @@ -59,7 +53,7 @@ import org.springframework.web.servlet.view.AbstractView; * @author Sebastien Deleuze * @since 3.1.2 */ -public class MappingJackson2JsonView extends AbstractView { +public class MappingJackson2JsonView extends AbstractJackson2View { /** * Default content type: "application/json". @@ -72,23 +66,12 @@ public class MappingJackson2JsonView extends AbstractView { */ public static final String DEFAULT_JSONP_CONTENT_TYPE = "application/javascript"; - - private ObjectMapper objectMapper = new ObjectMapper(); - - private JsonEncoding encoding = JsonEncoding.UTF8; - private String jsonPrefix; - private Boolean prettyPrint; - private Set modelKeys; private boolean extractValueFromSingleKeyModel = false; - private boolean disableCaching = true; - - private boolean updateContentLength = false; - private Set jsonpParameterNames = new LinkedHashSet(Arrays.asList("jsonp", "callback")); @@ -96,45 +79,7 @@ public class MappingJackson2JsonView extends AbstractView { * Construct a new {@code MappingJackson2JsonView}, setting the content type to {@code application/json}. */ public MappingJackson2JsonView() { - setContentType(DEFAULT_CONTENT_TYPE); - setExposePathVariables(false); - } - - - /** - * Set the {@code ObjectMapper} for this view. - * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} will be used. - *

Setting a custom-configured {@code ObjectMapper} is one way to take further control of - * the JSON serialization process. The other option is to use Jackson's provided annotations - * on the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. - */ - public void setObjectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "'objectMapper' must not be null"); - this.objectMapper = objectMapper; - configurePrettyPrint(); - } - - /** - * Return the {@code ObjectMapper} for this view. - */ - public final ObjectMapper getObjectMapper() { - return this.objectMapper; - } - - /** - * Set the {@code JsonEncoding} for this view. - * By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used. - */ - public void setEncoding(JsonEncoding encoding) { - Assert.notNull(encoding, "'encoding' must not be null"); - this.encoding = encoding; - } - - /** - * Return the {@code JsonEncoding} for this view. - */ - public final JsonEncoding getEncoding() { - return this.encoding; + super(new ObjectMapper(), DEFAULT_CONTENT_TYPE); } /** @@ -160,29 +105,9 @@ public class MappingJackson2JsonView extends AbstractView { } /** - * Whether to use the default pretty printer when writing JSON. - * This is a shortcut for setting up an {@code ObjectMapper} as follows: - *

-	 * ObjectMapper mapper = new ObjectMapper();
-	 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
-	 * 
- *

The default value is {@code false}. - */ - public void setPrettyPrint(boolean prettyPrint) { - this.prettyPrint = prettyPrint; - configurePrettyPrint(); - } - - private void configurePrettyPrint() { - if (this.prettyPrint != null) { - this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); - } - } - - /** - * Set the attribute in the model that should be rendered by this view. - * When set, all other model attributes will be ignored. + * {@inheritDoc} */ + @Override public void setModelKey(String modelKey) { this.modelKeys = Collections.singleton(modelKey); } @@ -232,24 +157,6 @@ public class MappingJackson2JsonView extends AbstractView { this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel; } - /** - * Disables caching of the generated JSON. - *

Default is {@code true}, which will prevent the client from caching the generated JSON. - */ - public void setDisableCaching(boolean disableCaching) { - this.disableCaching = disableCaching; - } - - /** - * Whether to update the 'Content-Length' header of the response. When set to - * {@code true}, the response is buffered in order to determine the content - * length and set the 'Content-Length' header of the response. - *

The default setting is {@code false}. - */ - public void setUpdateContentLength(boolean updateContentLength) { - this.updateContentLength = updateContentLength; - } - /** * Set JSONP request parameter names. Each time a request has one of those * parameters, the resulting JSON will be wrapped into a function named as @@ -262,40 +169,6 @@ public class MappingJackson2JsonView extends AbstractView { this.jsonpParameterNames = jsonpParameterNames; } - - @Override - protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { - setResponseContentType(request, response); - response.setCharacterEncoding(this.encoding.getJavaName()); - if (this.disableCaching) { - response.addHeader("Pragma", "no-cache"); - response.addHeader("Cache-Control", "no-cache, no-store, max-age=0"); - response.addDateHeader("Expires", 1L); - } - } - - @Override - protected void renderMergedOutputModel(Map model, HttpServletRequest request, - HttpServletResponse response) throws Exception { - - OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream()); - - Class serializationView = (Class) model.get(JsonView.class.getName()); - String jsonpParameterValue = getJsonpParameterValue(request); - Object value = filterModel(model); - if (serializationView != null || jsonpParameterValue != null) { - MappingJacksonValue container = new MappingJacksonValue(value); - container.setSerializationView(serializationView); - container.setJsonpFunction(jsonpParameterValue); - value = container; - } - - writeContent(stream, value, this.jsonPrefix); - if (this.updateContentLength) { - writeToResponse(response, (ByteArrayOutputStream) stream); - } - } - private String getJsonpParameterValue(HttpServletRequest request) { if (this.jsonpParameterNames != null) { for (String name : this.jsonpParameterNames) { @@ -316,6 +189,7 @@ public class MappingJackson2JsonView extends AbstractView { * @param model the model, as passed on to {@link #renderMergedOutputModel} * @return the value to be rendered */ + @Override protected Object filterModel(Map model) { Map result = new HashMap(model.size()); Set modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet()); @@ -328,41 +202,44 @@ public class MappingJackson2JsonView extends AbstractView { return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result); } - /** - * Write the actual JSON content to the stream. - * @param stream the output stream to use - * @param value the value to be rendered, as returned from {@link #filterModel} - * @param jsonPrefix the prefix for this view's JSON output - * (as indicated through {@link #setJsonPrefix}/{@link #setPrefixJson}) - * @throws IOException if writing failed - */ - protected void writeContent(OutputStream stream, Object value, String jsonPrefix) - throws IOException { - - JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding); - if (jsonPrefix != null) { - generator.writeRaw(jsonPrefix); + @Override + protected Object filterAndWrapModel(Map model, HttpServletRequest request) { + Object value = super.filterAndWrapModel(model, request); + String jsonpParameterValue = getJsonpParameterValue(request); + if (jsonpParameterValue != null) { + if(value instanceof MappingJacksonValue) { + ((MappingJacksonValue) value).setJsonpFunction(jsonpParameterValue); + } else { + MappingJacksonValue container = new MappingJacksonValue(value); + container.setJsonpFunction(jsonpParameterValue); + value = container; + } + } + return value; + } + + @Override + protected void writePrefix(JsonGenerator generator, Object object) throws IOException { + if (this.jsonPrefix != null) { + generator.writeRaw(this.jsonPrefix); } - Class serializationView = null; String jsonpFunction = null; - if (value instanceof MappingJacksonValue) { - MappingJacksonValue container = (MappingJacksonValue) value; - value = container.getValue(); - serializationView = container.getSerializationView(); - jsonpFunction = container.getJsonpFunction(); + if (object instanceof MappingJacksonValue) { + jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction(); } if (jsonpFunction != null) { generator.writeRaw(jsonpFunction + "(" ); } - if (serializationView != null) { - this.objectMapper.writerWithView(serializationView).writeValue(generator, value); - } - else { - this.objectMapper.writeValue(generator, value); + } + + @Override + protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { + String jsonpFunction = null; + if (object instanceof MappingJacksonValue) { + jsonpFunction = ((MappingJacksonValue)object).getJsonpFunction(); } if (jsonpFunction != null) { generator.writeRaw(");"); - generator.flush(); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java new file mode 100644 index 0000000000..96cb9f71e9 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java @@ -0,0 +1,90 @@ +/* + * 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.util.Map; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +import org.springframework.validation.BindingResult; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.json.AbstractJackson2View; + +/** + * Spring MVC {@link View} that renders XML content by serializing the model for the current request + * using Jackson 2's {@link XmlMapper}. + * + *

The Object to be serialized is supplied as a parameter in the model. The first serializable + * entry is used. Users can either specify a specific entry in the model via the + * {@link #setModelKey(String) sourceKey} property. + * + *

Compatible with Jackson 2.1 and higher. + * + * @author Sebastien Deleuze + * @since 4.1 + */ +public class MappingJackson2XmlView extends AbstractJackson2View { + + public static final String DEFAULT_CONTENT_TYPE = "application/xml"; + + + private String modelKey; + + + public MappingJackson2XmlView() { + super(new XmlMapper(), DEFAULT_CONTENT_TYPE); + } + + /** + * {@inheritDoc} + */ + @Override + public void setModelKey(String modelKey) { + this.modelKey = modelKey; + } + + /** + * Filter out undesired attributes from the given model. + * The return value can be either another {@link Map} or a single value object. + * @param model the model, as passed on to {@link #renderMergedOutputModel} + * @return the value to be rendered + */ + @Override + protected Object filterModel(Map model) { + Object value = null; + if (this.modelKey != null) { + value = model.get(this.modelKey); + if (value == null) { + throw new IllegalStateException( + "Model contains no object with key [" + this.modelKey + "]"); + } + } else { + for (Map.Entry entry : model.entrySet()) { + if (!(entry.getValue() instanceof BindingResult) && + !entry.getKey().equals(JsonView.class.getName())) { + if(value != null) { + throw new IllegalStateException("Model contains more than one object to render, only one is supported"); + } + value = entry.getValue(); + } + } + } + return value; + } + +} \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java new file mode 100644 index 0000000000..24d8406b64 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlViewTests.java @@ -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 model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + model.put("foo", bean); + + view.render(model, request, response); + + assertTrue(response.getContentAsString().length() > 0); + assertTrue(response.getContentAsString().contains("custom")); + + 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 model = new HashMap(); + model.put("foo", bean); + + view.render(model, request, response); + + String result = response.getContentAsString(); + assertTrue(result.length() > 0); + assertTrue(result.contains("custom")); + + validateResult(); + } + + @Test + public void renderOnlySpecifiedModelKey() throws Exception { + + view.setModelKey("bar"); + Map model = new HashMap(); + 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 model = new TreeMap(); + 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 model = new HashMap(); + 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 { + + @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 createSerializer(SerializerProvider prov, JavaType type) throws JsonMappingException { + if (type.getRawClass() == TestBeanSimple.class) { + return new TestBeanSimpleSerializer(); + } + else { + return super.createSerializer(prov, type); + } + } + } + +} diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 6f87b61d51..4192d79f03 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -31616,7 +31616,7 @@ response. However, it stops short of writing any error content to the body of th response while your application may need to add developer-friendly content to every error response for example when providing a REST API. You can prepare a `ModelAndView` and render error content through view resolution -- i.e. by configuring a -`ContentNegotiatingViewResolver`, `MappingJacksonJsonView`, and so on. However, you may +`ContentNegotiatingViewResolver`, `MappingJackson2JsonView`, and so on. However, you may prefer to use `@ExceptionHandler` methods instead. If you prefer to write error content via `@ExceptionHandler` methods you can extend @@ -32192,9 +32192,12 @@ This is the complete list of HttpMessageConverters set up by mvc:annotation-driv .. `FormHttpMessageConverter` converts form data to/from a `MultiValueMap`. .. `Jaxb2RootElementHttpMessageConverter` converts Java objects to/from XML -- added if -JAXB2 is present on the classpath. -.. `MappingJackson2HttpMessageConverter` (or `MappingJacksonHttpMessageConverter`) -converts to/from JSON -- added if Jackson 2 (or Jackson) is present on the classpath. +JAXB2 is present and Jackson 2 XML extension is not present on the classpath. +.. `MappingJackson2HttpMessageConverter` converts to/from JSON -- added if Jackson 2 +is present on the classpath. +.. `MappingJackson2XmlHttpMessageConverter` converts to/from XML -- added if +https://github.com/FasterXML/jackson-dataformat-xml[Jackson 2 XML extension] is present +on the classpath. .. `AtomFeedHttpMessageConverter` converts Atom feeds -- added if Rome is present on the classpath. .. `RssChannelHttpMessageConverter` converts RSS feeds -- added if Rome is present on @@ -35255,10 +35258,10 @@ https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-sp [[view-xml-marshalling]] === XML Marshalling View -The `MarhsallingView` uses an XML `Marshaller` defined in the `org.springframework.oxm` +The `MarshallingView` uses an XML `Marshaller` defined in the `org.springframework.oxm` package to render the response content as XML. The object to be marshalled can be set explicitly using `MarhsallingView`'s `modelKey` bean property. Alternatively, the view -will iterate over all model properties and marshal only those types that are supported +will iterate over all model properties and marshal the first type that is supported by the `Marshaller`. For more information on the functionality in the `org.springframework.oxm` package refer to the chapter <>. @@ -35268,8 +35271,7 @@ Mappers>>. [[view-json-mapping]] === JSON Mapping View -The `MappingJackson2JsonView` (or `MappingJacksonJsonView` depending on the the Jackson -version you have) uses the Jackson library's `ObjectMapper` to render the response +The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` to render the response content as JSON. By default, the entire contents of the model map (with the exception of framework-specific classes) will be encoded as JSON. For cases where the contents of the map need to be filtered, users may specify a specific set of model attributes to encode @@ -35285,6 +35287,23 @@ serializers/deserializers need to be provided for specific types. +[[view-xml-mapping]] +=== XML Mapping View +The `MappingJackson2XmlView` uses the +https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML extension]'s `XmlMapper` +to render the response content as XML. If the model contains multiples entries, the +object to be serialized should be set explicitly using `MappingJackson2XmlView`'s +`modelKey` bean property. If the model contains a single entry, it will be serialized +automatically. + +XML mapping can be customized as needed through the use of JAXB or Jackson's provided +annotations. When further control is needed, a custom `XmlMapper` can be injected +through the `ObjectMapper` property for cases where custom XML +serializers/deserializers need to be provided for specific types. + + + + [[web-integration]] == Integrating with other web frameworks @@ -40459,7 +40478,7 @@ can be injected via constructor or bean properties. By default this converter su [[rest-mapping-json-converter]] -===== MappingJackson2HttpMessageConverter (or MappingJacksonHttpMessageConverter with Jackson 1.x) +===== MappingJackson2HttpMessageConverter An `HttpMessageConverter` implementation that can read and write JSON using Jackson's `ObjectMapper`. JSON mapping can be customized as needed through the use of Jackson's provided annotations. When further control is needed, a custom `ObjectMapper` can be @@ -40468,6 +40487,17 @@ serializers/deserializers need to be provided for specific types. By default thi converter supports ( `application/json`). +[[rest-mapping-xml-converter]] +===== MappingJackson2XmlHttpMessageConverter +An `HttpMessageConverter` implementation that can read and write XML using +https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's +`XmlMapper`. XML mapping can be customized as needed through the use of JAXB +or Jackson's provided annotations. When further control is needed, a custom `XmlMapper` +can be injected through the `ObjectMapper` property for cases where custom XML +serializers/deserializers need to be provided for specific types. By default this +converter supports ( `application/xml`). + + [[rest-source-converter]] ===== SourceHttpMessageConverter An `HttpMessageConverter` implementation that can read and write