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.MuleMessage;
10  import org.mule.api.construct.FlowConstruct;
11  import org.mule.api.endpoint.InboundEndpoint;
12  import org.mule.api.lifecycle.CreateException;
13  import org.mule.api.transaction.Transaction;
14  import org.mule.api.transaction.TransactionCallback;
15  import org.mule.api.transport.Connector;
16  import org.mule.retry.policies.NoRetryPolicyTemplate;
17  import org.mule.transaction.TransactionCoordination;
18  import org.mule.transaction.TransactionTemplate;
19  import org.mule.transaction.XaTransaction;
20  import org.mule.transport.ConnectException;
21  import org.mule.transport.TransactedPollingMessageReceiver;
22  import org.mule.transport.jms.filters.JmsSelectorFilter;
23  import org.mule.transport.jms.redelivery.RedeliveryHandler;
24  import org.mule.util.ClassUtils;
25  import org.mule.util.MapUtils;
26  
27  import java.util.List;
28  
29  import javax.jms.Destination;
30  import javax.jms.JMSException;
31  import javax.jms.Message;
32  import javax.jms.MessageConsumer;
33  import javax.jms.Session;
34  
35  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
36  import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicReference;
37  
38  public class XaTransactedJmsMessageReceiver extends TransactedPollingMessageReceiver
39  {
40      public static final long DEFAULT_JMS_POLL_FREQUENCY = 100;
41      public static final TimeUnit DEFAULT_JMS_POLL_TIMEUNIT = TimeUnit.MILLISECONDS;
42      
43      protected final JmsConnector connector;
44      protected boolean reuseConsumer;
45      protected boolean reuseSession;
46      protected final ThreadContextLocal context = new ThreadContextLocal();
47      protected final long timeout;
48      private final AtomicReference/*<RedeliveryHandler>*/ redeliveryHandler = new AtomicReference();
49  
50      /**
51       * Holder receiving the session and consumer for this thread.
52       */
53      protected static class JmsThreadContext
54      {
55          public Session session;
56          public MessageConsumer consumer;
57      }
58  
59      /**
60       * Strongly typed ThreadLocal for ThreadContext.
61       */
62      protected static class ThreadContextLocal extends ThreadLocal
63      {
64          public JmsThreadContext getContext()
65          {
66              return (JmsThreadContext)get();
67          }
68  
69          @Override
70          protected Object initialValue()
71          {
72              return new JmsThreadContext();
73          }
74      }
75  
76      public XaTransactedJmsMessageReceiver(Connector connector, FlowConstruct flowConstruct, InboundEndpoint endpoint)
77          throws CreateException
78      {
79          super(connector, flowConstruct, endpoint);
80          // TODO AP: find appropriate value for polling frequency with the scheduler;
81          // see setFrequency/setTimeUnit & VMMessageReceiver for more
82          this.setTimeUnit(DEFAULT_JMS_POLL_TIMEUNIT);
83          this.setFrequency(MapUtils.getLongValue(endpoint.getProperties(), "pollingFrequency", DEFAULT_JMS_POLL_FREQUENCY));
84  
85          this.connector = (JmsConnector) connector;
86          this.timeout = endpoint.getTransactionConfig().getTimeout();
87  
88          // If reconnection is configured, default reuse strategy to false
89          // as some jms brokers will not detect lost connections if the
90          // same consumer / session is used
91          if (retryTemplate != null && !(retryTemplate instanceof NoRetryPolicyTemplate))
92          {
93              this.reuseConsumer = false;
94              this.reuseSession = false;
95          }
96  
97          // User may override reuse strategy if necessary. This is available for legacy reasons, 
98          // but this approach is not recommended and should never be set when using XA.
99          this.reuseConsumer = MapUtils.getBooleanValue(endpoint.getProperties(), "reuseConsumer",
100             this.reuseConsumer);
101         this.reuseSession = MapUtils.getBooleanValue(endpoint.getProperties(), "reuseSession",
102             this.reuseSession);
103 
104         // Do extra validation, XA Topic & reuse are incompatible. See MULE-2622
105         boolean topic = this.connector.getTopicResolver().isTopic(getEndpoint());
106         if (topic && (reuseConsumer || reuseSession))
107         {
108             logger.warn("Destination " + getEndpoint().getEndpointURI() + " is a topic and XA transaction was " +
109                         "configured. Forcing 'reuseSession' and 'reuseConsumer' to false. Set these " +
110                         "on endpoint to avoid the message.");
111             reuseConsumer = false;
112             reuseSession = false;
113         }
114 
115         // Check if the destination is a queue and
116         // if we are in transactional mode.
117         // If true, set receiveMessagesInTransaction to true.
118         // It will start multiple threads, depending on the threading profile.
119 
120         // If we're using topics we don't want to use multiple receivers as we'll get
121         // the same message multiple times
122         this.setUseMultipleTransactedReceivers(!topic);
123     }
124 
125     @Override
126     protected void doDispose()
127     {
128         // template method
129     }
130 
131     @Override
132     protected void doConnect() throws Exception
133     {
134         if (redeliveryHandler.compareAndSet(null, connector.getRedeliveryHandlerFactory().create()))
135         {
136             ((RedeliveryHandler) redeliveryHandler.get()).setConnector(this.connector);
137         }
138     }
139 
140     @Override
141     protected void doDisconnect() throws Exception
142     {
143         if (connector.isConnected())
144         {
145             // TODO All resources will be close by transaction or by connection close
146             closeResource(true);
147         }
148     }
149 
150     /**
151      * The poll method is overriden from the {@link TransactedPollingMessageReceiver}
152      */
153     @Override
154     public void poll() throws Exception
155     {
156         logger.debug("Polling...");
157         
158         TransactionTemplate<Void> tt = new TransactionTemplate<Void>(
159                                                 endpoint.getTransactionConfig(),
160                                                 connector.getMuleContext());
161         TransactionCallback<Void> cb = new TransactionCallback<Void>()
162         {
163             public Void doInTransaction() throws Exception
164             {
165                 try
166                 {
167                     List messages = getMessages();
168                     if (messages != null && messages.size() > 0)
169                     {
170                         for (Object message : messages)
171                         {
172                             processMessage(message);
173                         }
174                     }
175                     return null;
176                 }
177                 catch (Exception e)
178                 {
179                     // There is not a need to close resources here,
180                     // they will be close by XaTransaction,  
181                     JmsThreadContext ctx = context.getContext();
182                     ctx.consumer = null;
183                     Transaction tx = TransactionCoordination.getInstance().getTransaction();
184                     if (ctx.session != null && tx instanceof XaTransaction.MuleXaObject)
185                     {
186                         if (ctx.session instanceof XaTransaction.MuleXaObject)
187                         {
188                             ((XaTransaction.MuleXaObject) ctx.session).setReuseObject(false);
189                         }
190                         else
191                         {
192                             logger.warn("Session should be XA, but is of type " + ctx.session.getClass().getName());
193                         }
194                     }
195                     ctx.session = null;
196                     throw e;
197                 }
198             }
199         };
200         
201         tt.execute(cb);
202     }
203 
204     @Override
205     protected List<MuleMessage> getMessages() throws Exception
206     {
207         Session session = this.connector.getSessionFromTransaction();
208         Transaction tx = TransactionCoordination.getInstance().getTransaction();
209         MessageConsumer consumer = createConsumer();
210 
211         // Retrieve message
212         Message message = null;
213         try
214         {
215             message = consumer.receive(timeout);
216         }
217         catch (JMSException e)
218         {
219             // If we're being disconnected, ignore the exception
220             if (!this.isConnected())
221             {
222                 // ignore
223             }
224             else
225             {
226                 throw e;
227             }
228         }
229         
230         if (message == null)
231         {
232             if (tx != null)
233             {
234                 tx.setRollbackOnly();
235             }
236             return null;
237         }
238         message = connector.preProcessMessage(message, session);
239 
240         // Process message
241         if (logger.isDebugEnabled())
242         {
243             logger.debug("Message received it is of type: " +
244                     ClassUtils.getSimpleName(message.getClass()));
245             if (message.getJMSDestination() != null)
246             {
247                 logger.debug("Message received on " + message.getJMSDestination() + " ("
248                              + message.getJMSDestination().getClass().getName() + ")");
249             }
250             else
251             {
252                 logger.debug("Message received on unknown destination");
253             }
254             logger.debug("Message CorrelationId is: " + message.getJMSCorrelationID());
255             logger.debug("Jms Message Id is: " + message.getJMSMessageID());
256         }
257 
258         if (message.getJMSRedelivered())
259         {
260             if (logger.isDebugEnabled())
261             {
262                 logger.debug("Message with correlationId: " + message.getJMSCorrelationID()
263                              + " is redelivered. handing off to Exception Handler");
264             }
265             ((RedeliveryHandler) redeliveryHandler.get()).handleRedelivery(message, endpoint, flowConstruct);
266         }
267 
268         MuleMessage messageToRoute = createMuleMessage(message, endpoint.getEncoding());
269         routeMessage(messageToRoute);
270         return null;
271     }
272 
273     @Override
274     protected void processMessage(Object msg) throws Exception
275     {
276         // This method is never called as the
277         // message is processed when received
278     }
279 
280     /**
281      * Close Sesison and consumer
282      */
283     protected void closeResource(boolean force)
284     {
285         JmsThreadContext ctx = context.getContext();
286         if (ctx == null)
287         {
288             return;
289         }
290         
291         // Close consumer
292         if (force || !reuseSession || !reuseConsumer)
293         {
294             connector.closeQuietly(ctx.consumer);
295             ctx.consumer = null;
296         }
297             
298         // Do not close session if a transaction is in progress
299         // the session will be closed by the transaction
300         if (force || !reuseSession)
301         {
302             connector.closeQuietly(ctx.session);
303             ctx.session = null;
304         }        
305     }
306 
307     /**
308      * Create a consumer for the jms destination
309      * 
310      * @throws Exception
311      */
312     protected MessageConsumer createConsumer() throws Exception
313     {
314         logger.debug("Create a consumer for the jms destination");
315         try
316         {
317             JmsSupport jmsSupport = this.connector.getJmsSupport();
318 
319             JmsThreadContext ctx = context.getContext();
320             if (ctx == null)
321             {
322                 ctx = new JmsThreadContext();
323             }
324             
325             Session session;
326             Transaction tx = TransactionCoordination.getInstance().getTransaction();
327             if (this.reuseSession && ctx.session != null)
328             {
329                 session = ctx.session;
330                 tx.bindResource(this.connector.getConnection(), session);
331             }
332             else
333             {
334                 session = this.connector.getSession(endpoint);
335                 if (session != null && tx != null)
336                 {
337                     if (session instanceof XaTransaction.MuleXaObject)
338                     {
339                         ((XaTransaction.MuleXaObject) session).setReuseObject(reuseSession);
340                     }
341                     else
342                     {
343                         logger.warn("Session should be XA, but is of type " + session.getClass().getName());
344                     }
345                 }
346             }
347             
348             if (reuseSession)
349             {
350                 ctx.session = session;
351             }
352             
353             // TODO How can I verify that the consumer is active?
354             if (this.reuseConsumer && ctx.consumer != null)
355             {
356                 return ctx.consumer;
357             }
358 
359             // Create destination
360             final boolean topic = connector.getTopicResolver().isTopic(endpoint);
361             Destination dest = jmsSupport.createDestination(session, endpoint);
362 
363             // Extract jms selector
364             String selector = null;
365             if (endpoint.getFilter() != null && endpoint.getFilter() instanceof JmsSelectorFilter)
366             {
367                 selector = ((JmsSelectorFilter)endpoint.getFilter()).getExpression();
368             }
369             else if (endpoint.getProperties() != null)
370             {
371                 // still allow the selector to be set as a property on the endpoint
372                 // to be backward compatible
373                 selector = (String)endpoint.getProperties().get(JmsConstants.JMS_SELECTOR_PROPERTY);
374             }
375             String tempDurable = (String)endpoint.getProperties().get("durable");
376             boolean durable = connector.isDurable();
377             if (tempDurable != null)
378             {
379                 durable = Boolean.valueOf(tempDurable);
380             }
381 
382             // Get the durable subscriber name if there is one
383             String durableName = (String)endpoint.getProperties().get("durableName");
384             if (durableName == null && durable && topic)
385             {
386                 durableName = "mule." + connector.getName() + "." + endpoint.getEndpointURI().getAddress();
387                 logger.debug("Jms Connector for this receiver is durable but no durable name has been specified. Defaulting to: "
388                              + durableName);
389             }
390 
391             // Create consumer
392             MessageConsumer consumer = jmsSupport.createConsumer(session, dest, selector, connector.isNoLocal(),
393                 durableName, topic, endpoint);
394             if (reuseConsumer)
395             {
396                 ctx.consumer = consumer;
397             }
398             return consumer;
399         }
400         catch (JMSException e)
401         {
402             throw new ConnectException(e, this);
403         }
404     }
405 }