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.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.List;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027
028import org.springframework.lang.Nullable;
029import org.springframework.messaging.Message;
030import org.springframework.messaging.MessageHeaders;
031import org.springframework.messaging.support.MessageBuilder;
032import org.springframework.messaging.support.MessageHeaderAccessor;
033import org.springframework.util.Assert;
034import org.springframework.util.MimeType;
035
036/**
037 * Abstract base class for {@link SmartMessageConverter} implementations including
038 * support for common properties and a partial implementation of the conversion methods,
039 * mainly to check if the converter supports the conversion based on the payload class
040 * and MIME type.
041 *
042 * @author Rossen Stoyanchev
043 * @author Sebastien Deleuze
044 * @author Juergen Hoeller
045 * @since 4.0
046 */
047public abstract class AbstractMessageConverter implements SmartMessageConverter {
048
049        protected final Log logger = LogFactory.getLog(getClass());
050
051        private final List<MimeType> supportedMimeTypes = new ArrayList<>(4);
052
053        @Nullable
054        private ContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver();
055
056        private boolean strictContentTypeMatch = false;
057
058        private Class<?> serializedPayloadClass = byte[].class;
059
060
061        /**
062         * Constructor with a single MIME type.
063         * @param supportedMimeType the supported MIME type
064         */
065        protected AbstractMessageConverter(MimeType supportedMimeType) {
066                this(Collections.singletonList(supportedMimeType));
067        }
068
069        /**
070         * Constructor with one or more MIME types via vararg.
071         * @param supportedMimeTypes the supported MIME types
072         * @since 5.2.2
073         */
074        protected AbstractMessageConverter(MimeType... supportedMimeTypes) {
075                this(Arrays.asList(supportedMimeTypes));
076        }
077
078        /**
079         * Constructor with a Collection of MIME types.
080         * @param supportedMimeTypes the supported MIME types
081         */
082        protected AbstractMessageConverter(Collection<MimeType> supportedMimeTypes) {
083                this.supportedMimeTypes.addAll(supportedMimeTypes);
084        }
085
086
087        /**
088         * Return the supported MIME types.
089         */
090        public List<MimeType> getSupportedMimeTypes() {
091                return Collections.unmodifiableList(this.supportedMimeTypes);
092        }
093
094        /**
095         * Allows subclasses to add more supported mime types.
096         * @since 5.2.2
097         */
098        protected void addSupportedMimeTypes(MimeType... supportedMimeTypes) {
099                this.supportedMimeTypes.addAll(Arrays.asList(supportedMimeTypes));
100        }
101
102        /**
103         * Configure the {@link ContentTypeResolver} to use to resolve the content
104         * type of an input message.
105         * <p>Note that if no resolver is configured, then
106         * {@link #setStrictContentTypeMatch(boolean) strictContentTypeMatch} should
107         * be left as {@code false} (the default) or otherwise this converter will
108         * ignore all messages.
109         * <p>By default, a {@code DefaultContentTypeResolver} instance is used.
110         */
111        public void setContentTypeResolver(@Nullable ContentTypeResolver resolver) {
112                this.contentTypeResolver = resolver;
113        }
114
115        /**
116         * Return the configured {@link ContentTypeResolver}.
117         */
118        @Nullable
119        public ContentTypeResolver getContentTypeResolver() {
120                return this.contentTypeResolver;
121        }
122
123        /**
124         * Whether this converter should convert messages for which no content type
125         * could be resolved through the configured
126         * {@link org.springframework.messaging.converter.ContentTypeResolver}.
127         * <p>A converter can configured to be strict only when a
128         * {@link #setContentTypeResolver contentTypeResolver} is configured and the
129         * list of {@link #getSupportedMimeTypes() supportedMimeTypes} is not be empty.
130         * <p>When this flag is set to {@code true}, {@link #supportsMimeType(MessageHeaders)}
131         * will return {@code false} if the {@link #setContentTypeResolver contentTypeResolver}
132         * is not defined or if no content-type header is present.
133         */
134        public void setStrictContentTypeMatch(boolean strictContentTypeMatch) {
135                if (strictContentTypeMatch) {
136                        Assert.notEmpty(getSupportedMimeTypes(), "Strict match requires non-empty list of supported mime types");
137                        Assert.notNull(getContentTypeResolver(), "Strict match requires ContentTypeResolver");
138                }
139                this.strictContentTypeMatch = strictContentTypeMatch;
140        }
141
142        /**
143         * Whether content type resolution must produce a value that matches one of
144         * the supported MIME types.
145         */
146        public boolean isStrictContentTypeMatch() {
147                return this.strictContentTypeMatch;
148        }
149
150        /**
151         * Configure the preferred serialization class to use (byte[] or String) when
152         * converting an Object payload to a {@link Message}.
153         * <p>The default value is byte[].
154         * @param payloadClass either byte[] or String
155         */
156        public void setSerializedPayloadClass(Class<?> payloadClass) {
157                Assert.isTrue(byte[].class == payloadClass || String.class == payloadClass,
158                                () -> "Payload class must be byte[] or String: " + payloadClass);
159                this.serializedPayloadClass = payloadClass;
160        }
161
162        /**
163         * Return the configured preferred serialization payload class.
164         */
165        public Class<?> getSerializedPayloadClass() {
166                return this.serializedPayloadClass;
167        }
168
169
170        @Override
171        @Nullable
172        public final Object fromMessage(Message<?> message, Class<?> targetClass) {
173                return fromMessage(message, targetClass, null);
174        }
175
176        @Override
177        @Nullable
178        public final Object fromMessage(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
179                if (!canConvertFrom(message, targetClass)) {
180                        return null;
181                }
182                return convertFromInternal(message, targetClass, conversionHint);
183        }
184
185        @Override
186        @Nullable
187        public final Message<?> toMessage(Object payload, @Nullable MessageHeaders headers) {
188                return toMessage(payload, headers, null);
189        }
190
191        @Override
192        @Nullable
193        public final Message<?> toMessage(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
194                if (!canConvertTo(payload, headers)) {
195                        return null;
196                }
197
198                Object payloadToUse = convertToInternal(payload, headers, conversionHint);
199                if (payloadToUse == null) {
200                        return null;
201                }
202
203                MimeType mimeType = getDefaultContentType(payloadToUse);
204                if (headers != null) {
205                        MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(headers, MessageHeaderAccessor.class);
206                        if (accessor != null && accessor.isMutable()) {
207                                if (mimeType != null) {
208                                        accessor.setHeaderIfAbsent(MessageHeaders.CONTENT_TYPE, mimeType);
209                                }
210                                return MessageBuilder.createMessage(payloadToUse, accessor.getMessageHeaders());
211                        }
212                }
213
214                MessageBuilder<?> builder = MessageBuilder.withPayload(payloadToUse);
215                if (headers != null) {
216                        builder.copyHeaders(headers);
217                }
218                if (mimeType != null) {
219                        builder.setHeaderIfAbsent(MessageHeaders.CONTENT_TYPE, mimeType);
220                }
221                return builder.build();
222        }
223
224
225        protected boolean canConvertFrom(Message<?> message, Class<?> targetClass) {
226                return (supports(targetClass) && supportsMimeType(message.getHeaders()));
227        }
228
229        protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) {
230                return (supports(payload.getClass()) && supportsMimeType(headers));
231        }
232
233        protected boolean supportsMimeType(@Nullable MessageHeaders headers) {
234                if (getSupportedMimeTypes().isEmpty()) {
235                        return true;
236                }
237                MimeType mimeType = getMimeType(headers);
238                if (mimeType == null) {
239                        return !isStrictContentTypeMatch();
240                }
241                for (MimeType current : getSupportedMimeTypes()) {
242                        if (current.getType().equals(mimeType.getType()) && current.getSubtype().equals(mimeType.getSubtype())) {
243                                return true;
244                        }
245                }
246                return false;
247        }
248
249        @Nullable
250        protected MimeType getMimeType(@Nullable MessageHeaders headers) {
251                return (headers != null && this.contentTypeResolver != null ? this.contentTypeResolver.resolve(headers) : null);
252        }
253
254        /**
255         * Return the default content type for the payload. Called when
256         * {@link #toMessage(Object, MessageHeaders)} is invoked without
257         * message headers or without a content type header.
258         * <p>By default, this returns the first element of the
259         * {@link #getSupportedMimeTypes() supportedMimeTypes}, if any.
260         * Can be overridden in subclasses.
261         * @param payload the payload being converted to a message
262         * @return the content type, or {@code null} if not known
263         */
264        @Nullable
265        protected MimeType getDefaultContentType(Object payload) {
266                List<MimeType> mimeTypes = getSupportedMimeTypes();
267                return (!mimeTypes.isEmpty() ? mimeTypes.get(0) : null);
268        }
269
270
271        /**
272         * Whether the given class is supported by this converter.
273         * @param clazz the class to test for support
274         * @return {@code true} if supported; {@code false} otherwise
275         */
276        protected abstract boolean supports(Class<?> clazz);
277
278        /**
279         * Convert the message payload from serialized form to an Object.
280         * @param message the input message
281         * @param targetClass the target class for the conversion
282         * @param conversionHint an extra object passed to the {@link MessageConverter},
283         * e.g. the associated {@code MethodParameter} (may be {@code null}}
284         * @return the result of the conversion, or {@code null} if the converter cannot
285         * perform the conversion
286         * @since 4.2
287         */
288        @Nullable
289        protected Object convertFromInternal(
290                        Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
291
292                return null;
293        }
294
295        /**
296         * Convert the payload object to serialized form.
297         * @param payload the Object to convert
298         * @param headers optional headers for the message (may be {@code null})
299         * @param conversionHint an extra object passed to the {@link MessageConverter},
300         * e.g. the associated {@code MethodParameter} (may be {@code null}}
301         * @return the resulting payload for the message, or {@code null} if the converter
302         * cannot perform the conversion
303         * @since 4.2
304         */
305        @Nullable
306        protected Object convertToInternal(
307                        Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
308
309                return null;
310        }
311
312}