#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.Globalization;
using System.IO;
using System.Xml;
using Common.Logging;
using Spring.Core.IO;
using Spring.Objects.Factory.Config;
using Spring.Objects.Factory.Support;

#endregion

namespace Spring.Objects.Factory.Xml
{
    /// <summary>
    /// XML resource reader. 
    /// </summary>
    /// <remarks>
    /// <p>
    /// Navigates through an XML resource and invokes parsers registered
    /// with the <see cref="NamespaceParserRegistry"/>.
    /// </p>
    /// </remarks>
    /// <author>Rod Johnson</author>
    /// <author>Juergen Hoeller</author>
    /// <author>Rick Evans (.NET)</author>
    public class DefaultObjectDefinitionDocumentReader : IObjectDefinitionDocumentReader
    {
        #region Constants

        /// <summary>
        /// The shared <see cref="Common.Logging.ILog"/> instance for this class (and derived classes). 
        /// </summary>
        protected static readonly ILog log =
            LogManager.GetLogger(typeof(DefaultObjectDefinitionDocumentReader));

        #endregion

        #region Fields

        private XmlReaderContext readerContext;

        #endregion

        #region Constructor (s) / Destructor

        /// <summary>
        /// Creates a new instance of the DefaultObjectDefinitionDocumentReader class.
        /// </summary>
        public DefaultObjectDefinitionDocumentReader()
        {
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets the reader context.
        /// </summary>
        /// <value>The reader context.</value>
        public XmlReaderContext ReaderContext
        {
            get { return readerContext; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Read object definitions from the given DOM element, and register
        /// them with the given object registry.
        /// </summary>
        /// <param name="doc">The DOM element containing object definitions, usually the
        /// root (document) element.</param>
        /// <param name="readerContext">The current context of the reader.  Includes
        /// the resource being parsed</param>
        /// <returns>
        /// The number of object definitions that were loaded.
        /// </returns>
        /// <exception cref="Spring.Objects.ObjectsException">
        /// In case of parsing errors.
        /// </exception>
        public void RegisterObjectDefinitions(XmlDocument doc, XmlReaderContext readerContext)
        {
            //int objectDefinitionCounter = 0;

            this.readerContext = readerContext;

            #region Instrumentation

            if (log.IsDebugEnabled)
            {
                log.Debug("Loading object definitions.");
            }

            #endregion

            XmlElement root = doc.DocumentElement;

            ObjectDefinitionParserHelper parserHelper = CreateHelper(readerContext, root);


            PreProcessXml(root);

            ParseObjectDefinitions(root, parserHelper);

            PostProcessXml(root);

            #region Instrumentation

            if (log.IsDebugEnabled)
            {
                log.Debug(
                    string.Format(
                        "Found {0} <{1}> elements defining objects.",
                        readerContext.Registry.ObjectDefinitionCount,
                        ObjectDefinitionConstants.ObjectElement));
            }

            #endregion
        }

        /// <summary>
        /// Parses object definitions starting at the given <see cref="XmlElement"/> 
        /// using the passed <see cref="ObjectDefinitionParserHelper"/>.
        /// </summary>
        /// <param name="root">The root element to start parsing from.</param>
        /// <param name="helper">The <see cref="ObjectDefinitionParserHelper"/> instance to use.</param>
        /// <exception cref="ObjectDefinitionStoreException">
        /// in case an error happens during parsing and registering object definitions
        /// </exception>
        protected virtual void ParseObjectDefinitions(XmlElement root, ObjectDefinitionParserHelper helper)
        {
            if (helper.IsDefaultNamespace(root.NamespaceURI))
            {
                foreach (XmlNode node in root.ChildNodes)
                {
                    if (node.NodeType != XmlNodeType.Element) continue;

                    try
                    {
                        XmlElement element = (XmlElement)node;
                        if (helper.IsDefaultNamespace(element.NamespaceURI))
                        {
                            ParseDefaultElement(element, helper);
                        }
                        else
                        {
                            helper.ParseCustomElement(element);
                        }
                    }
                    catch (ObjectDefinitionStoreException)
                    {
                        throw;
                    }
                    catch (Exception ex)
                    {
                        helper.ReaderContext.ReportException(node, null, "Failed parsing element", ex);
                    }
                }
            }
            else
            {
                helper.ParseCustomElement(root);
            }
        }

        private void ParseDefaultElement(XmlElement element, ObjectDefinitionParserHelper helper)
        {
            if (element.LocalName == ObjectDefinitionConstants.ImportElement)
            {
                ImportObjectDefinitionResource(element);
            }
            else if (element.LocalName == ObjectDefinitionConstants.AliasElement)
            {
                 ParseAlias(element, helper.ReaderContext.Registry);
            }
            else if (element.LocalName == ObjectDefinitionConstants.ObjectElement)
            {
                ProcessObjectDefinition(element, helper);
            }
        }

        /// <summary>
        /// Process an alias element.
        /// </summary>
        protected virtual void ProcessAlias(XmlElement element)
        {
            this.ParseAlias(element, this.ReaderContext.Registry);    
        }

        /// <summary>
        /// Process the object element
        /// </summary>
        protected virtual void ProcessObjectDefinition(XmlElement element, ObjectDefinitionParserHelper helper)
        {
            // TODO: add event handling
            try
            {
                ObjectDefinitionHolder bdHolder = helper.ParseObjectDefinitionElement(element);
                if (bdHolder == null)
                {
                    return;
                }
                bdHolder = helper.DecorateObjectDefinitionIfRequired(element, bdHolder);

                #region Instrumentation

                if (log.IsDebugEnabled)
                {
                    log.Debug(string.Format(CultureInfo.InvariantCulture, "Registering object definition with id '{0}'.", bdHolder.ObjectName));
                }

                #endregion

                ObjectDefinitionReaderUtils.RegisterObjectDefinition(bdHolder, ReaderContext.Registry);
                // TODO: Send registration event.
                // ReaderContext.FireComponentRegistered(new BeanComponentDefinition(bdHolder));
            }
            catch (ObjectDefinitionStoreException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new ObjectDefinitionStoreException(
                    string.Format("Failed parsing object definition '{0}'", element.OuterXml), ex);
            }
        }

        /// <summary>
        /// Loads external XML object definitions from the resource described by the supplied
        /// <paramref name="resource"/>.
        /// </summary>
        /// <param name="resource">The XML element describing the resource.</param>
        /// <exception cref="Spring.Objects.Factory.ObjectDefinitionStoreException">
        /// If the resource could not be imported.
        /// </exception>
        protected virtual void ImportObjectDefinitionResource(XmlElement resource)
        {
            string location = resource.GetAttribute(ObjectDefinitionConstants.ImportResourceAttribute);
            try
            {
                #region Instrumentation

                if (log.IsDebugEnabled)
                {
                    log.Debug(string.Format(
                                  CultureInfo.InvariantCulture,
                                  "Attempting to import object definitions from '{0}'.", location));
                }

                #endregion

                IResource importResource = ReaderContext.Resource.CreateRelative(location);
                ReaderContext.Reader.LoadObjectDefinitions(importResource);
            }
            catch (IOException ex)
            {
                ReaderContext.ReportException(resource, null, string.Format(
                                                                  CultureInfo.InvariantCulture,
                                                                  "Invalid relative resource location '{0}' to import object definitions from.",
                                                                  location), ex);
            }
        }

        /// <summary>
        /// Parses the given alias element, registering the alias with the registry.
        /// </summary>
        /// <param name="aliasElement">The alias element.</param>
        /// <param name="registry">The registry.</param>
        protected virtual void ParseAlias(XmlElement aliasElement, IObjectDefinitionRegistry registry)
        {
            string name = aliasElement.GetAttribute(ObjectDefinitionConstants.NameAttribute);
            string alias = aliasElement.GetAttribute(ObjectDefinitionConstants.AliasAttribute);
            registry.RegisterAlias(name, alias);
        }

        /// <summary>
        /// Parse an object definition and register it with the object factory..
        /// </summary>
        /// <param name="element">The element containing the object definition.</param>
        /// <param name="helper">The helper.</param>
        /// <seealso cref="Spring.Objects.Factory.Support.ObjectDefinitionReaderUtils.RegisterObjectDefinition"/>
        protected virtual void RegisterObjectDefinition(XmlElement element, ObjectDefinitionParserHelper helper)
        {
            ProcessObjectDefinition(element, helper);
        }

        /// <summary>
        /// <para>
        /// Allow the XML to be extensible by processing any custom element types last,
        /// after we finished processing the objct definitions. This method is a natural
        /// extension point for any other custom post-processing of the XML.
        /// </para><para>
        /// The default implementation is empty. Subclasses can override this method to
        /// convert custom elements into standard Spring object definitions, for example.
        /// Implementors have access to the parser's object definition reader and the
        /// underlying XML resource, through the corresponding properties.
        /// </para>
        /// </summary>
        /// <param name="root">The root.</param>
        protected virtual void PostProcessXml(XmlElement root)
        {
        }

        /// <summary>
        /// Allow the XML to be extensible by processing any custom element types first,
        /// before we start to process the object definitions.
        /// </summary>
        /// <remarks>This method is a natural
        /// extension point for any other custom pre-processing of the XML.
        /// <p>The default implementation is empty. Subclasses can override this method to
        /// convert custom elements into standard Spring object definitions, for example.
        /// Implementors have access to the parser's object definition reader and the
        /// underlying XML resource, through the corresponding properties.
        /// </p>
        /// </remarks>
        /// <param name="root">The root element of the XML document.</param>
        protected virtual void PreProcessXml(XmlElement root)
        {
        }

        /// <summary>
        /// Creates an <see cref="ObjectDefinitionParserHelper"/> instance for the given <paramref name="readerContext"/> and <paramref name="root"/> element.
        /// </summary>
        /// <param name="readerContext">the <see cref="XmlReaderContext"/> to create the <see cref="ObjectDefinitionParserHelper"/> </param>
        /// <param name="root">the root <see cref="XmlElement"/> to start reading from</param>
        /// <returns>a new <see cref="ObjectDefinitionParserHelper"/> instance</returns>
        protected virtual ObjectDefinitionParserHelper CreateHelper(XmlReaderContext readerContext, XmlElement root)
        {
            ObjectDefinitionParserHelper helper = new ObjectDefinitionParserHelper(readerContext, root);
            return helper;
        }

//        private INamespaceParser GetNamespaceParser(XmlElement element, ObjectDefinitionParserHelper helper)
//        {
//            INamespaceParser parser = NamespaceParserRegistry.GetParser(element.NamespaceURI);
//            if (parser == null)
//            {
//                helper.ReaderContext.ReportException(element, null, GetNoParserForNamespaceMessage(element.NamespaceURI));
//            }
//            return parser;
//        }
//
//        private string GetNoParserForNamespaceMessage(string namespaceURI)
//        {
//            return "There is no parser registered for namespace '" + namespaceURI + "'";
//        }

        #endregion
    }
}
