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.messaging.converter;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.StringWriter;
022import java.io.Writer;
023import java.lang.reflect.Type;
024import java.nio.charset.Charset;
025import java.util.concurrent.atomic.AtomicReference;
026
027import com.fasterxml.jackson.annotation.JsonView;
028import com.fasterxml.jackson.core.JsonEncoding;
029import com.fasterxml.jackson.core.JsonGenerator;
030import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
031import com.fasterxml.jackson.databind.DeserializationFeature;
032import com.fasterxml.jackson.databind.JavaType;
033import com.fasterxml.jackson.databind.JsonMappingException;
034import com.fasterxml.jackson.databind.MapperFeature;
035import com.fasterxml.jackson.databind.ObjectMapper;
036import com.fasterxml.jackson.databind.SerializationFeature;
037
038import org.springframework.core.GenericTypeResolver;
039import org.springframework.core.MethodParameter;
040import org.springframework.lang.Nullable;
041import org.springframework.messaging.Message;
042import org.springframework.messaging.MessageHeaders;
043import org.springframework.util.Assert;
044import org.springframework.util.ClassUtils;
045import org.springframework.util.MimeType;
046
047/**
048 * A Jackson 2 based {@link MessageConverter} implementation.
049 *
050 * <p>It customizes Jackson's default properties with the following ones:
051 * <ul>
052 * <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li>
053 * <li>{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled</li>
054 * </ul>
055 *
056 * <p>Compatible with Jackson 2.9 and higher, as of Spring 5.1.
057 *
058 * @author Rossen Stoyanchev
059 * @author Juergen Hoeller
060 * @author Sebastien Deleuze
061 * @since 4.0
062 */
063public class MappingJackson2MessageConverter extends AbstractMessageConverter {
064
065        private ObjectMapper objectMapper;
066
067        @Nullable
068        private Boolean prettyPrint;
069
070
071        /**
072         * Construct a {@code MappingJackson2MessageConverter} supporting
073         * the {@code application/json} MIME type with {@code UTF-8} character set.
074         */
075        public MappingJackson2MessageConverter() {
076                super(new MimeType("application", "json"));
077                this.objectMapper = initObjectMapper();
078        }
079
080        /**
081         * Construct a {@code MappingJackson2MessageConverter} supporting
082         * one or more custom MIME types.
083         * @param supportedMimeTypes the supported MIME types
084         * @since 4.1.5
085         */
086        public MappingJackson2MessageConverter(MimeType... supportedMimeTypes) {
087                super(supportedMimeTypes);
088                this.objectMapper = initObjectMapper();
089        }
090
091
092        private ObjectMapper initObjectMapper() {
093                ObjectMapper objectMapper = new ObjectMapper();
094                objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
095                objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
096                return objectMapper;
097        }
098
099        /**
100         * Set the {@code ObjectMapper} for this converter.
101         * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
102         * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further
103         * control of the JSON serialization process. For example, an extended
104         * {@link com.fasterxml.jackson.databind.ser.SerializerFactory} can be
105         * configured that provides custom serializers for specific types. The other
106         * option for refining the serialization process is to use Jackson's provided
107         * annotations on the types to be serialized, in which case a custom-configured
108         * ObjectMapper is unnecessary.
109         */
110        public void setObjectMapper(ObjectMapper objectMapper) {
111                Assert.notNull(objectMapper, "ObjectMapper must not be null");
112                this.objectMapper = objectMapper;
113                configurePrettyPrint();
114        }
115
116        /**
117         * Return the underlying {@code ObjectMapper} for this converter.
118         */
119        public ObjectMapper getObjectMapper() {
120                return this.objectMapper;
121        }
122
123        /**
124         * Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
125         * This is a shortcut for setting up an {@code ObjectMapper} as follows:
126         * <pre class="code">
127         * ObjectMapper mapper = new ObjectMapper();
128         * mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
129         * converter.setObjectMapper(mapper);
130         * </pre>
131         */
132        public void setPrettyPrint(boolean prettyPrint) {
133                this.prettyPrint = prettyPrint;
134                configurePrettyPrint();
135        }
136
137        private void configurePrettyPrint() {
138                if (this.prettyPrint != null) {
139                        this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
140                }
141        }
142
143
144        @Override
145        protected boolean canConvertFrom(Message<?> message, @Nullable Class<?> targetClass) {
146                if (targetClass == null || !supportsMimeType(message.getHeaders())) {
147                        return false;
148                }
149                JavaType javaType = this.objectMapper.constructType(targetClass);
150                AtomicReference<Throwable> causeRef = new AtomicReference<>();
151                if (this.objectMapper.canDeserialize(javaType, causeRef)) {
152                        return true;
153                }
154                logWarningIfNecessary(javaType, causeRef.get());
155                return false;
156        }
157
158        @Override
159        protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) {
160                if (!supportsMimeType(headers)) {
161                        return false;
162                }
163                AtomicReference<Throwable> causeRef = new AtomicReference<>();
164                if (this.objectMapper.canSerialize(payload.getClass(), causeRef)) {
165                        return true;
166                }
167                logWarningIfNecessary(payload.getClass(), causeRef.get());
168                return false;
169        }
170
171        /**
172         * Determine whether to log the given exception coming from a
173         * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
174         * @param type the class that Jackson tested for (de-)serializability
175         * @param cause the Jackson-thrown exception to evaluate
176         * (typically a {@link JsonMappingException})
177         * @since 4.3
178         */
179        protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) {
180                if (cause == null) {
181                        return;
182                }
183
184                // Do not log warning for serializer not found (note: different message wording on Jackson 2.9)
185                boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage().startsWith("Cannot find"));
186
187                if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) {
188                        String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") +
189                                        "serialization for type [" + type + "]";
190                        if (debugLevel) {
191                                logger.debug(msg, cause);
192                        }
193                        else if (logger.isDebugEnabled()) {
194                                logger.warn(msg, cause);
195                        }
196                        else {
197                                logger.warn(msg + ": " + cause);
198                        }
199                }
200        }
201
202        @Override
203        protected boolean supports(Class<?> clazz) {
204                // should not be called, since we override canConvertFrom/canConvertTo instead
205                throw new UnsupportedOperationException();
206        }
207
208        @Override
209        @Nullable
210        protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
211                JavaType javaType = getJavaType(targetClass, conversionHint);
212                Object payload = message.getPayload();
213                Class<?> view = getSerializationView(conversionHint);
214                try {
215                        if (ClassUtils.isAssignableValue(targetClass, payload)) {
216                                return payload;
217                        }
218                        else if (payload instanceof byte[]) {
219                                if (view != null) {
220                                        return this.objectMapper.readerWithView(view).forType(javaType).readValue((byte[]) payload);
221                                }
222                                else {
223                                        return this.objectMapper.readValue((byte[]) payload, javaType);
224                                }
225                        }
226                        else {
227                                // Assuming a text-based source payload
228                                if (view != null) {
229                                        return this.objectMapper.readerWithView(view).forType(javaType).readValue(payload.toString());
230                                }
231                                else {
232                                        return this.objectMapper.readValue(payload.toString(), javaType);
233                                }
234                        }
235                }
236                catch (IOException ex) {
237                        throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex);
238                }
239        }
240
241        private JavaType getJavaType(Class<?> targetClass, @Nullable Object conversionHint) {
242                if (conversionHint instanceof MethodParameter) {
243                        MethodParameter param = (MethodParameter) conversionHint;
244                        param = param.nestedIfOptional();
245                        if (Message.class.isAssignableFrom(param.getParameterType())) {
246                                param = param.nested();
247                        }
248                        Type genericParameterType = param.getNestedGenericParameterType();
249                        Class<?> contextClass = param.getContainingClass();
250                        Type type = GenericTypeResolver.resolveType(genericParameterType, contextClass);
251                        return this.objectMapper.constructType(type);
252                }
253                return this.objectMapper.constructType(targetClass);
254        }
255
256        @Override
257        @Nullable
258        protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers,
259                        @Nullable Object conversionHint) {
260
261                try {
262                        Class<?> view = getSerializationView(conversionHint);
263                        if (byte[].class == getSerializedPayloadClass()) {
264                                ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
265                                JsonEncoding encoding = getJsonEncoding(getMimeType(headers));
266                                try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding)) {
267                                        if (view != null) {
268                                                this.objectMapper.writerWithView(view).writeValue(generator, payload);
269                                        }
270                                        else {
271                                                this.objectMapper.writeValue(generator, payload);
272                                        }
273                                        payload = out.toByteArray();
274                                }
275                        }
276                        else {
277                                // Assuming a text-based target payload
278                                Writer writer = new StringWriter(1024);
279                                if (view != null) {
280                                        this.objectMapper.writerWithView(view).writeValue(writer, payload);
281                                }
282                                else {
283                                        this.objectMapper.writeValue(writer, payload);
284                                }
285                                payload = writer.toString();
286                        }
287                }
288                catch (IOException ex) {
289                        throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
290                }
291                return payload;
292        }
293
294        /**
295         * Determine a Jackson serialization view based on the given conversion hint.
296         * @param conversionHint the conversion hint Object as passed into the
297         * converter for the current conversion attempt
298         * @return the serialization view class, or {@code null} if none
299         * @since 4.2
300         */
301        @Nullable
302        protected Class<?> getSerializationView(@Nullable Object conversionHint) {
303                if (conversionHint instanceof MethodParameter) {
304                        MethodParameter param = (MethodParameter) conversionHint;
305                        JsonView annotation = (param.getParameterIndex() >= 0 ?
306                                        param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class));
307                        if (annotation != null) {
308                                return extractViewClass(annotation, conversionHint);
309                        }
310                }
311                else if (conversionHint instanceof JsonView) {
312                        return extractViewClass((JsonView) conversionHint, conversionHint);
313                }
314                else if (conversionHint instanceof Class) {
315                        return (Class<?>) conversionHint;
316                }
317
318                // No JSON view specified...
319                return null;
320        }
321
322        private Class<?> extractViewClass(JsonView annotation, Object conversionHint) {
323                Class<?>[] classes = annotation.value();
324                if (classes.length != 1) {
325                        throw new IllegalArgumentException(
326                                        "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint);
327                }
328                return classes[0];
329        }
330
331        /**
332         * Determine the JSON encoding to use for the given content type.
333         * @param contentType the MIME type from the MessageHeaders, if any
334         * @return the JSON encoding to use (never {@code null})
335         */
336        protected JsonEncoding getJsonEncoding(@Nullable MimeType contentType) {
337                if (contentType != null && contentType.getCharset() != null) {
338                        Charset charset = contentType.getCharset();
339                        for (JsonEncoding encoding : JsonEncoding.values()) {
340                                if (charset.name().equals(encoding.getJavaName())) {
341                                        return encoding;
342                                }
343                        }
344                }
345                return JsonEncoding.UTF8;
346        }
347
348}