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.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.util.Map;
023
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletResponse;
026
027import com.fasterxml.jackson.annotation.JsonView;
028import com.fasterxml.jackson.core.JsonEncoding;
029import com.fasterxml.jackson.core.JsonGenerator;
030import com.fasterxml.jackson.databind.ObjectMapper;
031import com.fasterxml.jackson.databind.ObjectWriter;
032import com.fasterxml.jackson.databind.SerializationFeature;
033import com.fasterxml.jackson.databind.ser.FilterProvider;
034
035import org.springframework.http.converter.json.MappingJacksonValue;
036import org.springframework.lang.Nullable;
037import org.springframework.util.Assert;
038import org.springframework.web.servlet.view.AbstractView;
039
040/**
041 * Abstract base class for Jackson based and content type independent
042 * {@link AbstractView} implementations.
043 *
044 * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
045 *
046 * @author Jeremy Grelle
047 * @author Arjen Poutsma
048 * @author Rossen Stoyanchev
049 * @author Juergen Hoeller
050 * @author Sebastien Deleuze
051 * @since 4.1
052 */
053public abstract class AbstractJackson2View extends AbstractView {
054
055        private ObjectMapper objectMapper;
056
057        private JsonEncoding encoding = JsonEncoding.UTF8;
058
059        @Nullable
060        private Boolean prettyPrint;
061
062        private boolean disableCaching = true;
063
064        protected boolean updateContentLength = false;
065
066
067        protected AbstractJackson2View(ObjectMapper objectMapper, String contentType) {
068                this.objectMapper = objectMapper;
069                configurePrettyPrint();
070                setContentType(contentType);
071                setExposePathVariables(false);
072        }
073
074        /**
075         * Set the {@code ObjectMapper} for this view.
076         * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} will be used.
077         * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control of
078         * the JSON serialization process. The other option is to use Jackson's provided annotations
079         * on the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
080         */
081        public void setObjectMapper(ObjectMapper objectMapper) {
082                this.objectMapper = objectMapper;
083                configurePrettyPrint();
084        }
085
086        /**
087         * Return the {@code ObjectMapper} for this view.
088         */
089        public final ObjectMapper getObjectMapper() {
090                return this.objectMapper;
091        }
092
093        /**
094         * Set the {@code JsonEncoding} for this view.
095         * By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used.
096         */
097        public void setEncoding(JsonEncoding encoding) {
098                Assert.notNull(encoding, "'encoding' must not be null");
099                this.encoding = encoding;
100        }
101
102        /**
103         * Return the {@code JsonEncoding} for this view.
104         */
105        public final JsonEncoding getEncoding() {
106                return this.encoding;
107        }
108
109        /**
110         * Whether to use the default pretty printer when writing the output.
111         * This is a shortcut for setting up an {@code ObjectMapper} as follows:
112         * <pre class="code">
113         * ObjectMapper mapper = new ObjectMapper();
114         * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
115         * </pre>
116         * <p>The default value is {@code false}.
117         */
118        public void setPrettyPrint(boolean prettyPrint) {
119                this.prettyPrint = prettyPrint;
120                configurePrettyPrint();
121        }
122
123        private void configurePrettyPrint() {
124                if (this.prettyPrint != null) {
125                        this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
126                }
127        }
128
129        /**
130         * Disables caching of the generated JSON.
131         * <p>Default is {@code true}, which will prevent the client from caching the generated JSON.
132         */
133        public void setDisableCaching(boolean disableCaching) {
134                this.disableCaching = disableCaching;
135        }
136
137        /**
138         * Whether to update the 'Content-Length' header of the response. When set to
139         * {@code true}, the response is buffered in order to determine the content
140         * length and set the 'Content-Length' header of the response.
141         * <p>The default setting is {@code false}.
142         */
143        public void setUpdateContentLength(boolean updateContentLength) {
144                this.updateContentLength = updateContentLength;
145        }
146
147        @Override
148        protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
149                setResponseContentType(request, response);
150                response.setCharacterEncoding(this.encoding.getJavaName());
151                if (this.disableCaching) {
152                        response.addHeader("Cache-Control", "no-store");
153                }
154        }
155
156        @Override
157        protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
158                        HttpServletResponse response) throws Exception {
159
160                ByteArrayOutputStream temporaryStream = null;
161                OutputStream stream;
162
163                if (this.updateContentLength) {
164                        temporaryStream = createTemporaryOutputStream();
165                        stream = temporaryStream;
166                }
167                else {
168                        stream = response.getOutputStream();
169                }
170
171                Object value = filterAndWrapModel(model, request);
172                writeContent(stream, value);
173
174                if (temporaryStream != null) {
175                        writeToResponse(response, temporaryStream);
176                }
177        }
178
179        /**
180         * Filter and optionally wrap the model in {@link MappingJacksonValue} container.
181         * @param model the model, as passed on to {@link #renderMergedOutputModel}
182         * @param request current HTTP request
183         * @return the wrapped or unwrapped value to be rendered
184         */
185        protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) {
186                Object value = filterModel(model);
187                Class<?> serializationView = (Class<?>) model.get(JsonView.class.getName());
188                FilterProvider filters = (FilterProvider) model.get(FilterProvider.class.getName());
189                if (serializationView != null || filters != null) {
190                        MappingJacksonValue container = new MappingJacksonValue(value);
191                        if (serializationView != null) {
192                                container.setSerializationView(serializationView);
193                        }
194                        if (filters != null) {
195                                container.setFilters(filters);
196                        }
197                        value = container;
198                }
199                return value;
200        }
201
202        /**
203         * Write the actual JSON content to the stream.
204         * @param stream the output stream to use
205         * @param object the value to be rendered, as returned from {@link #filterModel}
206         * @throws IOException if writing failed
207         */
208        protected void writeContent(OutputStream stream, Object object) throws IOException {
209                try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding)) {
210                        writePrefix(generator, object);
211
212                        Object value = object;
213                        Class<?> serializationView = null;
214                        FilterProvider filters = null;
215
216                        if (value instanceof MappingJacksonValue) {
217                                MappingJacksonValue container = (MappingJacksonValue) value;
218                                value = container.getValue();
219                                serializationView = container.getSerializationView();
220                                filters = container.getFilters();
221                        }
222
223                        ObjectWriter objectWriter = (serializationView != null ?
224                                        this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
225                        if (filters != null) {
226                                objectWriter = objectWriter.with(filters);
227                        }
228                        objectWriter.writeValue(generator, value);
229
230                        writeSuffix(generator, object);
231                        generator.flush();
232                }
233        }
234
235
236        /**
237         * Set the attribute in the model that should be rendered by this view.
238         * When set, all other model attributes will be ignored.
239         */
240        public abstract void setModelKey(String modelKey);
241
242        /**
243         * Filter out undesired attributes from the given model.
244         * The return value can be either another {@link Map} or a single value object.
245         * @param model the model, as passed on to {@link #renderMergedOutputModel}
246         * @return the value to be rendered
247         */
248        protected abstract Object filterModel(Map<String, Object> model);
249
250        /**
251         * Write a prefix before the main content.
252         * @param generator the generator to use for writing content.
253         * @param object the object to write to the output message.
254         */
255        protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
256        }
257
258        /**
259         * Write a suffix after the main content.
260         * @param generator the generator to use for writing content.
261         * @param object the object to write to the output message.
262         */
263        protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
264        }
265
266}