// Copyright (c) 2008, NTT DATA Corporation.
//
// 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.

using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using TERASOLUNA.Fw.Common.Logging;

namespace TERASOLUNA.Fw.Common.Configuration
{
    /// <summary>
    /// <see cref="XmlDocument"/> を指定された XML スキーマを使用して検証します。
    /// </summary>
    /// <remarks>
    /// <see cref="XmlDocument"/> を検証する場合には <see cref="XmlDocumentValidator.Validate"/> メソッドを実行する前に
    /// <see cref="XmlDocumentValidator.AddSchema(string, Assembly, string)"/> メソッドを呼び出し、
    /// 検証する XML スキーマを指定する必要があります。
    /// </remarks>
    public class XmlDocumentValidator
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(XmlDocumentValidator));

        /// <summary>
        /// 検証エラーのメッセージを保持する <see cref="StringBuilder"/> です。
        /// </summary>
        private StringBuilder _validationErrorMessageBuilder;

        /// <summary>
        /// XML 検証に用いる <see cref="XmlSchemaSet"/> です。
        /// </summary>
        private XmlSchemaSet _schemaSet;

        /// <summary>
        /// <see cref="XmlDocumentValidator"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <remarks>
        /// 検証エラーのメッセージを保持する <see cref="StringBuilder"/> 、
        ///  XML 検証に用いる <see cref="XmlSchemaSet"/> を初期化します。
        /// </remarks>
        public XmlDocumentValidator()
        {
            _validationErrorMessageBuilder = new StringBuilder();
            _schemaSet = new XmlSchemaSet();
        }

        /// <summary>
        /// XML 検証に使用する XML スキーマをアセンブリから読み込み、XML スキーマセットに追加します。
        /// </summary>
        /// <param name="nameSpace">追加する XML スキーマのターゲット名前空間。</param>
        /// <param name="asm">追加する XML スキーマが含まれるアセンブリ。</param>
        /// <param name="schema">追加する XML スキーマのアセンブリ中でのパス。</param>
        /// <remarks>
        /// 指定したアセンブリから XML スキーマを読み込み、
        /// <see cref="XmlSchemaSet"/> に追加します。
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="nameSpace"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="asm"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="schema"/> が null 参照です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="System.ArgumentException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item><description>
        /// <paramref name="nameSpace"/> が空文字です。
        /// </description></item>
        /// <item><description>
        /// <paramref name="schema"/> が空文字です。
        /// </description></item>
        /// </list>
        /// </exception>
        /// <exception cref="FileNotFoundException">
        /// <paramref name="asm"/> に <paramref name="schema"/> が存在しないです。
        /// </exception>
        public void AddSchema(string nameSpace, Assembly asm, string schema)
        {
            if (nameSpace == null)
            {
                ArgumentNullException exception = new ArgumentNullException("nameSpace"); 
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "nameSpace"), exception);
                }
                throw exception;
            }
            if (nameSpace.Length <= 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "nameSpace");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;

            }
            if (asm == null)
            {
                ArgumentNullException exception = new ArgumentNullException("asm");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "asm"), exception);
                }
                throw exception;
            }
            if (schema == null)
            {
                ArgumentNullException exception = new ArgumentNullException("schema");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "schema"), exception);
                }
                throw exception;
            }
            if (schema.Length <= 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "schema");
                ArgumentException exception = new ArgumentException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
             // アセンブリからスキーマの読み込み
            Stream stream = asm.GetManifestResourceStream(schema);
            if (stream == null)
            {
                // 引数に渡されたアセンブリ内にスキーマファイルがない場合、
                // スキーマのパスを基にアセンブリを探す
                string asmPath = schema;
                while (asmPath.LastIndexOf('.') > 0)
                {
                    asmPath = asmPath.Substring(0, asmPath.LastIndexOf('.'));
                    try
                    {
                        Assembly asembly = Assembly.Load(asmPath);
                        stream = asembly.GetManifestResourceStream(schema);
                        if (stream != null) break;
                    }
                    catch (FileNotFoundException)
                    {
                        // アセンブリが見つからない場合は次のループへ
                    }
                }
            }

            if (stream == null)
            {
                string message = string.Format(Properties.Resources.E_CONFIGURATION_SCHEMAFILE_NOT_FOUND, asm, schema);
                FileNotFoundException exception = new FileNotFoundException(message);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            
            XmlReader xmlReader = XmlReader.Create(stream);
            // スキーマセットへの追加
            _schemaSet.Add(nameSpace, xmlReader);
        }

        /// <summary>
        /// XML 検証に使用する XML スキーマを XML ファイルから読みとり、XML スキーマセットに追加します。
        /// </summary>
        /// <param name="nameSpace">追加する XML スキーマのターゲット名前空間。</param>
        /// <param name="validationFilePath">XML スキーマファイルのファイルパス。</param>
        /// <remarks>
        /// 指定した XML ファイルから XML スキーマを読み込み、
        /// <see cref="XmlSchemaSet"/> に追加します。
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="nameSpace"/> が null 参照です。
        /// </item>
        /// <item>
        /// <paramref name="ValidationFilePath"/> が null 参照です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="ArgumentException">
        /// 次のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <paramref name="nameSpace"/> が空文字です。
        /// </item>
        /// <item>
        /// <paramref name="ValidationFilePath"/> が空文字です。
        /// </item>
        /// </list>
        /// </exception>
        public void AddSchema(string nameSpace, string validationFilePath)
        {
            if (nameSpace == null)
            {
                ArgumentNullException exception = new ArgumentNullException("nameSpace");
                if(_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "nameSpace"), exception);
                }
                throw exception;
            }
            if (nameSpace.Length <= 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "nameSpace");
                ArgumentException exception = new ArgumentException(message);
                if(_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            if (validationFilePath == null)
            {
                ArgumentNullException exception = new ArgumentNullException("schemaFilePath");
                if(_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "schemaFilePath"), exception);
                }
                throw exception;
            }
            if (validationFilePath.Length <= 0)
            {
                string message = string.Format(Properties.Resources.E_EMPTY_STRING, "schemaFilePath");
                ArgumentException exception = new ArgumentException(message);
                if(_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }
            // XMLファイルからスキーマの読み込み
            XmlReader xmlReader = XmlReader.Create(validationFilePath);
            // スキーマセットへの追加
            _schemaSet.Add(nameSpace, xmlReader);
        }

        /// <summary>
        /// <see cref="XmlDocumentValidator.AddSchema(string, Assembly, string)"/> メソッドで追加された
        /// XML スキーマを用いて <see cref="XmlDocument"/> を検証します。
        /// </summary>
        /// <param name="xmlDoc">XMLスキーマ検証を行う <see cref="XmlDocument" /> 。</param>
        /// <remarks>
        /// <see cref="XmlDocumentValidator.AddSchema(string, Assembly, string)"/> メソッドで追加された
        /// XML スキーマを用いて <see cref="XmlDocument"/> 内の XML データを検証します。
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="xmlDoc"/> が null 参照です。
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// XML 検証に用いる <see cref="XmlSchemaSet"/> の <see cref="XmlSchemaSet.Count"/> が 0 以下です。
        /// </exception>
        /// <exception cref="XmlSchemaException">
        /// XMLスキーマ検証においてエラーです。
        /// </exception>
        public void Validate(XmlDocument xmlDoc)
        {
            if (xmlDoc == null)
            {
                ArgumentNullException exception = new ArgumentNullException("xmlDoc");
                if(_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(
                        Properties.Resources.E_NULL_ARGUMENT, "xmlDoc"), exception);
                }
                throw exception;
            }
            if (_schemaSet.Count <= 0)
            {
                InvalidOperationException exception = new InvalidOperationException(
                    Properties.Resources.E_CONFIGURATION_XMLSCHEMASET_NOT_FOUND);
                if(_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            // objXmlDocのSchemaプロパティにAddSchemaで追加されたXmlSchemaSetを追加
            xmlDoc.Schemas = _schemaSet;
            // objXmlDocの検証
            xmlDoc.Validate(this.ValidationCallBackEventHandler);

            if (_validationErrorMessageBuilder.Length > 0)
            {   // XMLスキーマ検証エラーがあった場合、XmlSchemaExceptionをスロー
                // 改行コードの削除
                int length = _validationErrorMessageBuilder.Length;
                _validationErrorMessageBuilder = _validationErrorMessageBuilder.Remove(length - 2, 2);
                XmlSchemaException exception = new XmlSchemaException(_validationErrorMessageBuilder.ToString());
                if(_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
        }

        /// <summary>
        /// XML スキーマ検証においてエラーが発生したときに呼び出されるデリゲートです。
        /// </summary>
        /// <param name="sender">イベントのソース。</param>
        /// <param name="e">イベントデータを格納している <see cref="ValidationEventArgs"/> 。</param>
        private void ValidationCallBackEventHandler(object sender, ValidationEventArgs e)
        {
            this._validationErrorMessageBuilder.AppendLine(GetMessage(e.Severity, e.Message));
        }

        /// <summary>
        /// XML スキーマ検証においてエラーが発生した場合のエラーメッセージを取得します。
        /// </summary>
        /// <param name="severityType">検証イベントの重大度。</param>
        /// <param name="message">検証イベントに対応している説明テキスト。</param>
        private static string GetMessage(XmlSeverityType severityType, string message)
        {
            switch (severityType)
            {
                case XmlSeverityType.Error:
                    return string.Format(string.Format(
                        Properties.Resources.E_CONFIGURATION_XMLSEVERITYTYPE_ERROR, message));
                case XmlSeverityType.Warning:
                    return string.Format(string.Format(
                        Properties.Resources.W_CONFIGURATION_XMLSEVERITYTYPE_WARNING, message));
                default:
                    return Properties.Resources.E_CONFIGURATION_NOT_SUPPORT_VALIDATION_SEVERITYTYPE;
            }
        }
    }
}
