/*
 * Copyright 2002-2010 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.springframework.transaction.jta;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.UserTransaction;

import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.util.ClassUtils;

/**
 * Special {@link JtaTransactionManager} variant for Oracle OC4J (10.1.3 and higher).
 * Supports the full power of Spring's transaction definitions on OC4J's
 * transaction coordinator, <i>beyond standard JTA</i>: transaction names
 * and per-transaction isolation levels.
 *
 * <p>Uses OC4J's special <code>begin(name)</code> method to start a JTA transaction,
 * in orderto make <b>Spring-driven transactions visible in OC4J's transaction
 * monitor</b>. In case of Spring's declarative transactions, the exposed name will
 * (by default) be the fully-qualified class name + "." + method name.
 *
 * <p>Supports a <b>per-transaction isolation level</b> through OC4J's corresponding
 * <code>OC4JTransaction.setTransactionIsolation(int)</code> method. This will
 * apply the specified isolation level (e.g. ISOLATION_SERIALIZABLE) to all
 * JDBC Connections that participate in the given transaction.
 *
 * <p>Automatically detects the available OC4J server version and adapts accordingly.
 * Supports the "com.evermind.server" package in OC4J 10.1.3.2 as well as the
 * "oracle.j2ee.transaction" package in later OC4J versions.
 *
 * <p>By default, the JTA UserTransaction and TransactionManager handles are
 * fetched directly from OC4J's <code>TransactionUtility</code> in 10.1.3.2+.
 * This can be overridden by specifying "userTransaction"/"userTransactionName"
 * and "transactionManager"/"transactionManagerName", passing in existing handles
 * or specifying corresponding JNDI locations to look up.
 *
 * <p>Thanks to Oracle for donating the original version of this extended OC4J
 * integration code to the Spring project!
 *
 * @author Paul Parkinson
 * @author Juergen Hoeller
 * @since 2.0.3
 * @see org.springframework.transaction.TransactionDefinition#getName
 * @see org.springframework.transaction.TransactionDefinition#getIsolationLevel
 * @see oracle.j2ee.transaction.OC4JTransactionManager#begin(String)
 * @see oracle.j2ee.transaction.OC4JTransaction#setTransactionIsolation
 * @see oracle.j2ee.transaction.TransactionUtility
 */
public class OC4JJtaTransactionManager extends JtaTransactionManager {

	private static final String TRANSACTION_UTILITY_CLASS_NAME =
			"oracle.j2ee.transaction.TransactionUtility";

	private static final String TRANSACTION_MANAGER_CLASS_NAME =
			"oracle.j2ee.transaction.OC4JTransactionManager";

	private static final String TRANSACTION_CLASS_NAME =
			"oracle.j2ee.transaction.OC4JTransaction";

	private static final String FALLBACK_TRANSACTION_MANAGER_CLASS_NAME =
			"com.evermind.server.ApplicationServerTransactionManager";

	private static final String FALLBACK_TRANSACTION_CLASS_NAME =
			"com.evermind.server.ApplicationServerTransaction";


	private Method beginWithNameMethod;

	private Method setTransactionIsolationMethod;


	@Override
	public void afterPropertiesSet() throws TransactionSystemException {
		super.afterPropertiesSet();
		loadOC4JTransactionClasses();
	}

	@Override
	protected UserTransaction retrieveUserTransaction() throws TransactionSystemException {
		try {
			Class transactionUtilityClass = getClass().getClassLoader().loadClass(TRANSACTION_UTILITY_CLASS_NAME);
			Method getInstanceMethod = transactionUtilityClass.getMethod("getInstance");
			Object transactionUtility = getInstanceMethod.invoke(null);
			logger.debug("Retrieving JTA UserTransaction from OC4J TransactionUtility");
			Method getUserTransactionMethod =
					transactionUtility.getClass().getMethod("getOC4JUserTransaction");
			return (UserTransaction) getUserTransactionMethod.invoke(transactionUtility);
		}
		catch (ClassNotFoundException ex) {
			logger.debug("Could not find OC4J 10.1.3.2 TransactionUtility: " + ex);
			// Return null to make the superclass perform its standard J2EE lookup,
			// which will work on earlier OC4J versions.
			return null;
		}
		catch (InvocationTargetException ex) {
			throw new TransactionSystemException(
					"OC4J's TransactionUtility.getOC4JUserTransaction() method failed", ex.getTargetException());
		}
		catch (Exception ex) {
			throw new TransactionSystemException(
					"Could not invoke OC4J's TransactionUtility.getOC4JUserTransaction() method", ex);
		}
	}

	private void loadOC4JTransactionClasses() throws TransactionSystemException {
		// Find available OC4J API (in "oracle.j2ee.transaction" or "com.evermind.server")
		Class transactionManagerClass = null;
		Class transactionClass = null;
		try {
			transactionManagerClass = getClass().getClassLoader().loadClass(TRANSACTION_MANAGER_CLASS_NAME);
			transactionClass = getClass().getClassLoader().loadClass(TRANSACTION_CLASS_NAME);
		}
		catch (ClassNotFoundException ex) {
			try {
				transactionManagerClass = getClass().getClassLoader().loadClass(FALLBACK_TRANSACTION_MANAGER_CLASS_NAME);
				transactionClass = getClass().getClassLoader().loadClass(FALLBACK_TRANSACTION_CLASS_NAME);
			}
			catch (ClassNotFoundException ex2) {
				throw new TransactionSystemException(
						"Could not initialize OC4JJtaTransactionManager because OC4J API classes are not available", ex);
			}
		}

		// Cache reflective Method references for later use.
		if (transactionManagerClass.isInstance(getUserTransaction())) {
			this.beginWithNameMethod = ClassUtils.getMethodIfAvailable(
					transactionManagerClass, "begin", String.class);
			this.setTransactionIsolationMethod = ClassUtils.getMethodIfAvailable(
					transactionClass, "setTransactionIsolation", int.class);
			logger.info("Support for OC4J transaction names and isolation levels available");
		}
		else {
			logger.info("Support for OC4J transaction names and isolation levels not available");
		}
	}


	@Override
	protected void doJtaBegin(JtaTransactionObject txObject, TransactionDefinition definition)
			throws NotSupportedException, SystemException {

		int timeout = determineTimeout(definition);
		applyTimeout(txObject, timeout);

		// Apply transaction name, if any, through the extended OC4J transaction begin method.
		if (this.beginWithNameMethod != null && definition.getName() != null) {
			/*
			oracle.j2ee.transaction.OC4JTransactionManager otm = (oracle.j2ee.transaction.OC4JTransactionManager) ut;
			otm.begin(definition.getName());
			*/
			try {
				this.beginWithNameMethod.invoke(txObject.getUserTransaction(), definition.getName());
			}
			catch (InvocationTargetException ex) {
				throw new TransactionSystemException(
						"OC4J's UserTransaction.begin(String) method failed", ex.getTargetException());
			}
			catch (Exception ex) {
				throw new TransactionSystemException(
						"Could not invoke OC4J's UserTransaction.begin(String) method", ex);
			}
		}
		else {
			// No OC4J UserTransaction available or no transaction name specified
			// -> standard JTA begin call.
			txObject.getUserTransaction().begin();
		}

		// Specify isolation level, if any, through the corresponding OC4J transaction method.
		if (this.setTransactionIsolationMethod != null) {
			if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
				try {
					Transaction tx = getTransactionManager().getTransaction();
					/*
					oracle.j2ee.transaction.OC4JTransaction otx = (oracle.j2ee.transaction.OC4JTransaction) tx;
					otx.setTransactionIsolation(definition.getIsolationLevel());
					*/
					this.setTransactionIsolationMethod.invoke(tx, definition.getIsolationLevel());
				}
				catch (InvocationTargetException ex) {
					throw new TransactionSystemException(
							"OC4J's Transaction.setTransactionIsolation(int) method failed", ex.getTargetException());
				}
				catch (Exception ex) {
					throw new TransactionSystemException(
							"Could not invoke OC4J's Transaction.setTransactionIsolation(int) method", ex);
				}
			}
		}
		else {
			applyIsolationLevel(txObject, definition.getIsolationLevel());
		}
	}


	@Override
	public Transaction createTransaction(String name, int timeout) throws NotSupportedException, SystemException {
		if (this.beginWithNameMethod != null && name != null) {
			UserTransaction ut = getUserTransaction();
			if (timeout >= 0) {
				ut.setTransactionTimeout(timeout);
			}
			try {
				this.beginWithNameMethod.invoke(ut, name);
			}
			catch (InvocationTargetException ex) {
				if (ex.getTargetException() instanceof NotSupportedException) {
					throw (NotSupportedException) ex.getTargetException();
				}
				else if (ex.getTargetException() instanceof SystemException) {
					throw (SystemException) ex.getTargetException();
				}
				else if (ex.getTargetException() instanceof RuntimeException) {
					throw (RuntimeException) ex.getTargetException();
				}
				else {
					throw new SystemException(
							"OC4J's begin(String) method failed with an unexpected error: " + ex.getTargetException());
				}
			}
			catch (Exception ex) {
				throw new SystemException("Could not invoke OC4J's UserTransaction.begin(String) method: " + ex);
			}
			return new ManagedTransactionAdapter(getTransactionManager());
		}

		else {
			// No name specified - standard JTA is sufficient.
			return super.createTransaction(name, timeout);
		}
	}

}
