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.ParameterizedType;
024import java.lang.reflect.Type;
025import java.lang.reflect.TypeVariable;
026import java.nio.charset.Charset;
027import java.util.Arrays;
028import java.util.concurrent.atomic.AtomicReference;
029
030import com.fasterxml.jackson.annotation.JsonView;
031import com.fasterxml.jackson.core.JsonEncoding;
032import com.fasterxml.jackson.core.JsonGenerator;
033import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
034import com.fasterxml.jackson.databind.DeserializationFeature;
035import com.fasterxml.jackson.databind.JavaType;
036import com.fasterxml.jackson.databind.JsonMappingException;
037import com.fasterxml.jackson.databind.MapperFeature;
038import com.fasterxml.jackson.databind.ObjectMapper;
039import com.fasterxml.jackson.databind.SerializationFeature;
040import com.fasterxml.jackson.databind.type.TypeFactory;
041
042import org.springframework.core.MethodParameter;
043import org.springframework.core.ResolvableType;
044import org.springframework.messaging.Message;
045import org.springframework.messaging.MessageHeaders;
046import org.springframework.util.Assert;
047import org.springframework.util.MimeType;
048
049/**
050 * A Jackson 2 based {@link MessageConverter} implementation.
051 *
052 * <p>It customizes Jackson's default properties with the following ones:
053 * <ul>
054 * <li>{@link MapperFeature#DEFAULT_VIEW_INCLUSION} is disabled</li>
055 * <li>{@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} is disabled</li>
056 * </ul>
057 *
058 * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
059 *
060 * @author Rossen Stoyanchev
061 * @author Juergen Hoeller
062 * @author Sebastien Deleuze
063 * @since 4.0
064 */
065public class MappingJackson2MessageConverter extends AbstractMessageConverter {
066
067        private ObjectMapper objectMapper;
068
069        private Boolean prettyPrint;
070
071
072        /**
073         * Construct a {@code MappingJackson2MessageConverter} supporting
074         * the {@code application/json} MIME type with {@code UTF-8} character set.
075         */
076        public MappingJackson2MessageConverter() {
077                super(new MimeType("application", "json", Charset.forName("UTF-8")));
078                initObjectMapper();
079        }
080
081        /**
082         * Construct a {@code MappingJackson2MessageConverter} supporting
083         * one or more custom MIME types.
084         * @param supportedMimeTypes the supported MIME types
085         * @since 4.1.5
086         */
087        public MappingJackson2MessageConverter(MimeType... supportedMimeTypes) {
088                super(Arrays.asList(supportedMimeTypes));
089                initObjectMapper();
090        }
091
092
093        private void initObjectMapper() {
094                this.objectMapper = new ObjectMapper();
095                this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
096                this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
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, Class<?> targetClass) {
146                if (targetClass == null || !supportsMimeType(message.getHeaders())) {
147                        return false;
148                }
149                JavaType javaType = this.objectMapper.getTypeFactory().constructType(targetClass);
150                AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
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, MessageHeaders headers) {
160                if (payload == null || !supportsMimeType(headers)) {
161                        return false;
162                }
163                AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>();
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, 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 &&
186                                (cause.getMessage().startsWith("Can not find") || cause.getMessage().startsWith("Cannot find")));
187
188                if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) {
189                        String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") +
190                                        "serialization for type [" + type + "]";
191                        if (debugLevel) {
192                                logger.debug(msg, cause);
193                        }
194                        else if (logger.isDebugEnabled()) {
195                                logger.warn(msg, cause);
196                        }
197                        else {
198                                logger.warn(msg + ": " + cause);
199                        }
200                }
201        }
202
203        @Override
204        protected boolean supports(Class<?> clazz) {
205                // should not be called, since we override canConvertFrom/canConvertTo instead
206                throw new UnsupportedOperationException();
207        }
208
209        @Override
210        protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
211                JavaType javaType = getJavaType(targetClass, conversionHint);
212                Object payload = message.getPayload();
213                Class<?> view = getSerializationView(conversionHint);
214                // Note: in the view case, calling withType instead of forType for compatibility with Jackson <2.5
215                try {
216                        if (payload instanceof byte[]) {
217                                if (view != null) {
218                                        return this.objectMapper.readerWithView(view).forType(javaType).readValue((byte[]) payload);
219                                }
220                                else {
221                                        return this.objectMapper.readValue((byte[]) payload, javaType);
222                                }
223                        }
224                        else {
225                                // Assuming a text-based source payload
226                                if (view != null) {
227                                        return this.objectMapper.readerWithView(view).forType(javaType).readValue(payload.toString());
228                                }
229                                else {
230                                        return this.objectMapper.readValue(payload.toString(), javaType);
231                                }
232                        }
233                }
234                catch (IOException ex) {
235                        throw new MessageConversionException(message, "Could not read JSON: " + ex.getMessage(), ex);
236                }
237        }
238
239        private JavaType getJavaType(Class<?> targetClass, Object conversionHint) {
240                if (conversionHint instanceof MethodParameter) {
241                        MethodParameter param = (MethodParameter) conversionHint;
242                        param = param.nestedIfOptional();
243                        if (Message.class.isAssignableFrom(param.getParameterType())) {
244                                param = param.nested();
245                        }
246                        Type genericParameterType = param.getNestedGenericParameterType();
247                        Class<?> contextClass = param.getContainingClass();
248                        return getJavaType(genericParameterType, contextClass);
249                }
250                return this.objectMapper.getTypeFactory().constructType(targetClass);
251        }
252
253        private JavaType getJavaType(Type type, Class<?> contextClass) {
254                TypeFactory typeFactory = this.objectMapper.getTypeFactory();
255                if (contextClass != null) {
256                        ResolvableType resolvedType = ResolvableType.forType(type);
257                        if (type instanceof TypeVariable) {
258                                ResolvableType resolvedTypeVariable = resolveVariable(
259                                                (TypeVariable<?>) type, ResolvableType.forClass(contextClass));
260                                if (resolvedTypeVariable != ResolvableType.NONE) {
261                                        return typeFactory.constructType(resolvedTypeVariable.resolve());
262                                }
263                        }
264                        else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
265                                ParameterizedType parameterizedType = (ParameterizedType) type;
266                                Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
267                                Type[] typeArguments = parameterizedType.getActualTypeArguments();
268                                for (int i = 0; i < typeArguments.length; i++) {
269                                        Type typeArgument = typeArguments[i];
270                                        if (typeArgument instanceof TypeVariable) {
271                                                ResolvableType resolvedTypeArgument = resolveVariable(
272                                                                (TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
273                                                if (resolvedTypeArgument != ResolvableType.NONE) {
274                                                        generics[i] = resolvedTypeArgument.resolve();
275                                                }
276                                                else {
277                                                        generics[i] = ResolvableType.forType(typeArgument).resolve();
278                                                }
279                                        }
280                                        else {
281                                                generics[i] = ResolvableType.forType(typeArgument).resolve();
282                                        }
283                                }
284                                return typeFactory.constructType(ResolvableType.
285                                                forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
286                        }
287                }
288                return typeFactory.constructType(type);
289        }
290
291        private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
292                ResolvableType resolvedType;
293                if (contextType.hasGenerics()) {
294                        resolvedType = ResolvableType.forType(typeVariable, contextType);
295                        if (resolvedType.resolve() != null) {
296                                return resolvedType;
297                        }
298                }
299
300                ResolvableType superType = contextType.getSuperType();
301                if (superType != ResolvableType.NONE) {
302                        resolvedType = resolveVariable(typeVariable, superType);
303                        if (resolvedType.resolve() != null) {
304                                return resolvedType;
305                        }
306                }
307                for (ResolvableType ifc : contextType.getInterfaces()) {
308                        resolvedType = resolveVariable(typeVariable, ifc);
309                        if (resolvedType.resolve() != null) {
310                                return resolvedType;
311                        }
312                }
313                return ResolvableType.NONE;
314        }
315
316        @Override
317        protected Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) {
318                try {
319                        Class<?> view = getSerializationView(conversionHint);
320                        if (byte[].class == getSerializedPayloadClass()) {
321                                ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
322                                JsonEncoding encoding = getJsonEncoding(getMimeType(headers));
323                                JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding);
324                                if (view != null) {
325                                        this.objectMapper.writerWithView(view).writeValue(generator, payload);
326                                }
327                                else {
328                                        this.objectMapper.writeValue(generator, payload);
329                                }
330                                payload = out.toByteArray();
331                        }
332                        else {
333                                // Assuming a text-based target payload
334                                Writer writer = new StringWriter(1024);
335                                if (view != null) {
336                                        this.objectMapper.writerWithView(view).writeValue(writer, payload);
337                                }
338                                else {
339                                        this.objectMapper.writeValue(writer, payload);
340                                }
341                                payload = writer.toString();
342                        }
343                }
344                catch (IOException ex) {
345                        throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
346                }
347                return payload;
348        }
349
350        /**
351         * Determine a Jackson serialization view based on the given conversion hint.
352         * @param conversionHint the conversion hint Object as passed into the
353         * converter for the current conversion attempt
354         * @return the serialization view class, or {@code null} if none
355         * @since 4.2
356         */
357        protected Class<?> getSerializationView(Object conversionHint) {
358                if (conversionHint instanceof MethodParameter) {
359                        MethodParameter param = (MethodParameter) conversionHint;
360                        JsonView annotation = (param.getParameterIndex() >= 0 ?
361                                        param.getParameterAnnotation(JsonView.class) : param.getMethodAnnotation(JsonView.class));
362                        if (annotation != null) {
363                                return extractViewClass(annotation, conversionHint);
364                        }
365                }
366                else if (conversionHint instanceof JsonView) {
367                        return extractViewClass((JsonView) conversionHint, conversionHint);
368                }
369                else if (conversionHint instanceof Class) {
370                        return (Class<?>) conversionHint;
371                }
372
373                // No JSON view specified...
374                return null;
375        }
376
377        private Class<?> extractViewClass(JsonView annotation, Object conversionHint) {
378                Class<?>[] classes = annotation.value();
379                if (classes.length != 1) {
380                        throw new IllegalArgumentException(
381                                        "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint);
382                }
383                return classes[0];
384        }
385
386        /**
387         * Determine the JSON encoding to use for the given content type.
388         * @param contentType the MIME type from the MessageHeaders, if any
389         * @return the JSON encoding to use (never {@code null})
390         */
391        protected JsonEncoding getJsonEncoding(MimeType contentType) {
392                if (contentType != null && contentType.getCharset() != null) {
393                        Charset charset = contentType.getCharset();
394                        for (JsonEncoding encoding : JsonEncoding.values()) {
395                                if (charset.name().equals(encoding.getJavaName())) {
396                                        return encoding;
397                                }
398                        }
399                }
400                return JsonEncoding.UTF8;
401        }
402
403}