001/*
002 * Copyright 2002-2020 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.http.converter.json;
018
019import java.io.IOException;
020import java.io.InputStreamReader;
021import java.io.OutputStream;
022import java.io.Reader;
023import java.lang.reflect.Type;
024import java.nio.charset.Charset;
025import java.nio.charset.StandardCharsets;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.concurrent.atomic.AtomicReference;
031
032import com.fasterxml.jackson.core.JsonEncoding;
033import com.fasterxml.jackson.core.JsonGenerator;
034import com.fasterxml.jackson.core.JsonProcessingException;
035import com.fasterxml.jackson.core.PrettyPrinter;
036import com.fasterxml.jackson.core.util.DefaultIndenter;
037import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
038import com.fasterxml.jackson.databind.JavaType;
039import com.fasterxml.jackson.databind.JsonMappingException;
040import com.fasterxml.jackson.databind.ObjectMapper;
041import com.fasterxml.jackson.databind.ObjectReader;
042import com.fasterxml.jackson.databind.ObjectWriter;
043import com.fasterxml.jackson.databind.SerializationConfig;
044import com.fasterxml.jackson.databind.SerializationFeature;
045import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
046import com.fasterxml.jackson.databind.ser.FilterProvider;
047
048import org.springframework.core.GenericTypeResolver;
049import org.springframework.http.HttpInputMessage;
050import org.springframework.http.HttpOutputMessage;
051import org.springframework.http.MediaType;
052import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
053import org.springframework.http.converter.HttpMessageConversionException;
054import org.springframework.http.converter.HttpMessageConverter;
055import org.springframework.http.converter.HttpMessageNotReadableException;
056import org.springframework.http.converter.HttpMessageNotWritableException;
057import org.springframework.lang.Nullable;
058import org.springframework.util.Assert;
059import org.springframework.util.StreamUtils;
060import org.springframework.util.TypeUtils;
061
062/**
063 * Abstract base class for Jackson based and content type independent
064 * {@link HttpMessageConverter} implementations.
065 *
066 * <p>Compatible with Jackson 2.9 and higher, as of Spring 5.0.
067 *
068 * @author Arjen Poutsma
069 * @author Keith Donald
070 * @author Rossen Stoyanchev
071 * @author Juergen Hoeller
072 * @author Sebastien Deleuze
073 * @since 4.1
074 * @see MappingJackson2HttpMessageConverter
075 */
076public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
077
078        private static final Map<String, JsonEncoding> ENCODINGS;
079
080        static {
081                ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1);
082                for (JsonEncoding encoding : JsonEncoding.values()) {
083                        ENCODINGS.put(encoding.getJavaName(), encoding);
084                }
085                ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
086        }
087
088
089        /**
090         * The default charset used by the converter.
091         */
092        @Nullable
093        @Deprecated
094        public static final Charset DEFAULT_CHARSET = null;
095
096
097        protected ObjectMapper objectMapper;
098
099        @Nullable
100        private Boolean prettyPrint;
101
102        @Nullable
103        private PrettyPrinter ssePrettyPrinter;
104
105
106        protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
107                this.objectMapper = objectMapper;
108                DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
109                prettyPrinter.indentObjectsWith(new DefaultIndenter("  ", "\ndata:"));
110                this.ssePrettyPrinter = prettyPrinter;
111        }
112
113        protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
114                this(objectMapper);
115                setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
116        }
117
118        protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
119                this(objectMapper);
120                setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
121        }
122
123
124        /**
125         * Set the {@code ObjectMapper} for this view.
126         * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
127         * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further
128         * control of the JSON serialization process. For example, an extended
129         * {@link com.fasterxml.jackson.databind.ser.SerializerFactory}
130         * can be configured that provides custom serializers for specific types.
131         * The other option for refining the serialization process is to use Jackson's
132         * provided annotations on the types to be serialized, in which case a
133         * custom-configured ObjectMapper is unnecessary.
134         */
135        public void setObjectMapper(ObjectMapper objectMapper) {
136                Assert.notNull(objectMapper, "ObjectMapper must not be null");
137                this.objectMapper = objectMapper;
138                configurePrettyPrint();
139        }
140
141        /**
142         * Return the underlying {@code ObjectMapper} for this view.
143         */
144        public ObjectMapper getObjectMapper() {
145                return this.objectMapper;
146        }
147
148        /**
149         * Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
150         * This is a shortcut for setting up an {@code ObjectMapper} as follows:
151         * <pre class="code">
152         * ObjectMapper mapper = new ObjectMapper();
153         * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
154         * converter.setObjectMapper(mapper);
155         * </pre>
156         */
157        public void setPrettyPrint(boolean prettyPrint) {
158                this.prettyPrint = prettyPrint;
159                configurePrettyPrint();
160        }
161
162        private void configurePrettyPrint() {
163                if (this.prettyPrint != null) {
164                        this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
165                }
166        }
167
168
169        @Override
170        public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
171                return canRead(clazz, null, mediaType);
172        }
173
174        @Override
175        public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
176                if (!canRead(mediaType)) {
177                        return false;
178                }
179                JavaType javaType = getJavaType(type, contextClass);
180                AtomicReference<Throwable> causeRef = new AtomicReference<>();
181                if (this.objectMapper.canDeserialize(javaType, causeRef)) {
182                        return true;
183                }
184                logWarningIfNecessary(javaType, causeRef.get());
185                return false;
186        }
187
188        @Override
189        public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
190                if (!canWrite(mediaType)) {
191                        return false;
192                }
193                if (mediaType != null && mediaType.getCharset() != null) {
194                        Charset charset = mediaType.getCharset();
195                        if (!ENCODINGS.containsKey(charset.name())) {
196                                return false;
197                        }
198                }
199                AtomicReference<Throwable> causeRef = new AtomicReference<>();
200                if (this.objectMapper.canSerialize(clazz, causeRef)) {
201                        return true;
202                }
203                logWarningIfNecessary(clazz, causeRef.get());
204                return false;
205        }
206
207        /**
208         * Determine whether to log the given exception coming from a
209         * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
210         * @param type the class that Jackson tested for (de-)serializability
211         * @param cause the Jackson-thrown exception to evaluate
212         * (typically a {@link JsonMappingException})
213         * @since 4.3
214         */
215        protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) {
216                if (cause == null) {
217                        return;
218                }
219
220                // Do not log warning for serializer not found (note: different message wording on Jackson 2.9)
221                boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage().startsWith("Cannot find"));
222
223                if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) {
224                        String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") +
225                                        "serialization for type [" + type + "]";
226                        if (debugLevel) {
227                                logger.debug(msg, cause);
228                        }
229                        else if (logger.isDebugEnabled()) {
230                                logger.warn(msg, cause);
231                        }
232                        else {
233                                logger.warn(msg + ": " + cause);
234                        }
235                }
236        }
237
238        @Override
239        public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
240                        throws IOException, HttpMessageNotReadableException {
241
242                JavaType javaType = getJavaType(type, contextClass);
243                return readJavaType(javaType, inputMessage);
244        }
245
246        @Override
247        protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
248                        throws IOException, HttpMessageNotReadableException {
249
250                JavaType javaType = getJavaType(clazz, null);
251                return readJavaType(javaType, inputMessage);
252        }
253
254        private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
255                MediaType contentType = inputMessage.getHeaders().getContentType();
256                Charset charset = getCharset(contentType);
257
258                boolean isUnicode = ENCODINGS.containsKey(charset.name());
259                try {
260                        if (inputMessage instanceof MappingJacksonInputMessage) {
261                                Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
262                                if (deserializationView != null) {
263                                        ObjectReader objectReader = this.objectMapper.readerWithView(deserializationView).forType(javaType);
264                                        if (isUnicode) {
265                                                return objectReader.readValue(inputMessage.getBody());
266                                        }
267                                        else {
268                                                Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
269                                                return objectReader.readValue(reader);
270                                        }
271                                }
272                        }
273                        if (isUnicode) {
274                                return this.objectMapper.readValue(inputMessage.getBody(), javaType);
275                        }
276                        else {
277                                Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
278                                return this.objectMapper.readValue(reader, javaType);
279                        }
280                }
281                catch (InvalidDefinitionException ex) {
282                        throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
283                }
284                catch (JsonProcessingException ex) {
285                        throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
286                }
287        }
288
289        /**
290         * Determine the charset to use for JSON input.
291         * <p>By default this is either the charset from the input {@code MediaType}
292         * or otherwise falling back on {@code UTF-8}. Can be overridden in subclasses.
293         * @param contentType the content type of the HTTP input message
294         * @return the charset to use
295         * @since 5.1.18
296         */
297        protected Charset getCharset(@Nullable MediaType contentType) {
298                if (contentType != null && contentType.getCharset() != null) {
299                        return contentType.getCharset();
300                }
301                else {
302                        return StandardCharsets.UTF_8;
303                }
304        }
305
306        @Override
307        protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
308                        throws IOException, HttpMessageNotWritableException {
309
310                MediaType contentType = outputMessage.getHeaders().getContentType();
311                JsonEncoding encoding = getJsonEncoding(contentType);
312
313                OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
314                JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding);
315                try {
316                        writePrefix(generator, object);
317
318                        Object value = object;
319                        Class<?> serializationView = null;
320                        FilterProvider filters = null;
321                        JavaType javaType = null;
322
323                        if (object instanceof MappingJacksonValue) {
324                                MappingJacksonValue container = (MappingJacksonValue) object;
325                                value = container.getValue();
326                                serializationView = container.getSerializationView();
327                                filters = container.getFilters();
328                        }
329                        if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
330                                javaType = getJavaType(type, null);
331                        }
332
333                        ObjectWriter objectWriter = (serializationView != null ?
334                                        this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
335                        if (filters != null) {
336                                objectWriter = objectWriter.with(filters);
337                        }
338                        if (javaType != null && javaType.isContainerType()) {
339                                objectWriter = objectWriter.forType(javaType);
340                        }
341                        SerializationConfig config = objectWriter.getConfig();
342                        if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
343                                        config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
344                                objectWriter = objectWriter.with(this.ssePrettyPrinter);
345                        }
346                        objectWriter.writeValue(generator, value);
347
348                        writeSuffix(generator, object);
349                        generator.flush();
350                        generator.close();
351                }
352                catch (InvalidDefinitionException ex) {
353                        throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
354                }
355                catch (JsonProcessingException ex) {
356                        throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
357                }
358        }
359
360        /**
361         * Write a prefix before the main content.
362         * @param generator the generator to use for writing content.
363         * @param object the object to write to the output message.
364         */
365        protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
366        }
367
368        /**
369         * Write a suffix after the main content.
370         * @param generator the generator to use for writing content.
371         * @param object the object to write to the output message.
372         */
373        protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
374        }
375
376        /**
377         * Return the Jackson {@link JavaType} for the specified type and context class.
378         * @param type the generic type to return the Jackson JavaType for
379         * @param contextClass a context class for the target type, for example a class
380         * in which the target type appears in a method signature (can be {@code null})
381         * @return the Jackson JavaType
382         */
383        protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
384                return this.objectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass));
385        }
386
387        /**
388         * Determine the JSON encoding to use for the given content type.
389         * @param contentType the media type as requested by the caller
390         * @return the JSON encoding to use (never {@code null})
391         */
392        protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) {
393                if (contentType != null && contentType.getCharset() != null) {
394                        Charset charset = contentType.getCharset();
395                        JsonEncoding encoding = ENCODINGS.get(charset.name());
396                        if (encoding != null) {
397                                return encoding;
398                        }
399                }
400                return JsonEncoding.UTF8;
401        }
402
403        @Override
404        @Nullable
405        protected MediaType getDefaultContentType(Object object) throws IOException {
406                if (object instanceof MappingJacksonValue) {
407                        object = ((MappingJacksonValue) object).getValue();
408                }
409                return super.getDefaultContentType(object);
410        }
411
412        @Override
413        protected Long getContentLength(Object object, @Nullable MediaType contentType) throws IOException {
414                if (object instanceof MappingJacksonValue) {
415                        object = ((MappingJacksonValue) object).getValue();
416                }
417                return super.getContentLength(object, contentType);
418        }
419
420}