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.Map;
023import java.util.Set;
024
025import com.fasterxml.jackson.annotation.JsonView;
026import com.fasterxml.jackson.core.JsonGenerator;
027import com.fasterxml.jackson.databind.ObjectMapper;
028import com.fasterxml.jackson.databind.ser.FilterProvider;
029
030import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
031import org.springframework.lang.Nullable;
032import org.springframework.util.CollectionUtils;
033import org.springframework.validation.BindingResult;
034import org.springframework.web.servlet.View;
035
036/**
037 * Spring MVC {@link View} that renders JSON content by serializing the model for the current request
038 * using <a href="https://github.com/FasterXML/jackson">Jackson 2's</a> {@link ObjectMapper}.
039 *
040 * <p>By default, the entire contents of the model map (with the exception of framework-specific classes)
041 * will be encoded as JSON. If the model contains only one key, you can have it extracted encoded as JSON
042 * alone via  {@link #setExtractValueFromSingleKeyModel}.
043 *
044 * <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}.
045 *
046 * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
047 *
048 * @author Jeremy Grelle
049 * @author Arjen Poutsma
050 * @author Rossen Stoyanchev
051 * @author Juergen Hoeller
052 * @author Sebastien Deleuze
053 * @since 3.1.2
054 */
055public class MappingJackson2JsonView extends AbstractJackson2View {
056
057        /**
058         * Default content type: "application/json".
059         * Overridable through {@link #setContentType}.
060         */
061        public static final String DEFAULT_CONTENT_TYPE = "application/json";
062
063        @Nullable
064        private String jsonPrefix;
065
066        @Nullable
067        private Set<String> modelKeys;
068
069        private boolean extractValueFromSingleKeyModel = false;
070
071
072        /**
073         * Construct a new {@code MappingJackson2JsonView} using default configuration
074         * provided by {@link Jackson2ObjectMapperBuilder} and setting the content type
075         * to {@code application/json}.
076         */
077        public MappingJackson2JsonView() {
078                super(Jackson2ObjectMapperBuilder.json().build(), DEFAULT_CONTENT_TYPE);
079        }
080
081        /**
082         * Construct a new {@code MappingJackson2JsonView} using the provided
083         * {@link ObjectMapper} and setting the content type to {@code application/json}.
084         * @since 4.2.1
085         */
086        public MappingJackson2JsonView(ObjectMapper objectMapper) {
087                super(objectMapper, DEFAULT_CONTENT_TYPE);
088        }
089
090
091        /**
092         * Specify a custom prefix to use for this view's JSON output.
093         * Default is none.
094         * @see #setPrefixJson
095         */
096        public void setJsonPrefix(String jsonPrefix) {
097                this.jsonPrefix = jsonPrefix;
098        }
099
100        /**
101         * Indicates whether the JSON output by this view should be prefixed with <tt>")]}', "</tt>.
102         * Default is {@code false}.
103         * <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
104         * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
105         * This prefix should be stripped before parsing the string as JSON.
106         * @see #setJsonPrefix
107         */
108        public void setPrefixJson(boolean prefixJson) {
109                this.jsonPrefix = (prefixJson ? ")]}', " : null);
110        }
111
112        /**
113         * {@inheritDoc}
114         */
115        @Override
116        public void setModelKey(String modelKey) {
117                this.modelKeys = Collections.singleton(modelKey);
118        }
119
120        /**
121         * Set the attributes in the model that should be rendered by this view.
122         * When set, all other model attributes will be ignored.
123         */
124        public void setModelKeys(@Nullable Set<String> modelKeys) {
125                this.modelKeys = modelKeys;
126        }
127
128        /**
129         * Return the attributes in the model that should be rendered by this view.
130         */
131        @Nullable
132        public final Set<String> getModelKeys() {
133                return this.modelKeys;
134        }
135
136        /**
137         * Set whether to serialize models containing a single attribute as a map or
138         * whether to extract the single value from the model and serialize it directly.
139         * <p>The effect of setting this flag is similar to using
140         * {@code MappingJackson2HttpMessageConverter} with an {@code @ResponseBody}
141         * request-handling method.
142         * <p>Default is {@code false}.
143         */
144        public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) {
145                this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel;
146        }
147
148        /**
149         * Filter out undesired attributes from the given model.
150         * The return value can be either another {@link Map} or a single value object.
151         * <p>The default implementation removes {@link BindingResult} instances and entries
152         * not included in the {@link #setModelKeys modelKeys} property.
153         * @param model the model, as passed on to {@link #renderMergedOutputModel}
154         * @return the value to be rendered
155         */
156        @Override
157        protected Object filterModel(Map<String, Object> model) {
158                Map<String, Object> result = new HashMap<>(model.size());
159                Set<String> modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet());
160                model.forEach((clazz, value) -> {
161                        if (!(value instanceof BindingResult) && modelKeys.contains(clazz) &&
162                                        !clazz.equals(JsonView.class.getName()) &&
163                                        !clazz.equals(FilterProvider.class.getName())) {
164                                result.put(clazz, value);
165                        }
166                });
167                return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result);
168        }
169
170        @Override
171        protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
172                if (this.jsonPrefix != null) {
173                        generator.writeRaw(this.jsonPrefix);
174                }
175        }
176
177}