View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.transport.jms;
8   
9   import org.mule.api.transport.OutputHandler;
10  import org.mule.util.ArrayUtils;
11  import org.mule.util.ClassUtils;
12  import org.mule.util.IOUtils;
13  import org.mule.util.StringUtils;
14  
15  import java.io.IOException;
16  import java.io.InputStream;
17  import java.io.ObjectOutputStream;
18  import java.io.Serializable;
19  import java.text.MessageFormat;
20  import java.util.ArrayList;
21  import java.util.Enumeration;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  
27  import javax.jms.BytesMessage;
28  import javax.jms.Destination;
29  import javax.jms.JMSException;
30  import javax.jms.MapMessage;
31  import javax.jms.Message;
32  import javax.jms.MessageEOFException;
33  import javax.jms.MessageFormatException;
34  import javax.jms.ObjectMessage;
35  import javax.jms.Queue;
36  import javax.jms.Session;
37  import javax.jms.StreamMessage;
38  import javax.jms.TextMessage;
39  import javax.jms.Topic;
40  
41  import org.apache.commons.io.output.ByteArrayOutputStream;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  
45  /**
46   * <code>JmsMessageUtils</code> contains helper method for dealing with JMS
47   * messages in Mule.
48   */
49  public class JmsMessageUtils
50  {
51      public static final char REPLACEMENT_CHAR = '_';
52  
53      private static final Log logger = LogFactory.getLog(JmsMessageUtils.class);
54  
55      /**
56       * Encode a String so that is is a valid JMS header name
57       *
58       * @param name the String to encode
59       * @return a valid JMS header name
60       */
61      public static String encodeHeader(String name)
62      {
63          // check against JMS 1.1 spec, sections 3.5.1 (3.8.1.1)
64          boolean nonCompliant = false;
65  
66          if (StringUtils.isEmpty(name))
67          {
68              throw new IllegalArgumentException("Header name to encode must not be null or empty");
69          }
70  
71          int i = 0, length = name.length();
72          while (i < length && Character.isJavaIdentifierPart(name.charAt(i)))
73          {
74              // zip through
75              i++;
76          }
77  
78          if (i == length)
79          {
80              // String is already valid
81              return name;
82          }
83          else
84          {
85              // make a copy, fix up remaining characters
86              StringBuffer sb = new StringBuffer(name);
87              for (int j = i; j < length; j++)
88              {
89                  if (!Character.isJavaIdentifierPart(sb.charAt(j)))
90                  {
91                      sb.setCharAt(j, REPLACEMENT_CHAR);
92                      nonCompliant = true;
93                  }
94              }
95  
96              if (nonCompliant)
97              {
98                  logger.warn(MessageFormat.format(
99                          "Header: {0} is not compliant with JMS specification (sec. 3.5.1, 3.8.1.1). It will cause " +
100                                 "problems in your and other applications. Please update your application code to correct this. " +
101                                 "Mule renamed it to {1}", name, sb.toString()));
102             }
103 
104             return sb.toString();
105         }
106     }
107 
108     public static Message toMessage(Object object, Session session) throws JMSException
109     {
110         if (object instanceof Message)
111         {
112             return (Message) object;
113         }
114         else if (object instanceof String)
115         {
116             return stringToMessage((String) object, session);
117         }
118         else if (object instanceof Map<?, ?> && validateMapMessageType((Map<?, ?>)object))
119         {
120             return mapToMessage((Map<?, ?>) object, session);
121         }
122         else if (object instanceof InputStream)
123         {
124             return inputStreamToMessage((InputStream) object, session);
125         }
126         else if (object instanceof List<?>)
127         {
128             return listToMessage((List<?>) object, session);
129         }
130         else if (object instanceof byte[])
131         {
132             return byteArrayToMessage((byte[]) object, session);
133         }
134         else if (object instanceof Serializable)
135         {
136             return serializableToMessage((Serializable) object, session);
137         }
138         else if (object instanceof OutputHandler)
139         {
140             return outputHandlerToMessage((OutputHandler) object, session);
141         }
142         else
143         {
144             throw new JMSException(
145                 "Source was not of a supported type. Valid types are Message, String, Map, InputStream, List, byte[], Serializable or OutputHandler, "
146                                 + "but was " + ClassUtils.getShortClassName(object, "<null>"));
147         }
148     }
149 
150     private static Message stringToMessage(String value, Session session) throws JMSException
151     {
152         return session.createTextMessage(value);
153     }
154 
155     private static Message mapToMessage(Map<?, ?> value, Session session) throws JMSException
156     {
157         MapMessage mMsg = session.createMapMessage();
158 
159         for (Iterator<?> i = value.entrySet().iterator(); i.hasNext();)
160         {
161             Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
162             mMsg.setObject(entry.getKey().toString(), entry.getValue());
163         }
164 
165         return mMsg;
166     }
167 
168     private static Message inputStreamToMessage(InputStream value, Session session) throws JMSException
169     {
170         StreamMessage streamMessage = session.createStreamMessage();
171         byte[] buffer = new byte[4096];
172         int len;
173 
174         try
175         {
176             while ((len = value.read(buffer)) != -1)
177             {
178                 streamMessage.writeBytes(buffer, 0, len);
179             }
180         }
181         catch (IOException e)
182         {
183             throw new JMSException("Failed to read input stream to create a stream message: " + e);
184         }
185         finally
186         {
187             IOUtils.closeQuietly(value);
188         }
189 
190         return streamMessage;
191     }
192 
193     private static Message listToMessage(List<?> value, Session session)
194         throws JMSException
195     {
196         StreamMessage sMsg = session.createStreamMessage();
197 
198         for (Iterator<?> iter = value.iterator(); iter.hasNext();)
199         {
200             Object o = iter.next();
201             if (validateStreamMessageType(o))
202             {
203                 sMsg.writeObject(o);
204             }
205             else
206             {
207                 throw new MessageFormatException(String.format(
208                     "Invalid type passed to StreamMessage: %s . Allowed types are: "
209                                     + "Boolean, Byte, Short, Character, Integer, Long, Float, Double,"
210                                     + "String and byte[]", ClassUtils.getShortClassName(o, "null")));
211             }
212         }
213         return sMsg;
214     }
215 
216     private static Message byteArrayToMessage(byte[] value, Session session) throws JMSException
217     {
218         BytesMessage bMsg = session.createBytesMessage();
219         bMsg.writeBytes(value);
220 
221         return bMsg;
222     }
223 
224     private static Message serializableToMessage(Serializable value, Session session) throws JMSException
225     {
226         ObjectMessage oMsg = session.createObjectMessage();
227         oMsg.setObject(value);
228 
229         return oMsg;
230     }
231 
232     private static Message outputHandlerToMessage(OutputHandler value, Session session) throws JMSException
233     {
234         ByteArrayOutputStream output = new ByteArrayOutputStream();
235         try
236         {
237             value.write(null, output);
238         }
239         catch (IOException e)
240         {
241             JMSException j = new JMSException("Could not serialize OutputHandler.");
242             j.initCause(e);
243             throw j;
244         }
245 
246         BytesMessage bMsg = session.createBytesMessage();
247         bMsg.writeBytes(output.toByteArray());
248 
249         return bMsg;
250     }
251 
252     public static Object toObject(Message source, String jmsSpec, String encoding) throws JMSException, IOException
253     {
254         if (source instanceof ObjectMessage)
255         {
256             return ((ObjectMessage) source).getObject();
257         }
258         else if (source instanceof MapMessage)
259         {
260             Map<String, Object> map = new HashMap<String, Object>();
261             MapMessage m = (MapMessage) source;
262 
263             for (Enumeration<?> e = m.getMapNames(); e.hasMoreElements();)
264             {
265                 String name = (String) e.nextElement();
266                 Object obj = m.getObject(name);
267                 map.put(name, obj);
268             }
269 
270             return map;
271         }
272         else if (source instanceof TextMessage)
273         {
274             return ((TextMessage) source).getText();
275         }
276         else if (source instanceof BytesMessage)
277         {
278             return toByteArray(source, jmsSpec, encoding);
279         }
280         else if (source instanceof StreamMessage)
281         {
282             List<Object> result = new ArrayList<Object>();
283             try
284             {
285                 StreamMessage sMsg = (StreamMessage) source;
286                 Object obj;
287                 while ((obj = sMsg.readObject()) != null)
288                 {
289                     result.add(obj);
290                 }
291             }
292             catch (MessageEOFException eof)
293             {
294                 // ignored
295             }
296             catch (Exception e)
297             {
298                 throw new JMSException("Failed to extract information from JMS Stream Message: " + e);
299             }
300             return result;
301         }
302 
303         // what else is there to do?
304         return source;
305     }
306 
307     /**
308      * @param message the message to receive the bytes from. Note this only works for
309      *                TextMessge, ObjectMessage, StreamMessage and BytesMessage.
310      * @param jmsSpec indicates the JMS API version, either
311      *                {@link JmsConstants#JMS_SPECIFICATION_102B} or
312      *                {@link JmsConstants#JMS_SPECIFICATION_11}. Any other value
313      *                including <code>null</code> is treated as fallback to
314      *                {@link JmsConstants#JMS_SPECIFICATION_102B}.
315      * @return a byte array corresponding with the message payload
316      * @throws JMSException        if the message can't be read or if the message passed is
317      *                             a MapMessage
318      * @throws java.io.IOException if a failure occurs while reading the stream and
319      *                             converting the message data
320      */
321     public static byte[] toByteArray(Message message, String jmsSpec, String encoding) throws JMSException, IOException
322     {
323         if (message instanceof BytesMessage)
324         {
325             BytesMessage bMsg = (BytesMessage) message;
326             bMsg.reset();
327 
328             if (JmsConstants.JMS_SPECIFICATION_11.equals(jmsSpec))
329             {
330                 long bmBodyLength = bMsg.getBodyLength();
331                 if (bmBodyLength > Integer.MAX_VALUE)
332                 {
333                     throw new JMSException("Size of BytesMessage exceeds Integer.MAX_VALUE; "
334                             + "please consider using JMS StreamMessage instead");
335                 }
336 
337                 if (bmBodyLength > 0)
338                 {
339                     byte[] bytes = new byte[(int) bmBodyLength];
340                     bMsg.readBytes(bytes);
341                     return bytes;
342                 }
343                 else
344                 {
345                     return ArrayUtils.EMPTY_BYTE_ARRAY;
346                 }
347             }
348             else
349             {
350                 ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
351                 byte[] buffer = new byte[4096];
352                 int len;
353 
354                 while ((len = bMsg.readBytes(buffer)) != -1)
355                 {
356                     baos.write(buffer, 0, len);
357                 }
358 
359                 if (baos.size() > 0)
360                 {
361                     return baos.toByteArray();
362                 }
363                 else
364                 {
365                     return ArrayUtils.EMPTY_BYTE_ARRAY;
366                 }
367             }
368         }
369         else if (message instanceof StreamMessage)
370         {
371             StreamMessage sMsg = (StreamMessage) message;
372             sMsg.reset();
373 
374             ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
375             byte[] buffer = new byte[4096];
376             int len;
377 
378             while ((len = sMsg.readBytes(buffer)) != -1)
379             {
380                 baos.write(buffer, 0, len);
381             }
382 
383             return baos.toByteArray();
384         }
385         else if (message instanceof ObjectMessage)
386         {
387             ObjectMessage oMsg = (ObjectMessage) message;
388             ByteArrayOutputStream baos = new ByteArrayOutputStream();
389             ObjectOutputStream os = new ObjectOutputStream(baos);
390             os.writeObject(oMsg.getObject());
391             os.flush();
392             os.close();
393             return baos.toByteArray();
394         }
395         else if (message instanceof TextMessage)
396         {
397             TextMessage tMsg = (TextMessage) message;
398             String tMsgText = tMsg.getText();
399 
400             if (null == tMsgText)
401             {
402                 // Avoid creating new instances of byte arrays, even empty ones. The
403                 // load on this part of the code can be high.
404                 return ArrayUtils.EMPTY_BYTE_ARRAY;
405             }
406             else
407             {
408                 return tMsgText.getBytes(encoding);
409             }
410         }
411         else
412         {
413             throw new JMSException("Cannot get bytes from Map Message");
414         }
415     }
416 
417     public static String getNameForDestination(Destination dest) throws JMSException
418     {
419         if (dest instanceof Queue)
420         {
421             return ((Queue) dest).getQueueName();
422         }
423         else if (dest instanceof Topic)
424         {
425             return ((Topic) dest).getTopicName();
426         }
427         else
428         {
429             return null;
430         }
431     }
432 
433     public static Message copyJMSProperties(Message from, Message to, JmsConnector connector)
434             throws JMSException
435     {
436         if (connector.supportsProperty(JmsConstants.JMS_CORRELATION_ID))
437         {
438             to.setJMSCorrelationID(from.getJMSCorrelationID());
439         }
440         if (connector.supportsProperty(JmsConstants.JMS_DELIVERY_MODE))
441         {
442             to.setJMSDeliveryMode(from.getJMSDeliveryMode());
443         }
444         if (connector.supportsProperty(JmsConstants.JMS_DESTINATION))
445         {
446             to.setJMSDestination(from.getJMSDestination());
447         }
448         if (connector.supportsProperty(JmsConstants.JMS_EXPIRATION))
449         {
450             to.setJMSExpiration(from.getJMSExpiration());
451         }
452         if (connector.supportsProperty(JmsConstants.JMS_MESSAGE_ID))
453         {
454             to.setJMSMessageID(from.getJMSMessageID());
455         }
456         if (connector.supportsProperty(JmsConstants.JMS_PRIORITY))
457         {
458             to.setJMSPriority(from.getJMSPriority());
459         }
460         if (connector.supportsProperty(JmsConstants.JMS_REDELIVERED))
461         {
462             to.setJMSRedelivered(from.getJMSRedelivered());
463         }
464         if (connector.supportsProperty(JmsConstants.JMS_REPLY_TO))
465         {
466             to.setJMSReplyTo(from.getJMSReplyTo());
467         }
468         if (connector.supportsProperty(JmsConstants.JMS_TIMESTAMP))
469         {
470             to.setJMSTimestamp(from.getJMSTimestamp());
471         }
472         if (connector.supportsProperty(JmsConstants.JMS_TYPE))
473         {
474             to.setJMSType(from.getJMSType());
475         }
476         return to;
477     }
478 
479     /**
480      * {@link StreamMessage#writeObject(Object)} accepts only primitives (and wrappers), String and byte[].
481      * An attempt to write anything else must fail with a MessageFormatException as per
482      * JMS 1.1 spec, Section 7.3 Standard Exceptions, page 89, 1st paragraph.
483      * <p/>
484      * Unfortunately, some JMS vendors are not compliant in this area, enforce here for consistent behavior.
485      *
486      * @param candidate object to validate
487      */
488     protected static boolean validateStreamMessageType(Object candidate)
489     {
490         if (candidate == null ||
491                 candidate instanceof Boolean ||
492                 candidate instanceof Byte ||
493                 candidate instanceof Short ||
494                 candidate instanceof Character ||
495                 candidate instanceof Integer ||
496                 candidate instanceof Long ||
497                 candidate instanceof Float ||
498                 candidate instanceof Double ||
499                 candidate instanceof String ||
500                 candidate instanceof byte[])
501         {
502             return true;
503         }
504 
505         return false;
506     }
507 
508     /**
509      * <code>MapMessage#writeObject(Object)</code> accepts only primitives (and wrappers), String and byte[].
510      * An attempt to write anything else must fail with a MessageFormatException as per
511      * JMS 1.1 spec, Section 7.3 Standard Exceptions, page 89, 1st paragraph.
512      * <p/>
513      * Unfortunately, some JMS vendors are not compliant in this area, enforce here for consistent behavior.
514      * Here we handle non-primitive maps as {@link ObjectMessage} rather than creating a {@link MapMessage}
515      *
516      * @param candidate Map to validate
517      */
518     protected static boolean validateMapMessageType(Map<?, ?> candidate)
519     {
520         for (Iterator<?> iterator = candidate.values().iterator(); iterator.hasNext();)
521         {
522             Object o = iterator.next();
523             if (!validateStreamMessageType(o))
524             {
525                     return false;
526             }
527         }
528         return true;
529     }
530 }