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