// 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;
using System.Collections.Generic;
using System.Configuration;
using System.Reflection;
using System.Web;
using TERASOLUNA.Fw.Common;
using TERASOLUNA.Fw.Common.BLogic;
using TERASOLUNA.Fw.Common.Configuration.BLogic;
using TERASOLUNA.Fw.Common.Logging;
using TERASOLUNA.Fw.Web.Configuration.RequestController;
using TERASOLUNA.Fw.Web.Validation;

namespace TERASOLUNA.Fw.Web.Controller
{
    /// <summary>
    /// リクエスト情報を元に対象となるリクエストコントローラを
    /// 生成するファクトリクラスです。
    /// </summary>
    /// <remarks>
    /// <para>このクラスを利用するには、
    /// アプリケーション構成ファイル( Web.config ) に &lt;httpHandlers&gt; に
    /// 要求を処理するハンドラとして、追加設定する必要があります。</para>
    /// <example>
    /// この例では、RequestController.aspx に POST 要求されたリクエストを処理するハンドラクラスの生成に、
    /// <see cref="RequestControllerFactory"/> を利用するように指定する方法を示します。
    /// <code>
    /// <![CDATA[
    /// <configuration>
    ///   <system.web>
    ///     <httpHandlers>
    ///       <add verb="POST" path="RequestController.aspx" type="TERASOLUNA.Fw.Web.Controller.RequestControllerFactory,TERASOLUNA.Fw.Web"/>
    ///     </httpHandlers>
    ///   </system.web>
    /// </configuration>
    /// ]]>
    /// </code>
    /// </example>
    /// </remarks>
    public class RequestControllerFactory : IHttpHandlerFactory
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(RequestControllerFactory));

        /// <summary>
        /// リクエスト名称が格納されるHTTPヘッダ要素名です。
        /// </summary>
        /// <remarks>
        /// <para>
        /// この定数の値は "RequestName" です。
        /// </para>
        /// </remarks>
        protected static readonly string REQUEST_NAME = "RequestName";

        /// <summary>
        /// 対象リクエストコントローラを特定できない場合に利用するリクエストタイプ名称です。
        /// </summary>
        /// <remarks>
        /// <para>
        /// この定数の値は "Unknown" です。
        /// </para>
        /// </remarks>
        protected static readonly string REQUEST_CTRL_UNKNOWN = "Unknown";

        /// <summary>
        /// <see cref="IBLogic"/> 実装クラスに対応する <see cref="Type"/> インスタンスを
        /// 格納するキャッシュです。
        /// </summary>
        private IDictionary<string, Type> _blogicTypes = new Dictionary<string, Type>();

        /// <summary>
        /// <see cref="IBLogic"/> 実装クラスに対応する <see cref="Type"/> インスタンスの
        /// キャッシュを取得します。
        /// </summary>
        /// <value>
        /// <see cref="IBLogic"/> 実装クラスに対応する <see cref="Type"/> インスタンスのキャッシュ。
        /// </value>
        protected IDictionary<string, Type> BLogicTypes
        {
            get
            {
                return _blogicTypes;
            }
        }

        /// <summary>
        /// <c>RequestName</c> ヘッダの設定値を元に、要求を処理する
        /// <see cref="IHttpHandler"/> インタフェース実装クラスを決定し、
        /// そのインスタンスを返します。
        /// </summary>
        /// <param name="context"> 
        /// HTTP 要求を処理するために使用する、組み込みのサーバー オブジェクト
        /// (たとえば、 Request 、 Response 、 Session 、および Server )
        /// への参照を提供する <see cref="HttpContext"/> クラスのインスタンス。</param>
        /// <param name="requestType"> クライアントが使用する HTTP データ転送メソッド ( GET または POST )。</param>
        /// <param name="url">
        /// 要求されたリソースの <see cref="HttpRequest.RawUrl"/> 。
        /// </param>
        /// <param name="pathTranslated">要求されたリソースの <see cref="HttpRequest.PhysicalApplicationPath"/> 。</param>
        /// <returns>要求を処理する新しい <see cref="IHttpHandler"/> インターフェイス実装クラスのインスタンス。</returns>
        /// <remarks>
        /// <para><c>RequestName</c> ヘッダの値を <see cref="HttpContext.Items"/> の
        /// キー <see cref="RequestControllerConstants.KEY_REQUEST_NAME"/> へ格納します。</para>
        /// <para><see cref="LoadAttributes"/> を呼び出し、対応するビジネスロジッククラスを解決します。</para>
        /// <para><see cref="GetTargetRequestController"/> を呼び出し、該当するリクエストコントローラを生成します。</para>
        /// <para><see cref="GetTargetRequestController"/> によるリクエストコントローラクラスの生成に失敗した場合、対象となるリクエストコントローラ識別子を
        ///  <see cref="REQUEST_CTRL_UNKNOWN"/> とし、<see cref="GetTargetRequestController"/> を再度呼び出します。</para>
        /// <para>例外が発生した場合は全て補足し、発生例外を <see cref="HttpContext.Items"/> の
        /// キー <see cref="RequestControllerConstants.KEY_CONTROLLER_FACTORY_ERR"/> へ格納します。</para>
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods"), 
        System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        public virtual IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
        {
            IHttpHandler handler = null;

            try
            {
                string reqCtrlName = null;
                string requestName = context.Request.Headers[REQUEST_NAME];

                if (string.IsNullOrEmpty(requestName))
                {
                    // リクエスト名が未設定の場合
                    InvalidRequestException exception = new InvalidRequestException(Properties.Resources.E_EMPTY_REQUEST_NAME);
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                else
                {
                    // リクエスト名称を保持しておく
                    context.Items[RequestControllerConstants.KEY_REQUEST_NAME] = requestName;

                    // 対象ビジネスロジックのタイプオブジェクトおよび付随する
                    // 属性情報を読み込む
                    LoadAttributes(context);
                    
                    reqCtrlName = context.Items[RequestControllerConstants.KEY_REQUEST_TYPE] as string;

                    handler = GetTargetRequestController(reqCtrlName);
                    
                }
            }
            catch (Exception ex)
            {
                Exception targetEx = ex;
                // リクエスト名が設定されていない、または例外が発生して対象となるリクエストコントローラが
                // 解決できなかった場合は、全てUnknownRequestControllerで処理する。
                try
                {
                    handler = GetTargetRequestController(REQUEST_CTRL_UNKNOWN);
                }
                catch (Exception newEx)
                {
                    if (_log.IsWarnEnabled)
                    {
                        _log.Warn(string.Format(
                            Properties.Resources.W_NOT_CREATE_REQUEST_CONTROLLER, context.Items[RequestControllerConstants.KEY_REQUEST_NAME]));
                    }

                    // 既定の不正リクエストコントローラを利用する。
                    handler = new UnknownRequestController();
                    targetEx = newEx;
                }

                // リクエストコントローラファクトリ内で発生したエラーをUnknownRequestControllerで
                // 処理するため、HttpContext.Items に格納しておく。
                context.Items[RequestControllerConstants.KEY_CONTROLLER_FACTORY_ERR] = targetEx;
            }

            return handler;
        }

        /// <summary>
        /// 対象ビジネスロジックのタイプオブジェクトおよび付随する属性情報を読み込みます。
        /// </summary>
        /// <param name="context"> 
        /// HTTP 要求を処理するために使用する、組み込みのサーバー オブジェクト
        /// (たとえば、 Request 、 Response 、 Session 、および Server )
        /// への参照を提供する <see cref="HttpContext"/> クラスのインスタンス。</param>
        /// <remarks>
        /// 要求された <see cref="IBLogic"/> 実装クラスの <see cref="Type"/> と、指定されている
        ///  <see cref="ControllerInfoAttribute"/> 属性、 <see cref="ValidationInfoAttribute"/> 属性の
        /// 設定値を取得し、<see cref="HttpContext.Items"/> へ以下の通り格納します。
        /// <list type="table">
        /// <listheader>
        /// <term>キー文字列</term>
        /// <description>設定値</description>
        /// </listheader>
        /// <item>
        /// <term><see cref="RequestControllerConstants.KEY_BLOGIC_CLASS_TYPE"/></term>
        /// <description>要求されたビジネスロジッククラスの <see cref="Type"/>。</description>
        /// </item>
        /// <item>
        /// <term><see cref="RequestControllerConstants.KEY_REQUEST_TYPE"/></term>
        /// <description><see cref="ControllerInfoAttribute"/> 属性の
        /// <see cref="ControllerInfoAttribute.RequestType"/> プロパティ設定値。</description>
        /// </item>
        /// <item>
        /// <term><see cref="RequestControllerConstants.KEY_INPUT_DATASET_CLASS_TYPE"/></term>
        /// <description><see cref="ControllerInfoAttribute"/> 属性の
        /// <see cref="ControllerInfoAttribute.InputDataSetType"/> プロパティ設定値。</description>
        /// </item>
        /// <item>
        /// <term><see cref="RequestControllerConstants.KEY_VALIDATION_FILE_PATH"/></term>
        /// <description><see cref="ValidationInfoAttribute"/> 属性の
        /// <see cref="ValidationInfoAttribute.ValidationFilePath"/> プロパティ設定値。</description>
        /// </item>
        /// </list>
        /// </remarks>
        /// <exception cref="InvalidRequestException">
        /// <c>RequestName</c> ヘッダ に設定された値に対応する
        /// ビジネスロジッククラスの <see cref="Type"/> が取得できません。
        /// </exception>
        protected virtual void LoadAttributes(HttpContext context)
        {
            string requestName = context.Request.Headers[REQUEST_NAME];

            // 対象ビジネスロジックのタイプを取得
            Type blogicType = null;
            try
            {
                bool canGetValue = false;
                ICollection blogicTypesCollection = (ICollection)_blogicTypes;
                lock (blogicTypesCollection.SyncRoot)
                {
                    canGetValue = _blogicTypes.TryGetValue(requestName, out blogicType);
                }

                if (!canGetValue)
                {
                    // ビジネスロジック設定ファイルからBLogicのタイプ名を取得
                    string blogicTypeName = BLogicConfiguration.GetBLogicTypeName(requestName);

                    if (blogicTypeName == null)
                    {
                        TerasolunaException exception = new TerasolunaException(string.Format(
                            Properties.Resources.E_BLOGIC_TYPE_NOT_FOUND, requestName));
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }

                    blogicType = Type.GetType(blogicTypeName);

                    if (blogicType == null)
                    {
                        TerasolunaException exception = new TerasolunaException(string.Format(
                            Properties.Resources.E_BLOGIC_IBLOGIC_TYPE_NOT_FOUND, blogicTypeName));
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }

                    lock (blogicTypesCollection.SyncRoot)
                    {
                        _blogicTypes[requestName] = blogicType;
                    }

                }
            }
            catch (TypeLoadException tle)
            {
                InvalidRequestException exception = new InvalidRequestException(string.Format(
                    Properties.Resources.E_UNKNOWN_REQUEST_NAME, requestName), tle);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            catch (ConfigurationErrorsException cee)
            {
                InvalidRequestException exception = new InvalidRequestException(string.Format(
                    Properties.Resources.E_UNKNOWN_REQUEST_NAME, requestName), cee);
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }

            context.Items[RequestControllerConstants.KEY_BLOGIC_CLASS_TYPE] = blogicType;

            // 付随する属性情報を取得
            object[] attrArray = blogicType.GetCustomAttributes(true);
            // 既定のリクエストタイプとして通常を指定しておく
            context.Items[RequestControllerConstants.KEY_REQUEST_TYPE] = RequestTypeNames.NORMAL;

            foreach (object attr in attrArray)
            {
                // ビジネスロジック属性値取得
                ControllerInfoAttribute ctrlInfoAttr = attr as ControllerInfoAttribute;
                if (ctrlInfoAttr != null)
                {
                    string requestType = ctrlInfoAttr.RequestType;
                    if (requestType != null)
                    {
                        context.Items[RequestControllerConstants.KEY_REQUEST_TYPE] = requestType;
                    }
                    context.Items[RequestControllerConstants.KEY_INPUT_DATASET_CLASS_TYPE] = ctrlInfoAttr.InputDataSetType;
                }
                // 入力値検証属性値取得
                ValidationInfoAttribute validationAttr = attr as ValidationInfoAttribute;
                if (validationAttr != null)
                {
                    context.Items[RequestControllerConstants.KEY_VALIDATION_FILE_PATH] = validationAttr.ValidationFilePath;
                    context.Items[RequestControllerConstants.KEY_VALIDATION_RULESET] = validationAttr.RuleSet;
                }
            }
        }

        /// <summary>
        /// リクエストコントローラの識別子から対象となる <see cref="IHttpHandler"/> インタフェースを実装したリクエストコントローラクラスのインスタンスを取得します。
        /// </summary>
        /// <param name="reqCtrlName">リクエストコントローラの識別子。</param>
        /// <returns>要求を処理する新しい <see cref="IHttpHandler"/> インタフェース実装クラスのインスタンス。</returns>
        /// <remarks>
        /// <para><paramref name="reqCtrlName"/> をキーとして、アプリケーション構成ファイル(Web.config)に設定された、
        /// リクエストコントローラクラスのインスタンスを生成します。</para>
        /// <para>アプリケーション構成ファイル(Web.config)に対応する <paramref name="reqCtrlName"/> をキーとした設定が、
        /// 行われていない場合は、 <see cref="RequestControllerFactory.CreateDefaultRequestController"/> を実行し、
        /// 既定のリクエストコントローラクラスを対象として生成します。</para>
        /// </remarks>
        /// <exception cref="ConfigurationErrorsException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// Web 構成ファイルを読み込むことができません。
        /// </item>
        /// <item>
        /// Web 構成ファイル( Web.config )に設定されたリクエストコントローラ設定において、 <paramref name="reqCtrlName"/> に対応する設定値が空文字となっています。
        /// </item>
        /// <item>
        /// Web 構成ファイル( Web.config )に設定されたリクエストコントローラ設定において、 <paramref name="reqCtrlName"/> に対応する設定値の <see cref="Type"/> が取得できません。
        /// </item>
        /// </list>
        /// </exception>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// 生成対象のコンストラクタが不正です。
        /// </item>
        /// <item>
        /// 生成対象のクラスが <see cref="IHttpHandler"/> インタフェースを実装していません。
        /// </item>
        /// <item>
        /// <paramref name="requestControllerName"/> に <see cref="RequestTypeNames.NORMAL"/> 、<see cref="RequestTypeNames.DOWNLOAD"/> 、 
        /// <see cref="RequestTypeNames.UPLOAD"/> 、 <see cref="REQUEST_CTRL_UNKNOWN"/> 以外の値が設定されています。
        /// </item>
        /// </list>
        /// </exception>
        protected virtual IHttpHandler GetTargetRequestController(string reqCtrlName)
        {
            IHttpHandler handler = null;
            string reqCtrlTypeName = RequestControllerConfiguration.GetRequestControllerType(reqCtrlName);

            if (reqCtrlTypeName == null)
            {
                // 対応するリクエストコントローラが設定されていない場合は
                // 既定のリクエストコントローラを使用する。
                handler = CreateDefaultRequestController(reqCtrlName);
                if (_log.IsDebugEnabled)
                {
                    _log.Debug(string.Format(Properties.Resources.D_CREATE_DEFAULT_REQUEST_CONTROLLER, reqCtrlName, handler.GetType().FullName));
                }
            }
            else if (reqCtrlTypeName.Length < 1)
            {
                // 対応するリクエストコントローラのキーは設定されているが値が空の場合
                ConfigurationErrorsException exception = new ConfigurationErrorsException(string.Format(
                    Properties.Resources.E_INVALID_REQUEST_CONTROLLER_CONFIG, reqCtrlName));
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            else
            {
                Type handlerType = Type.GetType(reqCtrlTypeName);
                if (handlerType == null)
                {
                    // 設定されているリクエストコントローラクラスのTypeインスタンスが生成できない場合。
                    ConfigurationErrorsException exception = new ConfigurationErrorsException(string.Format(
                        Properties.Resources.E_REQUEST_CONTROLLER_CLASS_NOT_FOUND, reqCtrlName, reqCtrlTypeName));
                    if (_log.IsErrorEnabled)
                    {
                        _log.Error(exception.Message, exception);
                    }
                    throw exception;
                }
                else
                {
                    try
                    {
                        handler = Activator.CreateInstance(handlerType) as IHttpHandler;
                    }
                    catch (MemberAccessException mae)
                    {
                        //アクセス自体不可
                        TerasolunaException exception = new TerasolunaException(string.Format(
                            Properties.Resources.E_INVALID_CONSTRUCTOR, handlerType.FullName), mae);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    catch (TargetInvocationException tie)
                    {
                        //コンストラクタで例外発生
                        TerasolunaException exception = new TerasolunaException(string.Format(
                            Properties.Resources.E_INVALID_CONSTRUCTOR, handlerType.FullName), tie);
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    if (handler == null)
                    {
                        // IHttpHandlerを実装していないクラスがリクエストコントローラとして設定されている場合。
                        TerasolunaException exception = new TerasolunaException(string.Format(
                            Properties.Resources.E_NOT_IMPLEMENT, reqCtrlTypeName, typeof(IHttpHandler).FullName));
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                    if (_log.IsDebugEnabled)
                    {
                        _log.Debug(string.Format(Properties.Resources.D_CREATE_REQUEST_CONTROLLER, reqCtrlName, handler.GetType().FullName));
                    }
                }
            }
            return handler;
        }

        /// <summary>
        /// 既定のリクエストコントローラを生成します。
        /// </summary>
        /// <param name="requestControllerName">対象のリクエストコントローラ識別名。</param>
        /// <returns>
        /// <paramref name="requestControllerName"/> に対応する <see cref="IHttpHandler"/> インタフェースを
        /// 実装した既定のリクエストコントローラクラスのインスタンス。
        /// </returns>
        /// <remarks>
        /// <para><paramref name="requestControllerName"/> と生成されるリクエストコントローラクラスは、
        /// 以下のように対応づけられています。</para>
        /// <list type="table">
        /// <listheader>
        /// <term><paramref name="requestControllerName"/></term>
        /// <description>リクエストコントローラクラス</description>
        /// </listheader>
        /// <item>
        /// <term><see cref="RequestTypeNames.NORMAL"/></term>
        /// <description><see cref="BLogicRequestController"/></description>
        /// </item>
        /// <item>
        /// <term><see cref="RequestTypeNames.DOWNLOAD"/></term>
        /// <description><see cref="FileDownloadRequestController"/></description>
        /// </item>
        /// <item>
        /// <term><see cref="RequestTypeNames.UPLOAD"/></term>
        /// <description><see cref="FileUploadRequestController"/></description>
        /// </item>
        /// <item>
        /// <term><see cref="REQUEST_CTRL_UNKNOWN"/></term>
        /// <description><see cref="UnknownRequestController"/></description>
        /// </item>
        /// </list>
        /// </remarks>
        /// <exception cref="TerasolunaException">
        /// <paramref name="requestControllerName"/> に <see cref="RequestTypeNames.NORMAL"/> 、<see cref="RequestTypeNames.DOWNLOAD"/> 、 
        /// <see cref="RequestTypeNames.UPLOAD"/> 、 <see cref="REQUEST_CTRL_UNKNOWN"/> 以外の値が設定されています。
        /// </exception>
        protected virtual IHttpHandler CreateDefaultRequestController(string requestControllerName)
        {
            if(RequestTypeNames.NORMAL.Equals(requestControllerName, StringComparison.OrdinalIgnoreCase))
            {
                return new BLogicRequestController();
            }
            else if (RequestTypeNames.MULTIPART_UPLOAD.Equals(requestControllerName, StringComparison.OrdinalIgnoreCase))
            {
                return new MultipartUploadRequestController();
            }
            else if (RequestTypeNames.UPLOAD.Equals(requestControllerName, StringComparison.OrdinalIgnoreCase))
            {
                return new FileUploadRequestController();
            }
            else if (RequestTypeNames.DOWNLOAD.Equals(requestControllerName, StringComparison.OrdinalIgnoreCase))
            {
                return new FileDownloadRequestController();
            }
            else if (REQUEST_CTRL_UNKNOWN.Equals(requestControllerName, StringComparison.OrdinalIgnoreCase))
            {
                return new UnknownRequestController();
            }
            else
            {
                TerasolunaException exception = new TerasolunaException(string.Format(
                    Properties.Resources.E_UNKNOWN_REQUEST_CONTROLLER, requestControllerName));
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
        }

        /// <summary>
        /// 既存の <see cref="IHttpHandler"/> 実装クラスのインスタンスを再利用するために
        /// ファクトリを有効にします。
        /// </summary>
        /// <param name="handler">再利用する <see cref="IHttpHandler"/> 実装クラスのインスタンス。</param>
        /// <remarks>
        /// <para>リクエスト処理が終了した際に <see cref="HttpRuntime"/> から呼び出されます。</para>
        /// <para><paramref name="handler"/> に指定されたクラスが <see cref="IDisposable"/> を実装している
        /// クラスである場合、<seealso cref="IDisposable.Dispose"/> を呼び出しリソースの解放処理を行います。</para>
        /// </remarks>
        public virtual void ReleaseHandler(IHttpHandler handler)
        {
            // ハンドラの持つリソースを解放する必要がある場合はここで実装する必要がある。

            // IDisposableを実装したハンドラの場合は解放処理を行う。
            IDisposable disposableHandler = handler as IDisposable;
            if (disposableHandler != null)
            {
                disposableHandler.Dispose();
            }
        }
    }
}
