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.jms.support.converter;
018
019import java.io.ByteArrayInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.StringReader;
023import java.io.StringWriter;
024
025import javax.jms.BytesMessage;
026import javax.jms.JMSException;
027import javax.jms.Message;
028import javax.jms.Session;
029import javax.jms.TextMessage;
030import javax.xml.transform.Result;
031import javax.xml.transform.Source;
032import javax.xml.transform.stream.StreamResult;
033import javax.xml.transform.stream.StreamSource;
034
035import org.springframework.beans.factory.InitializingBean;
036import org.springframework.lang.Nullable;
037import org.springframework.oxm.Marshaller;
038import org.springframework.oxm.Unmarshaller;
039import org.springframework.oxm.XmlMappingException;
040import org.springframework.util.Assert;
041
042/**
043 * Spring JMS {@link MessageConverter} that uses a {@link Marshaller} and {@link Unmarshaller}.
044 * Marshals an object to a {@link BytesMessage}, or to a {@link TextMessage} if the
045 * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}.
046 * Unmarshals from a {@link TextMessage} or {@link BytesMessage} to an object.
047 *
048 * @author Arjen Poutsma
049 * @author Juergen Hoeller
050 * @since 3.0
051 */
052public class MarshallingMessageConverter implements MessageConverter, InitializingBean {
053
054        @Nullable
055        private Marshaller marshaller;
056
057        @Nullable
058        private Unmarshaller unmarshaller;
059
060        private MessageType targetType = MessageType.BYTES;
061
062
063        /**
064         * Construct a new {@code MarshallingMessageConverter} with no {@link Marshaller}
065         * or {@link Unmarshaller} set. The marshaller must be set after construction by invoking
066         * {@link #setMarshaller(Marshaller)} and {@link #setUnmarshaller(Unmarshaller)} .
067         */
068        public MarshallingMessageConverter() {
069        }
070
071        /**
072         * Construct a new {@code MarshallingMessageConverter} with the given {@link Marshaller} set.
073         * <p>If the given {@link Marshaller} also implements the {@link Unmarshaller} interface,
074         * it is used for both marshalling and unmarshalling. Otherwise, an exception is thrown.
075         * <p>Note that all {@link Marshaller} implementations in Spring also implement the
076         * {@link Unmarshaller} interface, so that you can safely use this constructor.
077         * @param marshaller object used as marshaller and unmarshaller
078         * @throws IllegalArgumentException when {@code marshaller} does not implement the
079         * {@link Unmarshaller} interface as well
080         */
081        public MarshallingMessageConverter(Marshaller marshaller) {
082                Assert.notNull(marshaller, "Marshaller must not be null");
083                if (!(marshaller instanceof Unmarshaller)) {
084                        throw new IllegalArgumentException(
085                                        "Marshaller [" + marshaller + "] does not implement the Unmarshaller " +
086                                        "interface. Please set an Unmarshaller explicitly by using the " +
087                                        "MarshallingMessageConverter(Marshaller, Unmarshaller) constructor.");
088                }
089                else {
090                        this.marshaller = marshaller;
091                        this.unmarshaller = (Unmarshaller) marshaller;
092                }
093        }
094
095        /**
096         * Construct a new {@code MarshallingMessageConverter} with the
097         * given Marshaller and Unmarshaller.
098         * @param marshaller the Marshaller to use
099         * @param unmarshaller the Unmarshaller to use
100         */
101        public MarshallingMessageConverter(Marshaller marshaller, Unmarshaller unmarshaller) {
102                Assert.notNull(marshaller, "Marshaller must not be null");
103                Assert.notNull(unmarshaller, "Unmarshaller must not be null");
104                this.marshaller = marshaller;
105                this.unmarshaller = unmarshaller;
106        }
107
108
109        /**
110         * Set the {@link Marshaller} to be used by this message converter.
111         */
112        public void setMarshaller(Marshaller marshaller) {
113                Assert.notNull(marshaller, "Marshaller must not be null");
114                this.marshaller = marshaller;
115        }
116
117        /**
118         * Set the {@link Unmarshaller} to be used by this message converter.
119         */
120        public void setUnmarshaller(Unmarshaller unmarshaller) {
121                Assert.notNull(unmarshaller, "Unmarshaller must not be null");
122                this.unmarshaller = unmarshaller;
123        }
124
125        /**
126         * Specify whether {@link #toMessage(Object, Session)} should marshal to
127         * a {@link BytesMessage} or a {@link TextMessage}.
128         * <p>The default is {@link MessageType#BYTES}, i.e. this converter marshals
129         * to a {@link BytesMessage}. Note that the default version of this converter
130         * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only.
131         * @see MessageType#BYTES
132         * @see MessageType#TEXT
133         */
134        public void setTargetType(MessageType targetType) {
135                Assert.notNull(targetType, "MessageType must not be null");
136                this.targetType = targetType;
137        }
138
139        @Override
140        public void afterPropertiesSet() {
141                Assert.notNull(this.marshaller, "Property 'marshaller' is required");
142                Assert.notNull(this.unmarshaller, "Property 'unmarshaller' is required");
143        }
144
145
146        /**
147         * This implementation marshals the given object to a {@link javax.jms.TextMessage} or
148         * {@link javax.jms.BytesMessage}. The desired message type can be defined by setting
149         * the {@link #setTargetType "marshalTo"} property.
150         * @see #marshalToTextMessage
151         * @see #marshalToBytesMessage
152         */
153        @Override
154        public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException {
155                Assert.state(this.marshaller != null, "No Marshaller set");
156                try {
157                        switch (this.targetType) {
158                                case TEXT:
159                                        return marshalToTextMessage(object, session, this.marshaller);
160                                case BYTES:
161                                        return marshalToBytesMessage(object, session, this.marshaller);
162                                default:
163                                        return marshalToMessage(object, session, this.marshaller, this.targetType);
164                        }
165                }
166                catch (XmlMappingException | IOException ex) {
167                        throw new MessageConversionException("Could not marshal [" + object + "]", ex);
168                }
169        }
170
171        /**
172         * This implementation unmarshals the given {@link Message} into an object.
173         * @see #unmarshalFromTextMessage
174         * @see #unmarshalFromBytesMessage
175         */
176        @Override
177        public Object fromMessage(Message message) throws JMSException, MessageConversionException {
178                Assert.state(this.unmarshaller != null, "No Unmarshaller set");
179                try {
180                        if (message instanceof TextMessage) {
181                                TextMessage textMessage = (TextMessage) message;
182                                return unmarshalFromTextMessage(textMessage, this.unmarshaller);
183                        }
184                        else if (message instanceof BytesMessage) {
185                                BytesMessage bytesMessage = (BytesMessage) message;
186                                return unmarshalFromBytesMessage(bytesMessage, this.unmarshaller);
187                        }
188                        else {
189                                return unmarshalFromMessage(message, this.unmarshaller);
190                        }
191                }
192                catch (IOException ex) {
193                        throw new MessageConversionException("Could not access message content: " + message, ex);
194                }
195                catch (XmlMappingException ex) {
196                        throw new MessageConversionException("Could not unmarshal message: " + message, ex);
197                }
198        }
199
200
201        /**
202         * Marshal the given object to a {@link TextMessage}.
203         * @param object the object to be marshalled
204         * @param session current JMS session
205         * @param marshaller the marshaller to use
206         * @return the resulting message
207         * @throws JMSException if thrown by JMS methods
208         * @throws IOException in case of I/O errors
209         * @throws XmlMappingException in case of OXM mapping errors
210         * @see Session#createTextMessage
211         * @see Marshaller#marshal(Object, Result)
212         */
213        protected TextMessage marshalToTextMessage(Object object, Session session, Marshaller marshaller)
214                        throws JMSException, IOException, XmlMappingException {
215
216                StringWriter writer = new StringWriter(1024);
217                Result result = new StreamResult(writer);
218                marshaller.marshal(object, result);
219                return session.createTextMessage(writer.toString());
220        }
221
222        /**
223         * Marshal the given object to a {@link BytesMessage}.
224         * @param object the object to be marshalled
225         * @param session current JMS session
226         * @param marshaller the marshaller to use
227         * @return the resulting message
228         * @throws JMSException if thrown by JMS methods
229         * @throws IOException in case of I/O errors
230         * @throws XmlMappingException in case of OXM mapping errors
231         * @see Session#createBytesMessage
232         * @see Marshaller#marshal(Object, Result)
233         */
234        protected BytesMessage marshalToBytesMessage(Object object, Session session, Marshaller marshaller)
235                        throws JMSException, IOException, XmlMappingException {
236
237                ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
238                StreamResult streamResult = new StreamResult(bos);
239                marshaller.marshal(object, streamResult);
240                BytesMessage message = session.createBytesMessage();
241                message.writeBytes(bos.toByteArray());
242                return message;
243        }
244
245        /**
246         * Template method that allows for custom message marshalling.
247         * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or
248         * {@link MessageType#BYTES}.
249         * <p>The default implementation throws an {@link IllegalArgumentException}.
250         * @param object the object to marshal
251         * @param session the JMS session
252         * @param marshaller the marshaller to use
253         * @param targetType the target message type (other than TEXT or BYTES)
254         * @return the resulting message
255         * @throws JMSException if thrown by JMS methods
256         * @throws IOException in case of I/O errors
257         * @throws XmlMappingException in case of OXM mapping errors
258         */
259        protected Message marshalToMessage(Object object, Session session, Marshaller marshaller, MessageType targetType)
260                        throws JMSException, IOException, XmlMappingException {
261
262                throw new IllegalArgumentException("Unsupported message type [" + targetType +
263                                "]. MarshallingMessageConverter by default only supports TextMessages and BytesMessages.");
264        }
265
266
267        /**
268         * Unmarshal the given {@link TextMessage} into an object.
269         * @param message the message
270         * @param unmarshaller the unmarshaller to use
271         * @return the unmarshalled object
272         * @throws JMSException if thrown by JMS methods
273         * @throws IOException in case of I/O errors
274         * @throws XmlMappingException in case of OXM mapping errors
275         * @see Unmarshaller#unmarshal(Source)
276         */
277        protected Object unmarshalFromTextMessage(TextMessage message, Unmarshaller unmarshaller)
278                        throws JMSException, IOException, XmlMappingException {
279
280                Source source = new StreamSource(new StringReader(message.getText()));
281                return unmarshaller.unmarshal(source);
282        }
283
284        /**
285         * Unmarshal the given {@link BytesMessage} into an object.
286         * @param message the message
287         * @param unmarshaller the unmarshaller to use
288         * @return the unmarshalled object
289         * @throws JMSException if thrown by JMS methods
290         * @throws IOException in case of I/O errors
291         * @throws XmlMappingException in case of OXM mapping errors
292         * @see Unmarshaller#unmarshal(Source)
293         */
294        protected Object unmarshalFromBytesMessage(BytesMessage message, Unmarshaller unmarshaller)
295                        throws JMSException, IOException, XmlMappingException {
296
297                byte[] bytes = new byte[(int) message.getBodyLength()];
298                message.readBytes(bytes);
299                ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
300                StreamSource source = new StreamSource(bis);
301                return unmarshaller.unmarshal(source);
302        }
303
304        /**
305         * Template method that allows for custom message unmarshalling.
306         * Invoked when {@link #fromMessage(Message)} is invoked with a message
307         * that is not a {@link TextMessage} or {@link BytesMessage}.
308         * <p>The default implementation throws an {@link IllegalArgumentException}.
309         * @param message the message
310         * @param unmarshaller the unmarshaller to use
311         * @return the unmarshalled object
312         * @throws JMSException if thrown by JMS methods
313         * @throws IOException in case of I/O errors
314         * @throws XmlMappingException in case of OXM mapping errors
315         */
316        protected Object unmarshalFromMessage(Message message, Unmarshaller unmarshaller)
317                        throws JMSException, IOException, XmlMappingException {
318
319                throw new IllegalArgumentException("Unsupported message type [" + message.getClass() +
320                                "]. MarshallingMessageConverter by default only supports TextMessages and BytesMessages.");
321        }
322
323}