// 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.Collections.Generic;
using System.Configuration;
using System.Data;
using System.IO;
using System.Reflection;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using TERASOLUNA.Fw.Common.Logging;
using EntlibValidator = Microsoft.Practices.EnterpriseLibrary.Validation;
using TerasolunaValidator = TERASOLUNA.Fw.Common.Validation;

namespace TERASOLUNA.Fw.Common.Validation
{
    /// <summary>
    /// 入力値検証設定ファイルで検証ルールを指定し、<see cref="DataSet"/> に対する入力値検証を行うクラスです。
    /// <see cref="IValidator"/> を実装しています。内部処理に Validation Application Block で提供される Validator 機能を利用しています。
    /// </summary>
    public class VabValidator : TerasolunaValidator.IValidator
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(VabValidator));

        /// <summary>
        /// エラー処理機能や入力値検証プロバイダ機能で用いるXPathの形式です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "{0}[{1}]/{2}" です。
        /// </remarks>
        public static readonly string XPATH_FORMAT = "{0}[{1}]/{2}";

        /// <summary>
        /// <see cref="DataSet"/> に対する入力値検証を実施する際、利用する固定のルールセット文字列です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "Default" です。
        /// </remarks>
        public static readonly string DEFAULT_RULESET_STRING = "Default";

        /// <summary>
        /// 入力値検証設定ファイルのファイルパスを保持します。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は、 null です。
        /// </remarks>
        private string _validationFilePath = null;

        /// <summary>
        /// 入力値検証を実施する際に利用するルールセット文字列を保持します。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は、 "Default" です。
        /// </remarks>
        private string _ruleSet = DEFAULT_RULESET_STRING;


        /// <summary>
        /// 入力値検証設定ファイルのファイルパスを取得または設定します。
        /// </summary>
        /// <value>
        /// 入力値検証設定ファイルのファイルパス。
        /// </value>
        /// <remarks>
        /// デフォルトの値は、 null です。
        /// </remarks>
        public string ValidationFilePath
        {
            get
            {
                return _validationFilePath;
            }
            set
            {
                _validationFilePath = value;
            }
        }

        /// <summary>
        /// 入力値検証を用いる際に利用されるルールセット文字列を取得または設定します。
        /// </summary>
        /// <remarks>
        /// デフォルトの値は <seealso cref="DEFAULT_RULESET_STRING"/> です。
        /// </remarks>
        /// <value>ルールセット文字列。</value>
        public string RuleSet
        {
            get
            {
                return _ruleSet;
            }
            set
            {
                _ruleSet = value;
            }
        }

        /// <summary>
        /// 新しい <see cref="VabValidator"/> のインスタンスを初期化します。
        /// </summary>
        /// <remarks>
        /// デフォルトコンストラクタです。
        /// </remarks>
        public VabValidator() { }

        /// <summary><paramref name="dataSet"/> に対して、検証ルールに基づき入力値検証を行います</summary>
        /// <remarks>
        /// <para>
        /// <see cref="DataSet"/> 全体を検証します。検証ルールは <seealso cref="VabValidator.ValidationFilePath"/>
        /// で指定された設定ファイルより取得します。
        /// </para>
        /// <para>
        /// Validation Application Block は、アクセス時に例外が発生するプロパティに対して検証ができないことに注意して下さい。
        /// データセットのカラムの初期値が DBNull であり、値が null 時に例外をスローするように設定している場合、
        /// そのカラムへのアクセスが <see cref="StrongTypingException"/> をスローします。
        /// ただし、カラムの値が DBNull である場合でも例外がスローされません。
        /// プロパティへのアクセスが例外をスローした場合、このメソッドは
        /// <see cref="TerasolunaException"/> をスローします。
        /// </para>
        /// <para>
        /// 入力値検証設定ファイルには、各 <see cref="DataRow"/> の型を用いて検証ルールを定義します。
        /// デフォルトでルールセット名は"Default"となります。該当するルールセットが存在しない場合、検証は実行されません。
        /// また、検証対象のテーブルに行が存在しない場合も検証は実行されません。
        /// </para>
        /// <para>
        /// 本メソッドは、入力値検証エラーがある場合のみ、検証失敗となります。検証が実行されない場合であっても、
        /// 検証成功となります。
        /// </para>
        /// </remarks>
        /// <param name="dataSet">検証対象となるデータセット。</param>
        /// <returns>検証結果を格納した <see cref="ValidationResult"/> 。</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="dataSet"/> が null 参照です。
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// <list type="bullet">
        /// <item>
        /// <seealso cref="VabValidator.ValidationFilePath"/> が未指定です。
        /// </item>
        /// <item>
        /// <seealso cref="VabValidator.RuleSet"/> が未指定です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <see cref="DataSet"/> の DBNull 値を検証することはできません。
        /// </item>
        /// <item>
        /// <seealso cref="ValidationFilePath"/> に設定された値が不正です。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="FileNotFoundException">
        /// <seealso cref="ValidationFilePath"/> が存在しません。
        /// </exception>
        public virtual TerasolunaValidator.ValidationResult Validate(DataSet dataSet)
        {
            // 入力チェック
            if (dataSet == null)
            {
                ArgumentNullException exception = new ArgumentNullException("dataSet");
                if (_log.IsErrorEnabled)
                {
                    _log.Error(string.Format(Properties.Resources.E_NULL_ARGUMENT, "dataSet"), exception);
                }
                throw exception;
            }

            string validationFilePath = _validationFilePath;
            if (string.IsNullOrEmpty(validationFilePath))
            {
                InvalidOperationException exception = new InvalidOperationException(Properties.Resources.E_VALIDATION_FILE_UNDEFINED);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            string ruleSet = _ruleSet;
            if (string.IsNullOrEmpty(ruleSet))
            {
                InvalidOperationException exception = new InvalidOperationException(Properties.Resources.E_VALIDATION_RULESET_UNDEFINED);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }


            TerasolunaValidator.ValidationResult result = null;
            try
            {
                // 入力値検証設定ファイルの読み込み
                IConfigurationSource source = LoadConfigurationSource(validationFilePath);

                // 入力値検証の実行
                if (_log.IsDebugEnabled)
                {
                    _log.Debug(string.Format(Properties.Resources.D_VALIDATE_DATA_SET,
                    dataSet.GetType().FullName, validationFilePath, ruleSet));
                }
                result = ValidateDataSet(dataSet, ruleSet, source);
            }
            catch (TargetInvocationException e)
            {
                if (e.InnerException is StrongTypingException)
                {
                    TerasolunaException exception = new TerasolunaException(Properties.Resources.E_STRONG_TYPING_EXCEPTION, e);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                else
                {
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(e.InnerException.Message, e.InnerException);
                    }
                    throw e.InnerException;
                }
            }

            return result;
        }

        /// <summary>Validatorクラスのインスタンスを取得する内部メソッドです。</summary>
        /// <remarks>
        /// 引数で渡された <paramref name="source"/> と <paramref name="targetType"/> 、<paramref name="ruleSet"/>
        /// を用いて、<see cref="Microsoft.Practices.EnterpriseLibrary.Validation.Validator"/> のインスタンスを
        /// 生成します。
        /// </remarks>
        /// <param name="targetType">検証対象の型。</param>
        /// <param name="source">
        /// 設定情報を表す <see cref="IConfigurationSource"/> を実装したインスタンス。
        /// </param>
        /// <param name="ruleSet">
        /// 検証対象のルールセット文字列。
        /// </param>
        /// <returns><see cref="Microsoft.Practices.EnterpriseLibrary.Validation.Validator"/> インスタンス。</returns>
        /// <exception cref="TerasolunaException">
        /// 入力値検証設定情報が不正です。
        /// </exception>
        protected virtual EntlibValidator.Validator GetVabValidator(Type targetType,
                                                   string ruleSet,
                                                   IConfigurationSource source)
        {
            EntlibValidator.Validator validator = null;
            try
            {
                validator = EntlibValidator.ValidationFactory.CreateValidator(targetType, ruleSet, source);
            }
            catch (ArgumentException e)
            {
                TerasolunaException exception =
                    new TerasolunaException(Properties.Resources.E_VALIDATION_CONFIG_INFO_INVALID, e);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            catch (ConfigurationErrorsException e)
            {
                // 入力値検証設定情報が不正です。
                TerasolunaException exception =
                    new TerasolunaException(Properties.Resources.E_VALIDATION_CONFIG_INFO_INVALID, e);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            if (_log.IsDebugEnabled)
            {
                _log.Debug(string.Format(Properties.Resources.D_GENERATE_VALIDATOR,
                    targetType.FullName, ruleSet, _validationFilePath));
            }

            return validator;
        }

        /// <summary>引数で与えられた <paramref name="dataSet"/> の入力値検証を行います。</summary>
        /// <remarks>
        /// <para>
        /// 引数で与えられた <paramref name="dataSet"/> が持つ各テーブルの行クラス( <see cref="DataRow"/> )に
        /// 対して検証を行います。
        /// </para>
        /// <para>
        /// 入力値検証設定ファイルには、各行の実行時クラスの型に対応した検証ルール
        /// を記述する必要があります。
        /// </para>
        /// <para>
        /// ルールセット名として、<seealso cref="DEFAULT_RULESET_STRING"/> が利用されます。
        /// </para>
        /// </remarks>
        /// <param name="dataSet">検証対象の <see cref="DataSet"/> 。null であってはなりません。</param>
        /// <param name="ruleSet">ルールセット文字列。</param>
        /// <param name="configurationSource">入力値検証設定ファイルのデータ。</param>
        /// <returns>検証結果を格納した <see cref="ValidationResult"/> 。</returns>
        /// <exception cref="FormatException">
        /// format が無効です。または書式設定対象の引数を示す数が 0 より小さいか、書式項目の長さ以上です。 
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// 入力値検証設定情報が不正です。
        /// </exception>
        protected virtual TerasolunaValidator.ValidationResult ValidateDataSet(DataSet dataSet, string ruleSet,
                                                             IConfigurationSource configurationSource)
        {
            EntlibValidator.Validator entlibValidator = null;
            TerasolunaValidator.ValidationResult terasolunaResult = new TerasolunaValidator.ValidationResult();
            IList<MessageInfo> resultMessageList = terasolunaResult.Errors;
            EntlibValidator.ValidationResults rowResults = null;
            Type tp = null;

            // 全てのDataTableのDataRowに対して、入力値検証を実行
            foreach (DataTable table in dataSet.Tables)
            {
                if (IsValidationTarget(table, configurationSource))
                {
                    tp = table.Rows[0].GetType();
                    entlibValidator = GetVabValidator(tp, ruleSet, configurationSource);
                    for (int i = 0; i < table.Rows.Count; i++)
                    {
                        // DataRow単位で入力値検証を実行
                        DataRow row = table.Rows[i];

                        if (row.RowState != DataRowState.Deleted)
                        {
                            rowResults = entlibValidator.Validate(row);

                            if (!rowResults.IsValid)
                            {
                                ReFillResultMessage(rowResults, table.TableName, i, resultMessageList);
                            }
                        }
                    }
                }
            }

            return terasolunaResult;
        }

        /// <summary>
        /// 引数の <see cref="DataTable"/> が検証対象であるかをチェックする。
        /// </summary>
        /// <param name="table">検証対象の <see cref="DataTable"/> 。</param>
        /// <param name="source">入力値検証設定ファイルのデータです。</param>
        /// <returns>検証対象データ判定フラグ。</returns>
        protected virtual bool IsValidationTarget(DataTable table, IConfigurationSource source)
        {
            // デフォルトでは初期化済で1行以上のデータ列があるテーブルを検証対象とする
            // 特定の命名規約(table.TableName.StartWith("View")==trueなど)でフィルタなどを
            // かける場合、このメソッドをサブクラスでオーバーライドする。
            return table.IsInitialized && (table.Rows.Count != 0);
        }

        /// <summary>
        /// 引数で与えられた <paramref name="results"/> の個々のアイテムから、 <paramref name="list"/> に対して
        /// メッセージを詰め替えます。渡された <paramref name="xPath"/> を結果メッセージに設定します。
        /// </summary>
        /// <param name="results"><see cref="Microsoft.Practices.EnterpriseLibrary.Validation.Validator"/> 
        /// の実行結果を格納した列挙子。</param>
        /// <param name="xPath">データセットに対する検証時、検証エラーの位置を示す XPath 文字列。</param>
        /// <param name="list">検証結果を設定するための <see cref="MessageInfo"/> のリスト。</param>
        protected virtual void ReFillResultMessage(IEnumerable<EntlibValidator.ValidationResult> results,
                                   string xPath,
                                   IList<MessageInfo> list)
        {
            if (results != null)
            {
                foreach (EntlibValidator.ValidationResult result in results)
                {
                    ValidationMessageInfo vMessageInfo =
                        new ValidationMessageInfo(result.Key, result.Message, xPath);
                    vMessageInfo.Detail = result;
                    list.Add(vMessageInfo);
                    ReFillResultMessage(result.NestedValidationResults, xPath, list);
                }
            }
        }

        /// <summary>
        /// 引数で与えられた <paramref name="results"/> の個々のアイテムから、<paramref name="list"/> に対して
        /// メッセージを詰め替えます。渡された <paramref name="table"/> 、 <paramref name="rowIndex"/> を用いて
        ///  XPath を生成し、返却値に設定します。
        /// </summary>
        /// <remarks>
        /// <seealso cref="ValidateDataSet"/> から利用される内部メソッドです。
        /// </remarks>
        /// <param name="results"><see cref="Microsoft.Practices.EnterpriseLibrary.Validation.Validator"/>
        /// の実行結果を格納した列挙子。</param>
        /// <param name="tableName">検証が実行されたテーブル名。 XPath 生成に利用される。</param>
        /// <param name="rowIndex">検証が実行されたカラムのある行番号。 XPath 生成に利用される。</param>
        /// <param name="list">検証結果を設定するための <see cref="MessageInfo"/> のリスト。</param>
        /// <exception cref="FormatException">
        /// format が無効です。または書式設定対象の引数を示す数が 0 より小さいか、書式項目の長さ以上です。 
        /// </exception>
        protected virtual void ReFillResultMessage(IEnumerable<EntlibValidator.ValidationResult> results,
                                           string tableName,
                                           int rowIndex,
                                           IList<MessageInfo> list)
        {
            if (results != null)
            {
                foreach (EntlibValidator.ValidationResult result in results)
                {
                    string xPath = BuildXPath(tableName, rowIndex, result.Key);
                    ValidationMessageInfo vMessageInfo =
                        new ValidationMessageInfo(result.Key, result.Message, xPath);
                    vMessageInfo.Detail = result;
                    list.Add(vMessageInfo);
                    ReFillResultMessage(result.NestedValidationResults, xPath, list);
                }
            }
        }

        /// <summary>テーブル名、行インデックス、ノード名から XPath を生成します。</summary>
        /// <remarks>
        /// <para>
        /// 引数として渡された <paramref name="tableName"/> 、<paramref name="rowIndex"/> 、
        ///  <paramref name="nodeName"/> より、入力値検証機能で想定される形式の XPath 文字列を
        /// 生成します。
        /// </para>
        /// <para>
        /// 入力値検証機能で想定される XPath 文字列は以下のフォーマットです。
        /// </para>
        /// <example>
        /// <code>
        ///   テーブル名     = "MyTable"
        ///   行インデックス =  10
        ///   ノード名       = "MyColumn"
        /// </code>
        /// のとき、
        /// <code>
        ///   XPath文字列    => "MyTable[10]/MyColumn
        /// </code>
        /// </example>
        /// </remarks>
        /// <param name="tableName">テーブル名。</param>
        /// <param name="rowIndex">対象行のインデックス。</param>
        /// <param name="nodeName">対象のノード。</param>
        /// <returns>指定したパラメータの XPath 文字列。</returns>
        /// <exception cref="FormatException">
        /// format が無効です。または書式設定対象の引数を示す数が 0 より小さいか、書式項目の長さ以上です。 
        /// </exception>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "rowIndex+1")]
        protected virtual string BuildXPath(string tableName, int rowIndex, string nodeName)
        {
            return string.Format(XPATH_FORMAT, tableName, rowIndex + 1, nodeName);
        }

        /// <summary>設定情報を読み込んだ <see cref="IConfigurationSource"/> の実装クラスのインスタンスを生成します。</summary>
        /// <remarks>
        /// <para>
        /// <seealso cref="VabValidator.ValidationFilePath"/> に設定されたファイルパスから、
        /// <see cref="IConfigurationSource"/> の実装クラス <see cref="FileConfigurationSource"/> のインスタンスを生成するメソッドです。
        /// </para>
        /// </remarks>
        /// <param name="validationFilePath">入力値検証設定ファイルのパス。</param>
        /// <returns>設定ファイルから生成された <see cref="IConfigurationSource"/> 。</returns>
        /// <exception cref="TerasolunaException">
        /// <paramref name="validationFilePath"/> に設定された値が不正である。
        /// </exception>
        /// <exception cref="FileNotFoundException">
        /// <paramref name="validationFilePath"/> が存在しません。
        /// </exception>
        protected virtual IConfigurationSource LoadConfigurationSource(string validationFilePath)
        {
            string filePath = null;

            try
            {
                filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, validationFilePath);
            }
            catch (ArgumentException e)
            {
                string message = string.Format(Properties.Resources.E_INVALID_FILE_PATH, validationFilePath);
                TerasolunaException exception = new TerasolunaException(message, e);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            if (!File.Exists(filePath))
            {
                string message = string.Format(Properties.Resources.E_VALIDATION_CONFIG_FILE_NOT_FOUND, filePath);
                FileNotFoundException exception = new FileNotFoundException(message, filePath);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(message, exception);
                }
                throw exception;
            }

            FileConfigurationSource result = new FileConfigurationSource(filePath);

            if (_log.IsDebugEnabled)
            {
                _log.Debug(string.Format(Properties.Resources.D_LOAD_CONFIGURATION_SOURCE, filePath));
            }

            return result;
        }
    }
}
