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