package org.molgenis.data.transaction;
import org.apache.commons.logging.LogFactory;
import org.molgenis.data.MolgenisDataException;
import org.molgenis.data.populate.IdGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
/**
* TransactionManager used by Molgenis.
* <p>
* TransactionListeners can be registered and will be notified on transaction begin, commit and rollback of transactions
* that are not readonly.
* <p>
* Each transaction is given a unique transaction id.
*/
public class MolgenisTransactionManager extends DataSourceTransactionManager
{
private static final long serialVersionUID = 1L;
public static final String TRANSACTION_ID_RESOURCE_NAME = "transactionId";
private static final Logger LOG = LoggerFactory.getLogger(MolgenisTransactionManager.class);
private final IdGenerator idGenerator;
private final List<MolgenisTransactionListener> transactionListeners = new ArrayList<>();
private final TransactionExceptionTranslatorRegistry transactionExceptionTranslatorRegistry;
public MolgenisTransactionManager(IdGenerator idGenerator, DataSource dataSource,
TransactionExceptionTranslatorRegistry transactionExceptionTranslatorRegistry)
{
super(dataSource);
super.logger = LogFactory.getLog(DataSourceTransactionManager.class);
setNestedTransactionAllowed(false);
this.idGenerator = idGenerator;
this.transactionExceptionTranslatorRegistry = requireNonNull(transactionExceptionTranslatorRegistry);
}
public void addTransactionListener(MolgenisTransactionListener transactionListener)
{
//FIXME: make concurrent using ReeentrantReadWriteLock.
transactionListeners.add(transactionListener);
}
@Override
protected Object doGetTransaction() throws TransactionException
{
Object dataSourceTransactionManager = super.doGetTransaction();
String id;
if (TransactionSynchronizationManager.hasResource(TRANSACTION_ID_RESOURCE_NAME))
{
id = (String) TransactionSynchronizationManager.getResource(TRANSACTION_ID_RESOURCE_NAME);
}
else
{
id = idGenerator.generateId().toLowerCase();
}
return new MolgenisTransaction(id, dataSourceTransactionManager);
}
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException
{
MolgenisTransaction molgenisTransaction = (MolgenisTransaction) transaction;
if (LOG.isDebugEnabled())
{
LOG.debug("Start transaction [{}]", molgenisTransaction.getId());
}
super.doBegin(molgenisTransaction.getDataSourceTransaction(), definition);
if (!definition.isReadOnly())
{
TransactionSynchronizationManager.bindResource(TRANSACTION_ID_RESOURCE_NAME, molgenisTransaction.getId());
transactionListeners.forEach(j -> j.transactionStarted(molgenisTransaction.getId()));
}
}
@Override
protected void doCommit(DefaultTransactionStatus status) throws TransactionException
{
MolgenisTransaction transaction = (MolgenisTransaction) status.getTransaction();
if (LOG.isDebugEnabled())
{
LOG.debug("Commit transaction [{}]", transaction.getId());
}
DefaultTransactionStatus jpaTransactionStatus = new DefaultTransactionStatus(
transaction.getDataSourceTransaction(), status.isNewTransaction(), status.isNewSynchronization(),
status.isReadOnly(), status.isDebug(), status.getSuspendedResources());
if (!status.isReadOnly())
{
transactionListeners.forEach(j -> j.commitTransaction(transaction.getId()));
}
try
{
super.doCommit(jpaTransactionStatus);
}
catch (TransactionException e)
{
throw translateTransactionException(e);
}
if (!status.isReadOnly())
{
transactionListeners.forEach(j -> j.afterCommitTransaction(transaction.getId()));
}
}
@Override
protected void doRollback(DefaultTransactionStatus status) throws TransactionException
{
MolgenisTransaction transaction = (MolgenisTransaction) status.getTransaction();
if (LOG.isDebugEnabled())
{
LOG.debug("Rollback transaction [{}]", transaction.getId());
}
DefaultTransactionStatus jpaTransactionStatus = new DefaultTransactionStatus(
transaction.getDataSourceTransaction(), status.isNewTransaction(), status.isNewSynchronization(),
status.isReadOnly(), status.isDebug(), status.getSuspendedResources());
if (!status.isReadOnly())
{
transactionListeners.forEach(j -> j.rollbackTransaction(transaction.getId()));
}
super.doRollback(jpaTransactionStatus);
}
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status)
{
MolgenisTransaction transaction = (MolgenisTransaction) status.getTransaction();
DefaultTransactionStatus jpaTransactionStatus = new DefaultTransactionStatus(
transaction.getDataSourceTransaction(), status.isNewTransaction(), status.isNewSynchronization(),
status.isReadOnly(), status.isDebug(), status.getSuspendedResources());
super.doSetRollbackOnly(jpaTransactionStatus);
}
@Override
protected boolean isExistingTransaction(Object transaction)
{
return super.isExistingTransaction(((MolgenisTransaction) transaction).getDataSourceTransaction());
}
@Override
protected void doCleanupAfterCompletion(Object transaction)
{
MolgenisTransaction molgenisTransaction = (MolgenisTransaction) transaction;
if (LOG.isDebugEnabled())
{
LOG.debug("Cleanup transaction [{}]", molgenisTransaction.getId());
}
super.doCleanupAfterCompletion(molgenisTransaction.getDataSourceTransaction());
TransactionSynchronizationManager.unbindResourceIfPossible(TRANSACTION_ID_RESOURCE_NAME);
transactionListeners.forEach(j ->
{
j.doCleanupAfterCompletion(molgenisTransaction.getId());
});
}
@Override
protected Object doSuspend(Object transaction)
{
MolgenisTransaction molgenisTransaction = (MolgenisTransaction) transaction;
return super.doSuspend(molgenisTransaction.getDataSourceTransaction());
}
@Override
protected void doResume(Object transaction, Object suspendedResources)
{
MolgenisTransaction molgenisTransaction = (MolgenisTransaction) transaction;
super.doResume(molgenisTransaction.getDataSourceTransaction(), suspendedResources);
}
private MolgenisDataException translateTransactionException(TransactionException transactionException)
{
for (TransactionExceptionTranslator transactionExceptionTranslator : transactionExceptionTranslatorRegistry
.getTransactionExceptionTranslators())
{
MolgenisDataException molgenisDataException = transactionExceptionTranslator
.doTranslate(transactionException);
if (molgenisDataException != null)
{
return molgenisDataException;
}
}
throw new RuntimeException(
format("Unexpected exception class [%s]", transactionException.getClass().getSimpleName()),
transactionException);
}
}