#region License

/* 
 * Copyright © 2002-2005 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

#region Imports

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Configuration;
using Common.Logging;
using Spring.Context.Events;
using Spring.Util;

#endregion

namespace Spring.Context.Support
{
    /// <summary> 
    /// Provides access to a central registry of 
    /// <see cref="Spring.Context.IApplicationContext"/>s.
    /// </summary>
    /// <remarks>
    /// <p>
    /// A singleton implementation to access one or more application contexts.  Application
    /// context instances are cached.
    /// </p>
    /// <p>Note that the use of this class or similar is unnecessary except (sometimes) for
    /// a small amount of glue code.  Excessive usage will lead to code that is more tightly
    /// coupled, and harder to modify or test.  Consider refactoring your code to use standard
    /// Dependency Injection techniques or implement the interface IApplicationContextAware to
    /// obtain a reference to an application context.</p>
    /// </remarks>
    /// <author>Mark Pollack</author>
    /// <author>Aleksandar Seovic</author>
    /// <seealso cref="Spring.Context.IApplicationContext"/>
    public sealed class ContextRegistry
    {
        /// <summary>
        /// The shared <see cref="Common.Logging.ILog"/> instance for this class (and derived classes).
        /// </summary>
        private static readonly ILog log = LogManager.GetLogger(typeof(ContextRegistry));

        private static readonly object syncRoot = new Object();
        private static readonly ContextRegistry instance = new ContextRegistry();
        private static string rootContextName = null;

        private IDictionary contextMap = CollectionsUtil.CreateCaseInsensitiveHashtable();

        #region Constructor (s) / Destructor

        // CLOVER:OFF

        /// <summary>
        /// Creates a new instance of the ContextRegistry class.
        /// </summary>
        /// <remarks>
        /// <p>
        /// Explicit static constructor to tell C# compiler
        /// not to mark type as beforefieldinit.
        /// </p>
        /// </remarks>
        static ContextRegistry()
        { }

        // CLOVER:ON

        #endregion

        /// <summary>
        /// This event is fired, if ContextRegistry.Clear() is called.<br/>
        /// Clients may register to get informed
        /// </summary>
        /// <remarks>
        /// This event is fired while still holding a lock on the Registry.<br/>
        /// 'sender' parameter is sent as typeof(ContextRegistry), EventArgs are not used
        /// </remarks>
        public static event EventHandler Cleared;

        /// <summary>
        /// Gets an object that should be used to synchronize access to ContextRegistry
        /// from the calling code.
        /// </summary>
        public static object SyncRoot
        {
            get { return syncRoot; }
        }

        /// <summary> 
        /// Registers an instance of an
        /// <see cref="Spring.Context.IApplicationContext"/>. 
        /// </summary> 
        /// <remarks>
        /// <p>
        /// This is usually called via a
        /// <see cref="Spring.Context.Support.ContextHandler"/> inside a .NET
        /// application configuration file. 
        /// </p>
        /// </remarks>
        /// <param name="context">The application context to be registered.</param>
        /// <exception cref="Spring.Context.ApplicationContextException">
        /// If a context has previously been registered using the same name
        /// </exception>
        public static void RegisterContext(IApplicationContext context)
        {
            lock (syncRoot)
            {
                if (instance.contextMap.Contains(context.Name))
                {
                    IApplicationContext ctx = (IApplicationContext)instance.contextMap[context.Name];
                    throw new ApplicationContextException(
                        string.Format("Existing context '{0}' already registered under name '{1}'.",
                                      ctx, context.Name));
                }
                instance.contextMap[context.Name] = context;
                context.ContextEvent += new ApplicationEventHandler(OnContextEvent);

                #region Instrumentation

                if (log.IsDebugEnabled)
                {
                    log.Debug(String.Format(
                        "Registering context '{0}' under name '{1}'.", context, context.Name));
                }

                #endregion

                if (rootContextName == null)
                {
                    rootContextName = context.Name;
                }
            }
        }

        /// <summary>
        /// Handles events raised by an application context.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnContextEvent(object sender, ApplicationEventArgs e)
        {
            ContextEventArgs cea = e as ContextEventArgs;
            if (cea != null
                && cea.Event == ContextEventArgs.ContextEvent.Closed
                && sender is IApplicationContext)
            {
                // we know the context is registered!
                UnregisterContext((IApplicationContext)sender);
            }
        }

        /// <summary>
        /// Removes the context from the registry 
        /// </summary>
        /// <remarks>
        /// Has no effect if the context wasn't registered
        /// </remarks>
        /// <param name="context">´the context to remove from the registry</param>
        private static void UnregisterContext(IApplicationContext context)
        {
            AssertUtils.ArgumentNotNull(context, "context");
            lock (syncRoot)
            {
                if (IsContextRegistered(context.Name))
                {
                    instance.contextMap.Remove(context.Name);
                    if (rootContextName == context.Name)
                    {
                        rootContextName = null;
                    }
                }
            }
        }

        /// <summary>
        /// Returns the root application context.
        /// </summary>
        /// <remarks>
        /// <p>
        /// The first call to GetContext will create the context 
        /// as specified in the .NET application configuration file 
        /// under the location spring/context.
        /// </p>
        /// </remarks>
        /// <returns>The root application context.</returns>
        public static IApplicationContext GetContext()
        {
            lock (syncRoot)
            {
                InitializeContextIfNeeded();
                if (rootContextName == null)
                {
                    throw new ApplicationContextException(
                        "No context registered. Use the 'RegisterContext' method or the 'spring/context' section from your configuration file.");
                }
                return GetContext(rootContextName);
            }
        }

        /// <summary>
        /// Returns context based on specified name.
        /// </summary>
        /// <remarks>
        /// <p>
        /// The first call to GetContext will create the context 
        /// as specified in the .NET application configuration file 
        /// under the location spring/context.
        /// </p>
        /// </remarks>
        /// <param name="name">The context name.</param>
        /// <returns>The specified context, or null, if context with that name doesn't exists.</returns>
        /// <exception cref="System.ArgumentException">
        /// If the context name is null or empty
        /// </exception>
        public static IApplicationContext GetContext(string name)
        {
            if (StringUtils.IsNullOrEmpty(name))
            {
                throw new ArgumentException(
                    "The context name passed to the GetContext method cannot be null or empty.");
            }
            else
            {
                lock (syncRoot)
                {
                    InitializeContextIfNeeded();
                    IApplicationContext ctx = (IApplicationContext)instance.contextMap[name];
                    if (ctx == null)
                    {
                        throw new ApplicationContextException(String.Format(
                            "No context registered under name '{0}'. Use the 'RegisterContext' method or the 'spring/context' section from your configuration file.",
                            name));
                    }

                    #region Instrumentation

                    if (log.IsDebugEnabled)
                    {
                        log.Debug(String.Format(
                            "Returning context '{0}' registered under name '{1}'.", ctx, name));
                    }

                    #endregion

                    return ctx;
                }
            }
        }

        /// <summary>
        /// Removes all registered
        /// <see cref="Spring.Context.IApplicationContext"/>s from this
        /// registry.
        /// </summary>
        /// <remarks>
        /// Raises the <see cref="ContextRegistry.Cleared"/> event while still holding a lock on <see cref="ContextRegistry.SyncRoot"/>
        /// </remarks>
        public static void Clear()
        {
            lock (syncRoot)
            {
                ArrayList contexts = new ArrayList(instance.contextMap.Values);
                foreach (IApplicationContext ctx in contexts)
                {
                    ctx.Dispose();
                }

                #region Instrumentation

                // contexts will be removed from contextMap during OnContextEvent handler 
                // but someone might choose to override AbstractApplicationContext.Dispose() without 
                // calling base.Dispose() ...
                if (log.IsWarnEnabled)
                {
                    if (instance.contextMap.Count > 0)
                    {
                        log.Warn(
                            String.Format(
                                "Not all contexts were removed from registry during cleanup - did you forget to call base.Dispose() when overriding AbstractApplicationContext.Dispose()?"));
                    }
                }

                #endregion

                instance.contextMap.Clear();
                rootContextName = null;
                // mark section dirty - force re-read from disk next time
                ConfigurationUtils.RefreshSection(AbstractApplicationContext.ContextSectionName);
                DynamicCodeManager.Clear();
                if (Cleared != null)
                {
                    Cleared(typeof(ContextRegistry), EventArgs.Empty);
                }
            }
        }

        /// <summary>
        /// Allows to check, if a context is already registered
        /// </summary>
        /// <param name="name">The context name.</param>
        /// <returns>true, if the context is already registered. false otherwise</returns>
        public static bool IsContextRegistered(string name)
        {
            lock (instance)
            {
                return (instance.contextMap[name] != null);
            }
        }

        private static bool rootContextCurrentlyInCreation;

        private static void InitializeContextIfNeeded()
        {
            if (rootContextName == null)
            {
                if (rootContextCurrentlyInCreation)
                {
                    throw new InvalidOperationException("root context is currently in creation. You must not call ContextRegistry.GetContext() from e.g. constructors of your singleton objects");
                }

                rootContextCurrentlyInCreation = true;
                try
                {
                    ConfigurationUtils.GetSection(AbstractApplicationContext.ContextSectionName);
                }
                finally
                {
                    rootContextCurrentlyInCreation = false;
                }
            }
        }
    }
}

