/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 *
 */
package org.hibernate.transaction;

import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

import org.hibernate.HibernateException;
import org.hibernate.Transaction;
import org.hibernate.TransactionException;
import org.hibernate.jdbc.JDBCContext;
import org.hibernate.util.JTAHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link Transaction} implementation based on transaction management through
 * a JTA {@link UserTransaction}.  Similar to {@link CMTTransaction}, except
 * here we are actually managing the transactions through the Hibernate
 * transaction mechanism.
 *
 * @author Gavin King
 * @author Steve Ebersole
 * @author Les Hazlewood
 */
public class JTATransaction implements Transaction {

	private static final Logger log = LoggerFactory.getLogger( JTATransaction.class );

	private final JDBCContext jdbcContext;
	private final TransactionFactory.Context transactionContext;

	private UserTransaction userTransaction;
	private boolean newTransaction;
	private boolean begun;
	private boolean commitFailed;
	private boolean commitSucceeded;
	private boolean callback;

	public JTATransaction(
			UserTransaction userTransaction,
			JDBCContext jdbcContext,
			TransactionFactory.Context transactionContext) {
		this.jdbcContext = jdbcContext;
		this.transactionContext = transactionContext;
		this.userTransaction = userTransaction;
	}

	/**
	 * {@inheritDoc}
	 */
	public void begin() throws HibernateException {
		if ( begun ) {
			return;
		}
		if ( commitFailed ) {
			throw new TransactionException( "cannot re-start transaction after failed commit" );
		}

		log.debug( "begin" );

		try {
			newTransaction = userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION;
			if ( newTransaction ) {
				userTransaction.begin();
				log.debug( "Began a new JTA transaction" );
			}
		}
		catch ( Exception e ) {
			log.error( "JTA transaction begin failed", e );
			throw new TransactionException( "JTA transaction begin failed", e );
		}

		/*if (newTransaction) {
			// don't need a synchronization since we are committing
			// or rolling back the transaction ourselves - assuming
			// that we do no work in beforeTransactionCompletion()
			synchronization = false;
		}*/

		boolean synchronization = jdbcContext.registerSynchronizationIfPossible();

		if ( !newTransaction && !synchronization ) {
			log.warn( "You should set hibernate.transaction.manager_lookup_class if cache is enabled" );
		}

		if ( !synchronization ) {
			//if we could not register a synchronization,
			//do the before/after completion callbacks
			//ourself (but we need to let jdbcContext
			//know that this is what we are going to
			//do, so it doesn't keep trying to register
			//synchronizations)
			callback = jdbcContext.registerCallbackIfNecessary();
		}

		begun = true;
		commitSucceeded = false;

		jdbcContext.afterTransactionBegin( this );
	}

	/**
	 * {@inheritDoc}
	 */
	public void commit() throws HibernateException {
		if ( !begun ) {
			throw new TransactionException( "Transaction not successfully started" );
		}

		log.debug( "commit" );

		boolean flush = !transactionContext.isFlushModeNever()
				&& ( callback || !transactionContext.isFlushBeforeCompletionEnabled() );

		if ( flush ) {
			transactionContext.managedFlush(); //if an exception occurs during flush, user must call rollback()
		}

		if ( callback && newTransaction ) {
			jdbcContext.beforeTransactionCompletion( this );
		}

		closeIfRequired();

		if ( newTransaction ) {
			try {
				userTransaction.commit();
				commitSucceeded = true;
				log.debug( "Committed JTA UserTransaction" );
			}
			catch ( Exception e ) {
				commitFailed = true; // so the transaction is already rolled back, by JTA spec
				log.error( "JTA commit failed", e );
				throw new TransactionException( "JTA commit failed: ", e );
			}
			finally {
				afterCommitRollback();
			}
		}
		else {
			// this one only really needed for badly-behaved applications!
			// (if the TransactionManager has a Sychronization registered,
			// its a noop)
			// (actually we do need it for downgrading locks)
			afterCommitRollback();
		}

	}

	/**
	 * {@inheritDoc}
	 */
	public void rollback() throws HibernateException {
		if ( !begun && !commitFailed ) {
			throw new TransactionException( "Transaction not successfully started" );
		}

		log.debug( "rollback" );

		try {
			closeIfRequired();
		}
		catch ( Exception e ) {
			// swallow it, and continue to roll back JTA transaction
			log.error( "could not close session during rollback", e );
		}

		try {
			if ( newTransaction ) {
				if ( !commitFailed ) {
					userTransaction.rollback();
					log.debug( "Rolled back JTA UserTransaction" );
				}
			}
			else {
				userTransaction.setRollbackOnly();
				log.debug( "set JTA UserTransaction to rollback only" );
			}
		}
		catch ( Exception e ) {
			log.error( "JTA rollback failed", e );
			throw new TransactionException( "JTA rollback failed", e );
		}
		finally {
			afterCommitRollback();
		}
	}

	private static final int NULL = Integer.MIN_VALUE;

	private void afterCommitRollback() throws TransactionException {

		begun = false;
		// this method is a noop if there is a Synchronization!
		if ( callback ) {
			if ( !newTransaction ) {
				log.warn( "You should set hibernate.transaction.manager_lookup_class if cache is enabled" );
			}
			int status = NULL;
			try {
				status = userTransaction.getStatus();
			}
			catch ( Exception e ) {
				log.error( "Could not determine transaction status after commit", e );
				throw new TransactionException( "Could not determine transaction status after commit", e );
			}
			finally {
				jdbcContext.afterTransactionCompletion( status == Status.STATUS_COMMITTED, this );
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean wasRolledBack() throws TransactionException {
		final int status;
		try {
			status = userTransaction.getStatus();
		}
		catch ( SystemException se ) {
			log.error( "Could not determine transaction status", se );
			throw new TransactionException( "Could not determine transaction status", se );
		}
		if ( status == Status.STATUS_UNKNOWN ) {
			throw new TransactionException( "Could not determine transaction status" );
		}
		else {
			return JTAHelper.isRollback( status );
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean wasCommitted() throws TransactionException {
		final int status;
		try {
			status = userTransaction.getStatus();
		}
		catch ( SystemException se ) {
			log.error( "Could not determine transaction status", se );
			throw new TransactionException( "Could not determine transaction status: ", se );
		}
		if ( status == Status.STATUS_UNKNOWN ) {
			throw new TransactionException( "Could not determine transaction status" );
		}
		else {
			return status == Status.STATUS_COMMITTED;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public boolean isActive() throws TransactionException {
		if ( !begun || commitFailed || commitSucceeded ) {
			return false;
		}

		final int status;
		try {
			status = userTransaction.getStatus();
		}
		catch ( SystemException se ) {
			log.error( "Could not determine transaction status", se );
			throw new TransactionException( "Could not determine transaction status: ", se );
		}
		if ( status == Status.STATUS_UNKNOWN ) {
			throw new TransactionException( "Could not determine transaction status" );
		}
		else {
			return status == Status.STATUS_ACTIVE;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void registerSynchronization(Synchronization sync) throws HibernateException {
		if ( getTransactionManager() == null ) {
			throw new IllegalStateException( "JTA TransactionManager not available" );
		}
		else {
			try {
				getTransactionManager().getTransaction().registerSynchronization( sync );
			}
			catch ( Exception e ) {
				throw new TransactionException( "could not register synchronization", e );
			}
		}
	}

	/**
	 * Getter for property 'transactionManager'.
	 *
	 * @return Value for property 'transactionManager'.
	 */
	private TransactionManager getTransactionManager() {
		return transactionContext.getFactory().getTransactionManager();
	}

	private void closeIfRequired() throws HibernateException {
		boolean close = callback &&
				transactionContext.shouldAutoClose() &&
				!transactionContext.isClosed();
		if ( close ) {
			transactionContext.managedClose();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void setTimeout(int seconds) {
		try {
			userTransaction.setTransactionTimeout( seconds );
		}
		catch ( SystemException se ) {
			throw new TransactionException( "could not set transaction timeout", se );
		}
	}

	/**
	 * Getter for property 'userTransaction'.
	 *
	 * @return Value for property 'userTransaction'.
	 */
	protected UserTransaction getUserTransaction() {
		return userTransaction;
	}
}
