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 (jmsConsumer != 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                if (jmsConsumerSession == null) {
619                    jmsConsumerSession = jmsConnection.createSession(transactedSessions, acknowledgeMode);
620                    if (reconnected)
621                        jmsConsumerSession.recover();
622                    log.debug("Created JMS Consumer Session for channel %s (transacted: %s, ack: %s)", channel.getId(), transactedSessions, acknowledgeMode);
623                }
624                
625                if (reconnectTimer != null)
626                        reconnectTimer.cancel();
627                
628                try {                   
629                        // When failing over, JMS can be in a temporary illegal state. Give it some time to recover. 
630                        int retryCount = failoverRetryCount;
631                        do {
632                                try {
633                                        jmsConsumer = jmsConsumerSession.createConsumer(getConsumerDestination(topic), selector, noLocal);
634                                        if (retryCount < failoverRetryCount) // We come from a failover, try to recover session
635                                                reconnected = true;
636                                        break;
637                                }
638                                catch (Exception e) {
639                                        if (retryCount <= 0)
640                                                throw e;
641                                        
642                                        if (log.isDebugEnabled())
643                                                log.debug(e, "Could not create JMS Consumer (retrying %d time)", retryCount);
644                                        else
645                                                log.info("Could not create JMS Consumer (retrying %d time)", retryCount);
646                                        
647                                        try {
648                                                Thread.sleep(failoverRetryInterval);
649                                        }
650                                        catch (Exception f) {
651                                                throw new ServiceException("Could not sleep when retrying to create JMS Consumer", f.getMessage(), e);
652                                        }
653                                }
654                        }
655                        while (retryCount-- > 0);
656                        
657                        jmsConsumer.setMessageListener(this);
658                        log.debug("Created JMS Consumer for channel %s", channel.getId());
659                }
660                catch (Exception e) {
661                        close();
662                        throw e;
663                }
664            }
665
666            public void setSelector(String selector) throws Exception {
667                if (jmsConsumer != null) {
668                    jmsConsumer.close();
669                    jmsConsumer = null;
670                }
671
672                connect(selector);
673                log.debug("Changed selector to %s for JMS Consumer of channel %s", selector, channel.getId());
674            }
675            
676            public void reset() {
677                jmsConsumer = null;
678                jmsConsumerSession = null;
679                
680                final TimerTask reconnectTask = new TimerTask() {
681                                        @Override
682                                        public void run() {
683                                                try {
684                                                        connect(selector);
685                                                        reconnectTimer.cancel();
686                                                        reconnectTimer = null;
687                                                }
688                                                catch (Exception e) {
689                                                        // Wait for next task run
690                                                }
691                                        }
692                                };
693                                if (reconnectTimer != null)
694                                        reconnectTimer.cancel();
695                                
696                                reconnectTimer = new Timer();
697                                reconnectTimer.schedule(reconnectTask, failoverRetryInterval, reconnectRetryInterval);
698            }
699
700            public void close() throws JMSException {
701                                if (reconnectTimer != null)
702                                        reconnectTimer.cancel();
703                                
704                try {
705                        if (jmsConsumer != null) {
706                            jmsConsumer.close();
707                            jmsConsumer = null;
708                        }
709                }
710                finally {
711                        if (jmsConsumerSession != null) {
712                            jmsConsumerSession.close();
713                            jmsConsumerSession = null;
714                        }
715                }
716            }
717            
718            public void onMessage(javax.jms.Message message) {
719                if (!(message instanceof ObjectMessage) && !(message instanceof TextMessage)) {
720                    log.error("JMS Adapter message type not allowed: %s", message.getClass().getName());
721
722                    try {
723                        if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE)
724                            message.acknowledge();
725
726                        if (transactedSessions)
727                            jmsConsumerSession.commit();
728                    }
729                    catch (JMSException e) {
730                        log.error(e, "Could not ack/commit JMS onMessage");
731                    }
732                }
733
734                log.debug("Delivering JMS message to channel %s subscription %s", channel.getId(), subscriptionId);
735                
736                AsyncMessage dmsg = new AsyncMessage();
737                try {
738                    Serializable msg = null;
739
740                    if (textMessages) {
741                        TextMessage jmsMessage = (TextMessage)message;
742                        msg = jmsMessage.getText();
743                    }
744                    else {
745                        ObjectMessage jmsMessage = (ObjectMessage)message;
746                        if (useJBossTCCLDeserializationWorkaround) {
747                                // On JBoss 6, try to deserialize with application class loader if the previous attempt fails
748                            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
749                            try {
750                                    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
751                                    msg = jmsMessage.getObject();
752                            }
753                            finally {
754                                Thread.currentThread().setContextClassLoader(contextClassLoader);
755                            }
756                        }
757                        try {
758                                msg = jmsMessage.getObject();
759                        }
760                        catch (JMSException e) {
761                                // On JBoss 6, try to deserialize with application class loader if the previous attempt fails
762                            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
763                            try {
764                                    Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
765                                    msg = jmsMessage.getObject();
766                                    useJBossTCCLDeserializationWorkaround = true;
767                            }
768                            finally {
769                                Thread.currentThread().setContextClassLoader(contextClassLoader);
770                            }
771                        }
772                    }
773
774                    dmsg.setDestination(getDestination().getId());
775                    
776                    if (Boolean.TRUE.equals(message.getBooleanProperty(Gravity.BYTEARRAY_BODY_HEADER))) {
777                        getGravity().initThread(null, channel.getClientType());
778                        try {
779                                ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
780                                AMF3Serializer ser = new AMF3Serializer(baos);
781                                ser.writeObject(msg);
782                                ser.close();
783                                baos.close();
784                                dmsg.setBody(baos.toByteArray());
785                        }
786                        finally {
787                                getGravity().releaseThread();
788                        }
789                    }
790                    else
791                        dmsg.setBody(msg);
792                    
793                    dmsg.setMessageId(denormalizeJMSMessageID(message.getJMSMessageID()));
794                    dmsg.setCorrelationId(denormalizeJMSMessageID(message.getJMSCorrelationID()));
795                    dmsg.setTimestamp(message.getJMSTimestamp());
796                    dmsg.setTimeToLive(message.getJMSExpiration());
797
798                    Enumeration<?> ename = message.getPropertyNames();
799                    while (ename.hasMoreElements()) {
800                        String pname = (String)ename.nextElement();
801                        dmsg.setHeader(pname, message.getObjectProperty(pname));
802                    }
803                        
804                    dmsg.setHeader("JMSType", message.getJMSType());
805                    dmsg.setHeader("JMSPriority", Integer.valueOf(message.getJMSPriority()));
806                    dmsg.setHeader("JMSRedelivered", Boolean.valueOf(message.getJMSRedelivered()));
807                    dmsg.setHeader("JMSDeliveryMode", Integer.valueOf(message.getJMSDeliveryMode()));
808                    dmsg.setHeader(AsyncMessage.DESTINATION_CLIENT_ID_HEADER, subscriptionId);
809                    
810                    channel.receive(dmsg);
811                }
812                catch (IOException e) {
813                    if (transactedSessions) {
814                        try {
815                            jmsConsumerSession.rollback();
816                        }
817                        catch (JMSException f) {
818                            log.error("Could not rollback JMS session, messageId: %s", dmsg.getMessageId());
819                        }
820                    }
821
822                    throw new RuntimeException("IO Error", e);
823                }
824                catch (JMSException e) {
825                    if (transactedSessions) {
826                        try {
827                            jmsConsumerSession.rollback();
828                        }
829                        catch (JMSException f) {
830                            log.error("Could not rollback JMS session, messageId: %s", dmsg.getMessageId());
831                        }
832                    }
833
834                    throw new RuntimeException("JMS Error", e);
835                }
836                catch (MessageReceivingException e) {
837                    if (transactedSessions) {
838                        try {
839                            jmsConsumerSession.rollback();
840                        }
841                        catch (JMSException f) {
842                            log.error("Could not rollback JMS session, messageId: %s", dmsg.getMessageId());
843                        }
844                    }
845
846                    throw new RuntimeException("Channel delivery Error", e);
847                }
848
849                try {
850                    if (acknowledgeMode == Session.CLIENT_ACKNOWLEDGE)
851                        message.acknowledge();
852
853                    if (transactedSessions && !useGlassFishNoCommitWorkaround)
854                        jmsConsumerSession.commit();
855                }
856                catch (JMSException e) {
857                    if (e.getMessage() != null && e.getMessage().startsWith("MQJMSRA_DS4001"))
858                        useGlassFishNoCommitWorkaround = true;
859                    else
860                        log.error(e, "Could not ack/commit JMS onMessage, messageId: %s", dmsg.getMessageId());
861
862                    // Message already delivered to client, should rollback or not ?
863                }
864            }
865
866                private String denormalizeJMSMessageID(String messageId) {
867                if (messageId != null && messageId.startsWith("ID:"))
868                        messageId = messageId.substring(3);
869                        return messageId;
870                }
871        }
872    }
873}