001/**
002 *   GRANITE DATA SERVICES
003 *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004 *
005 *   This file is part of the Granite Data Services Platform.
006 *
007 *   Granite Data Services is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *
012 *   Granite Data Services is distributed in the hope that it will be useful,
013 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
015 *   General Public License for more details.
016 *
017 *   You should have received a copy of the GNU Lesser General Public
018 *   License along with this library; if not, write to the Free Software
019 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020 *   USA, or see <http://www.gnu.org/licenses/>.
021 */
022package org.granite.gravity.adapters;
023
024import java.io.ByteArrayInputStream;
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.Serializable;
028import java.util.Date;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.Map;
032import java.util.Properties;
033import java.util.Timer;
034import java.util.TimerTask;
035
036import javax.jms.ConnectionFactory;
037import javax.jms.Destination;
038import javax.jms.ExceptionListener;
039import javax.jms.JMSException;
040import javax.jms.MessageListener;
041import javax.jms.ObjectMessage;
042import javax.jms.Session;
043import javax.jms.TextMessage;
044import javax.naming.Context;
045import javax.naming.InitialContext;
046import javax.naming.NamingException;
047
048import org.granite.clustering.DistributedDataFactory;
049import org.granite.clustering.TransientReference;
050import org.granite.context.GraniteContext;
051import org.granite.gravity.Channel;
052import org.granite.gravity.Gravity;
053import org.granite.gravity.MessageReceivingException;
054import org.granite.logging.Logger;
055import org.granite.messaging.amf.io.AMF3Deserializer;
056import org.granite.messaging.amf.io.AMF3Serializer;
057import org.granite.messaging.service.ServiceException;
058import org.granite.messaging.webapp.ServletGraniteContext;
059import org.granite.util.XMap;
060
061import flex.messaging.messages.AcknowledgeMessage;
062import flex.messaging.messages.AsyncMessage;
063import flex.messaging.messages.CommandMessage;
064import flex.messaging.messages.ErrorMessage;
065
066/**
067 * @author William DRAI
068 */
069public class JMSServiceAdapter extends ServiceAdapter {
070
071    private static final Logger log = Logger.getLogger(JMSServiceAdapter.class);
072    
073    public static final long DEFAULT_FAILOVER_RETRY_INTERVAL = 1000L;
074    public static final long DEFAULT_RECONNECT_RETRY_INTERVAL = 20000L;
075    public static final int DEFAULT_FAILOVER_RETRY_COUNT = 4;
076
077    protected ConnectionFactory jmsConnectionFactory = null;
078    protected javax.jms.Destination jmsDestination = null;
079    protected Map<String, JMSClient> jmsClients = new HashMap<String, JMSClient>();
080    protected String destinationName = null;
081    protected boolean textMessages = false;
082    protected boolean transactedSessions = false;
083    protected int acknowledgeMode = Session.AUTO_ACKNOWLEDGE;
084    protected int messagePriority = javax.jms.Message.DEFAULT_PRIORITY;
085    protected int deliveryMode = javax.jms.Message.DEFAULT_DELIVERY_MODE;
086    protected boolean noLocal = false;
087    protected boolean sessionSelector = false;
088    
089    protected long failoverRetryInterval = DEFAULT_FAILOVER_RETRY_INTERVAL;
090    protected int failoverRetryCount = DEFAULT_FAILOVER_RETRY_COUNT;
091    protected long reconnectRetryInterval = DEFAULT_RECONNECT_RETRY_INTERVAL;
092
093    @Override
094    public void configure(XMap adapterProperties, XMap destinationProperties) throws ServiceException {
095        super.configure(adapterProperties, destinationProperties);
096
097        log.info("Using JMS configuration: %s", destinationProperties.getOne("jms"));
098        
099        destinationName = destinationProperties.get("jms/destination-name");
100        
101        if (Boolean.TRUE.toString().equals(destinationProperties.get("jms/transacted-sessions")))
102            transactedSessions = true;
103        
104        String ackMode = destinationProperties.get("jms/acknowledge-mode");
105        if ("AUTO_ACKNOWLEDGE".equals(ackMode))
106            acknowledgeMode = Session.AUTO_ACKNOWLEDGE;
107        else if ("CLIENT_ACKNOWLEDGE".equals(ackMode))
108            acknowledgeMode = Session.CLIENT_ACKNOWLEDGE;
109        else if ("DUPS_OK_ACKNOWLEDGE".equals(ackMode))
110            acknowledgeMode = Session.DUPS_OK_ACKNOWLEDGE;
111        else if (ackMode != null)
112                log.warn("Unsupported acknowledge mode: %s (using default AUTO_ACKNOWLEDGE)", ackMode);
113        
114        if ("javax.jms.TextMessage".equals(destinationProperties.get("jms/message-type")))
115            textMessages = true;
116        
117        if (Boolean.TRUE.toString().equals(destinationProperties.get("jms/no-local")))
118                noLocal = true;
119
120        if (Boolean.TRUE.toString().equals(destinationProperties.get("session-selector")))
121                sessionSelector = true;
122        
123        failoverRetryInterval = destinationProperties.get("jms/failover-retry-interval", Long.TYPE, DEFAULT_FAILOVER_RETRY_INTERVAL);
124        if (failoverRetryInterval <= 0) {
125                log.warn("Illegal failover retry interval: %d (using default %d)", failoverRetryInterval, DEFAULT_FAILOVER_RETRY_INTERVAL);
126                failoverRetryInterval = DEFAULT_FAILOVER_RETRY_INTERVAL;
127        }
128        
129        failoverRetryCount = destinationProperties.get("jms/failover-retry-count", Integer.TYPE, DEFAULT_FAILOVER_RETRY_COUNT);
130        if (failoverRetryCount <= 0) {
131                log.warn("Illegal failover retry count: %s (using default %d)", failoverRetryCount, DEFAULT_FAILOVER_RETRY_COUNT);
132                failoverRetryCount = DEFAULT_FAILOVER_RETRY_COUNT;
133        }
134        
135        reconnectRetryInterval = destinationProperties.get("jms/reconnect-retry-interval", Long.TYPE, DEFAULT_RECONNECT_RETRY_INTERVAL);
136        if (reconnectRetryInterval <= 0) {
137                log.warn("Illegal reconnect retry interval: %d (using default %d)", reconnectRetryInterval, DEFAULT_RECONNECT_RETRY_INTERVAL);
138                reconnectRetryInterval = DEFAULT_RECONNECT_RETRY_INTERVAL;
139        }
140
141        Properties environment = new Properties();
142        for (XMap property : destinationProperties.getAll("jms/initial-context-environment/property")) {
143                String name = property.get("name");
144                String value = property.get("value");
145                
146                if ("Context.PROVIDER_URL".equals(name))
147                        environment.put(Context.PROVIDER_URL, value);
148                else if ("Context.INITIAL_CONTEXT_FACTORY".equals(name))
149                        environment.put(Context.INITIAL_CONTEXT_FACTORY, value);
150                else if ("Context.URL_PKG_PREFIXES".equals(name))
151                        environment.put(Context.URL_PKG_PREFIXES, value);
152                else if ("Context.SECURITY_PRINCIPAL".equals(name))
153                        environment.put(Context.SECURITY_PRINCIPAL, value);
154                else if ("Context.SECURITY_CREDENTIALS".equals(name))
155                        environment.put(Context.SECURITY_CREDENTIALS, value);
156                else
157                        log.warn("Unknown InitialContext property: %s (ignored)", name);
158        }
159
160        InitialContext initialContext = null;
161        try {
162                initialContext = new InitialContext(environment.size() > 0 ? environment : null);
163        }
164            catch (NamingException e) {
165                log.error(e, "Could not initialize JNDI context");
166                throw new ServiceException("Error configuring JMS Adapter", e);
167            }
168            
169        String cfJndiName = destinationProperties.get("jms/connection-factory");
170        try {
171                jmsConnectionFactory = (ConnectionFactory)initialContext.lookup(cfJndiName);
172        }
173        catch (NamingException e) {
174                log.error(e, "Could not find JMS ConnectionFactory named %s in JNDI", cfJndiName);
175            throw new ServiceException("Error configuring JMS Adapter", e);
176        }
177        
178        String dsJndiName = destinationProperties.get("jms/destination-jndi-name");
179        try {
180                jmsDestination = (Destination)initialContext.lookup(dsJndiName);
181        }
182        catch (NamingException e) {
183                log.error(e, "Could not find JMS destination named %s in JNDI", dsJndiName);
184            throw new ServiceException("Error configuring JMS Adapter", e);
185        }
186    }
187
188    protected javax.jms.Destination getProducerDestination(String topic) {
189        return jmsDestination;
190    }
191
192    protected javax.jms.Destination getConsumerDestination(String topic) {
193        return jmsDestination;
194    }
195
196    @Override
197    public void start() throws ServiceException {
198        super.start();
199    }
200
201    @Override
202    public void stop() throws ServiceException {
203        super.stop();
204
205        for (JMSClient jmsClient : jmsClients.values()) {
206                try {
207                        jmsClient.close();
208                }
209                catch (Exception e) {
210                        log.warn(e, "Could not close JMSClient: %s", jmsClient);
211                }
212        }
213        jmsClients.clear();
214    }
215
216
217    private synchronized JMSClient connectJMSClient(Channel client, String destination) throws Exception {
218        JMSClient jmsClient = jmsClients.get(client.getId());
219        if (jmsClient == null) {
220            jmsClient = new JMSClientImpl(client);
221            jmsClient.connect();
222            jmsClients.put(client.getId(), jmsClient);
223            if (sessionSelector && GraniteContext.getCurrentInstance() instanceof ServletGraniteContext)
224                ((ServletGraniteContext)GraniteContext.getCurrentInstance()).getSessionMap().put(JMSClient.JMSCLIENT_KEY_PREFIX + destination, jmsClient);
225            log.debug("JMS client connected for channel " + client.getId());
226        }
227        return jmsClient;
228    }
229
230    private synchronized void closeJMSClientIfNecessary(Channel channel, String destination) throws Exception {
231        JMSClient jmsClient = jmsClients.get(channel.getId());
232        if (jmsClient != null && !jmsClient.hasActiveConsumer()) {
233            jmsClient.close();
234            jmsClients.remove(channel.getId());
235            if (sessionSelector && GraniteContext.getCurrentInstance() instanceof ServletGraniteContext)
236                ((ServletGraniteContext)GraniteContext.getCurrentInstance()).getSessionMap().remove(JMSClient.JMSCLIENT_KEY_PREFIX + destination);
237            log.debug("JMS client closed for channel " + channel.getId());
238        }
239    }
240
241    @Override
242    public Object invoke(Channel fromClient, AsyncMessage message) {
243        String topicId = (String)message.getHeader(AsyncMessage.SUBTOPIC_HEADER);
244        
245        if (getSecurityPolicy().canPublish(fromClient, topicId, message)) {
246                try {
247                    JMSClient jmsClient = connectJMSClient(fromClient, message.getDestination());
248                    jmsClient.send(message);
249        
250                    AsyncMessage reply = new AcknowledgeMessage(message);
251                    reply.setMessageId(message.getMessageId());
252        
253                    return reply;
254                }
255                catch (Exception e) {
256                        log.error(e, "Error sending message");
257                    ErrorMessage error = new ErrorMessage(message, null);
258                    error.setFaultString("JMS Adapter error " + e.getMessage());
259        
260                    return error;
261                }
262        }
263
264        log.debug("Channel %s tried to publish a message to topic %s", fromClient, topicId);
265        ErrorMessage error = new ErrorMessage(message, null);
266        error.setFaultString("Server.Publish.Denied");
267        return error;
268    }
269
270    @Override
271    public Object manage(Channel fromChannel, CommandMessage message) {
272        String topicId = (String)message.getHeader(AsyncMessage.SUBTOPIC_HEADER);
273
274        if (message.getOperation() == CommandMessage.SUBSCRIBE_OPERATION) {
275                if (getSecurityPolicy().canSubscribe(fromChannel, topicId, message)) {
276                    try {
277                        JMSClient jmsClient = connectJMSClient(fromChannel, message.getDestination());
278                        jmsClient.subscribe(message);
279        
280                        AsyncMessage reply = new AcknowledgeMessage(message);
281                        return reply;
282                    }
283                    catch (Exception e) {
284                        throw new RuntimeException("JMSAdapter subscribe error on topic: " + message, e);
285                    }
286                }
287
288                log.debug("Channel %s tried to subscribe to topic %s", fromChannel, topicId);
289                ErrorMessage error = new ErrorMessage(message, null);
290            error.setFaultString("Server.Subscribe.Denied");
291            return error;
292        }
293        else if (message.getOperation() == CommandMessage.UNSUBSCRIBE_OPERATION) {
294            try {
295                JMSClient jmsClient = connectJMSClient(fromChannel, message.getDestination());
296                jmsClient.unsubscribe(message);
297                closeJMSClientIfNecessary(fromChannel, message.getDestination());
298
299                AsyncMessage reply = new AcknowledgeMessage(message);
300                return reply;
301            }
302            catch (Exception e) {
303                throw new RuntimeException("JMSAdapter unsubscribe error on topic: " + message, e);
304            }
305        }
306
307        return null;
308    }
309
310
311    @TransientReference
312    private class JMSClientImpl implements JMSClient {
313
314        private Channel channel = null;
315        private String topic = null;
316        private javax.jms.Connection jmsConnection = null;
317        private javax.jms.Session jmsProducerSession = null;
318        private javax.jms.MessageProducer jmsProducer = null;
319        private Map<String, JMSConsumer> consumers = new HashMap<String, JMSConsumer>();
320        private boolean useGlassFishNoExceptionListenerWorkaround = false;
321        private boolean useGlassFishNoCommitWorkaround = false;
322        
323        private ExceptionListener connectionExceptionListener = new ConnectionExceptionListener();
324        
325        private class ConnectionExceptionListener implements ExceptionListener {
326
327                        public void onException(JMSException ex) {
328                                // Connection failure, force reconnection of the producer on next send
329                                jmsProducer = null;
330                                for (JMSConsumer consumer : consumers.values())
331                                        consumer.reset();
332                                consumers.clear();
333                                jmsConnection = null;
334                                jmsProducerSession = null;
335                        }
336        }
337
338
339        public JMSClientImpl(Channel channel) {
340            this.channel = channel;            
341        }
342
343        public boolean hasActiveConsumer() {
344            return consumers != null && !consumers.isEmpty();
345        }
346
347
348        public void connect() throws ServiceException {
349                if (jmsConnection != null)
350                        return;
351                
352            try {
353                jmsConnection = jmsConnectionFactory.createConnection();
354                if (!useGlassFishNoExceptionListenerWorkaround) {
355                        try {
356                                jmsConnection.setExceptionListener(connectionExceptionListener);
357                        }
358                        catch (JMSException e) {
359                                if (e.getMessage().startsWith("MQJMSRA_DC2001: Unsupported:setExceptionListener()"))
360                                        useGlassFishNoExceptionListenerWorkaround = true;
361                                else
362                                        throw e;
363                        }
364                }
365                jmsConnection.start();
366                log.debug("JMS client connected for channel " + channel.getId());
367            }
368            catch (JMSException e) {
369                throw new ServiceException("JMS Initialize error", e);
370            }
371        }
372
373        public void close() throws ServiceException {
374            try {
375                if (jmsProducer != null)
376                    jmsProducer.close();
377            }
378            catch (JMSException e) {
379                log.error(e, "Could not close JMS Producer for channel " + channel.getId());
380            }
381            finally {
382                try {
383                        if (jmsProducerSession != null)
384                            jmsProducerSession.close();
385                    }
386                    catch (JMSException e) {
387                        log.error(e, "Could not close JMS Producer Session for channel " + channel.getId());
388                    }
389            }
390            for (JMSConsumer consumer : consumers.values()) {
391                try {
392                        consumer.close();
393                }
394                catch (JMSException e) {
395                        log.error(e, "Could not close JMS Consumer " + consumer.subscriptionId + " for channel " + channel.getId());
396                }
397            }
398            try {
399                jmsConnection.stop();
400            }
401            catch (JMSException e) {
402                log.debug(e, "Could not stop JMS Connection for channel " + channel.getId());
403            }
404            finally {
405                        try {
406                                jmsConnection.close();
407                        }
408                    catch (JMSException e) {
409                        throw new ServiceException("JMS Stop error", e);
410                    }
411                        finally {
412                                consumers.clear();
413                        }
414            }
415        }
416        
417        private void createProducer(String topic) throws Exception {
418            try {
419                // When failing over, JMS can be in a temporary illegal state. Give it some time to recover. 
420                int retryCount = failoverRetryCount;
421                do {
422                        try {
423                                jmsProducer = jmsProducerSession.createProducer(getProducerDestination(topic != null ? topic : this.topic));
424                                if (retryCount < failoverRetryCount) // We come from a failover, try to recover session
425                                        jmsProducerSession.recover();
426                                break;
427                        }
428                        catch (Exception e) {
429                                if (retryCount <= 0)
430                                        throw e;
431                                
432                                if (log.isDebugEnabled())
433                                        log.debug(e, "Could not create JMS Producer (retrying %d time)", retryCount);
434                                else
435                                        log.info("Could not create JMS Producer (retrying %d time)", retryCount);
436                                
437                                try {
438                                        Thread.sleep(failoverRetryInterval);
439                                }
440                                catch (Exception f) {
441                                        throw new ServiceException("Could not sleep when retrying to create JMS Producer", f.getMessage(), e);
442                                }
443                        }
444                }
445                while (retryCount-- > 0);
446                
447                jmsProducer.setPriority(messagePriority);
448                jmsProducer.setDeliveryMode(deliveryMode);
449                log.debug("Created JMS Producer for channel %s", channel.getId());
450            }
451            catch (JMSException e) {
452                jmsProducerSession.close();
453                jmsProducerSession = null;
454                throw e;
455            }
456        }
457
458        public void send(AsyncMessage message) throws Exception {
459            Object msg = null;
460            if (Boolean.TRUE.equals(message.getHeader(Gravity.BYTEARRAY_BODY_HEADER))) {
461                byte[] byteArray = (byte[])message.getBody();
462                ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
463                AMF3Deserializer deser = new AMF3Deserializer(bais);
464                msg = deser.readObject();
465                deser.close(); // makes jdk7 happy (Resource leak: 'deser' is never closed)...
466            }
467            else
468                msg = message.getBody();
469            
470            internalSend(message.getHeaders(), msg, message.getMessageId(), message.getCorrelationId(), message.getTimestamp(), message.getTimeToLive());
471        }
472
473        public void send(Map<String, ?> params, Object msg, long timeToLive) throws Exception {
474                internalSend(params, msg, null, null, new Date().getTime(), timeToLive);
475        }
476        
477        public void internalSend(Map<String, ?> headers, Object msg, String messageId, String correlationId, long timestamp, long timeToLive) throws Exception {
478            String topic = (String)headers.get(AsyncMessage.SUBTOPIC_HEADER);
479                
480            if (jmsProducerSession == null) {
481                jmsProducerSession = jmsConnection.createSession(transactedSessions, acknowledgeMode);
482                log.debug("Created JMS Producer Session for channel %s (transacted: %s, ack: %s)", channel.getId(), transactedSessions, acknowledgeMode);
483            }
484            
485            if (jmsProducer == null)
486                createProducer(topic);
487            
488            javax.jms.Message jmsMessage = null;
489            if (textMessages)
490                jmsMessage = jmsProducerSession.createTextMessage(msg.toString());
491            else
492                jmsMessage = jmsProducerSession.createObjectMessage((Serializable)msg);
493
494            jmsMessage.setJMSMessageID(normalizeJMSMessageID(messageId));
495            jmsMessage.setJMSCorrelationID(normalizeJMSMessageID(correlationId));
496            jmsMessage.setJMSTimestamp(timestamp);
497            jmsMessage.setJMSExpiration(timeToLive);
498            
499            for (Map.Entry<String, ?> me : headers.entrySet()) {
500                if ("JMSType".equals(me.getKey())) {
501                    if (me.getValue() instanceof String)
502                        jmsMessage.setJMSType((String)me.getValue());
503                }
504                else if ("JMSPriority".equals(me.getKey())) {
505                    if (me.getValue() instanceof Integer)
506                        jmsMessage.setJMSPriority(((Integer)me.getValue()).intValue());
507                }
508                else if (me.getValue() instanceof String)
509                    jmsMessage.setStringProperty(me.getKey(), (String)me.getValue());
510                else if (me.getValue() instanceof Boolean)
511                    jmsMessage.setBooleanProperty(me.getKey(), ((Boolean)me.getValue()).booleanValue());
512                else if (me.getValue() instanceof Integer)
513                    jmsMessage.setIntProperty(me.getKey(), ((Integer)me.getValue()).intValue());
514                else if (me.getValue() instanceof Long)
515                    jmsMessage.setLongProperty(me.getKey(), ((Long)me.getValue()).longValue());
516                else if (me.getValue() instanceof Double)
517                    jmsMessage.setDoubleProperty(me.getKey(), ((Double)me.getValue()).doubleValue());
518                else
519                    jmsMessage.setObjectProperty(me.getKey(), me.getValue());
520            }
521
522            jmsProducer.send(jmsMessage);
523            
524            if (transactedSessions && !useGlassFishNoCommitWorkaround) {
525                // If we are in a container-managed transaction (data dispatch from an EJB interceptor for ex.), we should not commit the session
526                // but the behaviour is different between JBoss and GlassFish
527                try {
528                        jmsProducerSession.commit();
529                }
530                catch (JMSException e) {
531                        if (e.getMessage() != null && e.getMessage().startsWith("MQJMSRA_DS4001"))
532                        useGlassFishNoCommitWorkaround = true;
533                    else
534                                log.error(e, "Could not commit JMS Session for channel %s", channel.getId());
535                }
536            }
537        }
538
539                private String normalizeJMSMessageID(String messageId) {
540            if (messageId != null && !messageId.startsWith("ID:"))
541                messageId = "ID:" + messageId;
542                        return messageId;
543                }
544
545        public void subscribe(CommandMessage message) throws Exception {
546            String subscriptionId = (String)message.getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER);
547            String selector = (String)message.getHeader(CommandMessage.SELECTOR_HEADER);
548            this.topic = (String)message.getHeader(AsyncMessage.SUBTOPIC_HEADER);
549
550            internalSubscribe(subscriptionId, selector, message.getDestination(), this.topic);
551        }
552        
553        public void subscribe(String selector, String destination, String topic) throws Exception {
554                DistributedDataFactory distributedDataFactory = GraniteContext.getCurrentInstance().getGraniteConfig().getDistributedDataFactory();
555                String subscriptionId = distributedDataFactory.getInstance().getDestinationSubscriptionId(destination);
556                if (subscriptionId != null)
557                        internalSubscribe(subscriptionId, selector, destination, topic);
558        }
559        
560        private void internalSubscribe(String subscriptionId, String selector, String destination, String topic) throws Exception {
561            synchronized (consumers) {
562                JMSConsumer consumer = consumers.get(subscriptionId);
563                if (consumer == null) {
564                    consumer = new JMSConsumer(subscriptionId, selector, noLocal);
565                    consumer.connect(selector);
566                    consumers.put(subscriptionId, consumer);
567                }
568                else
569                    consumer.setSelector(selector);
570                channel.addSubscription(destination, topic, subscriptionId, false);
571            }
572        }
573
574        public void unsubscribe(CommandMessage message) throws Exception {
575            String subscriptionId = (String)message.getHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER);
576
577            synchronized (consumers) {
578                JMSConsumer consumer = consumers.get(subscriptionId);
579                try {
580                        if (consumer != null)
581                            consumer.close();
582                }
583                finally {
584                        consumers.remove(subscriptionId);
585                        channel.removeSubscription(subscriptionId);
586                }
587            }
588        }
589
590
591        private class JMSConsumer implements MessageListener {
592
593            private String subscriptionId = null;
594            private javax.jms.Session jmsConsumerSession = null;
595            private javax.jms.MessageConsumer jmsConsumer = null;
596            private boolean noLocal = false;
597            private String selector = null;
598            private boolean useJBossTCCLDeserializationWorkaround = false;
599            private boolean useGlassFishNoCommitWorkaround = false;
600            private boolean reconnected = false;
601            private Timer reconnectTimer = null;
602
603            public JMSConsumer(String subscriptionId, String selector, boolean noLocal) throws Exception {
604                this.subscriptionId = subscriptionId;
605                this.noLocal = noLocal;
606                this.selector = selector;
607            }
608            
609            public void connect(String selector) throws Exception {
610                if (jmsConsumerSession != null)
611                        return;
612                
613                this.selector = selector;
614                
615                // Reconnect to the JMS provider in case no producer has already done it
616                JMSClientImpl.this.connect();
617                
618                jmsConsumerSession = jmsConnection.createSession(transactedSessions, acknowledgeMode);
619                if (reconnected)
620                        jmsConsumerSession.recover();
621                log.debug("Created JMS Consumer Session for channel %s (transacted: %s, ack: %s)", channel.getId(), transactedSessions, acknowledgeMode);
622                
623                if (reconnectTimer != null)
624                        reconnectTimer.cancel();
625                
626                try {                   
627                        // When failing over, JMS can be in a temporary illegal state. Give it some time to recover. 
628                        int retryCount = failoverRetryCount;
629                        do {
630                                try {
631                                        jmsConsumer = jmsConsumerSession.createConsumer(getConsumerDestination(topic), selector, noLocal);
632                                        if (retryCount < failoverRetryCount) // We come from a failover, try to recover session
633                                                reconnected = true;
634                                        break;
635                                }
636                                catch (Exception e) {
637                                        if (retryCount <= 0)
638                                                throw e;
639                                        
640                                        if (log.isDebugEnabled())
641                                                log.debug(e, "Could not create JMS Consumer (retrying %d time)", retryCount);
642                                        else
643                                                log.info("Could not create JMS Consumer (retrying %d time)", retryCount);
644                                        
645                                        try {
646                                                Thread.sleep(failoverRetryInterval);
647                                        }
648                                        catch (Exception f) {
649                                                throw new ServiceException("Could not sleep when retrying to create JMS Consumer", f.getMessage(), e);
650                                        }
651                                }
652                        }
653                        while (retryCount-- > 0);
654                        
655                        jmsConsumer.setMessageListener(this);
656                        log.debug("Created JMS Consumer for channel %s", channel.getId());
657                }
658                catch (Exception e) {
659                        close();
660                        throw e;
661                }
662            }
663
664            public void setSelector(String selector) throws Exception {
665                if (jmsConsumer != null) {
666                    jmsConsumer.close();
667                    jmsConsumer = null;
668                }
669                
670                connect(selector);
671                log.debug("Changed selector to %s for JMS Consumer of channel %s", selector, channel.getId());
672            }
673            
674            public void reset() {
675                jmsConsumer = null;
676                jmsConsumerSession = null;
677                
678                final TimerTask reconnectTask = new TimerTask() {
679                                        @Override
680                                        public void run() {
681                                                try {
682                                                        connect(selector);
683                                                        reconnectTimer.cancel();
684                                                        reconnectTimer = null;
685                                                }
686                                                catch (Exception e) {
687                                                        // Wait for next task run
688                                                }
689                                        }
690                                };
691                                if (reconnectTimer != null)
692                                        reconnectTimer.cancel();
693                                
694                                reconnectTimer = new Timer();
695                                reconnectTimer.schedule(reconnectTask, failoverRetryInterval, reconnectRetryInterval);
696            }
697
698            public void close() throws JMSException {
699                                if (reconnectTimer != null)
700                                        reconnectTimer.cancel();
701                                
702                try {
703                        if (jmsConsumer != null) {
704                            jmsConsumer.close();
705                            jmsConsumer = null;
706                        }
707                }
708                finally {
709                        if (jmsConsumerSession != null) {
710                            jmsConsumerSession.close();
711                            jmsConsumerSession = null;
712                        }
713                }
714            }
715            
716            public void onMessage(javax.jms.Message message) {
717                if (!(message instanceof ObjectMessage) && !(message instanceof TextMessage)) {
718                    log.error("JMS Adapter message type not allowed: %s", message.getClass().getName());
719
720                    try {
721                        if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE)
722                            message.acknowledge();
723
724                        if (transactedSessions)
725                            jmsConsumerSession.commit();
726                    }
727                    catch (JMSException e) {
728                        log.error(e, "Could not ack/commit JMS onMessage");
729                    }
730                }
731
732                log.debug("Delivering JMS message to channel %s subscription %s", channel.getId(), subscriptionId);
733                
734                AsyncMessage dmsg = new AsyncMessage();
735                try {
736                    Serializable msg = null;
737
738                    if (textMessages) {
739                        TextMessage jmsMessage = (TextMessage)message;
740                        msg = jmsMessage.getText();
741                    }
742                    else {
743                        ObjectMessage jmsMessage = (ObjectMessage)message;
744                        if (useJBossTCCLDeserializationWorkaround) {
745                                // On JBoss 6, try to deserialize with application class loader if the previous attempt fails
746                            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
747                            try {
748                                    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
749                                    msg = jmsMessage.getObject();
750                            }
751                            finally {
752                                Thread.currentThread().setContextClassLoader(contextClassLoader);
753                            }
754                        }
755                        try {
756                                msg = jmsMessage.getObject();
757                        }
758                        catch (JMSException e) {
759                                // On JBoss 6, try to deserialize with application class loader if the previous attempt fails
760                            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
761                            try {
762                                    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
763                                    msg = jmsMessage.getObject();
764                                    useJBossTCCLDeserializationWorkaround = true;
765                            }
766                            finally {
767                                Thread.currentThread().setContextClassLoader(contextClassLoader);
768                            }
769                        }
770                    }
771
772                    dmsg.setDestination(getDestination().getId());
773                    
774                    if (Boolean.TRUE.equals(message.getBooleanProperty(Gravity.BYTEARRAY_BODY_HEADER))) {
775                        getGravity().initThread(null, channel.getClientType());
776                        try {
777                                ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
778                                AMF3Serializer ser = new AMF3Serializer(baos);
779                                ser.writeObject(msg);
780                                ser.close();
781                                baos.close();
782                                dmsg.setBody(baos.toByteArray());
783                        }
784                        finally {
785                                getGravity().releaseThread();
786                        }
787                    }
788                    else
789                        dmsg.setBody(msg);
790                    
791                    dmsg.setMessageId(denormalizeJMSMessageID(message.getJMSMessageID()));
792                    dmsg.setCorrelationId(denormalizeJMSMessageID(message.getJMSCorrelationID()));
793                    dmsg.setTimestamp(message.getJMSTimestamp());
794                    dmsg.setTimeToLive(message.getJMSExpiration());
795
796                    Enumeration<?> ename = message.getPropertyNames();
797                    while (ename.hasMoreElements()) {
798                        String pname = (String)ename.nextElement();
799                        dmsg.setHeader(pname, message.getObjectProperty(pname));
800                    }
801                        
802                    dmsg.setHeader("JMSType", message.getJMSType());
803                    dmsg.setHeader("JMSPriority", Integer.valueOf(message.getJMSPriority()));
804                    dmsg.setHeader("JMSRedelivered", Boolean.valueOf(message.getJMSRedelivered()));
805                    dmsg.setHeader("JMSDeliveryMode", Integer.valueOf(message.getJMSDeliveryMode()));
806                    dmsg.setHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER, subscriptionId);
807                    
808                    channel.receive(dmsg);
809                }
810                catch (IOException e) {
811                    if (transactedSessions) {
812                        try {
813                            jmsConsumerSession.rollback();
814                        }
815                        catch (JMSException f) {
816                            log.error("Could not rollback JMS session, messageId: %s", dmsg.getMessageId());
817                        }
818                    }
819
820                    throw new RuntimeException("IO Error", e);
821                }
822                catch (JMSException e) {
823                    if (transactedSessions) {
824                        try {
825                            jmsConsumerSession.rollback();
826                        }
827                        catch (JMSException f) {
828                            log.error("Could not rollback JMS session, messageId: %s", dmsg.getMessageId());
829                        }
830                    }
831
832                    throw new RuntimeException("JMS Error", e);
833                }
834                catch (MessageReceivingException e) {
835                    if (transactedSessions) {
836                        try {
837                            jmsConsumerSession.rollback();
838                        }
839                        catch (JMSException f) {
840                            log.error("Could not rollback JMS session, messageId: %s", dmsg.getMessageId());
841                        }
842                    }
843
844                    throw new RuntimeException("Channel delivery Error", e);
845                }
846
847                try {
848                    if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE)
849                        message.acknowledge();
850
851                    if (transactedSessions && !useGlassFishNoCommitWorkaround)
852                        jmsConsumerSession.commit();
853                }
854                catch (JMSException e) {
855                    if (e.getMessage() != null && e.getMessage().startsWith("MQJMSRA_DS4001"))
856                        useGlassFishNoCommitWorkaround = true;
857                    else
858                        log.error(e, "Could not ack/commit JMS onMessage, messageId: %s", dmsg.getMessageId());
859
860                    // Message already delivered to client, should rollback or not ?
861                }
862            }
863
864                private String denormalizeJMSMessageID(String messageId) {
865                if (messageId != null && messageId.startsWith("ID:"))
866                        messageId = messageId.substring(3);
867                        return messageId;
868                }
869        }
870    }
871}