From f57bc1aaaafa0ccb94cfef57e34cf8aa099ade6f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Feb 2013 22:25:26 +0100 Subject: [PATCH] MappingJackson(2)JsonView allows subclasses to access the ObjectMapper and to override content writing Issue: SPR-7619 --- .../view/json/MappingJackson2JsonView.java | 101 ++++++++++------- .../view/json/MappingJacksonJsonView.java | 104 +++++++++++------- 2 files changed, 126 insertions(+), 79 deletions(-) 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 7510ab304e..29c670a138 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -17,6 +17,7 @@ package org.springframework.web.servlet.view.json; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.HashMap; @@ -27,7 +28,6 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -48,13 +48,15 @@ import org.springframework.web.servlet.view.AbstractView; * @author Jeremy Grelle * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1.2 * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter */ public class MappingJackson2JsonView extends AbstractView { /** - * Default content type. Overridable as bean property. + * Default content type: "application/json". + * Overridable through {@link #setContentType}. */ public static final String DEFAULT_CONTENT_TYPE = "application/json"; @@ -75,8 +77,9 @@ public class MappingJackson2JsonView extends AbstractView { private boolean updateContentLength = false; + /** - * Construct a new {@code JacksonJsonView}, setting the content type to {@code application/json}. + * Construct a new {@code MappingJackson2JsonView}, setting the content type to {@code application/json}. */ public MappingJackson2JsonView() { setContentType(DEFAULT_CONTENT_TYPE); @@ -85,13 +88,11 @@ public class MappingJackson2JsonView extends AbstractView { /** - * Sets 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 {@code 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. + * 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"); @@ -99,14 +100,15 @@ public class MappingJackson2JsonView extends AbstractView { configurePrettyPrint(); } - private void configurePrettyPrint() { - if (this.prettyPrint != null) { - this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); - } + /** + * Return the {@code ObjectMapper} for this view. + */ + public final ObjectMapper getObjectMapper() { + return this.objectMapper; } /** - * Set the {@code JsonEncoding} for this converter. + * Set the {@code JsonEncoding} for this view. * By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used. */ public void setEncoding(JsonEncoding encoding) { @@ -114,9 +116,16 @@ public class MappingJackson2JsonView extends AbstractView { this.encoding = encoding; } + /** + * Return the {@code JsonEncoding} for this view. + */ + public final JsonEncoding getEncoding() { + return this.encoding; + } + /** * Indicates whether the JSON output by this view should be prefixed with "{} && ". - * Default is false. + * Default is {@code false}. *

Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. * This prefix does not affect the evaluation of JSON, but if JSON validation is performed @@ -127,12 +136,11 @@ public class MappingJackson2JsonView extends AbstractView { } /** - * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * 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);
-	 * converter.setObjectMapper(mapper);
 	 * 
*

The default value is {@code false}. */ @@ -141,6 +149,12 @@ public class MappingJackson2JsonView extends AbstractView { 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. @@ -160,7 +174,7 @@ public class MappingJackson2JsonView extends AbstractView { /** * Return the attributes in the model that should be rendered by this view. */ - public Set getModelKeys() { + public final Set getModelKeys() { return this.modelKeys; } @@ -179,7 +193,7 @@ public class MappingJackson2JsonView extends AbstractView { * @deprecated use {@link #getModelKeys()} instead */ @Deprecated - public Set getRenderedAttributes() { + public final Set getRenderedAttributes() { return this.modelKeys; } @@ -212,6 +226,7 @@ public class MappingJackson2JsonView extends AbstractView { this.updateContentLength = updateContentLength; } + @Override protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { setResponseContentType(request, response); @@ -227,34 +242,21 @@ public class MappingJackson2JsonView extends AbstractView { protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { - OutputStream stream = this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream(); - + OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream()); Object value = filterModel(model); - JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(stream, this.encoding); - - // A workaround for JsonGenerators not applying serialization features - // https://github.com/FasterXML/jackson-databind/issues/12 - if (this.objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { - generator.useDefaultPrettyPrinter(); - } - - if (this.prefixJson) { - generator.writeRaw("{} && "); - } - this.objectMapper.writeValue(generator, value); - + writeContent(stream, value, this.prefixJson); if (this.updateContentLength) { writeToResponse(response, (ByteArrayOutputStream) stream); } } /** - * Filters out undesired attributes from the given model. + * Filter out undesired attributes from the given model. * The return value can be either another {@link Map} or a single value object. *

The default implementation removes {@link BindingResult} instances and entries * not included in the {@link #setRenderedAttributes renderedAttributes} property. * @param model the model, as passed on to {@link #renderMergedOutputModel} - * @return the object to be rendered + * @return the value to be rendered */ protected Object filterModel(Map model) { Map result = new HashMap(model.size()); @@ -267,4 +269,27 @@ 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 prefixJson whether the JSON output by this view should be prefixed + * with "{} && " (as indicated through {@link #setPrefixJson}) + * @throws IOException if writing failed + */ + protected void writeContent(OutputStream stream, Object value, boolean prefixJson) throws IOException { + JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(stream, this.encoding); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + generator.useDefaultPrettyPrinter(); + } + + if (prefixJson) { + generator.writeRaw("{} && "); + } + this.objectMapper.writeValue(generator, value); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java index 5b97da49a8..ef0ad59d99 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -17,12 +17,12 @@ package org.springframework.web.servlet.view.json; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -30,15 +30,13 @@ import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.SerializationConfig; -import org.codehaus.jackson.map.SerializerFactory; + import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.validation.BindingResult; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.AbstractView; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; - /** * Spring MVC {@link View} that renders JSON content by serializing the model for the current request * using Jackson's {@link ObjectMapper}. @@ -50,13 +48,15 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; * @author Jeremy Grelle * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.0 * @see org.springframework.http.converter.json.MappingJacksonHttpMessageConverter */ public class MappingJacksonJsonView extends AbstractView { /** - * Default content type. Overridable as bean property. + * Default content type: "application/json". + * Overridable through {@link #setContentType}. */ public static final String DEFAULT_CONTENT_TYPE = "application/json"; @@ -77,8 +77,9 @@ public class MappingJacksonJsonView extends AbstractView { private boolean updateContentLength = false; + /** - * Construct a new {@code JacksonJsonView}, setting the content type to {@code application/json}. + * Construct a new {@code MappingJacksonJsonView}, setting the content type to {@code application/json}. */ public MappingJacksonJsonView() { setContentType(DEFAULT_CONTENT_TYPE); @@ -87,13 +88,11 @@ public class MappingJacksonJsonView extends AbstractView { /** - * Sets 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 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. + * 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"); @@ -101,14 +100,15 @@ public class MappingJacksonJsonView extends AbstractView { configurePrettyPrint(); } - private void configurePrettyPrint() { - if (this.prettyPrint != null) { - this.objectMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, this.prettyPrint); - } + /** + * Return the {@code ObjectMapper} for this view. + */ + public final ObjectMapper getObjectMapper() { + return this.objectMapper; } /** - * Set the {@code JsonEncoding} for this converter. + * Set the {@code JsonEncoding} for this view. * By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used. */ public void setEncoding(JsonEncoding encoding) { @@ -116,9 +116,16 @@ public class MappingJacksonJsonView extends AbstractView { this.encoding = encoding; } + /** + * Return the {@code JsonEncoding} for this view. + */ + public final JsonEncoding getEncoding() { + return this.encoding; + } + /** * Indicates whether the JSON output by this view should be prefixed with "{} && ". - * Default is false. + * Default is {@code false}. *

Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. * This prefix does not affect the evaluation of JSON, but if JSON validation is performed @@ -129,12 +136,11 @@ public class MappingJacksonJsonView extends AbstractView { } /** - * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. + * 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(SerializationConfig.Feature.INDENT_OUTPUT, true);
-	 * converter.setObjectMapper(mapper);
 	 * 
*

The default value is {@code false}. */ @@ -143,6 +149,12 @@ public class MappingJacksonJsonView extends AbstractView { configurePrettyPrint(); } + private void configurePrettyPrint() { + if (this.prettyPrint != null) { + this.objectMapper.configure(SerializationConfig.Feature.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. @@ -162,7 +174,7 @@ public class MappingJacksonJsonView extends AbstractView { /** * Return the attributes in the model that should be rendered by this view. */ - public Set getModelKeys() { + public final Set getModelKeys() { return this.modelKeys; } @@ -181,7 +193,7 @@ public class MappingJacksonJsonView extends AbstractView { * @deprecated use {@link #getModelKeys()} instead */ @Deprecated - public Set getRenderedAttributes() { + public final Set getRenderedAttributes() { return this.modelKeys; } @@ -230,34 +242,21 @@ public class MappingJacksonJsonView extends AbstractView { protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { - OutputStream stream = this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream(); - + OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream()); Object value = filterModel(model); - JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(stream, this.encoding); - - // A workaround for JsonGenerators not applying serialization features - // https://github.com/FasterXML/jackson-databind/issues/12 - if (this.objectMapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) { - generator.useDefaultPrettyPrinter(); - } - - if (this.prefixJson) { - generator.writeRaw("{} && "); - } - this.objectMapper.writeValue(generator, value); - + writeContent(stream, value, this.prefixJson); if (this.updateContentLength) { writeToResponse(response, (ByteArrayOutputStream) stream); } } /** - * Filters out undesired attributes from the given model. + * Filter out undesired attributes from the given model. * The return value can be either another {@link Map} or a single value object. *

The default implementation removes {@link BindingResult} instances and entries * not included in the {@link #setRenderedAttributes renderedAttributes} property. * @param model the model, as passed on to {@link #renderMergedOutputModel} - * @return the object to be rendered + * @return the value to be rendered */ protected Object filterModel(Map model) { Map result = new HashMap(model.size()); @@ -270,4 +269,27 @@ public class MappingJacksonJsonView 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 prefixJson whether the JSON output by this view should be prefixed + * with "{} && " (as indicated through {@link #setPrefixJson}) + * @throws IOException if writing failed + */ + protected void writeContent(OutputStream stream, Object value, boolean prefixJson) throws IOException { + JsonGenerator generator = this.objectMapper.getJsonFactory().createJsonGenerator(stream, this.encoding); + + // A workaround for JsonGenerators not applying serialization features + // https://github.com/FasterXML/jackson-databind/issues/12 + if (this.objectMapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) { + generator.useDefaultPrettyPrinter(); + } + + if (prefixJson) { + generator.writeRaw("{} && "); + } + this.objectMapper.writeValue(generator, value); + } + }