/*
* JBoss, Home of Professional Open Source
* Copyright (c) 2010, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt 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.ejb3.tx2.impl;
import org.jboss.ejb3.tx2.spi.TransactionalComponent;
import org.jboss.logging.Logger;
import org.jboss.tm.TransactionTimeoutConfiguration;
import org.jboss.util.deadlock.ApplicationDeadlockException;
import javax.ejb.ApplicationException;
import javax.ejb.EJBException;
import javax.ejb.EJBTransactionRequiredException;
import javax.ejb.EJBTransactionRolledbackException;
import javax.ejb.TransactionAttributeType;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.Random;
/**
* Ensure the correct exceptions are thrown based on both caller
* transactional context and supported Transaction Attribute Type
* <p/>
* EJB3 13.6.2.6
* EJB3 Core Specification 14.3.1 Table 14
*
* @author <a href="mailto:andrew.rubinger@redhat.com">ALR</a>
* @author <a href="mailto:cdewolf@redhat.com">Carlo de Wolf</a>
*/
public abstract class CMTTxInterceptor
{
private static final Logger log = Logger.getLogger(CMTTxInterceptor.class);
private static final int MAX_RETRIES = 5;
private static final Random RANDOM = new Random();
/**
* Returns the {@link TransactionalComponent} applicable for this interceptor
*
* @return
*/
protected abstract TransactionalComponent getTransactionalComponent();
/**
* The <code>endTransaction</code> method ends a transaction and
* translates any exceptions into
* TransactionRolledBack[Local]Exception or SystemException.
*
* @param tm a <code>TransactionManager</code> value
* @param tx a <code>Transaction</code> value
*/
protected void endTransaction(TransactionManager tm, Transaction tx)
{
try
{
if (tx != tm.getTransaction())
{
throw new IllegalStateException("Wrong tx on thread: expected " + tx + ", actual " + tm.getTransaction());
}
if (tx.getStatus() == Status.STATUS_MARKED_ROLLBACK)
{
tm.rollback();
}
else
{
// Commit tx
// This will happen if
// a) everything goes well
// b) app. exception was thrown
tm.commit();
}
}
catch (RollbackException e)
{
handleEndTransactionException(e);
}
catch (HeuristicMixedException e)
{
handleEndTransactionException(e);
}
catch (HeuristicRollbackException e)
{
handleEndTransactionException(e);
}
catch (SystemException e)
{
handleEndTransactionException(e);
}
}
protected int getCurrentTransactionTimeout() throws SystemException
{
TransactionManager tm = this.getTransactionManager();
if (tm instanceof TransactionTimeoutConfiguration)
{
return ((TransactionTimeoutConfiguration) tm).getTransactionTimeout();
}
return 0;
}
protected void handleEndTransactionException(Exception e)
{
if (e instanceof RollbackException)
{
throw new EJBTransactionRolledbackException("Transaction rolled back", e);
}
throw new EJBException(e);
}
protected void handleInCallerTx(InvocationContext invocation, Throwable t, Transaction tx) throws Exception
{
ApplicationException ae = this.getApplicationException(t.getClass());
if (ae != null)
{
if (ae.rollback())
{
setRollbackOnly(tx);
}
// an app exception can never be an Error
throw (Exception) t;
}
// if it's part of throws clause, then treat it as an application exception (section 14.2.1 of EJB3.1 spec)
Method invokedMethod = invocation.getMethod();
if (invokedMethod != null && this.isPartOfThrowsClause(invokedMethod, t))
{
throw (Exception) t;
}
// if it's not EJBTransactionRolledbackException
if (!(t instanceof EJBTransactionRolledbackException))
{
if (t instanceof Error)
{
//t = new EJBTransactionRolledbackException(formatException("Unexpected Error", t));
Throwable cause = t;
t = new EJBTransactionRolledbackException("Unexpected Error");
t.initCause(cause);
}
// If this is an EJBException, pass through to the caller
else if (t instanceof EJBException || t instanceof RemoteException)
{
// Leave Exception as-is (this is in place to handle specifically, and not
// as a generic RuntimeException
}
else if (t instanceof RuntimeException)
{
t = new EJBTransactionRolledbackException(t.getMessage(), (Exception) t);
}
else // application exception
{
throw (Exception) t;
}
}
setRollbackOnly(tx);
log.error(t);
throw (Exception) t;
}
protected void handleExceptionInOurTx(InvocationContext invocation, Throwable t, Transaction tx) throws Exception
{
ApplicationException ae = this.getApplicationException(t.getClass());
if (ae != null)
{
if (ae.rollback())
{
setRollbackOnly(tx);
}
throw (Exception) t;
}
// if it's part of throws clause, then treat it as an application exception (section 14.2.1 of EJB3.1 spec)
Method invokedMethod = invocation.getMethod();
if (invokedMethod != null && this.isPartOfThrowsClause(invokedMethod, t))
{
throw (Exception) t;
}
// if it's neither EJBException nor RemoteException
if (!(t instanceof EJBException || t instanceof RemoteException))
{
// errors and unchecked are wrapped into EJBException
if (t instanceof Error)
{
//t = new EJBException(formatException("Unexpected Error", t));
Throwable cause = t;
t = new EJBException("Unexpected Error");
t.initCause(cause);
}
else if (t instanceof RuntimeException)
{
t = new EJBException((Exception) t);
}
else
{
// an application exception
throw (Exception) t;
}
}
setRollbackOnly(tx);
throw (Exception) t;
}
@AroundInvoke
public Object invoke(InvocationContext invocation) throws Exception
{
Method invokedMethod = invocation.getMethod();
TransactionAttributeType txAttr = this.getTransactionalComponent().getTransactionAttributeType(invokedMethod);
switch (txAttr)
{
case MANDATORY:
return mandatory(invocation);
case NEVER:
return never(invocation);
case NOT_SUPPORTED:
return notSupported(invocation);
case REQUIRED:
return required(invocation);
case REQUIRES_NEW:
return requiresNew(invocation);
case SUPPORTS:
return supports(invocation);
default:
throw new IllegalStateException("Unexpected tx attribute " + txAttr + " on " + invocation);
}
}
protected Object invokeInCallerTx(InvocationContext invocation, Transaction tx) throws Exception
{
try
{
return invocation.proceed();
}
catch (Throwable t)
{
handleInCallerTx(invocation, t, tx);
}
throw new RuntimeException("UNREACHABLE");
}
protected Object invokeInNoTx(InvocationContext invocation) throws Exception
{
return invocation.proceed();
}
protected Object invokeInOurTx(InvocationContext invocation, TransactionManager tm) throws Exception
{
for (int i = 0; i < MAX_RETRIES; i++)
{
tm.begin();
Transaction tx = tm.getTransaction();
try
{
try
{
return invocation.proceed();
}
catch (Throwable t)
{
handleExceptionInOurTx(invocation, t, tx);
}
finally
{
endTransaction(tm, tx);
}
}
catch (Exception ex)
{
ApplicationDeadlockException deadlock = ApplicationDeadlockException.isADE(ex);
if (deadlock != null)
{
if (!deadlock.retryable() || i + 1 >= MAX_RETRIES)
{
throw deadlock;
}
log.warn(deadlock.getMessage() + " retrying " + (i + 1));
Thread.sleep(RANDOM.nextInt(1 + i), RANDOM.nextInt(1000));
}
else
{
throw ex;
}
}
}
throw new RuntimeException("UNREACHABLE");
}
protected Object mandatory(InvocationContext invocation) throws Exception
{
TransactionManager tm = this.getTransactionManager();
Transaction tx = tm.getTransaction();
if (tx == null)
{
throw new EJBTransactionRequiredException("Transaction is required for invocation: " + invocation);
}
return invokeInCallerTx(invocation, tx);
}
protected Object never(InvocationContext invocation) throws Exception
{
TransactionManager tm = this.getTransactionManager();
if (tm.getTransaction() != null)
{
throw new EJBException("Transaction present on server in Never call (EJB3 13.6.2.6)");
}
return invokeInNoTx(invocation);
}
protected Object notSupported(InvocationContext invocation) throws Exception
{
TransactionManager tm = this.getTransactionManager();
Transaction tx = tm.getTransaction();
if (tx != null)
{
tm.suspend();
try
{
return invokeInNoTx(invocation);
}
catch (Exception e)
{
// If application exception was thrown, rethrow
if (this.getApplicationException(e.getClass()) != null)
{
throw e;
}
// Otherwise wrap in EJBException
else
{
throw new EJBException(e);
}
}
finally
{
tm.resume(tx);
}
}
else
{
return invokeInNoTx(invocation);
}
}
protected Object required(InvocationContext invocation) throws Exception
{
int oldTimeout = getCurrentTransactionTimeout();
Method invokedMethod = invocation.getMethod();
int timeout = this.getTransactionalComponent().getTransactionTimeout(invokedMethod);
TransactionManager tm = this.getTransactionManager();
try
{
if (timeout != -1 && tm != null)
{
tm.setTransactionTimeout(timeout);
}
Transaction tx = tm.getTransaction();
if (tx == null)
{
return invokeInOurTx(invocation, tm);
}
else
{
return invokeInCallerTx(invocation, tx);
}
}
finally
{
if (tm != null)
{
tm.setTransactionTimeout(oldTimeout);
}
}
}
protected Object requiresNew(InvocationContext invocation) throws Exception
{
int oldTimeout = getCurrentTransactionTimeout();
Method invokedMethod = invocation.getMethod();
int timeout = this.getTransactionalComponent().getTransactionTimeout(invokedMethod);
TransactionManager tm = this.getTransactionManager();
try
{
if (timeout != -1 && tm != null)
{
tm.setTransactionTimeout(timeout);
}
Transaction tx = tm.getTransaction();
if (tx != null)
{
tm.suspend();
try
{
return invokeInOurTx(invocation, tm);
}
finally
{
tm.resume(tx);
}
}
else
{
return invokeInOurTx(invocation, tm);
}
}
finally
{
if (tm != null)
{
tm.setTransactionTimeout(oldTimeout);
}
}
}
/**
* The <code>setRollbackOnly</code> method calls setRollbackOnly()
* on the invocation's transaction and logs any exceptions than may
* occur.
*
* @param tx the transaction
*/
protected void setRollbackOnly(Transaction tx)
{
try
{
tx.setRollbackOnly();
}
catch (SystemException ex)
{
log.error("SystemException while setting transaction for rollback only", ex);
}
catch (IllegalStateException ex)
{
log.error("IllegalStateException while setting transaction for rollback only", ex);
}
}
protected Object supports(InvocationContext invocation) throws Exception
{
Transaction tx = this.getTransactionManager().getTransaction();
if (tx == null)
{
return invokeInNoTx(invocation);
}
else
{
return invokeInCallerTx(invocation, tx);
}
}
private TransactionManager getTransactionManager()
{
return this.getTransactionalComponent().getTransactionManager();
}
private ApplicationException getApplicationException(Class<?> exceptionClass)
{
TransactionalComponent txComponent = this.getTransactionalComponent();
return txComponent.getApplicationException(exceptionClass);
}
/**
* Returns true if the passed {@link Throwable} is part of the throws clause of the passed {@link Method}.
* Else, returns false.
*
* @param m The method whose throws clause will be checked
* @param t The Throwable which will be compared against the throws clause of the passed method
* @return
*/
private boolean isPartOfThrowsClause(Method m, Throwable t)
{
Class<?>[] exceptionTypes = m.getExceptionTypes();
for (Class<?> exceptionType : exceptionTypes)
{
if (exceptionType.equals(t.getClass()))
{
return true;
}
}
return false;
}
}