// 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.IO;
using System.Net.Mime;
using System.Text;
using System.Web;
using TERASOLUNA.Fw.Common;
using TERASOLUNA.Fw.Common.Logging;
using TERASOLUNA.Fw.Web.BLogic;

namespace TERASOLUNA.Fw.Web.Controller
{
    /// <summary>
    /// クライアントからのファイルダウンロードを伴うビジネスロジック実行要求を
    /// 解決するコントローラクラスです。
    /// </summary>
    public class FileDownloadRequestController : BLogicRequestController
    {
        /// <summary>
        /// <see cref="ILog"/> 実装クラスのインスタンスです。
        /// </summary>
        /// <remarks>
        /// ログ出力に利用します。
        /// </remarks>
        private static ILog _log = LogFactory.GetLogger(typeof(FileDownloadRequestController));

        /// <summary>
        /// Content-Dispositon ヘッダ名称。
        /// </summary>
        /// <remarks>
        /// この定数の値は "content-disposition" です。
        /// </remarks>
        private static readonly string HEADER_CONTENT_DISPOSITION = "content-disposition";

        /// <summary>
        /// Base64 を表すエンコーディング識別名です。
        /// </summary>
        /// <remarks>
        /// この定数の値は "B" です。
        /// </remarks>
        private static readonly string MIME_ENCODE_TYPE = "B";
        /// <summary>
        /// ファイル名称のエンコーディングです。( ISO-2022-JP )
        /// </summary>
        /// <remarks>
        /// この定数の値は "ISO-2022-JP" です。
        /// </remarks>
        private readonly Encoding FILENAME_ENCOING = Encoding.GetEncoding("ISO-2022-JP");

        /// <summary>
        /// ビジネスロジックを実行します。
        /// </summary>
        /// <param name="context"> 
        /// HTTP 要求を処理するために使用する、組み込みのサーバー オブジェクト
        /// (たとえば、 Request 、 Response 、 Session 、および Server )
        /// への参照を提供する <see cref="HttpContext"/> クラスのインスタンス。
        /// </param>
        /// <returns>正常終了の場合 true 、業務エラーが発生した場合 false を返却します。</returns>
        /// <remarks>
        /// <see cref="BLogicRequestController.ExecuteBLogic"/> を呼び出した後、
        /// ビジネスロジック結果オブジェクトが <see cref="FileDownloadBLogicResult"/> 
        /// であるか検証を行います。
        /// </remarks>
        /// <exception cref="TerasolunaException">
        /// 以下のような場合に例外をスローします。
        /// <list type="bullet">
        /// <item>
        /// <see cref="Common.BLogic.IBLogic"/> 実装クラスを生成できません。
        /// </item>
        /// <item>
        /// <seealso cref="Common.BLogic.IBLogic.Execute"/> メソッドの返却値が null 参照です。
        /// </item>
        /// <item>
        /// <description>
        /// ビジネスロジック結果オブジェクトが <see cref="FileDownloadBLogicResult"/>
        /// クラスではありません。
        /// </description>
        /// </item>
        /// <item>
        /// <description>
        /// ビジネスロジックが正常終了し、ビジネスロジック結果オブジェクト <seealso cref="FileDownloadBLogicResult"/> の 
        /// <seealso cref="FileDownloadBLogicResult.DownloadData"/> が設定されていない場合に、 
        /// <seealso cref="FileDownloadBLogicResult.DownloadFileInfo"/> に設定されたファイルが見つかりませんでした。
        /// </description>
        /// </item>
        /// </list>
        /// </exception>
        protected override bool ExecuteBLogic(HttpContext context)
        {
            bool isSuccess = base.ExecuteBLogic(context);

            FileDownloadBLogicResult result = context.Items[KEY_CONTEXT_ITEM_BLOGIC_RESULT] as FileDownloadBLogicResult;
            if (result == null)
            {
                // 結果がFileDownloadBLogicResultでない場合、例外をスローする。
                TerasolunaException exception = new TerasolunaException(string.Format(
                    Properties.Resources.E_INVALID_BLOGIC_RESULT_TYPE, typeof(FileDownloadBLogicResult).FullName));
                if (_log.IsErrorEnabled)
                {
                    _log.Error(exception.Message, exception);
                }
                throw exception;
            }
            if (isSuccess)
            {
                // ビジネスロジックが正常終了している場合。
                if (result.ResultData == null)
                {
                    // ResultDataが未設定の場合に、DownloadDataが設定されていなくて、DownloadFileInfoが存在
                    // しない場合、エラー。
                    if (result.DownloadData == null && !result.DownloadFileInfo.Exists)
                    {
                        TerasolunaException exception = new TerasolunaException(string.Format(
                            Properties.Resources.E_FILE_NOT_FOUND, result.DownloadFileInfo.Name));
                        if (_log.IsErrorEnabled)
                        {
                            _log.Error(exception.Message, exception);
                        }
                        throw exception;
                    }
                }
            }
                       
            return isSuccess;
        }

        /// <summary>
        /// 正常終了時のレスポンスを書き込みます。
        /// </summary>
        /// <param name="context"> 
        /// HTTP 要求を処理するために使用する、組み込みのサーバー オブジェクト
        /// (たとえば、Request、Response、Session、および Server)
        /// への参照を提供する <see cref="HttpContext"/> クラスのインスタンス。
        /// </param>
        /// <remarks>
        /// <para>
        /// <see cref="HttpContext.Items"/> の
        /// <see cref="BLogicRequestController.KEY_CONTEXT_ITEM_BLOGIC_RESULT"/> に格納された
        /// ビジネスロジック実行結果オブジェクトである <see cref="FileDownloadBLogicResult"/> の
        /// <see cref="FileDownloadBLogicResult.DownloadFileInfo"/>からファイル名称を取得し、
        /// <see cref="EncodeFileName"/> によりファイル名称をエンコードします。
        /// </para>
        /// <para>
        /// エンコードしたファイル情報を、
        /// <see cref="WriteSuccessBinaryResponseHeader"/>
        /// の引数に指定して呼び出し、正常終了時のレスポンスのヘッダ情報を設定します。
        /// </para>
        /// <para>
        /// <see cref="FileDownloadBLogicResult.DownloadData"/> が設定されていれば、
        /// <see cref="WriteResponseBodyFromByteArray"/> を呼び出し、 byte 配列を元に
        /// レスポンスデータを <see cref="HttpResponse.OutputStream"/> に書き込みます。
        /// 設定されていない場合は、
        /// <see cref="WriteResponseBodyFromFileInfo"/> を呼び出し、ファイル情報に指定された
        /// ファイルを元にレスポンスデータを <see cref="HttpResponse.OutputStream"/> に書き込みます。
        /// </para>
        /// </remarks>
        protected override void WriteSuccessResponse(HttpContext context)
        {
            FileDownloadBLogicResult result = context.Items[KEY_CONTEXT_ITEM_BLOGIC_RESULT] as FileDownloadBLogicResult;
            
            string encodedFileName = EncodeFileName(result.DownloadFileInfo.Name);

            // レスポンスヘッダ設定
            WriteSuccessBinaryResponseHeader(context.Response, encodedFileName);

            // ボディ書き込み
            if (result.DownloadData == null)
            {
                // ダウンロードデータが設定されていない場合は、ビジネスロジック結果オブジェクトのファイル情報に
                // 指定されたファイルパスのファイルをダウンロード対象とします。
                WriteResponseBodyFromFileInfo(context.Response, result.DownloadFileInfo);
            }
            else
            {
                // ダウンロードデータが設定されている場合は、設定されたビジネスロジック結果オブジェクトのダウンロードデータ
                // をダウンロード対象とします。
                byte[] downloadData = new byte[result.DownloadData.Count];
                result.DownloadData.CopyTo(downloadData, 0);
                WriteResponseBodyFromByteArray(context.Response, downloadData);
            }
        }

        /// <summary>
        /// ファイル名をエンコードします。
        /// </summary>
        /// <param name="fileName">エンコード対象のファイル名称。</param>
        /// <returns>エンコードされたファイル名称。</returns>
        /// <remarks>
        /// content-disposition ヘッダのfilenameパラメータに設定する
        /// ファイル名称をエンコードします。
        /// <para>次のフォーマットでエンコードされます。</para>
        /// <code>
        /// ?{文字コード名称}?{ASCIIエンコード形式}?{エンコードファイル名称}?=
        /// </code>
        /// <list type="table">
        /// <listheader>
        /// <term>
        /// フィールド
        /// </term>
        /// <description>
        /// 値
        /// </description>
        /// </listheader>
        /// <item>
        /// <term>
        /// 文字コード名称
        /// </term>
        /// <description>
        /// ISO-2022-JP
        /// </description>
        /// </item>
        /// <item>
        /// <term>
        /// ASCIIエンコード形式
        /// </term>
        /// <description>
        /// B
        /// </description>
        /// </item>
        /// <item>
        /// <term>
        /// エンコードファイル名称
        /// </term>
        /// <description>
        /// 文字コード名称に指定された文字コードにファイル名に<paramref name="fileName"/>
        /// をエンコードし、ファイル名称をBase64でエンコードされた文字列。
        /// </description>
        /// </item>
        /// </list>
        /// </remarks>
        protected virtual string EncodeFileName(string fileName)
        {
            byte[] encodingBytes = FILENAME_ENCOING.GetBytes(fileName);
            string bencodedFileName = System.Convert.ToBase64String(encodingBytes);
            string mimeBEncodedFileName = string.Format("?{0}?{1}?{2}?=", FILENAME_ENCOING.WebName, MIME_ENCODE_TYPE, bencodedFileName);
            return mimeBEncodedFileName;
        }

        /// <summary>
        /// レスポンスに正常終了時のヘッダ情報を設定します。
        /// </summary>
        /// <param name="response">ヘッダの設定対象となる <see cref="HttpResponse"/>。</param>
        /// <param name="fileName">content-dispositionヘッダに設定するファイル名称。</param>
        /// <remarks>
        /// 次の項目をヘッダに設定します。
        /// <list type="table">
        /// <listheader>
        /// <term>
        /// ヘッダ名称
        /// </term>
        /// <description>
        /// 設定値
        /// </description>
        /// </listheader>
        /// <item>
        /// <term>
        /// content-type
        /// </term>
        /// <description>
        /// application/octetstream
        /// </description>
        /// </item>
        /// <item>
        /// <term>
        /// ステータス文字列
        /// </term>
        /// <description>
        /// OK
        /// </description>
        /// </item>
        /// <item>
        /// <term>
        /// content-disposition
        /// </term>
        /// <description>
        /// <paramref name="fileName"/> に指定されたファイル名称。
        /// </description>
        /// </item>
        /// </list>
        /// </remarks>
        protected virtual void WriteSuccessBinaryResponseHeader(HttpResponse response, string fileName)
        {
            // content-type
            ContentType ct = new ContentType();
            ct.MediaType = MediaTypeNames.Application.Octet;
            response.ContentType = ct.ToString();
            // ステータス説明情報
            response.StatusDescription = "OK";
            // content-disposition
            ContentDisposition cd = new ContentDisposition();
            cd.FileName = fileName;
            response.AddHeader(HEADER_CONTENT_DISPOSITION, cd.ToString());
        }

        /// <summary>
        /// レスポンスの出力ストリームに指定されたバイト配列に
        /// 格納されたデータを書き込みます。
        /// </summary>
        /// <param name="response">書き込み対象となる <see cref="HttpResponse"/>。</param>
        /// <param name="byteArray">ビジネスロジックから返却されたダウンロードデータが格納されたバイト配列。</param>
        /// <remarks>
        /// <paramref name="byteArray"/> に指定された byte 配列のデータを <see cref="HttpResponse.OutputStream"/> 
        /// に書き込みます。
        /// </remarks>
        protected virtual void WriteResponseBodyFromByteArray(HttpResponse response, byte[] byteArray)
        {
            using (BinaryWriter bw = new BinaryWriter(response.OutputStream))
            {
                bw.Write(byteArray);
            }
        }

        /// <summary>
        /// レスポンスの出力ストリームに指定されたファイル情報に
        /// 該当するファイルの内容を直接書き込みます。
        /// </summary>
        /// <param name="response">書き込み対象となる <see cref="HttpResponse"/>。</param>
        /// <param name="fileInfo">ビジネスロジックから返却されたファイル情報。</param>
        /// <remarks>
        /// <paramref name="fileInfo"/> に指定された ファイル情報に設定されたパスに該当するファイル
        /// の内容を読み込み、 <see cref="HttpResponse.OutputStream"/> に書き込みます。
        /// </remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        protected virtual void WriteResponseBodyFromFileInfo(HttpResponse response, FileInfo fileInfo)
        {
            response.TransmitFile(fileInfo.FullName);
        }
    }
}
