OSDN Git Service

Wikipedia翻訳支援ツール Ver1.01時点のソース
[wptscs/wpts.git] / Wptscs / Models / MediaWiki.cs
1 // ================================================================================================\r
2 // <summary>\r
3 //      MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>\r
4 //\r
5 // <copyright file="MediaWiki.cs" company="honeplusのメモ帳">\r
6 //      Copyright (C) 2011 Honeplus. All rights reserved.</copyright>\r
7 // <author>\r
8 //      Honeplus</author>\r
9 // ================================================================================================\r
10 \r
11 namespace Honememo.Wptscs.Models\r
12 {\r
13     using System;\r
14     using System.Collections.Generic;\r
15     using System.IO;\r
16     using System.Web;\r
17     using System.Xml;\r
18     using System.Xml.Serialization;\r
19     using Honememo.Utilities;\r
20     using Honememo.Wptscs.Properties;\r
21 \r
22     /// <summary>\r
23     /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。\r
24     /// </summary>\r
25     public class MediaWiki : Website, IXmlSerializable\r
26     {\r
27         #region private変数\r
28         \r
29         /// <summary>\r
30         /// 名前空間情報取得用にアクセスするAPI。\r
31         /// </summary>\r
32         private string namespacePath;\r
33 \r
34         /// <summary>\r
35         /// 記事のXMLデータが存在するパス。\r
36         /// </summary>\r
37         private string exportPath;\r
38 \r
39         /// <summary>\r
40         /// リダイレクトの文字列。\r
41         /// </summary>\r
42         private string redirect;\r
43 \r
44         /// <summary>\r
45         /// テンプレートの名前空間を示す番号。\r
46         /// </summary>\r
47         private int? templateNamespace;\r
48 \r
49         /// <summary>\r
50         /// カテゴリの名前空間を示す番号。\r
51         /// </summary>\r
52         private int? categoryNamespace;\r
53 \r
54         /// <summary>\r
55         /// 画像の名前空間を示す番号。\r
56         /// </summary>\r
57         private int? fileNamespace;\r
58 \r
59         /// <summary>\r
60         /// Wikipedia書式のシステム定義変数。\r
61         /// </summary>\r
62         /// <remarks>初期値は http://www.mediawiki.org/wiki/Help:Magic_words を参照</remarks>\r
63         private IList<string> magicWords;\r
64 \r
65         /// <summary>\r
66         /// MediaWikiの名前空間の情報。\r
67         /// </summary>\r
68         private IDictionary<int, IList<string>> namespaces = new Dictionary<int, IList<string>>();\r
69 \r
70         #endregion\r
71 \r
72         #region コンストラクタ\r
73 \r
74         /// <summary>\r
75         /// コンストラクタ(MediaWiki全般)。\r
76         /// </summary>\r
77         /// <param name="language">ウェブサイトの言語。</param>\r
78         /// <param name="location">ウェブサイトの場所。</param>\r
79         public MediaWiki(Language language, string location)\r
80         {\r
81             // メンバ変数の初期設定\r
82             this.Language = language;\r
83             this.Location = location;\r
84         }\r
85 \r
86         /// <summary>\r
87         /// コンストラクタ(Wikipedia用)。\r
88         /// </summary>\r
89         /// <param name="language">ウェブサイトの言語。</param>\r
90         public MediaWiki(Language language)\r
91         {\r
92             // メンバ変数の初期設定\r
93             // ※ オーバーロードメソッドを呼んでいないのは、languageがnullのときに先にエラーになるから\r
94             this.Language = language;\r
95             this.Location = String.Format(Settings.Default.WikipediaLocation, language.Code);\r
96         }\r
97 \r
98         /// <summary>\r
99         /// コンストラクタ(シリアライズ or 拡張用)。\r
100         /// </summary>\r
101         protected MediaWiki()\r
102         {\r
103         }\r
104 \r
105         #endregion\r
106 \r
107         #region 設定ファイルに初期値を持つプロパティ\r
108         \r
109         /// <summary>\r
110         /// MediaWiki名前空間情報取得用にアクセスするAPI。\r
111         /// </summary>\r
112         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
113         public string NamespacePath\r
114         {\r
115             get\r
116             {\r
117                 if (String.IsNullOrEmpty(this.namespacePath))\r
118                 {\r
119                     return Settings.Default.MediaWikiNamespacePath;\r
120                 }\r
121 \r
122                 return this.namespacePath;\r
123             }\r
124 \r
125             set\r
126             {\r
127                 this.namespacePath = value;\r
128             }\r
129         }\r
130 \r
131         /// <summary>\r
132         /// 記事のXMLデータが存在するパス。\r
133         /// </summary>\r
134         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
135         public string ExportPath\r
136         {\r
137             get\r
138             {\r
139                 if (String.IsNullOrEmpty(this.exportPath))\r
140                 {\r
141                     return Settings.Default.MediaWikiExportPath;\r
142                 }\r
143 \r
144                 return this.exportPath;\r
145             }\r
146 \r
147             set\r
148             {\r
149                 this.exportPath = value;\r
150             }\r
151         }\r
152 \r
153         /// <summary>\r
154         /// リダイレクトの文字列。\r
155         /// </summary>\r
156         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
157         public string Redirect\r
158         {\r
159             get\r
160             {\r
161                 if (String.IsNullOrEmpty(this.redirect))\r
162                 {\r
163                     return Settings.Default.MediaWikiRedirect;\r
164                 }\r
165 \r
166                 return this.redirect;\r
167             }\r
168 \r
169             set\r
170             {\r
171                 this.redirect = value;\r
172             }\r
173         }\r
174 \r
175         /// <summary>\r
176         /// テンプレートの名前空間を示す番号。\r
177         /// </summary>\r
178         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
179         public int TemplateNamespace\r
180         {\r
181             get\r
182             {\r
183                 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;\r
184             }\r
185 \r
186             set\r
187             {\r
188                 this.templateNamespace = value;\r
189             }\r
190         }\r
191 \r
192         /// <summary>\r
193         /// カテゴリの名前空間を示す番号。\r
194         /// </summary>\r
195         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
196         public int CategoryNamespace\r
197         {\r
198             get\r
199             {\r
200                 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;\r
201             }\r
202 \r
203             set\r
204             {\r
205                 this.categoryNamespace = value;\r
206             }\r
207         }\r
208 \r
209         /// <summary>\r
210         /// 画像の名前空間を示す番号。\r
211         /// </summary>\r
212         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
213         public int FileNamespace\r
214         {\r
215             get\r
216             {\r
217                 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;\r
218             }\r
219 \r
220             set\r
221             {\r
222                 this.fileNamespace = value;\r
223             }\r
224         }\r
225 \r
226         /// <summary>\r
227         /// Wikipedia書式のシステム定義変数。\r
228         /// </summary>\r
229         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>\r
230         public IList<string> MagicWords\r
231         {\r
232             get\r
233             {\r
234                 if (this.magicWords == null)\r
235                 {\r
236                     string[] w = new string[Settings.Default.MediaWikiMagicWords.Count];\r
237                     Settings.Default.MediaWikiMagicWords.CopyTo(w, 0);\r
238                     return w;\r
239                 }\r
240 \r
241                 return this.magicWords;\r
242             }\r
243 \r
244             set\r
245             {\r
246                 this.magicWords = value;\r
247             }\r
248         }\r
249 \r
250         #endregion\r
251 \r
252         #region それ以外のプロパティ\r
253 \r
254         /// <summary>\r
255         /// MediaWikiの名前空間の情報。\r
256         /// </summary>\r
257         /// <remarks>値が指定されていない場合、サーバーから情報を取得。</remarks>\r
258         public IDictionary<int, IList<string>> Namespaces\r
259         {\r
260             get\r
261             {\r
262                 lock (this.namespaces)\r
263                 {\r
264                     // 値が設定されていない場合、サーバーから取得して初期化する\r
265                     // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため\r
266                     // ※ MagicWordsがnullでこちらが空で若干条件が違うのは、あちらは設定ファイルに\r
267                     //    保存する設定だが、こちらは設定ファイルに保存しない基本的に読み込み用の設定だから。\r
268                     if (this.namespaces.Count > 0)\r
269                     {\r
270                         return this.namespaces;\r
271                     }\r
272 \r
273                     // APIのXMLデータをMediaWikiサーバーから取得\r
274                     XmlDocument xml = new XmlDocument();\r
275                     using (Stream reader = this.GetStream(new Uri(new Uri(this.Location), this.NamespacePath)))\r
276                     {\r
277                         xml.Load(reader);\r
278                     }\r
279 \r
280                     // ルートエレメントまで取得し、フォーマットをチェック\r
281                     XmlElement rootElement = xml["api"];\r
282                     if (rootElement == null)\r
283                     {\r
284                         // XMLは取得できたが空 or フォーマットが想定外\r
285                         throw new InvalidDataException("parse failed");\r
286                     }\r
287 \r
288                     // クエリーを取得\r
289                     XmlElement queryElement = rootElement["query"];\r
290                     if (queryElement == null)\r
291                     {\r
292                         // フォーマットが想定外\r
293                         throw new InvalidDataException("parse failed");\r
294                     }\r
295 \r
296                     // ネームスペースブロックを取得、ネームスペースブロックまでは必須\r
297                     XmlElement namespacesElement = queryElement["namespaces"];\r
298                     if (namespacesElement == null)\r
299                     {\r
300                         // フォーマットが想定外\r
301                         throw new InvalidDataException("parse failed");\r
302                     }\r
303 \r
304                     // ネームスペースを取得\r
305                     foreach (XmlNode node in namespacesElement.ChildNodes)\r
306                     {\r
307                         XmlElement namespaceElement = node as XmlElement;\r
308                         if (namespaceElement != null)\r
309                         {\r
310                             try\r
311                             {\r
312                                 int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));\r
313                                 IList<string> values = new List<string>();\r
314                                 values.Add(namespaceElement.InnerText);\r
315                                 this.namespaces[id] = values;\r
316 \r
317                                 // あればシステム名?も設定\r
318                                 string canonical = namespaceElement.GetAttribute("canonical");\r
319                                 if (!String.IsNullOrEmpty(canonical))\r
320                                 {\r
321                                     values.Add(canonical);\r
322                                 }\r
323                             }\r
324                             catch (Exception e)\r
325                             {\r
326                                 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため\r
327                                 System.Diagnostics.Debug.WriteLine("MediaWiki.Namespaces > 例外発生 : " + e);\r
328                             }\r
329                         }\r
330                     }\r
331 \r
332                     // ネームスペースエイリアスブロックを取得、無い場合も想定\r
333                     XmlElement aliasesElement = queryElement["namespacealiases"];\r
334                     if (aliasesElement != null)\r
335                     {\r
336                         // ネームスペースエイリアスを取得\r
337                         foreach (XmlNode node in aliasesElement.ChildNodes)\r
338                         {\r
339                             XmlElement namespaceElement = node as XmlElement;\r
340                             if (namespaceElement != null)\r
341                             {\r
342                                 try\r
343                                 {\r
344                                     int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));\r
345                                     IList<string> values = new List<string>();\r
346                                     if (this.namespaces.ContainsKey(id))\r
347                                     {\r
348                                         values = this.namespaces[id];\r
349                                     }\r
350 \r
351                                     values.Add(namespaceElement.InnerText);\r
352                                 }\r
353                                 catch (Exception e)\r
354                                 {\r
355                                     // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため\r
356                                     System.Diagnostics.Debug.WriteLine("MediaWiki.Namespaces > 例外発生 : " + e);\r
357                                 }\r
358                             }\r
359                         }\r
360                     }\r
361                 }\r
362 \r
363                 return this.namespaces;\r
364             }\r
365 \r
366             set\r
367             {\r
368                 // ※必須な情報が設定されていない場合、ArgumentNullExceptionを返す\r
369                 if (value == null)\r
370                 {\r
371                     throw new ArgumentNullException("namespaces");\r
372                 }\r
373 \r
374                 this.namespaces = value;\r
375             }\r
376         }\r
377 \r
378         /// <summary>\r
379         /// Template:Documentation(言語間リンク等を別ページに記述するためのテンプレート)に相当するページ名。\r
380         /// </summary>\r
381         /// <remarks>空の場合、その言語版にはこれに相当する機能は無いものとして扱う。</remarks>\r
382         public string DocumentationTemplate\r
383         {\r
384             get;\r
385             set;\r
386         }\r
387 \r
388         /// <summary>\r
389         /// Template:Documentationで指定が無い場合に参照するページ名。\r
390         /// </summary>\r
391         /// <remarks>\r
392         /// ほとんどの言語では[[/Doc]]の模様。\r
393         /// 空の場合、明示的な指定が無い場合は参照不能として扱う。\r
394         /// </remarks>\r
395         public string DocumentationTemplateDefaultPage\r
396         {\r
397             get;\r
398             set;\r
399         }\r
400 \r
401         #endregion\r
402 \r
403         #region 公開メソッド\r
404 \r
405         /// <summary>\r
406         /// ページを取得。\r
407         /// </summary>\r
408         /// <param name="title">ページタイトル。</param>\r
409         /// <returns>取得したページ。</returns>\r
410         /// <remarks>取得できない場合(通信エラーなど)は例外を投げる。</remarks>\r
411         public override Page GetPage(string title)\r
412         {\r
413             // &amp; &nbsp; 等の特殊文字をデコード\r
414             // ※ 本当は呼び元側ですべき処理の気がするが、現状手ごろな場所が無いので\r
415             string decodeTitle = HttpUtility.HtmlDecode(title);\r
416 \r
417             // fileスキームの場合、記事名からファイルに使えない文字をエスケープ\r
418             // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので\r
419             string escapeTitle = decodeTitle;\r
420             if (new Uri(this.Location).Scheme == "file")\r
421             {\r
422                 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);\r
423             }\r
424 \r
425             // ページのXMLデータをMediaWikiサーバーから取得\r
426             XmlDocument xml = new XmlDocument();\r
427             using (Stream reader = this.GetStream(\r
428                 new Uri(new Uri(this.Location), String.Format(this.ExportPath, escapeTitle))))\r
429             {\r
430                 xml.Load(reader);\r
431             }\r
432 \r
433             // ルートエレメントまで取得し、フォーマットをチェック\r
434             XmlElement rootElement = xml["mediawiki"];\r
435             if (rootElement == null)\r
436             {\r
437                 // XMLは取得できたが空 or フォーマットが想定外\r
438                 throw new InvalidDataException("parse failed");\r
439             }\r
440 \r
441             // ページの解析\r
442             XmlElement pageElement = rootElement["page"];\r
443             if (pageElement == null)\r
444             {\r
445                 // ページ無し\r
446                 throw new FileNotFoundException("page not found");\r
447             }\r
448 \r
449             // ページ名、ページ本文、最終更新日時\r
450             // ※ 一応、各項目が無くても動作するようにする\r
451             string pageTitle = XmlUtils.InnerText(pageElement["title"], title);\r
452             string text = null;\r
453             DateTime? time = null;\r
454             XmlElement revisionElement = pageElement["revision"];\r
455             if (revisionElement != null)\r
456             {\r
457                 text = XmlUtils.InnerText(revisionElement["text"], null);\r
458                 XmlElement timeElement = revisionElement["timestamp"];\r
459                 if (timeElement != null)\r
460                 {\r
461                     time = new DateTime?(DateTime.Parse(timeElement.InnerText));\r
462                 }\r
463             }\r
464 \r
465             // ページ情報を作成して返す\r
466             return new MediaWikiPage(this, pageTitle, text, time);\r
467         }\r
468 \r
469         /// <summary>\r
470         /// 指定された文字列がWikipediaのシステム変数に相当かを判定。\r
471         /// </summary>\r
472         /// <param name="text">チェックする文字列。</param>\r
473         /// <returns><c>true</c> システム変数に相当。</returns>\r
474         public bool IsMagicWord(string text)\r
475         {\r
476             string s = text != null ? text : String.Empty;\r
477 \r
478             // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある\r
479             foreach (string variable in this.MagicWords)\r
480             {\r
481                 if (s == variable || s.StartsWith(variable + ":"))\r
482                 {\r
483                     return true;\r
484                 }\r
485             }\r
486 \r
487             return false;\r
488         }\r
489         \r
490         #endregion\r
491 \r
492         #region XMLシリアライズ用メソッド\r
493 \r
494         /// <summary>\r
495         /// シリアライズするXMLのスキーマ定義を返す。\r
496         /// </summary>\r
497         /// <returns>XML表現を記述するXmlSchema。</returns>\r
498         public System.Xml.Schema.XmlSchema GetSchema()\r
499         {\r
500             return null;\r
501         }\r
502 \r
503         /// <summary>\r
504         /// XMLからオブジェクトをデシリアライズする。\r
505         /// </summary>\r
506         /// <param name="reader">デシリアライズ元のXmlReader</param>\r
507         public void ReadXml(XmlReader reader)\r
508         {\r
509             XmlDocument xml = new XmlDocument();\r
510             xml.Load(reader);\r
511 \r
512             // Webサイト\r
513             // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす\r
514             XmlElement siteElement = xml.DocumentElement;\r
515             this.Location = siteElement.SelectSingleNode("Location").InnerText;\r
516 \r
517             using (XmlReader r = XmlReader.Create(\r
518                 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))\r
519             {\r
520                 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;\r
521             }\r
522 \r
523             this.NamespacePath = XmlUtils.InnerText(siteElement.SelectSingleNode("NamespacePath"));\r
524             this.ExportPath = XmlUtils.InnerText(siteElement.SelectSingleNode("ExportPath"));\r
525             this.Redirect = XmlUtils.InnerText(siteElement.SelectSingleNode("Redirect"));\r
526 \r
527             string text = XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace"));\r
528             if (!String.IsNullOrEmpty(text))\r
529             {\r
530                 this.TemplateNamespace = int.Parse(text);\r
531             }\r
532 \r
533             text = XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace"));\r
534             if (!String.IsNullOrEmpty(text))\r
535             {\r
536                 this.CategoryNamespace = int.Parse(text);\r
537             }\r
538 \r
539             text = XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace"));\r
540             if (!String.IsNullOrEmpty(text))\r
541             {\r
542                 this.FileNamespace = int.Parse(text);\r
543             }\r
544 \r
545             // システム定義変数\r
546             IList<string> variables = new List<string>();\r
547             foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))\r
548             {\r
549                 variables.Add(variableNode.InnerText);\r
550             }\r
551 \r
552             if (variables.Count > 0)\r
553             {\r
554                 // 初期値の都合上、値がある場合のみ\r
555                 this.MagicWords = variables;\r
556             }\r
557 \r
558             // Template:Documentationの設定\r
559             XmlElement docElement = siteElement.SelectSingleNode("DocumentationTemplate") as XmlElement;\r
560             if (docElement != null)\r
561             {\r
562                 this.DocumentationTemplate = docElement.InnerText;\r
563                 this.DocumentationTemplateDefaultPage = docElement.GetAttribute("DefaultPage");\r
564             }\r
565         }\r
566 \r
567         /// <summary>\r
568         /// オブジェクトをXMLにシリアライズする。\r
569         /// </summary>\r
570         /// <param name="writer">シリアライズ先のXmlWriter</param>\r
571         public void WriteXml(XmlWriter writer)\r
572         {\r
573             writer.WriteElementString("Location", this.Location);\r
574             new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);\r
575 \r
576             // MediaWiki固有の情報\r
577             // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力\r
578             writer.WriteElementString("NamespacePath", this.namespacePath);\r
579             writer.WriteElementString("ExportPath", this.exportPath);\r
580             writer.WriteElementString("Redirect", this.redirect);\r
581             writer.WriteElementString(\r
582                 "TemplateNamespace",\r
583                 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : String.Empty);\r
584             writer.WriteElementString(\r
585                 "CategoryNamespace",\r
586                 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : String.Empty);\r
587             writer.WriteElementString(\r
588                 "FileNamespace",\r
589                 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : String.Empty);\r
590 \r
591             // システム定義変数\r
592             writer.WriteStartElement("MagicWords");\r
593             if (this.magicWords != null)\r
594             {\r
595                 foreach (string variable in this.magicWords)\r
596                 {\r
597                     writer.WriteElementString("Variable", variable);\r
598                 }\r
599             }\r
600 \r
601             writer.WriteEndElement();\r
602 \r
603             // Template:Documentationの設定は一項目で出力\r
604             if (!String.IsNullOrEmpty(this.DocumentationTemplate))\r
605             {\r
606                 writer.WriteStartElement("DocumentationTemplate");\r
607                 writer.WriteAttributeString("DefaultPage", this.DocumentationTemplateDefaultPage);\r
608                 writer.WriteValue(this.DocumentationTemplate);\r
609                 writer.WriteEndElement();\r
610             }\r
611         }\r
612 \r
613         #endregion\r
614     }\r
615 }\r