/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.tm.usertx.client;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.LinkedList;
import java.util.Hashtable;
import javax.naming.InitialContext;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import javax.transaction.Transaction;
import javax.transaction.Status;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.RollbackException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import org.jboss.remoting.CannotConnectException;
import org.jboss.tm.TransactionPropagationContextFactory;
import org.jboss.tm.usertx.interfaces.UserTransactionSession;
import org.jboss.tm.usertx.interfaces.UserTransactionSessionFactory;
import org.jboss.logging.Logger;
import org.jboss.naming.NamingContextFactory;
/**
* The client-side UserTransaction implementation. This will delegate all
* UserTransaction calls to the server.
*
* <em>Warning:</em> This is only for stand-alone clients that do not have their
* own transaction service. No local work is done in the context of transactions
* started here, only work done in beans at the server. Instantiating objects of
* this class outside the server will change the JRMP GenericProxy so that
* outgoing calls use the propagation contexts of the transactions started
* here.
* @author <a href="mailto:osh@sparre.dk">Ole Husgaard</a>
* @author Scott.Stark@jboss.org
* @author <a href="mailto:galder.zamarreno@jboss.com">Galder Zamarreno</a>
* @version $Revision: 98993 $
*/
public class ClientUserTransaction
implements UserTransaction,
TransactionPropagationContextFactory,
Referenceable,
Serializable
{
// Static --------------------------------------------------------
/** @since at least jboss-3.2.0 */
private static final long serialVersionUID = 1747989355209242872L;
/**
* Our singleton instance.
*/
private static ClientUserTransaction singleton = new ClientUserTransaction();
private static final Logger log = Logger.getLogger(ClientUserTransaction.class);
private static boolean trace = log.isTraceEnabled();
/**
* Return a reference to the singleton instance.
*
* @return the singleton
*/
public static ClientUserTransaction getSingleton()
{
return singleton;
}
// Constructors --------------------------------------------------
/**
* Create a new instance.
*/
private ClientUserTransaction()
{
}
// Public --------------------------------------------------------
//
// implements interface UserTransaction
//
public void begin()
throws NotSupportedException, SystemException
{
if(getStatus() != Status.STATUS_NO_TRANSACTION)
throw new NotSupportedException("Attempt to start a nested transaction (the transaction started previously hasn't been ended yet).");
ThreadInfo info = getThreadInfo();
trace = log.isTraceEnabled(); // Only check for trace enabled once per transaction
if (trace)
{
log.trace("Calling UserTransaction.begin()");
}
try
{
Object tpc = getSession().begin(info.getTimeout());
info.push(tpc);
}
catch (SystemException e)
{
throw e;
}
catch (RemoteException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch (Exception e)
{
logCauseException(e);
throw new SystemException(e.toString());
}
}
public void commit()
throws RollbackException,
HeuristicMixedException,
HeuristicRollbackException,
SecurityException,
IllegalStateException,
SystemException
{
ThreadInfo info = getThreadInfo();
Object tpc = info.getTpc();
if (trace)
{
log.trace("Calling UserTransaction.commit(" + tpc + ")");
}
try
{
getSession().commit(tpc);
}
catch (RollbackException e)
{
throw e;
}
catch (HeuristicMixedException e)
{
throw e;
}
catch (HeuristicRollbackException e)
{
throw e;
}
catch (SecurityException e)
{
throw e;
}
catch (SystemException e)
{
throw e;
}
catch (IllegalStateException e)
{
throw e;
}
catch (RemoteException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch(CannotConnectException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch (Exception e)
{
logCauseException(e);
throw new SystemException(e.toString());
}
finally
{
info.pop();
}
}
public void rollback()
throws SecurityException,
IllegalStateException,
SystemException
{
ThreadInfo info = getThreadInfo();
Object tpc = info.getTpc();
if (trace)
{
log.trace("Calling UserTransaction.rollback(" + tpc + ")");
}
try
{
getSession().rollback(tpc);
}
catch (SecurityException e)
{
throw e;
}
catch (SystemException e)
{
throw e;
}
catch (IllegalStateException e)
{
throw e;
}
catch (RemoteException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch(CannotConnectException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch (Exception e)
{
logCauseException(e);
throw new SystemException(e.toString());
}
finally
{
info.pop();
}
}
public void setRollbackOnly()
throws IllegalStateException,
SystemException
{
ThreadInfo info = getThreadInfo();
Object tpc = info.getTpc();
if (trace)
{
log.trace("Calling UserTransaction.setRollbackOnly(" + tpc + ")");
}
try
{
getSession().setRollbackOnly(tpc);
}
catch (SystemException e)
{
throw e;
}
catch (IllegalStateException e)
{
throw e;
}
catch (RemoteException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch (Exception e)
{
logCauseException(e);
throw new SystemException(e.toString());
}
}
public int getStatus()
throws SystemException
{
ThreadInfo info = getThreadInfo();
Object tpc = info.getTpc();
if (log.isTraceEnabled())
{
log.trace("Calling UserTransaction.getStatus(" + tpc + ")");
}
if (tpc == null)
{
return Status.STATUS_NO_TRANSACTION;
}
try
{
return getSession().getStatus(tpc);
}
catch (SystemException e)
{
throw e;
}
catch (RemoteException e)
{
// destroy session gone bad.
destroySession();
logCauseException(e);
throw new SystemException(e.toString());
}
catch (Exception e)
{
logCauseException(e);
throw new SystemException(e.toString());
}
}
public void setTransactionTimeout(int seconds)
throws SystemException
{
getThreadInfo().setTimeout(seconds);
}
//
// implements interface TransactionPropagationContextFactory
//
public Object getTransactionPropagationContext()
{
return getThreadInfo().getTpc();
}
public Object getTransactionPropagationContext(Transaction tx)
{
// No need to implement in a stand-alone client.
throw new InternalError("Should not have been used.");
}
//
// implements interface Referenceable
//
public Reference getReference()
throws NamingException
{
Reference ref = new Reference("org.jboss.tm.usertx.client.ClientUserTransaction",
"org.jboss.tm.usertx.client.ClientUserTransactionObjectFactory",
null);
return ref;
}
// Private -------------------------------------------------------
/**
* The RMI remote interface to the real tx service session at the server.
*/
private UserTransactionSession session = null;
/**
* Storage of per-thread information used here.
*/
private transient ThreadLocal threadInfo = new ThreadLocal();
/**
* Create a new session.
*/
private synchronized void createSession()
{
// Destroy any old session.
if (session != null)
destroySession();
try
{
// Get a reference to the UT session factory.
UserTransactionSessionFactory factory;
Hashtable env = (Hashtable) NamingContextFactory.lastInitialContextEnv.get();
InitialContext ctx = new InitialContext(env);
factory = (UserTransactionSessionFactory) ctx.lookup("UserTransactionSessionFactory");
// Call factory to get a UT session.
session = factory.newInstance();
}
catch (Exception ex)
{
throw new RuntimeException("UT factory lookup failed", ex);
}
}
/**
* Destroy the current session.
*/
private synchronized void destroySession()
{
if (session != null)
{
try
{
session.destroy();
}
catch (RemoteException ex)
{
// Ignore.
}
session = null;
}
}
/**
* Get the session. This will create a session, if one does not already
* exist.
*/
private synchronized UserTransactionSession getSession()
{
if (session == null)
createSession();
return session;
}
/**
* Return the per-thread information, possibly creating it if needed.
*/
private ThreadInfo getThreadInfo()
{
ThreadInfo ret = (ThreadInfo) threadInfo.get();
if (ret == null)
{
ret = new ThreadInfo();
threadInfo.set(ret);
}
if (trace)
{
log.trace("Thread local: " + threadInfo);
log.trace("Thread info holder: " + ret);
}
return ret;
}
private void logCauseException(Exception e)
{
if (trace)
{
log.trace("Logging cause exception", e);
}
}
// Inner classes -------------------------------------------------
/**
* Per-thread data holder class. This stores the stack of TPCs for the
* transactions started by this thread.
*/
private class ThreadInfo
{
/**
* A stack of TPCs for transactions started by this thread. If the
* underlying service does not support nested transactions, its size is
* never greater than 1. Last element of the list denotes the stack top.
*/
private LinkedList tpcStack = new LinkedList();
/**
* The timeout value (in seconds) for new transactions started by this
* thread.
*/
private int timeout = 0;
/**
* Override to terminate any transactions that the thread may have
* forgotten.
*/
protected void finalize()
throws Throwable
{
if (trace)
{
log.trace("Tpc stack: finalize " + this);
}
try
{
while (!tpcStack.isEmpty())
{
Object tpc = getTpc();
pop();
try
{
getSession().rollback(tpc);
}
catch (Exception ex)
{
// ignore
}
}
}
catch (Throwable t)
{
// ignore
}
super.finalize();
}
/**
* Push the TPC of a newly started transaction on the stack.
*/
void push(Object tpc)
{
tpcStack.addLast(tpc);
if (trace)
{
log.trace("Tpc stack: added " + this + " tpc=" + tpc);
}
}
/**
* Pop the TPC of a newly terminated transaction from the stack.
*/
void pop()
{
Object tpc = tpcStack.removeLast();
if (trace)
{
log.trace("Tpc stack: removed " + this + " tpc=" + tpc);
}
}
/**
* Get the TPC at the top of the stack.
*/
Object getTpc()
{
Object tpc = (tpcStack.isEmpty()) ? null : tpcStack.getLast();
if (trace)
{
log.trace("Tpc stack: peek " + this + " tpc=" + tpc);
}
return tpc;
}
/**
* Return the default transaction timeout in seconds to use for new
* transactions started by this thread. A value of <code>0</code> means
* that a default timeout value should be used.
*/
int getTimeout()
{
return timeout;
}
/**
* Set the default transaction timeout in seconds to use for new
* transactions started by this thread.
*/
void setTimeout(int seconds)
{
timeout = seconds;
}
}
}