#region License

/*
 * Copyright 2002-2004 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.Collections.Specialized;
using System.Reflection;
using Common.Logging;
using Spring.Objects.Factory;
using Spring.Threading;
using Spring.Util;
using IsolationLevel=System.Data.IsolationLevel;

namespace Spring.Transaction.Interceptor
{
	/// <summary>
	/// Superclass for transaction aspects, such as the AOP Alliance-compatible
	/// <see cref="Spring.Transaction.Interceptor.TransactionInterceptor"/>.
	/// </summary>
	/// <remarks>
	/// <p>
	///  This enables the underlying Spring transaction infrastructure to be used
	///  to easily implement an aspect for any aspect system.
	/// </p>
	/// <p>
	/// Subclasses are responsible for calling methods in this class in the correct order.
	/// </p>
	/// <p>
	/// Uses the Strategy design pattern. A <see cref="Spring.Transaction.IPlatformTransactionManager"/>
	/// implementation will perform the actual transaction management
	/// </p>
	/// <p>
	/// A transaction aspect is serializable if its
	/// <see cref="Spring.Transaction.IPlatformTransactionManager"/> and
	/// <see cref="Spring.Transaction.Interceptor.ITransactionAttributeSource"/> are serializable.
	/// </p>
	/// </remarks>
	/// <author>Rod Johnson</author>
	/// <author>Juergen Hoeller</author>
	/// <author>Griffin Caprio (.NET)</author>
	/// <author>Mark Pollack (.NET)</author>
	[Serializable]
	public class TransactionAspectSupport : IInitializingObject
	{
        //TODO work on serialization support.

		#region TransactionInfo Class
		/// <summary>
		/// Opaque object used to hold transaction information.
		/// </summary>
		/// <remarks>
		/// <p>
		/// Subclasses must pass it back to method on this class, but not see its internals.
		/// </p>
		/// </remarks>
		internal protected class TransactionInfo
		{

			private ITransactionAttribute _transactionAttribute;
		    private string _joinpointIdentification;
            private ITransactionStatus _transactionStatus;
			private TransactionInfo _oldTransactionInfo;

            /// <summary>
            /// Creates a new instance of the
            /// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
            /// class for the supplied <paramref name="transactionAttribute"/>.
            /// </summary>
            /// <param name="transactionAttribute">The transaction attributes to associate with any transaction.</param>
            /// <param name="joinpointIdentification">The info for diagnostic display of joinpoint</param>
			public TransactionInfo( ITransactionAttribute transactionAttribute, string joinpointIdentification)
			{
				_transactionAttribute = transactionAttribute;
                _joinpointIdentification = joinpointIdentification;
			}

			/// <summary>
			/// Does this instance currently have a transaction?
			/// </summary>
			/// <value><b>True</b> if this instance has a transaction.</value>
			public bool HasTransaction
			{
				get { return _transactionStatus != null; }
			}

			/// <summary>
			/// Gets and sets the <see cref="Spring.Transaction.ITransactionStatus"/> for this object.
			/// </summary>
			public ITransactionStatus TransactionStatus
			{
				set { _transactionStatus = value; }
				get { return _transactionStatus; }
			}

			/// <summary>
			/// Binds this
			/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
			/// instance to the thread local storage variable for the current thread and
			/// backs up the existing
			/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
			/// object for the current thread.
			/// </summary>
			public void BindToThread()
			{
                // Expose current TransactionStatus, preserving any existing TransactionStatus
                // for restoration after this transaction is complete.
                TransactionInfo currentTransactionInfo = LogicalThreadContext.GetData(CURRENT_TRANSACTIONINFO_SLOTNAME) as TransactionInfo;
                _oldTransactionInfo = currentTransactionInfo;
                LogicalThreadContext.SetData(CURRENT_TRANSACTIONINFO_SLOTNAME, this);
			}

			/// <summary>
			/// Restores the previous
			/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
			/// object to the current thread.
			/// </summary>
			public void RestoreThreadLocalStatus()
			{
                // Use stack to restore old transaction TransactionInfo.
                // Will be null if none was set.
                LogicalThreadContext.SetData(CURRENT_TRANSACTIONINFO_SLOTNAME, _oldTransactionInfo);
			}

			/// <summary>
			/// Gets the current
			/// <see cref="Spring.Transaction.Interceptor.ITransactionAttribute"/> for this
			/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
			/// object.
			/// </summary>
			public ITransactionAttribute TransactionAttribute
			{
				get { return _transactionAttribute; }
			}

            /// <summary>
            /// Gets the joinpoint identification.
            /// </summary>
            /// <value>The joinpoint identification.</value>
		    public string JoinpointIdentification
		    {
		        get { return _joinpointIdentification; }
		    }
		}
		#endregion

	    #region Logging Definition

        [NonSerialized]
	    protected ILog log = LogManager.GetLogger(typeof (TransactionAspectSupport));

	    #endregion


        /// <summary>
        /// The name in thread local storage where the TransactionInfo object is located
        /// </summary>
        public const string CURRENT_TRANSACTIONINFO_SLOTNAME = "TransactionAspectSupport.CurrentTransactionInfoSlotName";

		/// <summary>
		/// The <see cref="Spring.Transaction.IPlatformTransactionManager"/> currently referenced by
		/// this aspect
		/// </summary>
		private IPlatformTransactionManager _transactionManager;

		/// <summary>
		/// The <see cref="Spring.Transaction.Interceptor.ITransactionAttributeSource"/> currently
		/// referenced by this aspect.
		/// </summary>
		private ITransactionAttributeSource _transactionAttributeSource;

		/// <summary>
		/// Creates a new instance of the
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport"/> class.
		/// </summary>
		public TransactionAspectSupport() {}

		/// <summary>
		/// Gets and sets the <see cref="Spring.Transaction.IPlatformTransactionManager"/> for
		/// this aspect.
		/// </summary>
		public IPlatformTransactionManager TransactionManager
		{
			get { return _transactionManager; }
			set { _transactionManager = value; }
		}

		/// <summary>
		/// Gets and sets the
		/// <see cref="Spring.Transaction.Interceptor.ITransactionAttributeSource"/> for
		/// this aspect.
		/// </summary>
		public ITransactionAttributeSource TransactionAttributeSource
		{
			get { return _transactionAttributeSource; }
			set { _transactionAttributeSource = value; }
		}

		/// <summary>
		/// Returns the <see cref="Spring.Transaction.ITransactionStatus"/>
		/// of the current method invocation.
		/// </summary>
		/// <remarks>
		/// Mainly intended for code that wants to set the current transaction
		/// rollback-only but not throw an application exception.
		/// </remarks>
		/// <exception cref="Spring.Transaction.NoTransactionException">
		/// If the transaction info cannot be found, because the method was invoked
		/// outside of an AOP invocation context.
		/// </exception>
		public static ITransactionStatus CurrentTransactionStatus
		{
			get { return CurrentTransactionInfo.TransactionStatus; }
		}

		/// <summary>
		/// Subclasses can use this to return the current
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>.
		/// </summary>
		/// <remarks>
		/// Only subclasses that cannot handle all operations in one method
		/// need to use this mechanism to get at the current
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>. 
		/// An around advice such as an AOP Alliance
		/// <see cref="AopAlliance.Intercept.IMethodInterceptor"/> can hold a reference to the 
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// throughout the aspect method.
		/// A <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// will be returned even if no transaction was created. The
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo.HasTransaction"/>
		/// property can be used to query this.
		/// </remarks>
		/// <exception cref="Spring.Transaction.NoTransactionException">
		/// If no transaction has been created by an aspect.
		/// </exception>
		protected static TransactionInfo CurrentTransactionInfo
		{
			get
			{
			    TransactionInfo currentTransactionInfo =
			        LogicalThreadContext.GetData(CURRENT_TRANSACTIONINFO_SLOTNAME) as TransactionInfo;
                if (currentTransactionInfo == null)
				{
					throw new NoTransactionException("No transaction aspect-managed ITransactionStatus in scope");
				}
                return currentTransactionInfo;
			}
		}
		/// <summary>
		/// Set properties with method names as keys and transaction attribute
		/// descriptors (parsed via <see cref="Spring.Transaction.Interceptor.TransactionAttributeEditor"/>) as values:
		/// e.g. key = "MyMethod", value = "PROPAGATION_REQUIRED,readOnly".
		/// </summary>
		/// <remarks>
		/// <note>
		/// Method names are always applied to the target class, no matter if defined in an
		/// interface or the class itself.
		/// </note>
		/// <p>
		/// Internally, a
		/// <see cref="Spring.Transaction.Interceptor.NameMatchTransactionAttributeSource"/>
		/// will be created from the given properties.
		/// </p>	
		/// </remarks>
		public NameValueCollection TransactionAttributes
		{
			set
			{
				NameMatchTransactionAttributeSource attributeSource = new NameMatchTransactionAttributeSource();
			    attributeSource.NameProperties = value;
				_transactionAttributeSource = attributeSource;
			}
		}

		/// <summary>
		/// Checks that the required properties are set.
		/// </summary>
		public void AfterPropertiesSet()
		{
			if ( null == _transactionManager)
			{
				throw new ArgumentException("IPlatformTransactionManager is required");
			}
			if ( null == _transactionAttributeSource )
			{
				throw new ArgumentException("Either 'TransactionAttributeSource' or 'TransactionAttribute' property is required: " +
					"If there are no transactional methods, don't use a TransactionInterceptor " + 
					"or TransactionProxyFactoryObject." );
			}
		}

		/// <summary>
		/// Create a transaction if necessary
		/// </summary>
		/// <param name="method">Method about to execute</param>
		/// <param name="targetType">Type that the method is on</param>
		/// <returns>
		/// A <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/> object,
		/// whether or not a transaction was created.
		/// <p>
		/// The
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo.HasTransaction"/>
		/// property on the
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// class can be used to tell if there was a transaction created.
		/// </p>
		/// </returns>
		protected TransactionInfo CreateTransactionIfNecessary( MethodInfo method, Type targetType )
		{
            // If the transaction attribute is null, the method is non-transactional.
			ITransactionAttribute sourceAttr = _transactionAttributeSource.ReturnTransactionAttribute( method, targetType );

		    return CreateTransactionIfNecessary(sourceAttr, MethodIdentification(method));
		}

        /// <summary>
        /// Creates the transaction if necessary.
        /// </summary>
        /// <param name="sourceAttr">The source transaction attribute.</param>
        /// <param name="joinpointIdentification">The joinpoint identification.</param>
        /// <returns>Transaction Info for declarative transaction management.</returns>
	    protected TransactionInfo CreateTransactionIfNecessary(ITransactionAttribute sourceAttr, string joinpointIdentification)
	    {
	        ITransactionAttribute txAttr = sourceAttr;

            // If no name specified, apply method identification as transaction name.
	        if (txAttr != null && txAttr.Name == null)
	        {
	            txAttr = new DelegatingTransactionAttributeWithName(txAttr, joinpointIdentification);
	        }

            TransactionInfo transactionInfo = new TransactionInfo(txAttr, joinpointIdentification);            
	        if ( txAttr != null )
	        {
	            // We need a transaction for this method
	            #region Instrumentation

	            if (log.IsDebugEnabled)
	            {
	                log.Debug("Getting transaction for " + transactionInfo.JoinpointIdentification);
	            }

	            #endregion
	            // The transaction manager will flag an error if an incompatible tx already exists
	            transactionInfo.TransactionStatus = _transactionManager.GetTransaction(txAttr);
	        } 
	        else
	        {
	            // The TransactionInfo.HasTransaction property will return
	            // false. We created it only to preserve the integrity of
	            // the ThreadLocal stack maintained in this class.
	            if (log.IsDebugEnabled)
	            {
                    log.Debug("Skipping transactional joinpoint [" + joinpointIdentification +
                              "] because no transaction manager has been configured");
	            }
	        }

	        // We always bind the TransactionInfo to the thread, even if we didn't create
	        // a new transaction here. This guarantees that the TransactionInfo stack
	        // will be managed correctly even if no transaction was created by this aspect.
	        transactionInfo.BindToThread( );
	        return transactionInfo;
	    }

        /// <summary>
        /// Identifies the method by providing the qualfied method name.
        /// </summary>
        /// <param name="methodInfo">The method info.</param>
        /// <returns>qualified mehtod name.</returns>
        protected string MethodIdentification(MethodInfo methodInfo)
        {
            return ObjectUtils.GetQualifiedMethodName(methodInfo);
        }

	    /// <summary>
		/// Execute after the successful completion of call, but not after an exception was handled.
		/// </summary>
		/// <remarks>
		/// <p>
		/// Do nothing if we didn't create a transaction.
		/// </p>
		/// </remarks>
		/// <param name="transactionInfo">
		/// The
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// about the current transaction.
		/// </param>
		protected void CommitTransactionAfterReturning( TransactionInfo transactionInfo )
		{
			if ( transactionInfo != null && transactionInfo.HasTransaction )
			{
                #region Instrumentation

			    if (log.IsDebugEnabled) 
			    {
                    log.Debug("Completing transaction for [" + transactionInfo.JoinpointIdentification + "]");
			    }

			    #endregion

			    _transactionManager.Commit( transactionInfo.TransactionStatus );
			}
		}

		/// <summary>
		/// Handle a exception, closing out the transaction.
		/// </summary>
		/// <remarks>
		/// <p>
		/// We may commit or roll back, depending on our configuration.
		/// </p>
		/// </remarks>
		/// <param name="transactionInfo">
		/// The
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// about the current transaction.
		/// </param>
		/// <param name="exception">The <see cref="System.Exception"/> encountered.</param>
		protected void CompleteTransactionAfterThrowing(
			TransactionInfo transactionInfo, Exception exception )
		{
			if ( transactionInfo != null &&  transactionInfo.HasTransaction )
			{
                if (log.IsDebugEnabled)
                {                    
                    log.Debug("Completing transaction for [" + transactionInfo.JoinpointIdentification + "] after exception: " + exception);
                }

				if ( transactionInfo.TransactionAttribute.RollbackOn( exception ))
				{
				    try
					{
						_transactionManager.Rollback( transactionInfo.TransactionStatus );
					}
					catch (Exception e)
					{
					    log.Error("Application exception overridden by rollback exception", e);
						throw;
					}
				} 
				else
				{
                    // We don't roll back on this exception.
				    // Will still roll back if TransactionStatus.RollbackOnly is true.
                    try
                    {
                        _transactionManager.Commit(transactionInfo.TransactionStatus);
                    } catch (Exception e)
                    {
                        log.Error("Application exception overriden by commit exception", e);
                        throw;
                    }

				}
			} 
		}
		/// <summary>
		/// Resets the 
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// for this thread.
		/// </summary>
		/// <remarks>
		/// <note>
		/// Call this in all cases: exceptions or normal return.
		/// </note>
		/// </remarks>
		/// <param name="transactionInfo">
		/// The 
		/// <see cref="Spring.Transaction.Interceptor.TransactionAspectSupport.TransactionInfo"/>
		/// about the current transaction. May be null.
		/// </param>
		protected void CleanupTransactionInfo( TransactionInfo transactionInfo )
		{
			if ( transactionInfo != null )
			{
				transactionInfo.RestoreThreadLocalStatus( );
			}
		}
	}
}
