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     */
022    package org.granite.gravity.adapters;
023    
024    import java.io.ByteArrayInputStream;
025    import java.io.ByteArrayOutputStream;
026    import java.io.IOException;
027    import java.io.Serializable;
028    import java.util.Date;
029    import java.util.Enumeration;
030    import java.util.HashMap;
031    import java.util.Map;
032    import java.util.Properties;
033    import java.util.Timer;
034    import java.util.TimerTask;
035    
036    import javax.jms.ConnectionFactory;
037    import javax.jms.Destination;
038    import javax.jms.ExceptionListener;
039    import javax.jms.JMSException;
040    import javax.jms.MessageListener;
041    import javax.jms.ObjectMessage;
042    import javax.jms.Session;
043    import javax.jms.TextMessage;
044    import javax.naming.Context;
045    import javax.naming.InitialContext;
046    import javax.naming.NamingException;
047    
048    import org.granite.clustering.DistributedDataFactory;
049    import org.granite.clustering.TransientReference;
050    import org.granite.context.GraniteContext;
051    import org.granite.gravity.Channel;
052    import org.granite.gravity.Gravity;
053    import org.granite.gravity.MessageReceivingException;
054    import org.granite.logging.Logger;
055    import org.granite.messaging.amf.io.AMF3Deserializer;
056    import org.granite.messaging.amf.io.AMF3Serializer;
057    import org.granite.messaging.service.ServiceException;
058    import org.granite.messaging.webapp.ServletGraniteContext;
059    import org.granite.util.XMap;
060    
061    import flex.messaging.messages.AcknowledgeMessage;
062    import flex.messaging.messages.AsyncMessage;
063    import flex.messaging.messages.CommandMessage;
064    import flex.messaging.messages.ErrorMessage;
065    
066    /**
067     * @author William DRAI
068     */
069    public 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    }