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.MuleEvent;
10  import org.mule.api.MuleException;
11  import org.mule.api.MuleMessage;
12  import org.mule.api.config.MuleProperties;
13  import org.mule.api.endpoint.EndpointBuilder;
14  import org.mule.api.endpoint.EndpointException;
15  import org.mule.api.endpoint.OutboundEndpoint;
16  import org.mule.api.lifecycle.InitialisationException;
17  import org.mule.api.transaction.Transaction;
18  import org.mule.api.transformer.TransformerException;
19  import org.mule.api.transport.DispatchException;
20  import org.mule.config.i18n.CoreMessages;
21  import org.mule.transaction.TransactionCoordination;
22  import org.mule.transport.AbstractMessageDispatcher;
23  import org.mule.transport.jms.i18n.JmsMessages;
24  import org.mule.util.ClassUtils;
25  import org.mule.util.NumberUtils;
26  import org.mule.util.concurrent.Latch;
27  import org.mule.util.concurrent.WaitableBoolean;
28  
29  import javax.jms.DeliveryMode;
30  import javax.jms.Destination;
31  import javax.jms.JMSException;
32  import javax.jms.Message;
33  import javax.jms.MessageConsumer;
34  import javax.jms.MessageListener;
35  import javax.jms.MessageProducer;
36  import javax.jms.Session;
37  import javax.jms.TemporaryQueue;
38  import javax.jms.TemporaryTopic;
39  
40  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
41  
42  /**
43   * <code>JmsMessageDispatcher</code> is responsible for dispatching messages to JMS
44   * destinations. All JMS semantics apply and settings such as replyTo and QoS
45   * properties are read from the event properties or defaults are used (according to
46   * the JMS specification)
47   */
48  public class JmsMessageDispatcher extends AbstractMessageDispatcher
49  {
50  
51      private JmsConnector connector;
52      private Session cachedSession;
53      private boolean disableTemporaryDestinations = false;
54      private boolean returnOriginalMessageAsReply = false;
55  
56      public JmsMessageDispatcher(OutboundEndpoint endpoint)
57      {
58          super(endpoint);
59          this.connector = (JmsConnector) endpoint.getConnector();
60          disableTemporaryDestinations = connector.isDisableTemporaryReplyToDestinations() ||
61              ("true".equals(endpoint.getProperty(JmsConstants.DISABLE_TEMP_DESTINATIONS_PROPERTY)));
62          returnOriginalMessageAsReply = connector.isReturnOriginalMessageAsReply() ||
63              ("true".equals(endpoint.getProperty(JmsConstants.RETURN_ORIGINAL_MESSAGE_PROPERTY)));
64          if (returnOriginalMessageAsReply && !disableTemporaryDestinations)
65          {
66              logger.warn("The returnOriginalMessageAsReply property will be ignored because disableTemporaryReplyToDestinations=false.  You need to disable temporary ReplyTo destinations in order for this propery to take effect.");
67          }
68      }
69  
70      @Override
71      protected void doDispatch(MuleEvent event) throws Exception
72      {
73          if (connector.getConnection() == null)
74          {
75              throw new IllegalStateException("No JMS Connection");
76          }
77          dispatchMessage(event, false);
78      }
79  
80      @Override
81      protected void doConnect() throws Exception
82      {
83          // template method
84      }
85  
86      @Override
87      protected void doDisconnect() throws Exception
88      {
89          // template method
90      }
91  
92      protected boolean isDisableTemporaryDestinations()
93      {
94          return disableTemporaryDestinations;
95      }
96  
97      private MuleMessage dispatchMessage(MuleEvent event, boolean doSend) throws Exception
98      {
99          Session session = null;
100         MessageProducer producer = null;
101         MessageConsumer consumer = null;
102         Destination replyTo = null;
103         boolean transacted = false;
104         boolean cached = false;
105         boolean useReplyToDestination;
106 
107         final Transaction muleTx = TransactionCoordination.getInstance().getTransaction();
108 
109         if (logger.isDebugEnabled())
110         {
111             logger.debug("dispatching on endpoint: " + event.getEndpoint().getEndpointURI()
112                     + ". MuleEvent id is: " + event.getId()
113                     + ". Outbound transformers are: " + event.getEndpoint().getTransformers());
114         }
115 
116         // assume session is transacted first, and thus, managed
117         boolean sessionManaged = true;
118         try
119         {
120             session = connector.getSessionFromTransaction();
121             if (session != null)
122             {
123                 transacted = true;
124             }
125             // Should we be caching sessions? Note this is not part of the JMS spec.
126             // and is turned off by default.
127             else if (event.getMessage().getOutboundProperty(JmsConstants.CACHE_JMS_SESSIONS_PROPERTY, connector.isCacheJmsSessions()))
128             {
129                 sessionManaged = false;
130                 cached = true;
131                 if (cachedSession != null)
132                 {
133                     session = cachedSession;
134                 }
135                 else
136                 {
137                     session = connector.getSession(event.getEndpoint());
138                     cachedSession = session;
139                 }
140             }
141             else
142             {
143                 // by now we're running with a different connector and connection
144                 sessionManaged = muleTx != null && muleTx.isXA();
145 
146                 session = connector.getSession(event.getEndpoint());
147                 if (event.getEndpoint().getTransactionConfig().isTransacted())
148                 {
149                     transacted = true;
150                 }
151             }
152 
153             // If a transaction is running, we can not receive any messages
154             // in the same transaction using a replyTo destination
155             useReplyToDestination = returnResponse(event, doSend) && !transacted;
156 
157             boolean topic = connector.getTopicResolver().isTopic(event.getEndpoint(), true);
158 
159             Destination dest = connector.getJmsSupport().createDestination(session, endpoint);
160             producer = connector.getJmsSupport().createProducer(session, dest, topic);
161 
162             Object message = event.getMessage().getPayload();
163             if (!(message instanceof Message))
164             {
165                 throw new DispatchException(
166                         JmsMessages.checkTransformer("JMS message", message.getClass(), connector.getName()),
167                         event, (OutboundEndpoint) endpoint);
168             }
169 
170             Message msg = (Message) message;
171 
172             MuleMessage eventMsg = event.getMessage();
173 
174             replyTo = getReplyToDestination(msg, session, event, useReplyToDestination, topic);
175 
176             // Set the replyTo property
177             if (replyTo != null)
178             {
179                 msg.setJMSReplyTo(replyTo);
180             }
181 
182             //Allow overrides to alter the message if necessary
183             processMessage(msg, event);
184 
185             // QoS support
186             long ttl = eventMsg.getOutboundProperty(JmsConstants.TIME_TO_LIVE_PROPERTY, Message.DEFAULT_TIME_TO_LIVE);
187             int priority = eventMsg.getOutboundProperty(JmsConstants.PRIORITY_PROPERTY, Message.DEFAULT_PRIORITY);
188             boolean persistent= eventMsg.getOutboundProperty(JmsConstants.PERSISTENT_DELIVERY_PROPERTY, connector.isPersistentDelivery());
189 
190             // If we are honouring the current QoS message headers we need to use the ones set on the current message
191             if (connector.isHonorQosHeaders())
192             {
193                 Object priorityProp = eventMsg.getInboundProperty(JmsConstants.JMS_PRIORITY);
194                 Object deliveryModeProp = eventMsg.getInboundProperty(JmsConstants.JMS_DELIVERY_MODE);
195 
196                 if (priorityProp != null)
197                 {
198                     priority = NumberUtils.toInt(priorityProp);
199                 }
200                 if (deliveryModeProp != null)
201                 {
202                     persistent = NumberUtils.toInt(deliveryModeProp) == DeliveryMode.PERSISTENT;
203                 }
204             }
205 
206             if (logger.isDebugEnabled())
207             {
208                 logger.debug("Sending message of type " + ClassUtils.getSimpleName(msg.getClass()));
209                 logger.debug("Sending JMS Message type " + msg.getJMSType() +
210                        "\n  JMSMessageID=" + msg.getJMSMessageID() +
211                        "\n  JMSCorrelationID=" + msg.getJMSCorrelationID() +
212                        "\n  JMSDeliveryMode=" + (persistent ? DeliveryMode.PERSISTENT : DeliveryMode.NON_PERSISTENT) +
213                        "\n  JMSPriority=" + priority +
214                        "\n  JMSReplyTo=" + msg.getJMSReplyTo());
215             }
216             connector.getJmsSupport().send(producer, msg, persistent, priority, ttl, topic, endpoint);
217 
218             if (useReplyToDestination && replyTo != null)
219             {
220                 consumer = createReplyToConsumer(msg, event, session, replyTo, topic);
221 
222                 if (topic)
223                 {
224                     // need to register a listener for a topic
225                     Latch l = new Latch();
226                     ReplyToListener listener = new ReplyToListener(l);
227                     consumer.setMessageListener(listener);
228 
229                     connector.getJmsSupport().send(producer, msg, persistent, priority, ttl, topic, endpoint);
230 
231                     int timeout = event.getTimeout();
232 
233                     if (logger.isDebugEnabled())
234                     {
235                         logger.debug("Waiting for return event for: " + timeout + " ms on " + replyTo);
236                     }
237 
238                     l.await(timeout, TimeUnit.MILLISECONDS);
239                     consumer.setMessageListener(null);
240                     listener.release();
241                     Message result = listener.getMessage();
242                     if (result == null)
243                     {
244                         logger.debug("No message was returned via replyTo destination");
245                         return createNullMuleMessage();
246                     }
247                     else
248                     {
249                         return createMessageWithJmsMessagePayload(result);
250                     }
251                 }
252                 else
253                 {
254                     int timeout = event.getTimeout();
255 
256                     if (logger.isDebugEnabled())
257                     {
258                         logger.debug("Waiting for return event for: " + timeout + " ms on " + replyTo);
259                     }
260 
261                     Message result = consumer.receive(timeout);
262                     if (result == null)
263                     {
264                         logger.debug("No message was returned via replyTo destination " + replyTo);
265                         return createNullMuleMessage();
266                     }
267                     else
268                     {
269                         return createMessageWithJmsMessagePayload(result);
270                     }
271                 }
272             }
273             else
274             {
275                 // In this case a response was never expected so we return null and not NullPayload.
276                 // This generally happens when dispatch is used for an asynchronous endpoint but can also occur when send() is used 
277                 // and disableTempDestinations is set.
278                 return returnOriginalMessageAsReply ? createMuleMessage(msg) : null;
279             }
280         }
281         finally
282         {
283             connector.closeQuietly(producer);
284             connector.closeQuietly(consumer);
285 
286             // TODO AP check if TopicResolver is to be utilized for temp destinations as well
287             if (replyTo != null && (replyTo instanceof TemporaryQueue || replyTo instanceof TemporaryTopic))
288             {
289                 if (replyTo instanceof TemporaryQueue)
290                 {
291                     connector.closeQuietly((TemporaryQueue) replyTo);
292                 }
293                 else
294                 {
295                     // hope there are no more non-standard tricks from JMS vendors
296                     // here ;)
297                     connector.closeQuietly((TemporaryTopic) replyTo);
298                 }
299             }
300 
301             if (!sessionManaged && transacted) { 
302                 handleMultiTx(session);
303             }
304 
305 
306             // If the session is from the current transaction, it is up to the
307             // transaction to close it.
308             if (session != null && !cached && !transacted)
309             {
310                 connector.closeQuietly(session);
311             }
312         }
313     }
314 
315     protected MuleMessage createMessageWithJmsMessagePayload(Message jmsMessage) throws Exception
316     {
317         MuleMessage muleMessage = createMuleMessage(jmsMessage);
318         Object payload = JmsMessageUtils.toObject(jmsMessage, connector.getSpecification(),
319             endpoint.getEncoding());
320         muleMessage.setPayload(payload);
321         return muleMessage;
322     }
323     
324     /**
325      * This method is called before the current message is transformed.  It can be used to do any message body or
326      * header processing before the transformer is called.
327      *
328      * @param message the current MuleMessage Being processed
329      * @throws Exception
330      */
331     protected void preTransformMessage(MuleMessage message) throws Exception
332     {
333         // nothing to do
334     }
335     
336     protected void handleMultiTx(Session session) throws Exception
337     {
338         logger.debug("Multi-transaction support is not available in Mule Community Edition.");
339     }
340 
341     @Override
342     protected MuleMessage doSend(MuleEvent event) throws Exception
343     {
344         return dispatchMessage(event, true);
345     }
346 
347     @Override
348     protected void doDispose()
349     {
350         // template method
351     }
352 
353     /**
354      * This method is called once the JMS message is created.  It allows subclasses to alter the
355      * message if necessary.
356      *
357      * @param msg   The JMS message that will be sent
358      * @param event the current event
359      * @throws JMSException if the JmsMessage cannot be written to, this should not happen because 
360      *          the JMSMessage passed in will always be newly created
361      */
362     protected void processMessage(Message msg, MuleEvent event) throws JMSException
363     {
364         // template Method
365     }
366 
367     /**
368      * Some JMS implementations do not support ReplyTo or require some further fiddling of the message
369      *
370      * @param msg   The JMS message that will be sent
371      * @param event the current event
372      * @return true if this request should honour any JMSReplyTo settings on the message
373      * @throws JMSException if the JmsMessage cannot be written to, this should not happen because the JMSMessage passed
374      *                      in will always be newly created
375      */
376     protected boolean isHandleReplyTo(Message msg, MuleEvent event) throws JMSException
377     {
378         return connector.supportsProperty(JmsConstants.JMS_REPLY_TO);
379     }
380 
381     protected MessageConsumer createReplyToConsumer(Message currentMessage, MuleEvent event,
382                                                     Session session, Destination replyTo, boolean topic) throws JMSException
383     {
384         String selector = null;
385         //Only used by topics
386         String durableName;
387         //If we're not using
388         if (!(replyTo instanceof TemporaryQueue || replyTo instanceof TemporaryTopic))
389         {
390             String jmsCorrelationId = currentMessage.getJMSCorrelationID();
391             if (jmsCorrelationId == null)
392             {
393                 jmsCorrelationId = currentMessage.getJMSMessageID();
394             }
395 
396             selector = "JMSCorrelationID='" + jmsCorrelationId + "'";
397             if (logger.isDebugEnabled())
398             {
399                 logger.debug("ReplyTo Selector is: " + selector);
400             }
401         }
402 
403         //We need to set the durableName and Selector if using topics
404         if (topic)
405         {
406             String tempDurable = (String) event.getEndpoint().getProperties().get(JmsConstants.DURABLE_PROPERTY);
407             boolean durable = connector.isDurable();
408             if (tempDurable != null)
409             {
410                 durable = Boolean.valueOf(tempDurable);
411             }
412             // Get the durable subscriber name if there is one
413             durableName = (String) event.getEndpoint().getProperties().get(
414                     JmsConstants.DURABLE_NAME_PROPERTY);
415             if (durableName == null && durable && topic)
416             {
417                 durableName = "mule." + connector.getName() + "." + event.getEndpoint().getEndpointURI().getAddress();
418                 if (logger.isDebugEnabled())
419                 {
420                     logger.debug("Jms Connector for this receiver is durable but no durable name has been specified. Defaulting to: " +
421                                  durableName);
422                 }
423             }
424         }
425         return connector.getJmsSupport().createConsumer(session, replyTo, selector,
426                                                         connector.isNoLocal(), null, topic, endpoint);
427     }
428 
429     protected Destination getReplyToDestination(Message message, Session session, MuleEvent event, boolean remoteSync, boolean topic) throws JMSException, EndpointException, InitialisationException
430     {
431         Destination replyTo = null;
432 
433         // Some JMS implementations might not support the ReplyTo property.
434         if (isHandleReplyTo(message, event))
435         {
436 
437             Object tempReplyTo = event.getMessage().getOutboundProperty(JmsConstants.JMS_REPLY_TO);
438             if (tempReplyTo == null)
439             {
440                 //It may be a Mule URI or global endpoint Ref
441                 tempReplyTo = event.getMessage().getOutboundProperty(MuleProperties.MULE_REPLY_TO_PROPERTY);
442                 if (tempReplyTo != null)
443                 {
444                     int i = tempReplyTo.toString().indexOf("://");
445                     if (i > -1)
446                     {
447                         tempReplyTo = tempReplyTo.toString().substring(i+3);
448                     }
449                     else
450                     {
451                         EndpointBuilder epb = event.getMuleContext().getRegistry().lookupEndpointBuilder(tempReplyTo.toString());
452                         if (epb != null)
453                         {
454                             tempReplyTo = epb.buildOutboundEndpoint().getEndpointURI().getAddress();
455                         }
456                     }
457                 }
458             }
459             if (tempReplyTo != null)
460             {
461                 if (tempReplyTo instanceof Destination)
462                 {
463                     replyTo = (Destination) tempReplyTo;
464                 }
465                 else
466                 {
467                     // TODO AP should this drill-down be moved into the resolver as well?
468                     boolean replyToTopic = false;
469                     String reply = tempReplyTo.toString();
470                     int i = reply.indexOf(":");
471                     if (i > -1)
472                     {
473                         // TODO MULE-1409 this check will not work for ActiveMQ 4.x,
474                         // as they have temp-queue://<destination> and temp-topic://<destination> URIs
475                         // Extract to a custom resolver for ActiveMQ4.x
476                         // The code path can be exercised, e.g. by a LoanBrokerESBTestCase
477                         String qtype = reply.substring(0, i);
478                         replyToTopic = JmsConstants.TOPIC_PROPERTY.equalsIgnoreCase(qtype);
479                         reply = reply.substring(i + 1);
480                     }
481                     replyTo = connector.getJmsSupport().createDestination(session, reply, replyToTopic, endpoint);
482                 }
483             }
484             // Are we going to wait for a return event ?
485             if (remoteSync && replyTo == null && !disableTemporaryDestinations)
486             {
487                 replyTo = connector.getJmsSupport().createTemporaryDestination(session, topic);
488             }
489         }
490         return replyTo;
491 
492     }
493 
494     protected class ReplyToListener implements MessageListener
495     {
496         private final Latch latch;
497         private volatile Message message;
498         private final WaitableBoolean released = new WaitableBoolean(false);
499 
500         public ReplyToListener(Latch latch)
501         {
502             this.latch = latch;
503         }
504 
505         public Message getMessage()
506         {
507             return message;
508         }
509 
510         public void release()
511         {
512             released.set(true);
513         }
514 
515         public void onMessage(Message message)
516         {
517             this.message = message;
518             latch.countDown();
519             try
520             {
521                 released.whenTrue(null);
522             }
523             catch (InterruptedException e)
524             {
525                 // ignored
526             }
527         }
528     }
529     
530     @Override
531     protected void applyOutboundTransformers(MuleEvent event) throws MuleException
532     {
533         try
534         {
535             preTransformMessage(event.getMessage());
536         }
537         catch (Exception e)
538         {
539             throw new TransformerException(CoreMessages.failedToInvoke("preTransformMessage"), e);
540         }
541         super.applyOutboundTransformers(event);
542     }
543 
544 }