Transaction.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.database.AppConnectionService;
import fr.paris.lutece.portal.service.database.PluginConnectionService;
import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.util.AppException;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

/**
 * Transaction
 */
public class Transaction
{
    /**
     * Status for opened transactions
     */
    public static final int OPENED = -1;

    /**
     * Status for committed transactions
     */
    public static final int COMMITTED = 0;

    /**
     * Status for roll backed transactions
     */
    public static final int ROLLEDBACK = 1;
    private static final String MESSAGE_PLUGIN = "Plugin : '";
    private static final String DEFAULT_MODULE_NAME = "core";
    private static final String LOGGER_DEBUG_SQL = "lutece.debug.sql.";

    /**
     * The last SQL query executed by this transaction
     */
    private String _strSQL = StringUtils.EMPTY;

    /** JDBC Connection */
    private Connection _connection;

    /** Connection Service providing connection from a defined pool */
    private PluginConnectionService _connectionService;

    /** Plugin name */
    private String _strPluginName;

    /** The debug logger */
    private Logger _logger;
    private PreparedStatement _statement;
    private int _nStatus = OPENED;
    private boolean _bAutoCommit;

    /**
     * Constructor
     */
    public Transaction( )
    {
        beginTransaction( null );
    }

    /**
     * Constructor
     * 
     * @param plugin
     *            The plugin owner of the transaction
     */
    public Transaction( Plugin plugin )
    {
        beginTransaction( plugin );
    }

    /**
     * Gets a prepared statement
     * 
     * @param strSQL
     *            The SQL statement
     * @return The prepared statement
     * @throws SQLException
     *             If an SQL error occurs
     */
    public PreparedStatement prepareStatement( String strSQL ) throws SQLException
    {
        return prepareStatement( strSQL, null, true );
    }

    /**
     * Gets a prepared statement
     * 
     * @param strSQL
     *            The SQL statement
     * @return The prepared statement
     * @throws SQLException
     *             If an SQL error occurs
     */
    public PreparedStatement prepareStatement( String strSQL, Integer autoGeneratedKeys ) throws SQLException
    {
        return prepareStatement( strSQL, autoGeneratedKeys, true );
    }

    /**
     * Gets a prepared statement
     * 
     * @param strSQL
     *            The SQL statement
     * @return The prepared statement
     * @throws SQLException
     *             If an SQL error occurs
     */
    public PreparedStatement prepareStatement( String strSQL, boolean bLogQueries ) throws SQLException
    {
        return prepareStatement( strSQL, null, bLogQueries );
    }

    /**
     * Gets a prepared statement
     *
     * @since 6.0.0
     * @param strSQL
     *            The SQL statement
     * @param autoGeneratedKeys
     *            a flag indicating whether auto-generated keys should be returned; For example one of <code>Statement.RETURN_GENERATED_KEYS</code> or
     *            <code>Statement.NO_GENERATED_KEYS</code>. See {@link PreparedStatement#prepareStatement(String, int)}
     * @return The prepared statement
     * @throws SQLException
     *             If an SQL error occurs
     */
    public PreparedStatement prepareStatement( String strSQL, Integer autoGeneratedKeys, boolean bLogQueries ) throws SQLException
    {
        // Close the previous statement if exists
        if ( _statement != null )
        {
            _statement.close( );
        }

        // Get a new statement
        _strSQL = strSQL;

        if ( _connection == null )
        {
            throw new SQLException( MESSAGE_PLUGIN + _strPluginName + "' - Connection has been closed. The new prepared statement can not be created : "
                    + ( bLogQueries ? _strSQL : "(query log disabled)" ) );
        }

        if ( autoGeneratedKeys != null )
        {
            _statement = _connection.prepareStatement( _strSQL, autoGeneratedKeys );
        }
        else
        {
            _statement = _connection.prepareStatement( _strSQL );
        }

        return _statement;
    }

    /**
     * The current prepared statement
     * 
     * @return The current statement
     */
    public PreparedStatement getStatement( )
    {
        return _statement;
    }

    /**
     * Execute the current statement
     * 
     * @throws SQLException
     *             If an SQL error occurs
     */
    public void executeStatement( ) throws SQLException
    {
        _logger.debug( "{} {}' - EXECUTE STATEMENT : {}", MESSAGE_PLUGIN, _strPluginName, _strSQL );
        _statement.executeUpdate( );
    }

    /**
     * Commit the transaction
     */
    public void commit( )
    {
        try
        {
            if ( _connection == null )
            {
                throw new SQLException( MESSAGE_PLUGIN + _strPluginName + "' - Transaction has already been closed and can not be committed" );
            }

            _connection.commit( );
            _logger.debug( "{} {}' - COMMIT TRANSACTION", MESSAGE_PLUGIN, _strPluginName );
            closeTransaction( COMMITTED );
        }
        catch( SQLException e )
        {
            rollback( e );
        }
    }

    /**
     * Rollback the transaction
     */
    public void rollback( )
    {
        rollback( null );
    }

    /**
     * Rollback the transaction
     * 
     * @param e
     *            The exception that cause the rollback
     */
    public void rollback( Exception e )
    {
        if ( e != null )
        {
            _logger.error( "Transaction Error - Rollback in progress {}", e.getMessage( ), e.getCause( ) );
        }

        try
        {
            if ( _connection != null )
            {
                _connection.rollback( );
                _logger.debug( "{} {}' - ROLLBACK TRANSACTION", MESSAGE_PLUGIN, _strPluginName );
            }
            else
            {
                _logger.debug( "{} {}' - TRANSACTION HAS ALREADY BEEN ROLLED BACK", MESSAGE_PLUGIN, _strPluginName );
            }
        }
        catch( SQLException ex )
        {
            _logger.error( "Transaction Error - Rollback error : {}", ex.getMessage( ), ex );
        }
        finally
        {
            closeTransaction( ROLLEDBACK );
        }
    }

    /**
     * Return the transaction status
     * 
     * @return The transaction status
     */
    public int getStatus( )
    {
        return _nStatus;
    }

    /**
     * Get the underlying connection.
     * 
     * @return The connection of this transaction. If the transaction has not begin, then return null.
     */
    protected Connection getConnection( )
    {
        return _connection;
    }

    /**
     * Begin a transaction
     * 
     * @param plugin
     *            The plugin owner of the transaction
     */
    protected void beginTransaction( Plugin plugin )
    {
        if ( plugin != null )
        {
            _strPluginName = plugin.getName( );
            _connectionService = plugin.getConnectionService( );
        }
        else
        {
            _strPluginName = DEFAULT_MODULE_NAME;
            _connectionService = AppConnectionService.getDefaultConnectionService( );
        }

        if ( _connectionService == null )
        {
            throw new AppException( "Database access error. Please check component installations and db.properties." );
        }

        _logger = LogManager.getLogger( LOGGER_DEBUG_SQL + _strPluginName );
        _logger.debug( "{}{}' - BEGIN TRANSACTION", MESSAGE_PLUGIN, _strPluginName );

        try
        {
            _connection = _connectionService.getConnection( );

            // Save the autocommit configuration of the connection
            _bAutoCommit = _connection.getAutoCommit( );
            _connection.setAutoCommit( false );
        }
        catch( SQLException e )
        {
            rollback( e );
        }
    }

    /**
     * Close the transaction
     * 
     * @param nStatus
     *            The status of the transaction
     */
    private void closeTransaction( int nStatus )
    {
        _nStatus = nStatus;

        try
        {
            if ( _statement != null )
            {
                _statement.close( );
            }

            // Restore the autocommit configuration of the connection
            if ( _connection != null )
            {
                _connection.setAutoCommit( _bAutoCommit );
            }
        }
        catch( SQLException ex )
        {
            _logger.error( "Transaction Error - Unable to close transaction {}", ex.getMessage( ), ex );
        }
        finally
        {
            _connectionService.freeConnection( _connection );
            _connection = null;
        }
    }

    /**
     * Checks that the transaction has been committed (or rolled back) before being destroyed and release all transaction resources (statement, connection, ...)
     * if not. {@inheritDoc }
     */
    @Override
    protected void finalize( ) throws Throwable
    {
        if ( _nStatus == OPENED )
        {
            _logger.error( "The transaction has not been commited" );
            closeTransaction( OPENED );
        }

        super.finalize( );
    }
}