001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.view.json; 018 019import java.io.IOException; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.LinkedHashSet; 023import java.util.Map; 024import java.util.Set; 025import java.util.regex.Pattern; 026import javax.servlet.http.HttpServletRequest; 027import javax.servlet.http.HttpServletResponse; 028 029import com.fasterxml.jackson.annotation.JsonView; 030import com.fasterxml.jackson.core.JsonGenerator; 031import com.fasterxml.jackson.databind.ObjectMapper; 032import com.fasterxml.jackson.databind.ser.FilterProvider; 033 034import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 035import org.springframework.http.converter.json.MappingJacksonValue; 036import org.springframework.util.CollectionUtils; 037import org.springframework.util.StringUtils; 038import org.springframework.validation.BindingResult; 039import org.springframework.web.servlet.View; 040 041/** 042 * Spring MVC {@link View} that renders JSON content by serializing the model for the current request 043 * using <a href="https://wiki.fasterxml.com/JacksonHome">Jackson 2's</a> {@link ObjectMapper}. 044 * 045 * <p>By default, the entire contents of the model map (with the exception of framework-specific classes) 046 * will be encoded as JSON. If the model contains only one key, you can have it extracted encoded as JSON 047 * alone via {@link #setExtractValueFromSingleKeyModel}. 048 * 049 * <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. 050 * 051 * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. 052 * 053 * @author Jeremy Grelle 054 * @author Arjen Poutsma 055 * @author Rossen Stoyanchev 056 * @author Juergen Hoeller 057 * @author Sebastien Deleuze 058 * @since 3.1.2 059 */ 060@SuppressWarnings("deprecation") 061public class MappingJackson2JsonView extends AbstractJackson2View { 062 063 /** 064 * Default content type: "application/json". 065 * Overridable through {@link #setContentType}. 066 */ 067 public static final String DEFAULT_CONTENT_TYPE = "application/json"; 068 069 /** 070 * Default content type for JSONP: "application/javascript". 071 * @deprecated Will be removed as of Spring Framework 5.1, use 072 * <a href="https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/html/cors.html">CORS</a> instead. 073 */ 074 @Deprecated 075 public static final String DEFAULT_JSONP_CONTENT_TYPE = "application/javascript"; 076 077 /** 078 * Pattern for validating jsonp callback parameter values. 079 */ 080 private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*"); 081 082 083 private String jsonPrefix; 084 085 private Set<String> modelKeys; 086 087 private boolean extractValueFromSingleKeyModel = false; 088 089 private Set<String> jsonpParameterNames = new LinkedHashSet<String>(); 090 091 092 /** 093 * Construct a new {@code MappingJackson2JsonView} using default configuration 094 * provided by {@link Jackson2ObjectMapperBuilder} and setting the content type 095 * to {@code application/json}. 096 */ 097 public MappingJackson2JsonView() { 098 super(Jackson2ObjectMapperBuilder.json().build(), DEFAULT_CONTENT_TYPE); 099 } 100 101 /** 102 * Construct a new {@code MappingJackson2JsonView} using the provided 103 * {@link ObjectMapper} and setting the content type to {@code application/json}. 104 * @since 4.2.1 105 */ 106 public MappingJackson2JsonView(ObjectMapper objectMapper) { 107 super(objectMapper, DEFAULT_CONTENT_TYPE); 108 } 109 110 111 /** 112 * Specify a custom prefix to use for this view's JSON output. 113 * Default is none. 114 * @see #setPrefixJson 115 */ 116 public void setJsonPrefix(String jsonPrefix) { 117 this.jsonPrefix = jsonPrefix; 118 } 119 120 /** 121 * Indicates whether the JSON output by this view should be prefixed with <tt>")]}', "</tt>. 122 * Default is {@code false}. 123 * <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. 124 * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. 125 * This prefix should be stripped before parsing the string as JSON. 126 * @see #setJsonPrefix 127 */ 128 public void setPrefixJson(boolean prefixJson) { 129 this.jsonPrefix = (prefixJson ? ")]}', " : null); 130 } 131 132 /** 133 * {@inheritDoc} 134 */ 135 @Override 136 public void setModelKey(String modelKey) { 137 this.modelKeys = Collections.singleton(modelKey); 138 } 139 140 /** 141 * Set the attributes in the model that should be rendered by this view. 142 * When set, all other model attributes will be ignored. 143 */ 144 public void setModelKeys(Set<String> modelKeys) { 145 this.modelKeys = modelKeys; 146 } 147 148 /** 149 * Return the attributes in the model that should be rendered by this view. 150 */ 151 public final Set<String> getModelKeys() { 152 return this.modelKeys; 153 } 154 155 /** 156 * Set whether to serialize models containing a single attribute as a map or 157 * whether to extract the single value from the model and serialize it directly. 158 * <p>The effect of setting this flag is similar to using 159 * {@code MappingJackson2HttpMessageConverter} with an {@code @ResponseBody} 160 * request-handling method. 161 * <p>Default is {@code false}. 162 */ 163 public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) { 164 this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel; 165 } 166 167 /** 168 * Set JSONP request parameter names. Each time a request has one of those 169 * parameters, the resulting JSON will be wrapped into a function named as 170 * specified by the JSONP request parameter value. 171 * <p>The parameter names configured by default are "jsonp" and "callback". 172 * @since 4.1 173 * @see <a href="https://en.wikipedia.org/wiki/JSONP">JSONP Wikipedia article</a> 174 * @deprecated Will be removed as of Spring Framework 5.1, use 175 * <a href="https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/html/cors.html">CORS</a> instead. 176 */ 177 @Deprecated 178 public void setJsonpParameterNames(Set<String> jsonpParameterNames) { 179 this.jsonpParameterNames = jsonpParameterNames; 180 } 181 182 private String getJsonpParameterValue(HttpServletRequest request) { 183 if (this.jsonpParameterNames != null) { 184 for (String name : this.jsonpParameterNames) { 185 String value = request.getParameter(name); 186 if (StringUtils.isEmpty(value)) { 187 continue; 188 } 189 if (!isValidJsonpQueryParam(value)) { 190 if (logger.isDebugEnabled()) { 191 logger.debug("Ignoring invalid jsonp parameter value: " + value); 192 } 193 continue; 194 } 195 return value; 196 } 197 } 198 return null; 199 } 200 201 /** 202 * Validate the jsonp query parameter value. The default implementation 203 * returns true if it consists of digits, letters, or "_" and ".". 204 * Invalid parameter values are ignored. 205 * @param value the query param value, never {@code null} 206 * @since 4.1.8 207 * @deprecated Will be removed as of Spring Framework 5.1, use 208 * <a href="https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/html/cors.html">CORS</a> instead. 209 */ 210 @Deprecated 211 protected boolean isValidJsonpQueryParam(String value) { 212 return CALLBACK_PARAM_PATTERN.matcher(value).matches(); 213 } 214 215 /** 216 * Filter out undesired attributes from the given model. 217 * The return value can be either another {@link Map} or a single value object. 218 * <p>The default implementation removes {@link BindingResult} instances and entries 219 * not included in the {@link #setModelKeys modelKeys} property. 220 * @param model the model, as passed on to {@link #renderMergedOutputModel} 221 * @return the value to be rendered 222 */ 223 @Override 224 protected Object filterModel(Map<String, Object> model) { 225 Map<String, Object> result = new HashMap<String, Object>(model.size()); 226 Set<String> modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet()); 227 for (Map.Entry<String, Object> entry : model.entrySet()) { 228 if (!(entry.getValue() instanceof BindingResult) && modelKeys.contains(entry.getKey()) && 229 !entry.getKey().equals(JsonView.class.getName()) && 230 !entry.getKey().equals(FilterProvider.class.getName())) { 231 result.put(entry.getKey(), entry.getValue()); 232 } 233 } 234 return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result); 235 } 236 237 @Override 238 protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) { 239 Object value = super.filterAndWrapModel(model, request); 240 String jsonpParameterValue = getJsonpParameterValue(request); 241 if (jsonpParameterValue != null) { 242 if (value instanceof MappingJacksonValue) { 243 ((MappingJacksonValue) value).setJsonpFunction(jsonpParameterValue); 244 } 245 else { 246 MappingJacksonValue container = new MappingJacksonValue(value); 247 container.setJsonpFunction(jsonpParameterValue); 248 value = container; 249 } 250 } 251 return value; 252 } 253 254 @Override 255 protected void writePrefix(JsonGenerator generator, Object object) throws IOException { 256 if (this.jsonPrefix != null) { 257 generator.writeRaw(this.jsonPrefix); 258 } 259 260 String jsonpFunction = null; 261 if (object instanceof MappingJacksonValue) { 262 jsonpFunction = ((MappingJacksonValue) object).getJsonpFunction(); 263 } 264 if (jsonpFunction != null) { 265 generator.writeRaw("/**/"); 266 generator.writeRaw(jsonpFunction + "("); 267 } 268 } 269 270 @Override 271 protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { 272 String jsonpFunction = null; 273 if (object instanceof MappingJacksonValue) { 274 jsonpFunction = ((MappingJacksonValue) object).getJsonpFunction(); 275 } 276 if (jsonpFunction != null) { 277 generator.writeRaw(");"); 278 } 279 } 280 281 @Override 282 protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) { 283 if (getJsonpParameterValue(request) != null) { 284 response.setContentType(DEFAULT_JSONP_CONTENT_TYPE); 285 } 286 else { 287 super.setResponseContentType(request, response); 288 } 289 } 290 291}