/*
 * Copyright 2002-2006 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.testng.spring.test;

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

/**
 * Convenient superclass for tests that should occur in a transaction, but normally
 * will roll the transaction back on the completion of each test.
 *
 * <p>This is useful in a range of circumstances, allowing the following benefits:
 * <ul>
 * <li>Ability to delete or insert any data in the database, without affecting other tests
 * <li>Providing a transactional context for any code requiring a transaction
 * <li>Ability to write anything to the database without any need to clean up.
 * </ul>
 *
 * <p>This class is typically very fast, compared to traditional setup/teardown scripts.
 *
 * <p>If data should be left in the database, call the <code>setComplete()</code>
 * method in each test. The "defaultRollback" property, which defaults to "true",
 * determines whether transactions will complete by default.
 *
 * <p>It is even possible to end the transaction early; for example, to verify lazy
 * loading behavior of an O/R mapping tool. (This is a valuable away to avoid
 * unexpected errors when testing a web UI, for example.)  Simply call the
 * <code>endTransaction()</code> method. Execution will then occur without a
 * transactional context.
 *
 * <p>The <code>startNewTransaction()</code> method may be called after a call to
 * <code>endTransaction()</code> if you wish to create a new transaction, quite
 * independent of the old transaction. The new transaction's default fate will be to
 * roll back, unless <code>setComplete()</code> is called again during the scope of the
 * new transaction. Any number of transactions may be created and ended in this way.
 * The final transaction will automatically be rolled back when the test case is
 * torn down.
 *
 * <p>Transactional behavior requires a single bean in the context implementing the
 * PlatformTransactionManager interface. This will be set by the superclass's
 * Dependency Injection mechanism. If using the superclass's Field Injection mechanism,
 * the implementation should be named "transactionManager". This mechanism allows the
 * use of this superclass even when there's more than one transaction manager in the context.
 * 
 * <p><i>This superclass can also be used without transaction management, if no
 * PlatformTransactionManager bean is found in the context provided. Be careful about
 * using this mode, as it allows the potential to permanently modify data.
 * This mode is available only if dependency checking is turned off in
 * the AbstractDependencyInjectionSpringContextTests superclass. The non-transactional
 * capability is provided to enable use of the same subclass in different environments.</i>
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @since 1.1.1
 */
public abstract class AbstractTransactionalSpringContextTests extends AbstractDependencyInjectionSpringContextTests {

	/** The transaction manager to use */
	protected PlatformTransactionManager transactionManager;

	/** Should we roll back by default? */
	private boolean defaultRollback = true;

	/** Should we commit the current transaction? */
	private boolean complete = false;

	/** Number of transactions started */
	private int transactionsStarted = 0;
	
	/**
	 * Default transaction definition is used.
	 * Subclasses can change this to cause different behaviour.
	 */
	private TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();

	/**
	 * TransactionStatus for this test. Typical subclasses won't need to use it.
	 */
	protected TransactionStatus transactionStatus;


	/**
	 * Default constructor for AbstractTransactionalSpringContextTests.
	 */
	public AbstractTransactionalSpringContextTests() {
	}

	/**
	 * Specify the transaction manager to use. No transaction management will be available
	 * if this is not set. (This mode works only if dependency checking is turned off in
	 * the AbstractDependencyInjectionSpringContextTests superclass.)
	 * <p>Populated through dependency injection by the superclass.
	 */
	public void setTransactionManager(PlatformTransactionManager ptm) {
		this.transactionManager = ptm;
	}

	/**
	 * Subclasses can set this value in their constructor to change
	 * default, which is always to roll the transaction back.
	 */
	public void setDefaultRollback(boolean defaultRollback) {
		this.defaultRollback = defaultRollback;
	}


	/**
	 * Call in an overridden <code>runBare()</code> method to prevent transactional execution.
	 */
	protected void preventTransaction() {
		this.transactionDefinition = null;
	}

	/**
	 * Override the transaction attributes that will be used.
	 * Call in an overridden <code>runBare()</code> method so that
	 * <code>setUp()</code> and <code>tearDown()</code> behavior is modified.
	 * @param customDefinition custom definition to override with
	 */
	protected void setTransactionDefinition(TransactionDefinition customDefinition) {
		this.transactionDefinition = customDefinition;
	}


	/**
	 * This implementation creates a transaction before test execution.
	 * Override <code>onSetUpBeforeTransaction()</code> and/or
	 * <code>onSetUpInTransaction()</code> to add custom set-up behavior.
	 * @see #onSetUpBeforeTransaction()
	 * @see #onSetUpInTransaction()
	 */
	protected final void onSetUp() throws Exception {
		this.complete = !this.defaultRollback;

		if (this.transactionManager == null) {
			logger.info("No transaction manager set: test will NOT run within a transaction");
		}
		else if (this.transactionDefinition == null) {
			logger.info("No transaction definition set: test will NOT run within a transaction");
		}
		else {
			onSetUpBeforeTransaction();
			startNewTransaction();
			try {
				onSetUpInTransaction();
			}
			catch (Exception ex) {
				endTransaction();
				throw ex;
			}
		}
	}

	/**
	 * Subclasses can override this method to perform any setup operations,
	 * such as populating a database table, <i>before</i> the transaction
	 * created by this class. Only invoked if there <i>is</i> a transaction:
	 * that is, if <code>preventTransaction()</code> has not been invoked in
	 * an overridden <code>runTest()</code> method.
	 * @throws Exception simply let any exception propagate
	 * @see #preventTransaction()
	 */
	protected void onSetUpBeforeTransaction() throws Exception {
	}

	/**
	 * Subclasses can override this method to perform any setup operations,
	 * such as populating a database table, <i>within</i> the transaction
	 * created by this class.
	 * <p><b>NB:</b> Not called if there is no transaction management, due to no
	 * transaction manager being provided in the context.
	 * <p>If any {@link Throwable} is thrown, the transaction that has been started
	 * prior to the execution of this method will be {@link #endTransaction() ended}
	 * (or rather an attempt will be made to {@link #endTransaction() end it gracefully});
	 * The offending {@link Throwable} will then be rethrown.
	 * @throws Exception simply let any exception propagate
	 */
	protected void onSetUpInTransaction() throws Exception {
	}


	/**
	 * This implementation ends the transaction after test execution.
	 * Override <code>onTearDownInTransaction()</code> and/or
	 * <code>onTearDownAfterTransaction()</code> to add custom tear-down behavior.
	 * <p>Note that <code>onTearDownInTransaction()</code> will only be called
	 * if a transaction is still active at the time of the test shutdown.
	 * In particular, it will <code>not</code> be called if the transaction has
	 * been completed with an explicit <code>endTransaction()</code> call before.
	 * @throws Exception simply let any exception propagate
	 * @see #onTearDownInTransaction()
	 * @see #onTearDownAfterTransaction()
	 * @see #endTransaction()
	 */
	protected final void onTearDown() throws Exception {
		// Call onTearDownInTransaction and end transaction if the transaction is still active.
		if (this.transactionStatus != null && !this.transactionStatus.isCompleted()) {
			try {
				onTearDownInTransaction();
			}
			finally {
				endTransaction();
			}
		}
		// Call onTearDownAfterTransaction if there was at least one transaction,
		// even if it has been completed early through an endTransaction() call.
		if (this.transactionsStarted > 0) {
			onTearDownAfterTransaction();
		}
	}

	/**
	 * Subclasses can override this method to run invariant tests here.
	 * The transaction is <i>still active</i> at this point, so any changes
	 * made in the transaction will still be visible. However, there is no need
	 * to clean up the database, as a rollback will follow automatically.
	 * <p><b>NB:</b> Not called if there is no actual transaction, for example
	 * due to no transaction manager being provided in the application context.
	 * @throws Exception simply let any exception propagate
	 */
	protected void onTearDownInTransaction() throws Exception {
	}

	/**
	 * Subclasses can override this method to perform cleanup after a transaction
	 * here. At this point, the transaction is <i>not active anymore</i>.
	 * @throws Exception simply let any exception propagate
	 */
	protected void onTearDownAfterTransaction() throws Exception {
	}


	/**
	 * Cause the transaction to commit for this test method,
	 * even if default is set to rollback.
	 * @throws IllegalStateException if the operation cannot be set to
	 * complete as no transaction manager was provided
	 */
	protected void setComplete() throws UnsupportedOperationException {
		if (this.transactionManager == null) {
			throw new IllegalStateException("No transaction manager set");
		}
		this.complete = true;
	}

  /**
   * Cause the transaction to rollback for this test method,
   * even if default is set to complete.
   * @throws IllegalStateException if the operation cannot be set to
   * complete as no transaction manager was provided
   */
  protected void setRollback() throws UnsupportedOperationException {
    if (this.transactionManager == null) {
      throw new IllegalStateException("No transaction manager set");
    }
    this.complete = false;
  }
  
  /**
	 * Immediately force a commit or rollback of the transaction,
	 * according to the complete flag.
	 * <p>Can be used to explicitly let the transaction end early,
	 * for example to check whether lazy associations of persistent objects
	 * work outside of a transaction (that is, have been initialized properly).
	 * @see #setComplete()
	 */
	protected void endTransaction() {
		if (this.transactionStatus != null) {
			try {
				if (!this.complete) {
					this.transactionManager.rollback(this.transactionStatus);
					logger.info("Rolled back transaction after test execution");
				}
				else {
					this.transactionManager.commit(this.transactionStatus);
					logger.info("Committed transaction after test execution");
				}
			}
			finally {
				this.transactionStatus = null;
			}
		}
	}

	/**
	 * Start a new transaction. Only call this method if <code>endTransaction()</code>
	 * has been called. <code>setComplete()</code> can be used again in the new transaction.
	 * The fate of the new transaction, by default, will be the usual rollback.
	 * @see #endTransaction()
	 * @see #setComplete()
	 */
	protected void startNewTransaction() throws TransactionException {
		if (this.transactionStatus != null) {
			throw new IllegalStateException("Cannot start new transaction without ending existing transaction: " +
					"Invoke endTransaction() before startNewTransaction()");
		}
		if (this.transactionManager == null) {
			throw new IllegalStateException("No transaction manager set");
		}

		this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
		++this.transactionsStarted;
		this.complete = !this.defaultRollback;

		if (logger.isInfoEnabled()) {
			logger.info("Began transaction (" + this.transactionsStarted + "): transaction manager [" +
					this.transactionManager + "]; default rollback = " + this.defaultRollback);
		}
	}

}
