/*
* Bitronix Transaction Manager
*
* Copyright (c) 2010, Bitronix Software.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package bitronix.tm.resource.jms;
import bitronix.tm.BitronixTransaction;
import bitronix.tm.internal.BitronixRollbackSystemException;
import bitronix.tm.internal.BitronixSystemException;
import bitronix.tm.resource.common.*;
import bitronix.tm.utils.Decoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jms.*;
import javax.jms.IllegalStateException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.xa.XAResource;
import java.io.Serializable;
import java.util.*;
import java.util.Map.Entry;
/**
* JMS Session wrapper that will send calls to either a XASession or to a non-XA Session depending on the calling
* context.
*
* @author lorban
*/
public class DualSessionWrapper extends AbstractXAResourceHolder implements Session, StateChangeListener {
private final static Logger log = LoggerFactory.getLogger(DualSessionWrapper.class);
private final JmsPooledConnection pooledConnection;
private final boolean transacted;
private final int acknowledgeMode;
private XASession xaSession;
private Session session;
private XAResource xaResource;
private MessageListener listener;
//TODO: shouldn't producers/consumers/subscribers be separated between XA and non-XA session ?
private final Map<MessageProducerConsumerKey, MessageProducer> messageProducers = new HashMap<MessageProducerConsumerKey, MessageProducer>();
private final Map<MessageProducerConsumerKey, MessageConsumer> messageConsumers = new HashMap<MessageProducerConsumerKey, MessageConsumer>();
private final Map<MessageProducerConsumerKey, TopicSubscriberWrapper> topicSubscribers = new HashMap<MessageProducerConsumerKey, TopicSubscriberWrapper>();
public DualSessionWrapper(JmsPooledConnection pooledConnection, boolean transacted, int acknowledgeMode) {
this.pooledConnection = pooledConnection;
this.transacted = transacted;
this.acknowledgeMode = acknowledgeMode;
if (log.isDebugEnabled()) log.debug("getting session handle from " + pooledConnection);
setState(STATE_ACCESSIBLE);
addStateChangeEventListener(this);
}
public PoolingConnectionFactory getPoolingConnectionFactory() {
return pooledConnection.getPoolingConnectionFactory();
}
public Session getSession() throws JMSException {
return getSession(false);
}
public Session getSession(boolean forceXa) throws JMSException {
if (getState() == STATE_CLOSED)
throw new IllegalStateException("session handle is closed");
if (forceXa) {
if (log.isDebugEnabled()) log.debug("choosing XA session (forced)");
return createXASession();
}
else {
BitronixTransaction currentTransaction = TransactionContextHelper.currentTransaction();
if (currentTransaction != null) {
if (log.isDebugEnabled()) log.debug("choosing XA session");
return createXASession();
}
if (log.isDebugEnabled()) log.debug("choosing non-XA session");
return createNonXASession();
}
}
private Session createNonXASession() throws JMSException {
// non-XA
if (session == null) {
session = pooledConnection.getXAConnection().createSession(transacted, acknowledgeMode);
if (listener != null) {
session.setMessageListener(listener);
if (log.isDebugEnabled()) log.debug("get non-XA session registered message listener: " + listener);
}
}
return session;
}
private Session createXASession() throws JMSException {
// XA
if (xaSession == null) {
xaSession = pooledConnection.getXAConnection().createXASession();
if (listener != null) {
xaSession.setMessageListener(listener);
if (log.isDebugEnabled()) log.debug("get XA session registered message listener: " + listener);
}
xaResource = xaSession.getXAResource();
}
return xaSession.getSession();
}
public String toString() {
return "a DualSessionWrapper in state " + Decoder.decodeXAStatefulHolderState(getState()) + " of " + pooledConnection;
}
/* wrapped Session methods that have special XA semantics */
public void close() throws JMSException {
if (getState() != STATE_ACCESSIBLE) {
if (log.isDebugEnabled()) log.debug("not closing already closed " + this);
return;
}
if (log.isDebugEnabled()) log.debug("closing " + this);
// delisting
try {
TransactionContextHelper.delistFromCurrentTransaction(this);
}
catch (BitronixRollbackSystemException ex) {
throw (JMSException) new TransactionRolledBackException("unilateral rollback of " + this).initCause(ex);
}
catch (SystemException ex) {
throw (JMSException) new JMSException("error delisting " + this).initCause(ex);
}
finally {
// requeuing
try {
TransactionContextHelper.requeue(this, pooledConnection.getPoolingConnectionFactory());
}
catch (BitronixSystemException ex) {
// this may hide the exception thrown by delistFromCurrentTransaction() but
// an error requeuing must absolutely be reported as an exception.
// Too bad if this happens... See JdbcPooledConnection.release() as well.
throw (JMSException) new JMSException("error requeuing " + this).initCause(ex);
}
}
}
public Date getLastReleaseDate() {
return null;
}
/*
* When the session is closed (directly or deferred) the action is to change its state to IN_POOL.
* There is no such state for JMS sessions, this just means that it has been closed -> force a
* state switch to CLOSED then clean up.
*/
public void stateChanged(XAStatefulHolder source, int oldState, int newState) {
if (newState == STATE_IN_POOL) {
setState(STATE_CLOSED);
}
else if (newState == STATE_CLOSED) {
if (log.isDebugEnabled()) log.debug("session state changing to CLOSED, cleaning it up: " + this);
if (xaSession != null) {
try {
xaSession.close();
} catch (JMSException ex) {
log.error("error closing XA session", ex);
}
xaSession = null;
xaResource = null;
}
if (session != null) {
try {
session.close();
} catch (JMSException ex) {
log.error("error closing session", ex);
}
session = null;
}
Iterator<Entry<MessageProducerConsumerKey, MessageProducer>> it = messageProducers.entrySet().iterator();
while (it.hasNext()) {
Entry<MessageProducerConsumerKey, MessageProducer> entry = it.next();
MessageProducerWrapper messageProducerWrapper = (MessageProducerWrapper) entry.getValue();
try {
messageProducerWrapper.close();
} catch (JMSException ex) {
log.error("error closing message producer", ex);
}
}
messageProducers.clear();
Iterator<Entry<MessageProducerConsumerKey, MessageConsumer>> it2 = messageConsumers.entrySet().iterator();
while (it2.hasNext()) {
Entry<MessageProducerConsumerKey, MessageConsumer> entry = it2.next();
MessageConsumerWrapper messageConsumerWrapper = (MessageConsumerWrapper) entry.getValue();
try {
messageConsumerWrapper.close();
} catch (JMSException ex) {
log.error("error closing message consumer", ex);
}
}
messageConsumers.clear();
} // if newState == STATE_CLOSED
}
public void stateChanging(XAStatefulHolder source, int currentState, int futureState) {
}
public MessageProducer createProducer(Destination destination) throws JMSException {
MessageProducerConsumerKey key = new MessageProducerConsumerKey(destination);
if (log.isDebugEnabled()) log.debug("looking for producer based on " + key);
MessageProducerWrapper messageProducer = (MessageProducerWrapper) messageProducers.get(key);
if (messageProducer == null) {
if (log.isDebugEnabled()) log.debug("found no producer based on " + key + ", creating it");
messageProducer = new MessageProducerWrapper(getSession().createProducer(destination), this, pooledConnection.getPoolingConnectionFactory());
if (pooledConnection.getPoolingConnectionFactory().getCacheProducersConsumers()) {
if (log.isDebugEnabled()) log.debug("caching producer via key " + key);
messageProducers.put(key, messageProducer);
}
}
else if (log.isDebugEnabled()) log.debug("found producer based on " + key + ", recycling it: " + messageProducer);
return messageProducer;
}
public MessageConsumer createConsumer(Destination destination) throws JMSException {
MessageProducerConsumerKey key = new MessageProducerConsumerKey(destination);
if (log.isDebugEnabled()) log.debug("looking for consumer based on " + key);
MessageConsumerWrapper messageConsumer = (MessageConsumerWrapper) messageConsumers.get(key);
if (messageConsumer == null) {
if (log.isDebugEnabled()) log.debug("found no consumer based on " + key + ", creating it");
messageConsumer = new MessageConsumerWrapper(getSession().createConsumer(destination), this, pooledConnection.getPoolingConnectionFactory());
if (pooledConnection.getPoolingConnectionFactory().getCacheProducersConsumers()) {
if (log.isDebugEnabled()) log.debug("caching consumer via key " + key);
messageConsumers.put(key, messageConsumer);
}
}
else if (log.isDebugEnabled()) log.debug("found consumer based on " + key + ", recycling it: " + messageConsumer);
return messageConsumer;
}
public MessageConsumer createConsumer(Destination destination, String messageSelector) throws JMSException {
MessageProducerConsumerKey key = new MessageProducerConsumerKey(destination, messageSelector);
if (log.isDebugEnabled()) log.debug("looking for consumer based on " + key);
MessageConsumerWrapper messageConsumer = (MessageConsumerWrapper) messageConsumers.get(key);
if (messageConsumer == null) {
if (log.isDebugEnabled()) log.debug("found no consumer based on " + key + ", creating it");
messageConsumer = new MessageConsumerWrapper(getSession().createConsumer(destination, messageSelector), this, pooledConnection.getPoolingConnectionFactory());
if (pooledConnection.getPoolingConnectionFactory().getCacheProducersConsumers()) {
if (log.isDebugEnabled()) log.debug("caching consumer via key " + key);
messageConsumers.put(key, messageConsumer);
}
}
else if (log.isDebugEnabled()) log.debug("found consumer based on " + key + ", recycling it: " + messageConsumer);
return messageConsumer;
}
public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean noLocal) throws JMSException {
MessageProducerConsumerKey key = new MessageProducerConsumerKey(destination, messageSelector, noLocal);
if (log.isDebugEnabled()) log.debug("looking for consumer based on " + key);
MessageConsumerWrapper messageConsumer = (MessageConsumerWrapper) messageConsumers.get(key);
if (messageConsumer == null) {
if (log.isDebugEnabled()) log.debug("found no consumer based on " + key + ", creating it");
messageConsumer = new MessageConsumerWrapper(getSession().createConsumer(destination, messageSelector, noLocal), this, pooledConnection.getPoolingConnectionFactory());
if (pooledConnection.getPoolingConnectionFactory().getCacheProducersConsumers()) {
if (log.isDebugEnabled()) log.debug("caching consumer via key " + key);
messageConsumers.put(key, messageConsumer);
}
}
else if (log.isDebugEnabled()) log.debug("found consumer based on " + key + ", recycling it: " + messageConsumer);
return messageConsumer;
}
public TopicSubscriber createDurableSubscriber(Topic topic, String name) throws JMSException {
MessageProducerConsumerKey key = new MessageProducerConsumerKey(topic);
if (log.isDebugEnabled()) log.debug("looking for durable subscriber based on " + key);
TopicSubscriberWrapper topicSubscriber = topicSubscribers.get(key);
if (topicSubscriber == null) {
if (log.isDebugEnabled()) log.debug("found no durable subscriber based on " + key + ", creating it");
topicSubscriber = new TopicSubscriberWrapper(getSession().createDurableSubscriber(topic, name), this, pooledConnection.getPoolingConnectionFactory());
if (pooledConnection.getPoolingConnectionFactory().getCacheProducersConsumers()) {
if (log.isDebugEnabled()) log.debug("caching durable subscriber via key " + key);
topicSubscribers.put(key, topicSubscriber);
}
}
else if (log.isDebugEnabled()) log.debug("found durable subscriber based on " + key + ", recycling it: " + topicSubscriber);
return topicSubscriber;
}
public TopicSubscriber createDurableSubscriber(Topic topic, String name, String messageSelector, boolean noLocal) throws JMSException {
MessageProducerConsumerKey key = new MessageProducerConsumerKey(topic, messageSelector, noLocal);
if (log.isDebugEnabled()) log.debug("looking for durable subscriber based on " + key);
TopicSubscriberWrapper topicSubscriber = topicSubscribers.get(key);
if (topicSubscriber == null) {
if (log.isDebugEnabled()) log.debug("found no durable subscriber based on " + key + ", creating it");
topicSubscriber = new TopicSubscriberWrapper(getSession().createDurableSubscriber(topic, name, messageSelector, noLocal), this, pooledConnection.getPoolingConnectionFactory());
if (pooledConnection.getPoolingConnectionFactory().getCacheProducersConsumers()) {
if (log.isDebugEnabled()) log.debug("caching durable subscriber via key " + key);
topicSubscribers.put(key, topicSubscriber);
}
}
else if (log.isDebugEnabled()) log.debug("found durable subscriber based on " + key + ", recycling it: " + topicSubscriber);
return topicSubscriber;
}
public MessageListener getMessageListener() throws JMSException {
return listener;
}
public void setMessageListener(MessageListener listener) throws JMSException {
if (getState() == STATE_CLOSED)
throw new IllegalStateException("session handle is closed");
if (session != null)
session.setMessageListener(listener);
if (xaSession != null)
xaSession.setMessageListener(listener);
this.listener = listener;
}
public void run() {
try {
Session session = getSession(true);
if (log.isDebugEnabled()) log.debug("running XA session " + session);
session.run();
} catch (JMSException ex) {
log.error("error getting session", ex);
}
}
/* XAResourceHolder implementation */
public XAResource getXAResource() {
return xaResource;
}
public ResourceBean getResourceBean() {
return getPoolingConnectionFactory();
}
/* XAStatefulHolder implementation */
public List<XAResourceHolder> getXAResourceHolders() {
return Arrays.asList((XAResourceHolder) this);
}
public XAResourceHolder getXAResourceHolderForXaResource(XAResource xaResource) {
if (xaResource == this.xaResource) {
return this;
}
return null;
}
public Object getConnectionHandle() throws Exception {
return null;
}
/* XA-enhanced methods */
public boolean getTransacted() throws JMSException {
if (isParticipatingInActiveGlobalTransaction())
return true; // for consistency with EJB 2.1 spec (17.3.5)
return getSession().getTransacted();
}
public int getAcknowledgeMode() throws JMSException {
if (isParticipatingInActiveGlobalTransaction())
return 0; // for consistency with EJB 2.1 spec (17.3.5)
return getSession().getAcknowledgeMode();
}
public void commit() throws JMSException {
if (isParticipatingInActiveGlobalTransaction())
throw new TransactionInProgressException("cannot commit a resource enlisted in a global transaction");
getSession().commit();
}
public void rollback() throws JMSException {
if (isParticipatingInActiveGlobalTransaction())
throw new TransactionInProgressException("cannot rollback a resource enlisted in a global transaction");
getSession().rollback();
}
public void recover() throws JMSException {
if (isParticipatingInActiveGlobalTransaction())
throw new TransactionInProgressException("cannot recover a resource enlisted in a global transaction");
getSession().recover();
}
public QueueBrowser createBrowser(javax.jms.Queue queue) throws JMSException {
enlistResource();
return getSession().createBrowser(queue);
}
public QueueBrowser createBrowser(javax.jms.Queue queue, String messageSelector) throws JMSException {
enlistResource();
return getSession().createBrowser(queue, messageSelector);
}
/* dumb wrapping of Session methods */
public BytesMessage createBytesMessage() throws JMSException {
return getSession().createBytesMessage();
}
public MapMessage createMapMessage() throws JMSException {
return getSession().createMapMessage();
}
public Message createMessage() throws JMSException {
return getSession().createMessage();
}
public ObjectMessage createObjectMessage() throws JMSException {
return getSession().createObjectMessage();
}
public ObjectMessage createObjectMessage(Serializable serializable) throws JMSException {
return getSession().createObjectMessage(serializable);
}
public StreamMessage createStreamMessage() throws JMSException {
return getSession().createStreamMessage();
}
public TextMessage createTextMessage() throws JMSException {
return getSession().createTextMessage();
}
public TextMessage createTextMessage(String text) throws JMSException {
return getSession().createTextMessage(text);
}
public javax.jms.Queue createQueue(String queueName) throws JMSException {
return getSession().createQueue(queueName);
}
public Topic createTopic(String topicName) throws JMSException {
return getSession().createTopic(topicName);
}
public TemporaryQueue createTemporaryQueue() throws JMSException {
return getSession().createTemporaryQueue();
}
public TemporaryTopic createTemporaryTopic() throws JMSException {
return getSession().createTemporaryTopic();
}
public void unsubscribe(String name) throws JMSException {
getSession().unsubscribe(name);
}
/**
* Enlist this session into the current transaction if automaticEnlistingEnabled = true for this resource.
* If no transaction is running then this method does nothing.
* @throws JMSException
*/
protected void enlistResource() throws JMSException {
PoolingConnectionFactory poolingConnectionFactory = pooledConnection.getPoolingConnectionFactory();
if (poolingConnectionFactory.getAutomaticEnlistingEnabled()) {
getSession(); // make sure the session is created before enlisting it
try {
TransactionContextHelper.enlistInCurrentTransaction(this);
} catch (SystemException ex) {
throw (JMSException) new JMSException("error enlisting " + this).initCause(ex);
} catch (RollbackException ex) {
throw (JMSException) new JMSException("error enlisting " + this).initCause(ex);
}
} // if getAutomaticEnlistingEnabled
}
}