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}