OSDN Git Service

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