#region License

/*
 * Copyright 2002-2008 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.
 */

#endregion

using System;
using System.Data;
using Common.Logging;
using Spring.Messaging.Ems.Common;
using Spring.Messaging.Ems.Core;
using Spring.Objects.Factory;
using Spring.Transaction;
using Spring.Transaction.Support;
using TIBCO.EMS;

namespace Spring.Messaging.Ems.Connections
{
    /// <summary>
    /// A <see cref="AbstractPlatformTransactionManager"/> implementation
    /// for a single EMS <code>ConnectionFactory</code>. Binds a 
    /// Connection/Session pair from the specified ConnecctionFactory to the thread,
    /// potentially allowing for one thread-bound Session per ConnectionFactory.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Application code is required to retrieve the transactional Session via
    /// <see cref="ConnectionFactoryUtils.GetTransactionalSession"/>.  Spring's
    /// <see cref="EmsTemplate"/> will autodetect a thread-bound Session and 
    /// automatically participate in it.
    /// </para>
    /// <para>
    /// Transaction synchronization is turned off by default, as this manager might be used
    /// alongside an IDbProvider based Spring transaction manager such as the
    /// AdoPlatformTransactionManager, which has stronger needs for synchronization.
    /// </para>
    /// </remarks>
    /// <author>Juergen Hoeller</author>
    /// <author>Mark Pollack (.NET)</author>
    public class EmsTransactionManager : AbstractPlatformTransactionManager, 
        IResourceTransactionManager, IInitializingObject
    {

        #region Logging Definition

        private static readonly ILog LOG = LogManager.GetLogger(typeof(EmsTransactionManager));

        #endregion 

        private IConnectionFactory connectionFactory;

        /// <summary>
        /// Initializes a new instance of the <see cref="EmsTransactionManager"/> class.
        /// </summary>
        /// <remarks>
        /// The ConnectionFactory has to be set before using the instance. 
        /// This constructor can be used to prepare a EmsTemplate via a ApplicationContext,
        /// typically setting the ConnectionFactory via ConnectionFactory property.
        /// <para>
        /// Turns off transaction synchronization by default, as this manager might
        /// be used alongside a dbprovider-based Spring transaction manager like
	    /// AdoPlatformTransactionManager, which has stronger needs for synchronization.
	    /// Only one manager is allowed to drive synchronization at any point of time.
        /// </para>
        /// </remarks>
        public EmsTransactionManager()
        {
            TransactionSynchronization = TransactionSynchronizationState.Never;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EmsTransactionManager"/> class
        /// given a ConnectionFactory.
        /// </summary>
        /// <param name="connectionFactory">The connection factory to obtain connections from.</param>
        public EmsTransactionManager(IConnectionFactory connectionFactory) : this()
        {
            ConnectionFactory = connectionFactory;
            AfterPropertiesSet();
        }


        /// <summary>
        /// Gets or sets the connection factory that this instance should manage transaction.
        /// for.
        /// </summary>
        /// <value>The connection factory.</value>
        public IConnectionFactory ConnectionFactory
        {
            get { return connectionFactory; }
            set
            {
                connectionFactory = value;
            }
        }

        #region IInitializingObject Members

        /// <summary>
        /// Make sure the ConnectionFactory has been set.
        /// </summary>
        public void AfterPropertiesSet()
        {
            if (ConnectionFactory == null)
            {
                throw new ArgumentException("Property 'ConnectionFactory' is required.");
            }
        }

        #endregion

        #region IResourceTransactionManager Members

        /// <summary>
        /// Gets the resource factory that this transaction manager operates on,
        /// In tihs case the ConnectionFactory
        /// </summary>
        /// <value>The ConnectionFactory.</value>
        public object ResourceFactory
        {
            get { return ConnectionFactory; }
        }

        #endregion


        /// <summary>
        /// Get the EmsTransactionObject.
        /// </summary>
        /// <returns>he EmsTransactionObject.</returns>
        protected override object DoGetTransaction()
        {
            EmsTransactionObject txObject = new EmsTransactionObject();

            txObject.ResourceHolder =
                (EmsResourceHolder) TransactionSynchronizationManager.GetResource(ConnectionFactory);
            return txObject;
        }


        /// <summary>
        /// Begin a new transaction with the given transaction definition.
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <param name="definition"><see cref="Spring.Transaction.ITransactionDefinition"/> instance, describing
        /// propagation behavior, isolation level, timeout etc.</param>
        /// <remarks>
        /// Does not have to care about applying the propagation behavior,
        /// as this has already been handled by this abstract manager.
        /// </remarks>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of creation or system errors.
        /// </exception>
        protected override void DoBegin(object transaction, ITransactionDefinition definition)
        {
            //This is the default value defined in DefaultTransactionDefinition
            if (definition.TransactionIsolationLevel != IsolationLevel.ReadCommitted)
            {
                throw new InvalidIsolationLevelException("EMS does not support an isoliation level concept");
            }
            EmsTransactionObject txObject = (EmsTransactionObject) transaction;
            IConnection con = null;
            ISession session = null;
            try
            {
                con = CreateConnection();
                session = CreateSession(con);
                if (LOG.IsDebugEnabled)
                {
                    log.Debug("Created EMS transaction on Session [" + session + "] from Connection [" + con + "]");
                }
                txObject.ResourceHolder = new EmsResourceHolder(ConnectionFactory, con, session);
                txObject.ResourceHolder.SynchronizedWithTransaction = true;
                int timeout = DetermineTimeout(definition);
                if (timeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT)
                {
                    txObject.ResourceHolder.TimeoutInSeconds = timeout;
                }
                TransactionSynchronizationManager.BindResource(ConnectionFactory, txObject.ResourceHolder);
                


            } catch (EMSException ex)
            {
                if (session != null)
                {
                    try
                    {
                        session.Close();
                    } catch (Exception)
                    {}
                }
                if (con != null)
                {
                    try
                    {
                        con.Close();
                    } catch (Exception){}
                }
                throw new CannotCreateTransactionException("Could not create EMS Transaction", ex);
            }
               
        }

        /// <summary>
        /// Suspend the resources of the current transaction.
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <returns>
        /// An object that holds suspended resources (will be kept unexamined for passing it into
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoResume"/>.)
        /// </returns>
        /// <remarks>
        /// Transaction synchronization will already have been suspended.
        /// </remarks>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// in case of system errors.
        /// </exception>
        protected override object DoSuspend(object transaction)
        {
            EmsTransactionObject txObject = (EmsTransactionObject) transaction;
            txObject.ResourceHolder = null;
            return TransactionSynchronizationManager.UnbindResource(ConnectionFactory);
        }

        /// <summary>
        /// Resume the resources of the current transaction.
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <param name="suspendedResources">The object that holds suspended resources as returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoSuspend"/>.</param>
        /// <remarks>Transaction synchronization will be resumed afterwards.
        /// </remarks>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoResume(object transaction, object suspendedResources)
        {
            EmsResourceHolder conHolder = (EmsResourceHolder) suspendedResources;
            TransactionSynchronizationManager.BindResource(ConnectionFactory, conHolder);
        }

        /// <summary>
        /// Perform an actual commit on the given transaction.
        /// </summary>
        /// <param name="status">The status representation of the transaction.</param>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoCommit(DefaultTransactionStatus status)
        {
            EmsTransactionObject txObject = (EmsTransactionObject)status.Transaction;
            ISession session = txObject.ResourceHolder.GetSession();
            try
            {
                if (status.Debug)
                {
                    LOG.Debug("Committing EMS transaction on Session [" + session + "]");
                }
                session.Commit();
            }
            catch (TransactionRolledBackException ex)
            {
                throw new UnexpectedRollbackException("EMS transaction rolled back", ex);
            }
            catch (EMSException ex)
            {                
                throw new TransactionSystemException("Could not commit EMS transaction.", ex);
            }
        }

        /// <summary>
        /// Perform an actual rollback on the given transaction.
        /// </summary>
        /// <param name="status">The status representation of the transaction.</param>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoRollback(DefaultTransactionStatus status)
        {
            EmsTransactionObject txObject = (EmsTransactionObject)status.Transaction;
            ISession session = txObject.ResourceHolder.GetSession();
            try
            {
                if (status.Debug)
                {
                    LOG.Debug("Rolling back EMS transaction on Session [" + session + "]");
                }
                session.Rollback();              
            }
            catch (EMSException ex)
            {
                throw new TransactionSystemException("Could not roll back EMS transaction.", ex);
            }
        }


        /// <summary>
        /// Set the given transaction rollback-only. Only called on rollback
        /// if the current transaction takes part in an existing one.
        /// </summary>
        /// <param name="status">The status representation of the transaction.</param>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override void DoSetRollbackOnly(DefaultTransactionStatus status)
        {
            EmsTransactionObject txObject = (EmsTransactionObject)status.Transaction;
            txObject.ResourceHolder.RollbackOnly = true;
        }

        /// <summary>
        /// Cleanup resources after transaction completion.
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <remarks>
        /// <para>
        /// Called after <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoCommit"/>
        /// and
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoRollback"/>
        /// execution on any outcome.
        /// </para>
        /// </remarks>
        protected override void DoCleanupAfterCompletion(object transaction)
        {
            EmsTransactionObject txObject = (EmsTransactionObject)transaction;
            TransactionSynchronizationManager.UnbindResource(ConnectionFactory);
            txObject.ResourceHolder.CloseAll();
            txObject.ResourceHolder.Clear();
        }

        /// <summary>
        /// Check if the given transaction object indicates an existing transaction
        /// (that is, a transaction which has already started).
        /// </summary>
        /// <param name="transaction">Transaction object returned by
        /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
        /// <returns>
        /// True if there is an existing transaction.
        /// </returns>
        /// <exception cref="Spring.Transaction.TransactionException">
        /// In the case of system errors.
        /// </exception>
        protected override bool IsExistingTransaction(object transaction)
        {
            EmsTransactionObject txObject = transaction as EmsTransactionObject;
            if (txObject != null)
            {
                return txObject.ResourceHolder != null;
            }
            return false;
        }

        /// <summary>
        /// Creates the connection via thie manager's ConnectionFactory.
        /// </summary>
        /// <returns>The new Connection</returns>
        /// <exception cref="EMSException">If thrown by underlying messaging APIs</exception>
        protected virtual IConnection CreateConnection()
        {
            return ConnectionFactory.CreateConnection();
        }

        /// <summary>
        /// Creates the session for the given Connection
        /// </summary>
        /// <param name="connection">The connection to create a Session for.</param>
        /// <returns>the new Session</returns>
        /// <exception cref="EMSException">If thrown by underlying messaging APIs</exception>
        protected virtual ISession CreateSession(IConnection connection)
        {
            return connection.CreateSession(true, Session.SESSION_TRANSACTED);
        }


        /// <summary>
        /// EMS Transaction object, representing a EmsResourceHolder.
        /// Used as transaction object by EMSTransactionManager
        /// </summary>
        internal class EmsTransactionObject : ISmartTransactionObject
        {
            private EmsResourceHolder resourceHolder;


            public EmsResourceHolder ResourceHolder
            {
                get { return resourceHolder; }
                set { resourceHolder = value; }
            }

            #region ISmartTransactionObject Members

            public bool RollbackOnly
            {
                get { return resourceHolder.RollbackOnly; }
            }

            #endregion
        }
    }
}