package org.dcm4chee.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import javax.enterprise.context.ApplicationScoped;
import javax.transaction.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@ApplicationScoped
public class TransactionSynchronization {
private static final Logger LOG = LoggerFactory.getLogger(TransactionSynchronization.class);
private static final String ON_COMMIT_RUNNER_TX_RESOURCE = OnCommitRunner.class.getName();
@Resource(lookup = "java:jboss/TransactionManager")
private TransactionManager tmManager;
@Resource(lookup = "java:comp/TransactionSynchronizationRegistry")
private TransactionSynchronizationRegistry synchronizationRegistry;
public TransactionSynchronizationRegistry getSynchronizationRegistry() {
return synchronizationRegistry;
}
public TransactionManager getTransactionManager() {
return tmManager;
}
/**
* @return {@link Status} of ongoing transaction (works also if no transaction is bound to the thread currently)
*/
public int getStatus() {
try {
return tmManager.getStatus();
} catch (SystemException e) {
throw new RuntimeException("Error while inquiring transaction status", e);
}
}
/**
* Executes a given runnable once (and only if) the current transaction was successfully committed.
* For multiple calls, the order of execution is preserved.
* </p>
* <ul>
* <li>If called within a transaction then the runnable is called after the ongoing transaction has been
* successfully committed (=AFTER_COMPLETION phase). The runnable is executed in the thread that
* committed the transaction</li>
* <li>If called without a transaction then the runnable is simply execute right-away using the calling thread
*</ul>
* @param r what to execute
*/
public void afterSuccessfulCommit(Runnable r) {
afterSuccessfulCommit(r, CallingThreadExecutor.INSTANCE);
}
/*
* Singleton executor that simply uses the calling thread for task execution
*/
private enum CallingThreadExecutor implements Executor {
INSTANCE;
@Override
public void execute(Runnable r) {
r.run();
}
}
/**
* Executes a given runnable once (and only if) the current transaction was successfully committed.
* For multiple calls, the order of execution is preserved.
* </p>
* <ul>
* <li>If called within a transaction then the runnable is called after the ongoing transaction has been
* successfully committed (=AFTER_COMPLETION phase)</li>
* <li>If called without a transaction then the runnable is simply execute right-away using the calling thread
*</ul>
*In all cases the runnable is executed in the give executor
* @param r what to execute
* @param executor Executor used to execute the runnable
*/
public void afterSuccessfulCommit(Runnable r, Executor executor) {
try {
Transaction transaction = tmManager.getTransaction();
if (transaction == null) {
// if no tx - just execute the callback
executor.execute(r);
} else {
// otherwise register the callback if necessary and add to the list
OnCommitRunner onCommitRunner = (OnCommitRunner)synchronizationRegistry.getResource(ON_COMMIT_RUNNER_TX_RESOURCE);
if (onCommitRunner == null) {
onCommitRunner = new OnCommitRunner(executor);
transaction.registerSynchronization(onCommitRunner);
synchronizationRegistry.putResource(ON_COMMIT_RUNNER_TX_RESOURCE, onCommitRunner);
}
onCommitRunner.addRunAfterCommit(r);
}
} catch (SystemException | RollbackException e) {
throw new RuntimeException("Cannot register on-commit hook", e);
}
}
private static class OnCommitRunner implements Synchronization {
private final Executor executor;
private final List<Runnable> toRunAfterCommit = new ArrayList<>();
private OnCommitRunner(Executor executor) {
this.executor = executor;
}
private void addRunAfterCommit(Runnable runnable) {
toRunAfterCommit.add(runnable);
}
@Override
public void beforeCompletion() {
// NOP
}
@Override
public void afterCompletion(int status) {
// run only if successfully committed
try {
if (status == Status.STATUS_COMMITTED) {
for (Runnable runnable : toRunAfterCommit) {
try {
executor.execute(runnable);
} catch (Exception e) {
LOG.error("Error while executing a callback after transaction commit",
e);
}
}
}
} finally {
toRunAfterCommit.clear();
}
}
}
}