1 /*
2  * Copyright 2002-2012 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.web.servlet.view.json;
18 
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Set;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
25 
26 import com.fasterxml.jackson.core.JsonEncoding;
27 import com.fasterxml.jackson.core.JsonGenerator;
28 import com.fasterxml.jackson.databind.ObjectMapper;
29 
30 import org.springframework.util.Assert;
31 import org.springframework.util.CollectionUtils;
32 import org.springframework.validation.BindingResult;
33 import org.springframework.web.servlet.View;
34 import org.springframework.web.servlet.view.AbstractView;
35 
36 /**
37  * Spring MVC {@link View} that renders JSON content by serializing the model for the current request
38  * using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
39  *
40  * <p>By default, the entire contents of the model map (with the exception of framework-specific classes)
41  * will be encoded as JSON. If the model contains only one key, you can have it extracted encoded as JSON
42  * alone via  {@link #setExtractValueFromSingleKeyModel}.
43  *
44  * @author Jeremy Grelle
45  * @author Arjen Poutsma
46  * @author Rossen Stoyanchev
47  * @since 3.1.2
48  * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
49  */
50 public class MappingJackson2JsonView extends AbstractView {
51 
52 	/**
53 	 * Default content type. Overridable as bean property.
54 	 */
55 	public static final String DEFAULT_CONTENT_TYPE = "application/json";
56 
57 
58 	private ObjectMapper objectMapper = new ObjectMapper();
59 
60 	private JsonEncoding encoding = JsonEncoding.UTF8;
61 
62 	private boolean prefixJson = false;
63 
64 	private Set<String> modelKeys;
65 
66 	private boolean extractValueFromSingleKeyModel = false;
67 
68 	private boolean disableCaching = true;
69 
70 
71 	/**
72 	 * Construct a new {@code JacksonJsonView}, setting the content type to {@code application/json}.
73 	 */
MappingJackson2JsonView()74 	public MappingJackson2JsonView() {
75 		setContentType(DEFAULT_CONTENT_TYPE);
76 		setExposePathVariables(false);
77 	}
78 
79 
80 	/**
81 	 * Sets the {@code ObjectMapper} for this view.
82 	 * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
83 	 * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control
84 	 * of the JSON serialization process. For example, an extended {@code SerializerFactory}
85 	 * can be configured that provides custom serializers for specific types. The other option
86 	 * for refining the serialization process is to use Jackson's provided annotations on the
87 	 * types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
88 	 */
setObjectMapper(ObjectMapper objectMapper)89 	public void setObjectMapper(ObjectMapper objectMapper) {
90 		Assert.notNull(objectMapper, "'objectMapper' must not be null");
91 		this.objectMapper = objectMapper;
92 	}
93 
94 	/**
95 	 * Set the {@code JsonEncoding} for this converter.
96 	 * By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used.
97 	 */
setEncoding(JsonEncoding encoding)98 	public void setEncoding(JsonEncoding encoding) {
99 		Assert.notNull(encoding, "'encoding' must not be null");
100 		this.encoding = encoding;
101 	}
102 
103 	/**
104 	 * Indicates whether the JSON output by this view should be prefixed with <tt>"{} && "</tt>.
105 	 * Default is false.
106 	 * <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
107 	 * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
108 	 * This prefix does not affect the evaluation of JSON, but if JSON validation is performed
109 	 * on the string, the prefix would need to be ignored.
110 	 */
setPrefixJson(boolean prefixJson)111 	public void setPrefixJson(boolean prefixJson) {
112 		this.prefixJson = prefixJson;
113 	}
114 
115 	/**
116 	 * Set the attribute in the model that should be rendered by this view.
117 	 * When set, all other model attributes will be ignored.
118 	 */
setModelKey(String modelKey)119 	public void setModelKey(String modelKey) {
120 		this.modelKeys = Collections.singleton(modelKey);
121 	}
122 
123 	/**
124 	 * Set the attributes in the model that should be rendered by this view.
125 	 * When set, all other model attributes will be ignored.
126 	 */
setModelKeys(Set<String> modelKeys)127 	public void setModelKeys(Set<String> modelKeys) {
128 		this.modelKeys = modelKeys;
129 	}
130 
131 	/**
132 	 * Return the attributes in the model that should be rendered by this view.
133 	 */
getModelKeys()134 	public Set<String> getModelKeys() {
135 		return this.modelKeys;
136 	}
137 
138 	/**
139 	 * Set the attributes in the model that should be rendered by this view.
140 	 * When set, all other model attributes will be ignored.
141 	 * @deprecated use {@link #setModelKeys(Set)} instead
142 	 */
143 	@Deprecated
setRenderedAttributes(Set<String> renderedAttributes)144 	public void setRenderedAttributes(Set<String> renderedAttributes) {
145 		this.modelKeys = renderedAttributes;
146 	}
147 
148 	/**
149 	 * Return the attributes in the model that should be rendered by this view.
150 	 * @deprecated use {@link #getModelKeys()} instead
151 	 */
152 	@Deprecated
getRenderedAttributes()153 	public Set<String> getRenderedAttributes() {
154 		return this.modelKeys;
155 	}
156 
157 	/**
158 	 * Set whether to serialize models containing a single attribute as a map or whether to
159 	 * extract the single value from the model and serialize it directly.
160 	 * <p>The effect of setting this flag is similar to using {@code MappingJacksonHttpMessageConverter}
161 	 * with an {@code @ResponseBody} request-handling method.
162 	 * <p>Default is {@code false}.
163 	 */
setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel)164 	public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) {
165 		this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel;
166 	}
167 
168 	/**
169 	 * Disables caching of the generated JSON.
170 	 * <p>Default is {@code true}, which will prevent the client from caching the generated JSON.
171 	 */
setDisableCaching(boolean disableCaching)172 	public void setDisableCaching(boolean disableCaching) {
173 		this.disableCaching = disableCaching;
174 	}
175 
176 
177 	@Override
prepareResponse(HttpServletRequest request, HttpServletResponse response)178 	protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
179 		response.setContentType(getContentType());
180 		response.setCharacterEncoding(this.encoding.getJavaName());
181 		if (this.disableCaching) {
182 			response.addHeader("Pragma", "no-cache");
183 			response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
184 			response.addDateHeader("Expires", 1L);
185 		}
186 	}
187 
188 	@Override
renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)189 	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
190 			HttpServletResponse response) throws Exception {
191 
192 		Object value = filterModel(model);
193 		JsonGenerator generator =
194 				this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding);
195 		if (this.prefixJson) {
196 			generator.writeRaw("{} && ");
197 		}
198 		this.objectMapper.writeValue(generator, value);
199 	}
200 
201 	/**
202 	 * Filters out undesired attributes from the given model.
203 	 * The return value can be either another {@link Map} or a single value object.
204 	 * <p>The default implementation removes {@link BindingResult} instances and entries
205 	 * not included in the {@link #setRenderedAttributes renderedAttributes} property.
206 	 * @param model the model, as passed on to {@link #renderMergedOutputModel}
207 	 * @return the object to be rendered
208 	 */
filterModel(Map<String, Object> model)209 	protected Object filterModel(Map<String, Object> model) {
210 		Map<String, Object> result = new HashMap<String, Object>(model.size());
211 		Set<String> renderedAttributes = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet());
212 		for (Map.Entry<String, Object> entry : model.entrySet()) {
213 			if (!(entry.getValue() instanceof BindingResult) && renderedAttributes.contains(entry.getKey())) {
214 				result.put(entry.getKey(), entry.getValue());
215 			}
216 		}
217 		return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result);
218 	}
219 
220 }
221