OSDN Git Service

#27617 各サイトでの動作を確認し設定を精査・ツールチップの文言を姉妹サイトも考慮したものに修正,
[wptscs/wpts.git] / Wptscs / Logics / Translator.cs
index d1a2a64..a36947f 100644 (file)
@@ -1,9 +1,9 @@
 // ================================================================================================
 // <summary>
-//      翻訳支援処理を実装するための共通クラスソース</summary>
+//      翻訳支援処理を実装するための抽象クラスソース</summary>
 //
 // <copyright file="Translator.cs" company="honeplusのメモ帳">
-//      Copyright (C) 2010 Honeplus. All rights reserved.</copyright>
+//      Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
 // <author>
 //      Honeplus</author>
 // ================================================================================================
 namespace Honememo.Wptscs.Logics
 {
     using System;
+    using System.Diagnostics;
     using System.IO;
     using System.Net;
     using System.Net.NetworkInformation;
     using System.Reflection;
+    using Honememo.Models;
     using Honememo.Utilities;
     using Honememo.Wptscs.Models;
     using Honememo.Wptscs.Properties;
+    using Honememo.Wptscs.Utilities;
+    using Honememo.Wptscs.Websites;
 
     /// <summary>
-    /// 翻訳支援処理を実装するための共通クラスです。
+    /// 翻訳支援処理を実装するための抽象クラスです。
     /// </summary>
     public abstract class Translator
     {
         #region private変数
 
         /// <summary>
-        /// 改行コード
+        /// 変換後テキスト
         /// </summary>
-        public static readonly string ENTER = "\r\n";
+        private string text = String.Empty;
 
         /// <summary>
-        /// ã\83­ã\82°ã\83¡ã\83\83ã\82»ã\83¼ã\82¸
+        /// ã\83­ã\82°ã\83\86ã\82­ã\82¹ã\83\88ç\94\9fæ\88\90ç\94¨ã\83­ã\82¬ã\83¼
         /// </summary>
-        private string log;
+        private Logger logger;
+
+        #endregion
+
+        #region コンストラクタ
 
         /// <summary>
-        /// 変換後テキスト
+        /// トランスレータインスタンスを生成する
         /// </summary>
-        private string text;
+        public Translator()
+        {
+            // ステータス管理については更新イベントを連鎖させる
+            this.Stopwatch = new Stopwatch();
+            this.Logger = new Logger();
+            this.StatusManager = new StatusManager<string>();
+            this.StatusManager.Changed += new EventHandler(
+                delegate
+                {
+                    if (this.StatusUpdated != null)
+                    {
+                        this.StatusUpdated(this, EventArgs.Empty);
+                    }
+                });
+        }
 
         #endregion
-        
+
         #region イベント
 
         /// <summary>
         /// ログ更新伝達イベント。
         /// </summary>
-        public event EventHandler LogUpdate;
+        public event EventHandler LogUpdated;
+
+        /// <summary>
+        /// 処理状態更新伝達イベント。
+        /// </summary>
+        public event EventHandler StatusUpdated;
 
         #endregion
 
-        #region プロパティ
+        #region 公開プロパティ
+
+        /// <summary>
+        /// 翻訳元言語のサイト。
+        /// </summary>
+        public Website From
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// 翻訳先言語のサイト。
+        /// </summary>
+        public Website To
+        {
+            get;
+            set;
+        }
 
         /// <summary>
         /// 言語間の項目の対訳表。
@@ -77,25 +122,34 @@ namespace Honememo.Wptscs.Logics
         /// </summary>
         public string Log
         {
-            // ※ 将来的には、ロジックでログメッセージを出すなんて形を止めて
-            //    データとして保持させてメッセージはビューで・・・としたいが、
-            //    手間を考えて当面はこの形のまま実装する。
             get
             {
-                return this.log;
+                return this.Logger.ToString();
             }
+        }
 
-            protected set
+        /// <summary>
+        /// 処理状態メッセージ。
+        /// </summary>
+        public string Status
+        {
+            get
             {
-                this.log = (value != null) ? value : String.Empty;
-                if (this.LogUpdate != null)
-                {
-                    this.LogUpdate(this, EventArgs.Empty);
-                }
+                // 内部的に実際に管理しているのはStatusManager
+                return StringUtils.DefaultString(this.StatusManager.Status);
             }
         }
 
         /// <summary>
+        /// 処理時間ストップウォッチ。
+        /// </summary>
+        public Stopwatch Stopwatch
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
         /// 変換後テキスト。
         /// </summary>
         public string Text
@@ -120,22 +174,43 @@ namespace Honememo.Wptscs.Logics
             set;
         }
 
+        #endregion
+
+        #region 実装支援用プロパティ
+
         /// <summary>
-        /// 翻訳元言語のサイト
+        /// ログテキスト生成用ロガー
         /// </summary>
-        public Website From
+        /// <exception cref="ArgumentNullException"><c>null</c>が指定された場合。</exception>
+        protected Logger Logger
         {
-            get;
-            set;
+            get
+            {
+                return this.logger;
+            }
+
+            set
+            {
+                // nullは不可。また、ロガー変更後はイベントを設定
+                this.logger = Validate.NotNull(value);
+                this.logger.LogUpdate += new EventHandler(
+                    delegate
+                    {
+                        if (this.LogUpdated != null)
+                        {
+                            this.LogUpdated(this, EventArgs.Empty);
+                        }
+                    });
+            }
         }
 
         /// <summary>
-        /// 翻訳先言語のサイト。
+        /// ステータス管理用オブジェクト。
         /// </summary>
-        public Website To
+        protected StatusManager<string> StatusManager
         {
             get;
-            set;
+            private set;
         }
 
         #endregion
@@ -143,15 +218,18 @@ namespace Honememo.Wptscs.Logics
         #region 静的メソッド
 
         /// <summary>
-        /// 翻訳支援処理のインスタンスを作成
+        /// 指定されたアプリケーション設定から翻訳支援処理のインスタンスを作成する
         /// </summary>
         /// <param name="config">アプリケーション設定。</param>
         /// <param name="from">翻訳元言語。</param>
         /// <param name="to">翻訳先言語。</param>
         /// <returns>生成したインスタンス。</returns>
+        /// <exception cref="NotImplementedException">
+        /// アプリケーション設定に指定されているトランスレータに引数無しのコンストラクタが存在しない場合。
+        /// </exception>
         /// <remarks>
         /// 設定は設定クラスより取得、無ければ一部自動生成する。
-        /// インスタンス生成失敗時は例外を投げる。
+        /// インスタンス生成失敗時は各種例外を投げる。
         /// </remarks>
         public static Translator Create(Config config, string from, string to)
         {
@@ -182,14 +260,15 @@ namespace Honememo.Wptscs.Logics
 
         #endregion
 
-        #region publicメソッド
+        #region 公開メソッド
 
         /// <summary>
         /// 翻訳支援処理実行。
         /// </summary>
         /// <param name="name">記事名。</param>
-        /// <returns><c>true</c> 処理成功</returns>
-        public virtual bool Run(string name)
+        /// <exception cref="ApplicationException">処理が中断された場合。中断の理由は<see cref="Logger"/>に出力される。</exception>
+        /// <exception cref="InvalidOperationException"><see cref="From"/>, <see cref="To"/>が設定されていない場合。</exception>
+        public void Run(string name)
         {
             // ※必須な情報が設定されていない場合、InvalidOperationExceptionを返す
             if (this.From == null || this.To == null)
@@ -197,152 +276,234 @@ namespace Honememo.Wptscs.Logics
                 throw new InvalidOperationException("From or To is null");
             }
 
-            // å¤\89æ\95°ã\82\92å\88\9dæ\9c\9få\8c\96
+            // å\88\9dæ\9c\9få\8c\96ã\82\84ã\82¹ã\83\88ã\83\83ã\83\97ã\82¦ã\82©ã\83\83ã\83\81ã\81®èµ·å\8b\95ã\81¨ã\81\84ã\81£ã\81\9få\89\8då\87¦ç\90\86ã\82\92å®\9fæ\96½
             this.Initialize();
-
-            // サーバー接続チェック
-            string host = new Uri(this.From.Location).Host;
-            if (!String.IsNullOrEmpty(host) && !Settings.Default.IgnoreError)
+            try
             {
-                if (!this.Ping(host))
+                // サーバー接続チェック
+                string host = new Uri(this.From.Location).Host;
+                if (!String.IsNullOrEmpty(host) && !Settings.Default.IgnoreError)
                 {
-                    return false;
+                    if (!this.Ping(host))
+                    {
+                        throw new ApplicationException("ping failed");
+                    }
                 }
-            }
 
-            // 翻訳支援処理実行部の本体を実行
-            // ※以降の処理は、継承クラスにて定義
-            return this.RunBody(name);
+                // ここまでの間に終了要求が出ているかを確認
+                this.ThrowExceptionIfCanceled();
+
+                // 翻訳支援処理実行部の本体を実行
+                // ※以降の処理は、継承クラスにて定義
+                this.RunBody(name);
+            }
+            finally
+            {
+                // 状態のクリアやストップウォッチの停止といった後処理を実施
+                this.Terminate();
+            }
         }
         
         #endregion
 
-        #region protectedメソッド
+        #region テンプレートメソッド
 
         /// <summary>
         /// 翻訳支援処理実行部の本体。
         /// </summary>
         /// <param name="name">記事名。</param>
-        /// <returns><c>true</c> 処理成功</returns>
+        /// <exception cref="ApplicationException">処理を中断する場合。中断の理由は<see cref="Logger"/>に出力する。</exception>
         /// <remarks>テンプレートメソッド的な構造になっています。</remarks>
-        protected abstract bool RunBody(string name);
+        protected abstract void RunBody(string name);
 
         /// <summary>
-        /// ログメッセージを1行追加出力
+        /// 翻訳支援処理実行時の初期化処理
         /// </summary>
-        /// <param name="log">ログメッセージ。</param>
-        protected void LogLine(string log)
+        /// <remarks>
+        /// <para>
+        /// <see cref="RunBody"/>実行前の初期化のタイミングでコールされる。
+        /// 本メソッドで例外が発生した場合、<see cref="RunBody"/>, <see cref="Terminate"/>はコールされない。
+        /// </para>
+        /// <para>
+        /// オーバーライド時は必ず親のメソッドもコールすること。
+        /// </para>
+        /// </remarks>
+        protected virtual void Initialize()
         {
-            // 直前のログが改行されていない場合、改行して出力
-            if (this.Log != String.Empty && this.Log.EndsWith(ENTER) == false)
-            {
-                this.Log += ENTER + log + ENTER;
-            }
-            else
-            {
-                this.Log += log + ENTER;
-            }
+            // ロガーや処理状態などを初期化、処理時間の測定を開始
+            this.Logger.Clear();
+            this.StatusManager.Clear();
+            this.Stopwatch.Reset();
+            this.Text = String.Empty;
+            this.CancellationPending = false;
+            this.From.WebProxy.Referer = null;
+            this.To.WebProxy.Referer = null;
+            this.Stopwatch.Start();
         }
 
         /// <summary>
-        /// ログメッセージを1行追加出力(入力された文字列を書式化して表示)
+        /// 翻訳支援処理実行時の終了処理
         /// </summary>
-        /// <param name="format">書式項目を含んだログメッセージ。</param>
-        /// <param name="args">書式設定対象オブジェクト配列。</param>
-        protected void LogLine(string format, params object[] args)
+        /// <remarks>
+        /// <para>
+        /// <see cref="RunBody"/>実行後の後処理のタイミングでコールされる。
+        /// ただし、サーバー接続チェックに失敗した場合などでは、
+        /// <see cref="RunBody"/>が実行されること無く本メソッドが呼ばれる場合もある。
+        /// </para>
+        /// <para>
+        /// オーバーライド時は必ず親のメソッドもコールすること。
+        /// </para>
+        /// </remarks>
+        protected virtual void Terminate()
         {
-            // オーバーロードメソッドをコール
-            this.LogLine(String.Format(format, args));
+            // 終了後は処理状態やRefererをクリア、処理時間を測定終了
+            this.StatusManager.Clear();
+            this.From.WebProxy.Referer = null;
+            this.To.WebProxy.Referer = null;
+            this.Stopwatch.Stop();
         }
 
+        #endregion
+
+        #region 実装支援用メソッド
+
         /// <summary>
-        /// ログメッセージを出力しつつページを取得
+        /// ログ出力によるエラー処理を含んだページ取得処理
         /// </summary>
         /// <param name="title">ページタイトル。</param>
-        /// <param name="notFoundMsg">取得できない場合に出力するメッセージ。</param>
-        /// <returns>取得したページ。ページが存在しない場合は <c>null</c> を返す。</returns>
-        /// <remarks>通信エラーなど例外が発生した場合は、別途エラーログを出力する。</remarks>
-        protected Page GetPage(string title, string notFoundMsg)
+        /// <param name="page">取得したページ。ページが存在しない場合は <c>null</c> を返す。</param>
+        /// <returns>処理が成功した(404も含む)場合<c>true</c>、失敗した(通信エラーなど)の場合<c>false</c>。</returns>
+        /// <exception cref="ApplicationException">
+        /// 想定外の例外が発生した場合でかつアプリケーション設定の<c>IgnoreError</c>が<c>false</c>の場合、
+        /// または<see cref="CancellationPending"/>が<c>true</c>の場合。
+        /// </exception>
+        /// <remarks>
+        /// 本メソッドは、大きく3パターンの動作を行う。
+        /// <list type="number">
+        /// <item><description>正常にページが取得できた → <c>true</c>でページを設定、ログ出力無し</description></item>
+        /// <item><description>404など想定内の例外でページが取得できなかった → <c>true</c>でページ無し、ログ出力無し</description></item>
+        /// <item><description>想定外の例外でページが取得できなかった → <c>false</c>でページ無し、ログ出力有り
+        ///                    or <see cref="ApplicationException"/>で処理中断(アプリケーション設定の<c>IgnoreError</c>による)。</description></item>
+        /// </list>
+        /// また、実行中は処理状態をサーバー接続中に更新する。
+        /// 実行前後には終了要求のチェックも行う。
+        /// </remarks>
+        protected bool TryGetPage(string title, out Page page)
         {
-            try
-            {
-                // 取得できた場合はここで終了
-                return this.From.GetPage(title);
-            }
-            catch (WebException e)
-            {
-                // 通信エラー
-                if (e.Status == WebExceptionStatus.ProtocolError
-                    && (e.Response as HttpWebResponse).StatusCode == HttpStatusCode.NotFound)
-                {
-                    // 404
-                    this.Log += notFoundMsg;
-                }
-                else
-                {
-                    // それ以外のエラー
-                    this.LogLine(Resources.RightArrow + " " + e.Message);
-                    if (e.Response != null)
-                    {
-                        this.LogLine(Resources.RightArrow + " " + String.Format(Resources.LogMessage_ErrorURL, e.Response.ResponseUri));
-                    }
-                }
-            }
-            catch (FileNotFoundException)
-            {
-                // ファイル無し
-                this.Log += notFoundMsg;
-            }
-            catch (Exception e)
+            // 通信開始の前に終了要求が出ているかを確認
+            this.ThrowExceptionIfCanceled();
+
+            // ページ取得処理、実行中は処理状態を変更
+            bool success;
+            using (var sm = this.StatusManager.Switch(Resources.StatusDownloading))
             {
-                // その他の想定外のエラー
-                this.LogLine(Resources.RightArrow + " " + e.Message);
+                success = this.TryGetPageBody(title, out page);
             }
 
-            // 取得失敗時いずれの場合もnull
-            return null;
+            // 通信終了後にも再度終了要求を確認
+            this.ThrowExceptionIfCanceled();
+            return success;
         }
 
-        #endregion
-
-        #region privateメソッド
-
         /// <summary>
-        /// ç¿»è¨³æ\94¯æ\8f´å\87¦ç\90\86å®\9fè¡\8cæ\99\82ã\81®å\88\9dæ\9c\9få\8c\96å\87¦ç\90\86
+        /// çµ\82äº\86è¦\81æ±\82ã\81\8cå\87ºã\81¦ã\81\84ã\82\8bå ´å\90\88ã\80\81ä¾\8bå¤\96ã\82\92æ\8a\95ã\81\92ã\82\8b
         /// </summary>
-        private void Initialize()
+        /// <exception cref="ApplicationException"><see cref="CancellationPending"/>が<c>true</c>の場合。</exception>
+        protected void ThrowExceptionIfCanceled()
         {
-            // 変数を初期化
-            this.log = String.Empty;
-            this.Text = String.Empty;
-            this.CancellationPending = false;
+            if (this.CancellationPending)
+            {
+                throw new ApplicationException("CancellationPending is true");
+            }
         }
 
+        #endregion
+
+        #region 内部処理用メソッド
+
         /// <summary>
         /// サーバー接続チェック。
         /// </summary>
         /// <param name="server">サーバー名。</param>
-        /// <returns><c>true</c> 接続成功。</returns>
+        /// <returns>接続成功の場合<c>true</c>。</returns>
+        /// <remarks>実行中は処理状態をサーバー接続中に更新する。</remarks>
         private bool Ping(string server)
         {
-            // サーバー接続チェック
-            Ping ping = new Ping();
-            try
+            // サーバー接続チェック、実行中は処理状態を変更
+            using (var sm = this.StatusManager.Switch(Resources.StatusPinging))
             {
-                PingReply reply = ping.Send(server);
-                if (reply.Status != IPStatus.Success)
+                // サーバー接続チェック
+                Ping ping = new Ping();
+                try
                 {
-                    this.LogLine(Resources.ErrorMessageConnectionFailed, reply.Status.ToString());
+                    PingReply reply = ping.Send(server);
+                    if (reply.Status != IPStatus.Success)
+                    {
+                        this.Logger.AddMessage(Resources.ErrorMessageConnectionFailed, reply.Status.ToString());
+                        return false;
+                    }
+                }
+                catch (Exception e)
+                {
+                    this.Logger.AddMessage(Resources.ErrorMessageConnectionFailed, e.InnerException.Message);
                     return false;
                 }
+
+                return true;
             }
-            catch (Exception e)
+        }
+
+        /// <summary>
+        /// ログ出力によるエラー処理を含んだページ取得処理本体。
+        /// </summary>
+        /// <param name="title">ページタイトル。</param>
+        /// <param name="page">取得したページ。ページが存在しない場合は<c>null</c>を返す。</param>
+        /// <returns>処理が成功した(404も含む)場合<c>true</c>、失敗した(通信エラーなど)の場合<c>false</c>。</returns>
+        /// <exception cref="ApplicationException">
+        /// 想定外の例外が発生した場合でかつアプリケーション設定の<c>IgnoreError</c>が<c>false</c>の場合。
+        /// </exception>
+        /// <remarks>
+        /// 本メソッドは、大きく3パターンの動作を行う。
+        /// <list type="number">
+        /// <item><description>正常にページが取得できた → <c>true</c>でページを設定、ログ出力無し</description></item>
+        /// <item><description>404など想定内の例外でページが取得できなかった → <c>true</c>でページ無し、ログ出力無し</description></item>
+        /// <item><description>想定外の例外でページが取得できなかった → <c>false</c>でページ無し、ログ出力有り
+        ///                    or <see cref="ApplicationException"/>で処理中断(アプリケーション設定の<c>IgnoreError</c>による)。</description></item>
+        /// </list>
+        /// </remarks>
+        private bool TryGetPageBody(string title, out Page page)
+        {
+            page = null;
+            try
+            {
+                // 普通に取得できた場合はここで終了
+                page = this.From.GetPage(title);
+                return true;
+            }
+            catch (FileNotFoundException)
             {
-                this.LogLine(Resources.ErrorMessageConnectionFailed, e.InnerException.Message);
+                // ページ無しによる例外も正常終了
+                return true;
+            }
+            catch (EndPeriodException)
+            {
+                // 末尾がピリオドで終わるページが処理できない既知の不具合への対応、警告メッセージを出す
+                this.Logger.AddResponse(Resources.LogMessageErrorPageName, title);
                 return false;
             }
+            catch (Exception e)
+            {
+                // その他例外の場合、まずエラー情報を出力
+                this.Logger.AddError(e);
 
-            return true;
+                // エラーを無視しない場合、ここで翻訳支援処理を中断する
+                if (!Settings.Default.IgnoreError)
+                {
+                    throw new ApplicationException(e.Message, e);
+                }
+
+                return false;
+            }
         }
 
         #endregion