View Javadoc

1   /*
2    * $Id: JmsConnector.java 12195 2008-06-27 21:57:59Z aguenther $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSource, Inc.  All rights reserved.  http://www.mulesource.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.transport.jms;
12  
13  import org.mule.api.MessagingException;
14  import org.mule.api.MuleException;
15  import org.mule.api.context.notification.ConnectionNotificationListener;
16  import org.mule.api.context.notification.ServerNotification;
17  import org.mule.api.endpoint.ImmutableEndpoint;
18  import org.mule.api.endpoint.InboundEndpoint;
19  import org.mule.api.lifecycle.InitialisationException;
20  import org.mule.api.lifecycle.StartException;
21  import org.mule.api.service.Service;
22  import org.mule.api.transaction.Transaction;
23  import org.mule.api.transaction.TransactionException;
24  import org.mule.api.transport.MessageAdapter;
25  import org.mule.api.transport.MessageReceiver;
26  import org.mule.api.transport.ReplyToHandler;
27  import org.mule.config.ExceptionHelper;
28  import org.mule.config.i18n.CoreMessages;
29  import org.mule.context.notification.ConnectionNotification;
30  import org.mule.context.notification.NotificationException;
31  import org.mule.transaction.TransactionCoordination;
32  import org.mule.transport.AbstractConnector;
33  import org.mule.transport.ConnectException;
34  import org.mule.transport.FatalConnectException;
35  import org.mule.transport.jms.i18n.JmsMessages;
36  import org.mule.transport.jms.xa.ConnectionFactoryWrapper;
37  
38  import java.text.MessageFormat;
39  import java.util.HashMap;
40  import java.util.Iterator;
41  import java.util.Map;
42  
43  import javax.jms.Connection;
44  import javax.jms.ConnectionFactory;
45  import javax.jms.ExceptionListener;
46  import javax.jms.JMSException;
47  import javax.jms.MessageConsumer;
48  import javax.jms.MessageProducer;
49  import javax.jms.Session;
50  import javax.jms.TemporaryQueue;
51  import javax.jms.TemporaryTopic;
52  import javax.jms.XAConnectionFactory;
53  import javax.naming.NamingException;
54  
55  import org.apache.commons.lang.UnhandledException;
56  
57  /**
58   * <code>JmsConnector</code> is a JMS 1.0.2b compliant connector that can be used
59   * by a Mule endpoint. The connector supports all JMS functionality including topics
60   * and queues, durable subscribers, acknowledgement modes and local transactions.
61   */
62  
63  public class JmsConnector extends AbstractConnector implements ConnectionNotificationListener
64  {
65  
66      public static final String JMS = "jms";
67  
68      ////////////////////////////////////////////////////////////////////////
69      // Properties
70      ////////////////////////////////////////////////////////////////////////
71  
72      private int acknowledgementMode = Session.AUTO_ACKNOWLEDGE;
73  
74      private String clientId;
75  
76      private boolean durable;
77  
78      private boolean noLocal;
79  
80      private boolean persistentDelivery;
81  
82      private boolean honorQosHeaders;
83  
84      private int maxRedelivery = 0;
85  
86      private boolean cacheJmsSessions = false;
87  
88      private boolean recoverJmsConnections = true;
89  
90      /** Whether to create a consumer on connect. */
91      private boolean eagerConsumer = true;
92  
93      ////////////////////////////////////////////////////////////////////////
94      // JMS Connection
95      ////////////////////////////////////////////////////////////////////////
96  
97      private ConnectionFactory connectionFactory;
98  
99      public String username = null;
100 
101     public String password = null;
102 
103     /**
104      * JMS Connection, not settable by the user.
105      */
106     private Connection connection;
107 
108     ////////////////////////////////////////////////////////////////////////
109     // Strategy classes
110     ////////////////////////////////////////////////////////////////////////
111 
112     private String specification = JmsConstants.JMS_SPECIFICATION_102B;
113 
114     private JmsSupport jmsSupport;
115 
116     private JmsTopicResolver topicResolver;
117 
118     private RedeliveryHandlerFactory redeliveryHandlerFactory;
119 
120     ////////////////////////////////////////////////////////////////////////
121     // Methods
122     ////////////////////////////////////////////////////////////////////////
123 
124     /* Register the Jms Exception reader if this class gets loaded */
125     static
126     {
127         ExceptionHelper.registerExceptionReader(new JmsExceptionReader());
128     }
129 
130     public String getProtocol()
131     {
132         return JMS;
133     }
134 
135     protected void doInitialise() throws InitialisationException
136     {
137         if (connectionFactory == null)
138         {
139             connectionFactory = getDefaultConnectionFactory();
140         }
141         if (connectionFactory == null)
142         {
143             throw new InitialisationException(JmsMessages.noConnectionFactorySet(), this);
144         }
145 
146         if (topicResolver == null)
147         {
148             topicResolver = new DefaultJmsTopicResolver(this);
149         }
150         if (redeliveryHandlerFactory == null)
151         {
152             redeliveryHandlerFactory = new DefaultRedeliveryHandlerFactory();
153         }
154 
155         try
156         {
157             muleContext.registerListener(this, getName());
158         }
159         catch (NotificationException nex)
160         {
161             throw new InitialisationException(nex, this);
162         }
163     }
164 
165     /** Override this method to provide a default ConnectionFactory for a vendor-specific JMS Connector. */
166     protected ConnectionFactory getDefaultConnectionFactory()
167     {
168         return null;
169     }
170 
171     protected void doDispose()
172     {
173         if (connection != null)
174         {
175             try
176             {
177                 connection.close();
178             }
179             catch (JMSException e)
180             {
181                 logger.error("Jms connector failed to dispose properly: ", e);
182             }
183             connection = null;
184         }
185     }
186 
187     protected Connection createConnection() throws NamingException, JMSException, InitialisationException
188     {
189         ConnectionFactory cf = this.connectionFactory;
190         Connection connection;
191 
192         try
193         {
194             if (cf instanceof XAConnectionFactory && muleContext.getTransactionManager() != null)
195             {
196                 cf = new ConnectionFactoryWrapper(cf);
197             }
198         }
199         catch (Exception e)
200         {
201             throw new InitialisationException(e, this);
202         }
203         if (cf == null)
204         {
205             throw new InitialisationException(JmsMessages.noConnectionFactorySet(), this);
206         }
207 
208 
209         if (username != null)
210         {
211             connection = jmsSupport.createConnection(cf, username, password);
212         }
213         else
214         {
215             connection = jmsSupport.createConnection(cf);
216         }
217 
218         if (clientId != null)
219         {
220             connection.setClientID(getClientId());
221         }
222 
223         // Register a JMS exception listener to detect failed connections.
224         // Existing connection strategy will be used to recover.
225 
226         if (recoverJmsConnections && connectionStrategy != null && connection != null)
227         {
228             connection.setExceptionListener(new ExceptionListener()
229             {
230                 public void onException(JMSException jmsException)
231                 {
232                     logger.debug("About to recycle myself due to remote JMS connection shutdown.");
233                     final JmsConnector jmsConnector = JmsConnector.this;
234                     try
235                     {
236                         jmsConnector.stop();
237                         jmsConnector.initialised.set(false);
238                     }
239                     catch (MuleException e)
240                     {
241                         logger.warn(e.getMessage(), e);
242                     }
243 
244                     try
245                     {
246                         connectionStrategy.connect(jmsConnector);
247 
248                         // TODO The following code fragment until jmsConnector.start() is a workaround as 
249                         // suggested in MULE-1720. The real solution will be the long awaited re-connection
250                         // strategy implementation.
251 
252                         // keep the receivers in memory so we can register them after initialization
253                         Map receivers = new HashMap(jmsConnector.getReceivers());
254                         jmsConnector.initialise();
255                         // register the receivers
256                         for (Iterator itReceivers = receivers.values().iterator(); itReceivers.hasNext();) 
257                         {
258                             MessageReceiver receiver = (MessageReceiver) itReceivers.next();
259                             try 
260                             {
261                                 jmsConnector.registerListener(receiver.getService(), receiver.getEndpoint());
262                             } 
263                             catch (Exception ex) 
264                             {
265                                 throw new FatalConnectException(ex, receiver);
266                             }
267                         }
268                         
269                         jmsConnector.start();
270                     }
271                     catch (FatalConnectException fcex)
272                     {
273                         logger.fatal("Failed to reconnect to JMS server. I'm giving up.");
274                     }
275                     catch (MuleException umoex)
276                     {
277                         throw new UnhandledException("Failed to recover a connector.", umoex);
278                     }
279                 }
280             });
281         }
282 
283         return connection;
284     }
285 
286     protected void doConnect() throws ConnectException
287     {
288         try
289         {
290             if (jmsSupport == null)
291             {
292                 if (JmsConstants.JMS_SPECIFICATION_102B.equals(specification))
293                 {
294                     jmsSupport = new Jms102bSupport(this);
295                 }
296                 else
297                 {
298                     jmsSupport = new Jms11Support(this);
299                 }
300             }
301         }
302         catch (Exception e)
303         {
304             throw new ConnectException(CoreMessages.failedToCreate("Jms Connector"), e, this);
305         }
306 
307         try
308         {
309             connection = createConnection();
310             if (started.get())
311             {
312                 connection.start();
313             }
314         }
315         catch (Exception e)
316         {
317             throw new ConnectException(e, this);
318         }
319     }
320 
321     protected void doDisconnect() throws ConnectException
322     {
323         try
324         {
325             if (connection != null)
326             {
327                 connection.close();
328             }
329         }
330         catch (Exception e)
331         {
332             throw new ConnectException(e, this);
333         }
334         finally
335         {
336             // connectionFactory = null;
337             connection = null;
338         }
339     }
340 
341     public MessageAdapter getMessageAdapter(Object message) throws MessagingException
342     {
343         JmsMessageAdapter adapter = (JmsMessageAdapter) super.getMessageAdapter(message);
344         adapter.setSpecification(this.getSpecification());
345         return adapter;
346     }
347 
348     protected Object getReceiverKey(Service service, InboundEndpoint endpoint)
349     {
350         return service.getName() + "~" + endpoint.getEndpointURI().getAddress();
351     }
352 
353     public Session getSessionFromTransaction()
354     {
355         Transaction tx = TransactionCoordination.getInstance().getTransaction();
356         if (tx != null)
357         {
358             if (tx.hasResource(connection))
359             {
360                 if (logger.isDebugEnabled())
361                 {
362                     logger.debug("Retrieving jms session from current transaction " + tx);
363                 }
364 
365                 Session session = (Session) tx.getResource(connection);
366 
367                 if (logger.isDebugEnabled())
368                 {
369                     logger.debug("Using " + session + " bound to transaction " + tx);
370                 }
371 
372                 return session;
373             }
374         }
375         return null;
376     }
377 
378     public Session getSession(ImmutableEndpoint endpoint) throws JMSException
379     {
380         final boolean topic = getTopicResolver().isTopic(endpoint);
381         return getSession(endpoint.getTransactionConfig().isTransacted(), topic);
382     }
383 
384     public Session getSession(boolean transacted, boolean topic) throws JMSException
385     {
386         if (!isConnected())
387         {
388             throw new JMSException("Not connected");
389         }
390         Session session = getSessionFromTransaction();
391         if (session != null)
392         {
393             return session;
394         }
395 
396         Transaction tx = TransactionCoordination.getInstance().getTransaction();
397 
398         if (logger.isDebugEnabled())
399         {
400             logger.debug(MessageFormat.format(
401                     "Retrieving new jms session from connection: " +
402                             "topic={0}, transacted={1}, ack mode={2}, nolocal={3}",
403                     new Object[]{Boolean.valueOf(topic),
404                                  Boolean.valueOf(transacted),
405                                  new Integer(acknowledgementMode),
406                                  Boolean.valueOf(noLocal)}));
407         }
408 
409         session = jmsSupport.createSession(connection, topic, transacted, acknowledgementMode, noLocal);
410         if (tx != null)
411         {
412             logger.debug("Binding session " + session + " to current transaction " + tx);
413             try
414             {
415                 tx.bindResource(connection, session);
416             }
417             catch (TransactionException e)
418             {
419                 closeQuietly(session);
420                 throw new RuntimeException("Could not bind session to current transaction", e);
421             }
422         }
423         return session;
424     }
425 
426     protected void doStart() throws MuleException
427     {
428         if (connection != null)
429         {
430             try
431             {
432                 connection.start();
433             }
434             catch (JMSException e)
435             {
436                 throw new StartException(CoreMessages.failedToStart("Jms Connection"), e, this);
437             }
438         }
439     }
440 
441     protected void doStop() throws MuleException
442     {
443         // template method
444     }
445 
446     public ReplyToHandler getReplyToHandler()
447     {
448         return new JmsReplyToHandler(this, getDefaultResponseTransformers());
449     }
450 
451     public void onNotification(ServerNotification notification)
452     {
453         if (notification.getAction() == ConnectionNotification.CONNECTION_DISCONNECTED
454                 || notification.getAction() == ConnectionNotification.CONNECTION_FAILED)
455         {
456             // Remove all dispatchers as any cached session will be invalidated
457             disposeDispatchers();
458             // TODO should we dispose receivers here as well (in case they are
459             // transactional)
460             // gives a harmless NPE at
461             // AbstractConnector.connect(AbstractConnector.java:927)
462             // disposeReceivers();
463         }
464     }
465 
466     /**
467      * This method may be overridden in case a certain JMS implementation does not
468      * support all the standard JMS properties.
469      */
470     public boolean supportsProperty(String property)
471     {
472         return true;
473     }
474 
475     /**
476      * This method may be overridden in order to apply pre-processing to the message
477      * as soon as it arrives.
478      *
479      * @param message - the incoming message
480      * @param session - the JMS session
481      * @return the preprocessed message
482      */
483     public javax.jms.Message preProcessMessage(javax.jms.Message message, Session session) throws Exception
484     {
485         return message;
486     }
487 
488     /**
489      * Closes the MessageProducer
490      *
491      * @param producer
492      * @throws JMSException
493      */
494     public void close(MessageProducer producer) throws JMSException
495     {
496         if (producer != null)
497         {
498             producer.close();
499         }
500     }
501 
502     /**
503      * Closes the MessageProducer without throwing an exception (an error message is
504      * logged instead).
505      *
506      * @param producer
507      */
508     public void closeQuietly(MessageProducer producer)
509     {
510         try
511         {
512             close(producer);
513         }
514         catch (JMSException e)
515         {
516             logger.error("Failed to close jms message producer", e);
517         }
518     }
519 
520     /**
521      * Closes the MessageConsumer
522      *
523      * @param consumer
524      * @throws JMSException
525      */
526     public void close(MessageConsumer consumer) throws JMSException
527     {
528         if (consumer != null)
529         {
530             consumer.close();
531         }
532     }
533 
534     /**
535      * Closes the MessageConsumer without throwing an exception (an error message is
536      * logged instead).
537      *
538      * @param consumer
539      */
540     public void closeQuietly(MessageConsumer consumer)
541     {
542         try
543         {
544             close(consumer);
545         }
546         catch (JMSException e)
547         {
548             logger.error("Failed to close jms message consumer", e);
549         }
550     }
551 
552     /**
553      * Closes the MuleSession
554      *
555      * @param session
556      * @throws JMSException
557      */
558     public void close(Session session) throws JMSException
559     {
560         if (session != null)
561         {
562             session.close();
563         }
564     }
565 
566     /**
567      * Closes the MuleSession without throwing an exception (an error message is logged
568      * instead).
569      *
570      * @param session
571      */
572     public void closeQuietly(Session session)
573     {
574         try
575         {
576             close(session);
577         }
578         catch (JMSException e)
579         {
580             logger.error("Failed to close jms session consumer", e);
581         }
582     }
583 
584     /**
585      * Closes the TemporaryQueue
586      *
587      * @param tempQueue
588      * @throws JMSException
589      */
590     public void close(TemporaryQueue tempQueue) throws JMSException
591     {
592         if (tempQueue != null)
593         {
594             tempQueue.delete();
595         }
596     }
597 
598     /**
599      * Closes the TemporaryQueue without throwing an exception (an error message is
600      * logged instead).
601      *
602      * @param tempQueue
603      */
604     public void closeQuietly(TemporaryQueue tempQueue)
605     {
606         try
607         {
608             close(tempQueue);
609         }
610         catch (JMSException e)
611         {
612             if (logger.isErrorEnabled())
613             {
614                 String queueName = "";
615                 try
616                 {
617                     queueName = tempQueue.getQueueName();
618                 }
619                 catch (JMSException innerEx)
620                 {
621                     // ignore, we are just trying to get the queue name
622                 }
623                 logger.info(MessageFormat.format(
624                         "Faled to delete a temporary queue '{0}' Reason: {1}",
625                         new Object[]{queueName, e.getMessage()}));
626             }
627         }
628     }
629 
630     /**
631      * Closes the TemporaryTopic
632      *
633      * @param tempTopic
634      * @throws JMSException
635      */
636     public void close(TemporaryTopic tempTopic) throws JMSException
637     {
638         if (tempTopic != null)
639         {
640             tempTopic.delete();
641         }
642     }
643 
644     /**
645      * Closes the TemporaryTopic without throwing an exception (an error message is
646      * logged instead).
647      *
648      * @param tempTopic
649      */
650     public void closeQuietly(TemporaryTopic tempTopic)
651     {
652         try
653         {
654             close(tempTopic);
655         }
656         catch (JMSException e)
657         {
658             if (logger.isErrorEnabled())
659             {
660                 String topicName = "";
661                 try
662                 {
663                     topicName = tempTopic.getTopicName();
664                 }
665                 catch (JMSException innerEx)
666                 {
667                     // ignore, we are just trying to get the topic name
668                 }
669                 logger.error("Faled to delete a temporary topic " + topicName, e);
670             }
671         }
672     }
673 
674     ////////////////////////////////////////////////////////////////////////
675     // Getters and Setters
676     ////////////////////////////////////////////////////////////////////////
677 
678     /** @return Returns the connection. */
679     public Connection getConnection()
680     {
681         return connection;
682     }
683 
684     protected void setConnection(Connection connection)
685     {
686         this.connection = connection;
687     }
688 
689     /** @return Returns the acknowledgeMode. */
690     public int getAcknowledgementMode()
691     {
692         return acknowledgementMode;
693     }
694 
695     /** @param acknowledgementMode The acknowledgementMode to set. */
696     public void setAcknowledgementMode(int acknowledgementMode)
697     {
698         this.acknowledgementMode = acknowledgementMode;
699     }
700 
701     /** @return Returns the durable. */
702     public boolean isDurable()
703     {
704         return durable;
705     }
706 
707     /** @param durable The durable to set. */
708     public void setDurable(boolean durable)
709     {
710         this.durable = durable;
711     }
712 
713     /** @return Returns the noLocal. */
714     public boolean isNoLocal()
715     {
716         return noLocal;
717     }
718 
719     /** @param noLocal The noLocal to set. */
720     public void setNoLocal(boolean noLocal)
721     {
722         this.noLocal = noLocal;
723     }
724 
725     /** @return Returns the persistentDelivery. */
726     public boolean isPersistentDelivery()
727     {
728         return persistentDelivery;
729     }
730 
731     /** @param persistentDelivery The persistentDelivery to set. */
732     public void setPersistentDelivery(boolean persistentDelivery)
733     {
734         this.persistentDelivery = persistentDelivery;
735     }
736 
737     public JmsSupport getJmsSupport()
738     {
739         return jmsSupport;
740     }
741 
742     public void setJmsSupport(JmsSupport jmsSupport)
743     {
744         this.jmsSupport = jmsSupport;
745     }
746 
747     public String getSpecification()
748     {
749         return specification;
750     }
751 
752     public void setSpecification(String specification)
753     {
754         this.specification = specification;
755     }
756 
757     public void setRecoverJmsConnections(boolean recover)
758     {
759         this.recoverJmsConnections = recover;
760     }
761 
762     public boolean isRecoverJmsConnections()
763     {
764         return this.recoverJmsConnections;
765     }
766 
767     public String getUsername()
768     {
769         return username;
770     }
771 
772     public void setUsername(String username)
773     {
774         this.username = username;
775     }
776 
777     public String getPassword()
778     {
779         return password;
780     }
781 
782     public void setPassword(String password)
783     {
784         this.password = password;
785     }
786 
787     public String getClientId()
788     {
789         return clientId;
790     }
791 
792     public void setClientId(String clientId)
793     {
794         this.clientId = clientId;
795     }
796 
797     public int getMaxRedelivery()
798     {
799         return maxRedelivery;
800     }
801 
802     public void setMaxRedelivery(int maxRedelivery)
803     {
804         this.maxRedelivery = maxRedelivery;
805     }
806 
807     public boolean isRemoteSyncEnabled()
808     {
809         return true;
810     }
811 
812 
813     /**
814      * Getter for property 'topicResolver'.
815      *
816      * @return Value for property 'topicResolver'.
817      */
818     public JmsTopicResolver getTopicResolver()
819     {
820         return topicResolver;
821     }
822 
823     /**
824      * Setter for property 'topicResolver'.
825      *
826      * @param topicResolver Value to set for property 'topicResolver'.
827      */
828     public void setTopicResolver(final JmsTopicResolver topicResolver)
829     {
830         this.topicResolver = topicResolver;
831     }
832 
833     /**
834      * Getter for property 'eagerConsumer'. Default
835      * is {@code true}.
836      *
837      * @return Value for property 'eagerConsumer'.
838      * @see #eagerConsumer
839      */
840     public boolean isEagerConsumer()
841     {
842         return eagerConsumer;
843     }
844 
845     /**
846      * A value of {@code true} will create a consumer on
847      * connect, in contrast to lazy instantiation in the poll loop.
848      * This setting very much depends on the JMS vendor.
849      * Affects transactional receivers, typical symptoms are:
850      * <ul>
851      * <li> consumer thread hanging forever, though a message is
852      * available
853      * <li>failure to consume the first message (the rest
854      * are fine)
855      * </ul>
856      * <p/>
857      *
858      * @param eagerConsumer Value to set for property 'eagerConsumer'.
859      * @see #eagerConsumer
860      * @see org.mule.transport.jms.XaTransactedJmsMessageReceiver
861      */
862     public void setEagerConsumer(final boolean eagerConsumer)
863     {
864         this.eagerConsumer = eagerConsumer;
865     }
866 
867     public boolean isCacheJmsSessions()
868     {
869         return cacheJmsSessions;
870     }
871 
872     public void setCacheJmsSessions(boolean cacheJmsSessions)
873     {
874         this.cacheJmsSessions = cacheJmsSessions;
875     }
876 
877     public ConnectionFactory getConnectionFactory()
878     {
879         return connectionFactory;
880     }
881 
882     public void setConnectionFactory(ConnectionFactory connectionFactory)
883     {
884         this.connectionFactory = connectionFactory;
885     }
886     
887     public RedeliveryHandlerFactory getRedeliveryHandlerFactory()
888     {
889         return redeliveryHandlerFactory;
890     }
891     
892     public void setRedeliveryHandlerFactory(RedeliveryHandlerFactory redeliveryHandlerFactory)
893     {
894         this.redeliveryHandlerFactory = redeliveryHandlerFactory;
895     }
896 
897     /**
898      * Sets the <code>honorQosHeaders</code> property, which determines whether
899      * {@link JmsMessageDispatcher} should honor incoming message's QoS headers
900      * (JMSPriority, JMSDeliveryMode).
901      * 
902      * @param honorQosHeaders <code>true</code> if {@link JmsMessageDispatcher}
903      *            should honor incoming message's QoS headers; otherwise
904      *            <code>false</code> Default is <code>false</code>, meaning that
905      *            connector settings will override message headers.
906      */
907    public void setHonorQosHeaders(boolean honorQosHeaders)
908    {
909        this.honorQosHeaders = honorQosHeaders;
910    }
911 
912    /**
913      * Gets the value of <code>honorQosHeaders</code> property.
914      * 
915      * @return <code>true</code> if <code>JmsMessageDispatcher</code> should
916      *         honor incoming message's QoS headers; otherwise <code>false</code>
917      *         Default is <code>false</code>, meaning that connector settings will
918      *         override message headers.
919      */
920    public boolean isHonorQosHeaders()
921    {
922        return honorQosHeaders;
923    }
924 }