All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.rholder.spring.transaction.TransactionBindingSupport Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco 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 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Alfresco. If not, see .
 */
package com.github.rholder.spring.transaction;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * Helper class to manage transaction synchronization. This provides helpers to
 * ensure that the necessary TransactionSynchronization instances
 * are registered on behalf of the application code.
 * 
 * @author Derek Hulley
 * @author Ray Holder, derived work modifications
 */
public abstract class TransactionBindingSupport
{    
    /**
     * The order of synchronization set to be 100 less than the Hibernate synchronization order
     */
    public static final int SESSION_SYNCHRONIZATION_ORDER =
        SessionFactoryUtils.SESSION_SYNCHRONIZATION_ORDER - 100;

    /** resource key to store the transaction synchronizer instance */
    private static final String RESOURCE_KEY_TXN_SYNCH = "txnSynch";
    
    private static Log logger = LogFactory.getLog(TransactionBindingSupport.class);
    
    /**
     * @return Returns the system time when the transaction started, or -1 if there is no current transaction.
     */
    public static long getTransactionStartTime()
    {
        /*
         * This method can be called outside of a transaction, so we can go direct to the synchronizations.
         */
        TransactionSynchronizationImpl txnSynch =
            (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH);
        if (txnSynch == null)
        {
            if (TransactionSynchronizationManager.isSynchronizationActive())
            {
                // need to lazily register synchronizations
                return registerSynchronizations().getTransactionStartTime();
            }
            else
            {
                return -1;   // not in a transaction
            }
        }
        else
        {
            return txnSynch.getTransactionStartTime();
        }
    }
    
    /**
     * Get a unique identifier associated with each transaction of each thread.  Null is returned if
     * no transaction is currently active.
     * 
     * @return Returns the transaction ID, or null if no transaction is present
     */
    public static String getTransactionId()
    {
        /*
         * Go direct to the synchronizations as we don't want to register a resource if one doesn't exist.
         * This method is heavily used, so the simple Map lookup on the ThreadLocal is the fastest.
         */
        
        TransactionSynchronizationImpl txnSynch =
                (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH);
        if (txnSynch == null)
        {
            if (TransactionSynchronizationManager.isSynchronizationActive())
            {
                // need to lazily register synchronizations
                return registerSynchronizations().getTransactionId();
            }
            else
            {
                return null;   // not in a transaction
            }
        }
        else
        {
            return txnSynch.getTransactionId();
        }
    }
    
    /**
     * 
     * @author Derek Hulley
     * @since 2.1.4
     */
    public static enum TxnReadState
    {
        /** No transaction is active */
        TXN_NONE,
        /** The current transaction is read-only */
        TXN_READ_ONLY,
        /** The current transaction supports writes */
        TXN_READ_WRITE
    }
    
    /**
     * @return      Returns the read-write state of the current transaction
     * @since 2.1.4
     */
    public static TxnReadState getTransactionReadState()
    {
        if (!TransactionSynchronizationManager.isSynchronizationActive())
        {
            return TxnReadState.TXN_NONE;
        }
        // Find the read-write state of the txn
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly())
        {
            return TxnReadState.TXN_READ_ONLY;
        }
        else
        {
            return TxnReadState.TXN_READ_WRITE;
        }
    }
    
    /**
     * Checks the state of the current transaction and throws an exception if a transaction
     * is not present or if the transaction is not read-write, if required.
     * 
     * @param requireReadWrite          true if the transaction must be read-write
     * 
     * @since 3.2
     */
    public static void checkTransactionReadState(boolean requireReadWrite)
    {
        if (!TransactionSynchronizationManager.isSynchronizationActive())
        {
            throw new IllegalStateException(
                    "The current operation requires an active " +
                    (requireReadWrite ? "read-write" : "") +
                    "transaction.");
        }
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly() && requireReadWrite)
        {
            throw new IllegalStateException("The current operation requires an active read-write transaction.");
        }
    }
    
    /**
     * Gets a resource associated with the current transaction, which must be active.
     * 

* All necessary synchronization instances will be registered automatically, if required. * * * @param key the thread resource map key * @return Returns a thread resource of null if not present * * @see TransactionalResourceHelper for helper methods to create and bind common collection types */ @SuppressWarnings("unchecked") public static R getResource(Object key) { // get the synchronization TransactionSynchronizationImpl txnSynch = getSynchronization(); // get the resource Object resource = txnSynch.resources.get(key); // done if (logger.isDebugEnabled()) { logger.debug("Fetched resource: \n" + " key: " + key + "\n" + " resource: " + resource); } return (R) resource; } /** * Binds a resource to the current transaction, which must be active. *

* All necessary synchronization instances will be registered automatically, if required. * * @param key * @param resource */ public static void bindResource(Object key, Object resource) { // get the synchronization TransactionSynchronizationImpl txnSynch = getSynchronization(); // bind the resource txnSynch.resources.put(key, resource); // done if (logger.isDebugEnabled()) { logger.debug("Bound resource: \n" + " key: " + key + "\n" + " resource: " + resource); } } /** * Unbinds a resource from the current transaction, which must be active. *

* All necessary synchronization instances will be registered automatically, if required. * * @param key */ public static void unbindResource(Object key) { // get the synchronization TransactionSynchronizationImpl txnSynch = getSynchronization(); // remove the resource txnSynch.resources.remove(key); // done if (logger.isDebugEnabled()) { logger.debug("Unbound resource: \n" + " key: " + key); } } /** * Method that registers a TransactionListener against the * transaction. *

* Setting this will ensure that the pre- and post-commit operations perform * the necessary cleanups using the listener. *

* Although bound within a Set, it would still be better for the caller * to only bind once per transaction, if possible. * * @param listener the TransactionListener to perform transaction completion * tasks on */ public static void bindListener(TransactionListener listener) { // get transaction-local synchronization TransactionSynchronizationImpl synch = getSynchronization(); // bind the service in boolean bound = synch.addListener(listener); // done if (logger.isDebugEnabled()) { logBoundService(listener, bound); } } /** * Use as part of a debug statement * * @param service the service to report * @param bound true if the service was just bound; false if it was previously bound */ private static void logBoundService(Object service, boolean bound) { if (bound) { logger.debug("Bound service: \n" + " transaction: " + getTransactionId() + "\n" + " service: " + service); } else { logger.debug("Service already bound: \n" + " transaction: " + getTransactionId() + "\n" + " service: " + service); } } /** * Gets the current transaction synchronization instance, which contains the locally bound * resources that are available to {@link #getResource(Object) retrieve} or * {@link #bindResource(Object, Object) add to}. *

* This method also ensures that the transaction binding has been performed. * * @return Returns the common synchronization instance used */ private static TransactionSynchronizationImpl getSynchronization() { // ensure synchronizations return registerSynchronizations(); } /** * Binds the Alfresco-specific to the transaction resources * * @return Returns the current or new synchronization implementation */ private static TransactionSynchronizationImpl registerSynchronizations() { /* * No thread synchronization or locking required as the resources are all threadlocal */ if (!TransactionSynchronizationManager.isSynchronizationActive()) { Thread currentThread = Thread.currentThread(); throw new RuntimeException("Transaction must be active and synchronization is required: " + currentThread); } TransactionSynchronizationImpl txnSynch = (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); if (txnSynch != null) { // synchronization already registered return txnSynch; } // we need a unique ID for the transaction String txnId = UUID.randomUUID().toString(); // register the synchronization txnSynch = new TransactionSynchronizationImpl(txnId); TransactionSynchronizationManager.registerSynchronization(txnSynch); // register the resource that will ensure we don't duplication the synchronization TransactionSynchronizationManager.bindResource(RESOURCE_KEY_TXN_SYNCH, txnSynch); // done if (logger.isDebugEnabled()) { logger.debug("Bound txn synch: " + txnSynch); } return txnSynch; } /** * Cleans out transaction resources if present */ private static void clearSynchronization() { if (TransactionSynchronizationManager.hasResource(RESOURCE_KEY_TXN_SYNCH)) { Object txnSynch = TransactionSynchronizationManager.unbindResource(RESOURCE_KEY_TXN_SYNCH); // done if (logger.isDebugEnabled()) { logger.debug("Unbound txn synch:" + txnSynch); } } } /** * Helper method to rebind the synchronization to the transaction * * @param txnSynch */ private static void rebindSynchronization(TransactionSynchronizationImpl txnSynch) { TransactionSynchronizationManager.bindResource(RESOURCE_KEY_TXN_SYNCH, txnSynch); if (logger.isDebugEnabled()) { logger.debug("Bound txn synch: " + txnSynch); } } /** * Handler of txn synchronization callbacks specific to internal * application requirements */ private static class TransactionSynchronizationImpl extends TransactionSynchronizationAdapter { private long txnStartTime; private final String txnId; private final LinkedHashSet listeners; private final Map resources; /** * Sets up the resource map * * @param txnId */ public TransactionSynchronizationImpl(String txnId) { this.txnStartTime = System.currentTimeMillis(); this.txnId = txnId; listeners = new LinkedHashSet(5); resources = new HashMap(17); } public long getTransactionStartTime() { return txnStartTime; } public String getTransactionId() { return txnId; } /** * @return Returns a set of TransactionListener instances that will be called * during end-of-transaction processing */ public boolean addListener(TransactionListener listener) { return listeners.add(listener); } /** * @return Returns the listeners in a list disconnected from the original set */ private List getListenersIterable() { return new ArrayList(listeners); } public String toString() { StringBuilder sb = new StringBuilder(50); sb.append("TransactionSychronizationImpl") .append("[ txnId=").append(txnId) .append(", resources=").append(resources) .append("]"); return sb.toString(); } /** * @see TransactionBindingSupport#SESSION_SYNCHRONIZATION_ORDER */ @Override public int getOrder() { return TransactionBindingSupport.SESSION_SYNCHRONIZATION_ORDER; } @Override public void suspend() { if (logger.isDebugEnabled()) { logger.debug("Suspending transaction: " + this); } TransactionBindingSupport.clearSynchronization(); } @Override public void resume() { if (logger.isDebugEnabled()) { logger.debug("Resuming transaction: " + this); } TransactionBindingSupport.rebindSynchronization(this); } /** * Pre-commit cleanup. *

* Ensures that the session transaction listeners are property executed. * The Lucene indexes are then prepared. */ @Override public void beforeCommit(boolean readOnly) { if (logger.isDebugEnabled()) { logger.debug("Before commit " + (readOnly ? "read-only" : "" ) + ": " + this); } // get the txn ID TransactionSynchronizationImpl synch = (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); if (synch == null) { throw new RuntimeException("No synchronization bound to thread"); } // These are still considered part of the transaction so are executed here doBeforeCommit(readOnly); } /** * Execute the beforeCommit event handlers for the registered listeners * * @param readOnly is read only */ private void doBeforeCommit(boolean readOnly) { doBeforeCommit(new HashSet(listeners.size()), readOnly); } /** * Executes the beforeCommit event handlers for the outstanding listeners. * This process is iterative as the process of calling listeners may lead to more listeners * being added. The new listeners will be processed until there no listeners remaining. * * @param visitedListeners a set containing the already visited listeners * @param readOnly is read only */ private void doBeforeCommit(Set visitedListeners, boolean readOnly) { Set pendingListeners = new HashSet(listeners); pendingListeners.removeAll(visitedListeners); if (pendingListeners.size() != 0) { for (TransactionListener listener : pendingListeners) { listener.beforeCommit(readOnly); visitedListeners.add(listener); } doBeforeCommit(visitedListeners, readOnly); } } @Override public void beforeCompletion() { if (logger.isDebugEnabled()) { logger.debug("Before completion: " + this); } // notify listeners for (TransactionListener listener : getListenersIterable()) { listener.beforeCompletion(); } } @Override public void afterCompletion(int status) { String statusStr = "unknown"; switch (status) { case TransactionSynchronization.STATUS_COMMITTED: statusStr = "committed"; break; case TransactionSynchronization.STATUS_ROLLED_BACK: statusStr = "rolled-back"; break; default: } if (logger.isDebugEnabled()) { logger.debug("After completion (" + statusStr + "): " + this); } List iterableListeners = getListenersIterable(); // notify listeners if (status == TransactionSynchronization.STATUS_COMMITTED) { for (TransactionListener listener : iterableListeners) { try { listener.afterCommit(); } catch (RuntimeException e) { logger.error("After completion (" + statusStr + ") listener exception: \n" + " listener: " + listener, e); } } } else { for (TransactionListener listener : iterableListeners) { try { listener.afterRollback(); } catch (RuntimeException e) { logger.error("After completion (" + statusStr + ") listener exception: \n" + " listener: " + listener, e); } } } // clear the thread's registrations and synchronizations TransactionBindingSupport.clearSynchronization(); } } }