/*
 * Copyright (c) 2002-2009 "Neo Technology,"
 *     Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 * 
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.kernel.impl.transaction.xaframework;

import java.io.IOException;
import javax.transaction.xa.XAException;

import org.neo4j.graphdb.TransactionFailureException;

/**
 * <CODE>XaTransaction</CODE> holds all the commands that participate in the
 * transaction and then either rollbacks or commits them. Here are two example
 * implementations:
 * <p>
 * 
 * <pre>
 * <CODE>
 * // Example of XaTransaction implementation where commands are written to
 * // to logical log directly when created
 * public class MyTransaction extends XaTransaction
 * {
 *     private List cmds = new java.util.LinkedList();
 * 
 *     public boolean isReadyOnly()
 *     {
 *         return cmds.size() == 0;
 *     }
 * 
 *     public void doAddCommand( XaCommand cmd )
 *     {
 *         cmds.add( cmd );
 *     }
 * 
 *     public void doRollback()
 *     {
 *         Iterator itr = cmds.iterator();
 *         while ( itr.hasNext() )
 *             ((XaCommand) itr.next()).rollback();
 *     }
 * 
 *     public void doCommit()
 *     {
 *         Iterator itr = cmds.iterator();
 *         while ( itr.hasNext() )
 *             ((XaCommand) itr.next()).execute();
 *     }
 * 
 *     public void doPrepare()
 *     {
 *         // do nothing since commands are added before prepare
 *     }
 * }
 * <CODE>
 * </pre>
 * 
 * Some other implementation that makes use of prepare could look something like
 * this:
 * 
 * <pre>
 * <CODE>
 * // Example of XaTransaction implementation where commands are written to
 * // to logical log when transaction is prepared
 * public class MyTransaction extends XaTransaction
 * {
 *     private List cmds = new java.util.LinkedList();
 * 
 *     public boolean isReadyOnly()
 *     {
 *         return cmds.size() == 0;
 *     }
 * 
 *     public void doAddCommand( XaCommand cmd )
 *     {
 *         // do nothing, we call addCommand in prepare 
 *     }
 * 
 *     public void doRollback()
 *     {
 *         Iterator itr = cmds.iterator();
 *         while ( itr.hasNext() )
 *             ((XaCommand) itr.next()).rollback();
 *     }
 * 
 *     public void doCommit()
 *     {
 *         Iterator itr = cmds.iterator();
 *         while ( itr.hasNext() )
 *             ((XaCommand) itr.next()).execute();
 *     }
 * 
 *     public void doPrepare()
 *     {
 *         Iterator itr = cmds.iterator();
 *         while ( itr.hasNext() )
 *             addCommand( (XaCommand) itr.next() );
 *     }
 * 
 * }
 * </CODE>
 * </pre>
 */
public abstract class XaTransaction
{
    /**
     * Returns <CODE>true</CODE> if read only transaction, that is no
     * modifications will be made once the transaction commits.
     * 
     * @return true if read only transaction
     */
    public abstract boolean isReadOnly();

    /**
     * When a command is added to transaction it will be passed via this method.
     * The <CODE>XaTransaction</CODE> needs to hold all the commands in memory
     * until it receives the <CODE>doCommit</CODE> or <CODE>doRollback</CODE>
     * call.
     * 
     * @param command
     *            The command to be added to transaction
     */
    protected abstract void doAddCommand( XaCommand command );

    /**
     * Rollbacks the transaction, loop through all commands and invoke <CODE>rollback()</CODE>.
     * 
     * @throws XAException
     *             If unable to rollback
     */
    protected abstract void doRollback() throws XAException;

    /**
     * Called when transaction is beeing prepared.
     * 
     * @throws XAException
     *             If unable to prepare
     */
    protected abstract void doPrepare() throws XAException;

    /**
     * Commits the transaction, loop through all commands and invoke 
     * <CODE>execute()</CODE>.
     * 
     * @throws XAEXception
     *             If unable to commit
     */
    protected abstract void doCommit() throws XAException;

    private final int identifier;
    private final XaLogicalLog log;
    private boolean isRecovered = false;
    private boolean committed = false;
    private boolean rolledback = false;
    private boolean prepared = false;

    public XaTransaction( int identifier, XaLogicalLog log )
    {
        if ( log == null )
        {
            throw new IllegalArgumentException( "LogicalLog is null" );
        }
        this.identifier = identifier;
        this.log = log;
    }

    /**
     * If this transacgtion is created during a recovery scan of the logical log
     * method will be called to mark the transaction "recovered".
     */
    protected void setRecovered()
    {
        isRecovered = true;
    }

    /**
     * Returns <CODE>true</CODE> if this is a "recovered transaction".
     * 
     * @return <CODE>true</CODE> if transaction was created during a recovery
     *         else <CODE>false</CODE> is returned
     */
    public boolean isRecovered()
    {
        return isRecovered;
    }

    /**
     * Returns the "internal" identifier for this transaction. See
     * {@link XaLogicalLog#getCurrentTxIdentifier}.
     * 
     * @return The transaction identifier
     */
    public final int getIdentifier()
    {
        return identifier;
    }

    /**
     * Adds the command to transaction. First writes the command to the logical
     * log then calls {@link #doAddCommand}. Also check
     * {@link XaConnectionHelpImpl} class documentation example.
     * 
     * @param command
     *            The command to add to transaction
     * @throws RuntimeException
     *             If problem writing command to logical log or this transaction
     *             is committed or rolled back
     */
    public final void addCommand( XaCommand command )
    {
        if ( committed )
        {
            throw new TransactionFailureException(
                "Cannot add command to committed transaction" );
        }
        if ( rolledback )
        {
            throw new TransactionFailureException(
                "Cannot add command to rolled back transaction" );
        }
        doAddCommand( command );
        try
        {
            log.writeCommand( command, identifier );
        }
        catch ( IOException e )
        {
            throw new TransactionFailureException(
                "Unable to write command to logical log.", e );
        }
    }

    /**
     * Used during recovery, calls {@link #doAddCommand}. Injects the command
     * into the transaction without writing to the logical log.
     * 
     * @param command
     *            The command that will be injected
     */
    protected void injectCommand( XaCommand command )
    {
        doAddCommand( command );
    }

    /**
     * Rollbacks the transaction, calls {@link #doRollback}.
     * 
     * @throws XAException
     *             If unable to rollback
     */
    public final void rollback() throws XAException
    {
        if ( committed )
        {
            throw new XAException(
                "Cannot rollback partialy commited transaction. Recover and "
                    + "commit" );
        }
        rolledback = true;
        doRollback();
    }

    /**
     * Called before prepare marker is written to logical log. Calls
     * {@link #doPrepare()}.
     * 
     * @throws XAException
     *             if unable to prepare
     */
    public final void prepare() throws XAException
    {
        if ( committed )
        {
            throw new XAException( "Cannot prepare comitted transaction" );
        }
        if ( rolledback )
        {
            throw new XAException( "Cannot prepare rolled back transaction" );
        }
        prepared = true;
        doPrepare();
    }

    /**
     * First registers the transaction identifier (see
     * {@link XaLogicalLog#getCurrentTxIdentifier} then calls {@link #doCommit}.
     * 
     * @throws XAException
     *             If unable to commit
     */
    public final void commit() throws XAException
    {
        if ( !prepared && !isRecovered() )
        {
            throw new XAException( "Cannot commit unprepared transaction" );
        }
        log.registerTxIdentifier( getIdentifier() );
        try
        {
            committed = true;
            doCommit();
        }
        finally
        {
            log.unregisterTxIdentifier();
        }
    }
}