TransactionManager.java

/*
 * Copyright (c) 2002-2022, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.util.sql;

import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.util.AppLogService;

import java.sql.Connection;
import java.sql.SQLException;

import java.util.HashMap;
import java.util.Map;

/**
 * Class to manage transactions
 */
public final class TransactionManager
{
    private static final String DEFAULT_POOL_NAME = "portal";
    private static ThreadLocal<Map<String, MultiPluginTransaction>> _tlTransactions = new ThreadLocal<>( );

    /**
     * Default constructor
     */
    private TransactionManager( )
    {
        // Do nothing
    }

    /**
     * Begin a transaction for the current thread using the pool of a specific plugin with the default transaction isolation. The default transaction isolation
     * is {@link Connection#TRANSACTION_READ_COMMITTED }.<br>
     * Note that only one transaction may be active at a time by pool for each thread.
     * 
     * @param plugin
     *            The plugin to use the pool of, or null to use the default pool.
     */
    public static void beginTransaction( Plugin plugin )
    {
        beginTransaction( plugin, Connection.TRANSACTION_READ_COMMITTED );
    }

    /**
     * Begin a transaction for the current thread using the pool of a specific plugin
     * 
     * @param plugin
     *            The plugin to use the pool of, or null to use the default pool.
     * @param nTransactionIsolation
     *            The transaction isolation. View {@link Connection#setTransactionIsolation(int) } to view the different available transaction isolations.
     */
    public static void beginTransaction( Plugin plugin, int nTransactionIsolation )
    {
        Map<String, MultiPluginTransaction> mapTransactions = _tlTransactions.get( );
        MultiPluginTransaction transaction = null;

        if ( mapTransactions == null )
        {
            mapTransactions = new HashMap<>( );
            _tlTransactions.set( mapTransactions );
        }
        else
        {
            transaction = mapTransactions.get( getPluginPool( plugin ) );
        }

        if ( ( transaction == null ) || ( transaction.getNbTransactionsOpened( ) <= 0 ) )
        {
            transaction = new MultiPluginTransaction( plugin );

            try
            {
                transaction.getConnection( ).setTransactionIsolation( nTransactionIsolation );
            }
            catch( SQLException e )
            {
                AppLogService.error( e.getMessage( ), e );
            }

            mapTransactions.put( getPluginPool( plugin ), transaction );
        }
        else
        {
            // A transaction has already been opened for this pool,
            // so we save that information to prevent the next call to the commit method to close the transaction.
            transaction.incrementNbTransactionsOpened( );
        }
    }

    /**
     * Get the current transaction for the pool of a given plugin.
     * 
     * @param plugin
     *            The plugin to use the pool of, or null to use the default pool.
     * @return The transaction, or null if no transaction is currently running.
     */
    public static MultiPluginTransaction getCurrentTransaction( Plugin plugin )
    {
        Map<String, MultiPluginTransaction> mapTransactions = _tlTransactions.get( );

        if ( mapTransactions != null )
        {
            return mapTransactions.get( getPluginPool( plugin ) );
        }

        return null;
    }

    /**
     * Commit the transaction associated to the pool of a given plugin.
     * 
     * @param plugin
     *            The plugin associated to the pool to commit the transaction of, or null to use the default pool.
     */
    public static void commitTransaction( Plugin plugin )
    {
        Map<String, MultiPluginTransaction> mapTransactions = _tlTransactions.get( );

        if ( mapTransactions != null )
        {
            String strPoolName = getPluginPool( plugin );
            MultiPluginTransaction transaction = mapTransactions.get( strPoolName );

            if ( transaction != null )
            {
                // If the number of transactions opened is 1 or less, then we commit the transaction
                if ( transaction.getNbTransactionsOpened( ) <= 1 )
                {
                    transaction.commit( );
                    mapTransactions.remove( strPoolName );
                }
                else
                {
                    // Otherwise, we decrement the number
                    transaction.decrementNbTransactionsOpened( );
                }
            }
        }
    }

    /**
     * Roll back a transaction associated to the pool of a given plugin. Note that any plugin can roll a transaction back.
     * 
     * @param plugin
     *            The plugin associated to the pool to roll back the transaction of, or null to use the default pool.
     */
    public static void rollBack( Plugin plugin )
    {
        rollBack( plugin, null );
    }

    /**
     * Roll back a transaction associated to the pool of a given plugin with an exception.
     * 
     * @param plugin
     *            The plugin associated to the pool to roll back the transaction of, or null to use the default pool.
     * @param e
     *            The exception to associates with the roll back.
     */
    public static void rollBack( Plugin plugin, Exception e )
    {
        Map<String, MultiPluginTransaction> mapTransactions = _tlTransactions.get( );

        if ( mapTransactions != null )
        {
            String strPoolName = getPluginPool( plugin );
            MultiPluginTransaction transaction = mapTransactions.get( strPoolName );

            // We roll back the transaction, no matter how much transactions has been opened.
            if ( transaction != null )
            {
                transaction.rollback( e );
                mapTransactions.remove( strPoolName );
            }
        }
    }

    /**
     * Roll back every transactions opened by the current thread. This method attempt to prevent connection leak.
     */
    public static void rollBackEveryTransaction( )
    {
        rollBackEveryTransaction( null );
    }

    /**
     * Roll back every transactions opened by the current thread. This method attempt to prevent connection leak.
     * 
     * @param e
     *            The exception that occurs and that may have prevent transaction from being properly closed (committed or roll backed)
     */
    public static void rollBackEveryTransaction( Throwable e )
    {
        Map<String, MultiPluginTransaction> mapTransactions = _tlTransactions.get( );

        if ( ( mapTransactions != null ) && ( mapTransactions.size( ) > 0 ) )
        {
            for ( MultiPluginTransaction transaction : mapTransactions.values( ) )
            {
                transaction.rollback( null );
            }

            mapTransactions.clear( );
        }
    }

    /**
     * Get the name of the pool of a given plugin
     * 
     * @param plugin
     *            The plugin to get the name of the pool, or null to get the name of the default pool.
     * @return The name of the pool
     */
    private static String getPluginPool( Plugin plugin )
    {
        return ( plugin != null ) ? plugin.getDbPoolName( ) : DEFAULT_POOL_NAME;
    }
}