OSDN Git Service

#30840 不要になっていたTemplate:Documentation絡みの処理も除去,
[wptscs/wpts.git] / Wptscs / Websites / MediaWiki.cs
1 // ================================================================================================
2 // <summary>
3 //      MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>
4 //
5 // <copyright file="MediaWiki.cs" company="honeplusのメモ帳">
6 //      Copyright (C) 2013 Honeplus. All rights reserved.</copyright>
7 // <author>
8 //      Honeplus</author>
9 // ================================================================================================
10
11 namespace Honememo.Wptscs.Websites
12 {
13     using System;
14     using System.Collections.Generic;
15     using System.IO;
16     using System.Linq;
17     using System.Xml;
18     using System.Xml.Linq;
19     using System.Xml.Serialization;
20     using Honememo.Models;
21     using Honememo.Utilities;
22     using Honememo.Wptscs.Models;
23     using Honememo.Wptscs.Properties;
24     using Honememo.Wptscs.Utilities;
25
26     /// <summary>
27     /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。
28     /// </summary>
29     public class MediaWiki : Website, IXmlSerializable
30     {
31         #region private変数
32         
33         /// <summary>
34         /// 名前空間情報取得用にアクセスするAPI。
35         /// </summary>
36         private string metaApi;
37
38         /// <summary>
39         /// MediaWiki記事データ取得用にアクセスするAPI。
40         /// </summary>
41         private string contentApi;
42
43         /// <summary>
44         /// MediaWiki言語間リンク取得用にアクセスするAPI。
45         /// </summary>
46         private string interlanguageApi;
47
48         /// <summary>
49         /// テンプレートの名前空間を示す番号。
50         /// </summary>
51         private int? templateNamespace;
52
53         /// <summary>
54         /// カテゴリの名前空間を示す番号。
55         /// </summary>
56         private int? categoryNamespace;
57
58         /// <summary>
59         /// 画像の名前空間を示す番号。
60         /// </summary>
61         private int? fileNamespace;
62
63         /// <summary>
64         /// MediaWiki書式のシステム定義変数。
65         /// </summary>
66         private ISet<string> magicWords;
67
68         /// <summary>
69         /// MediaWikiの名前空間の情報。
70         /// </summary>
71         private IDictionary<int, IgnoreCaseSet> namespaces;
72
73         /// <summary>
74         /// MediaWikiのウィキ間リンクのプレフィックス情報。
75         /// </summary>
76         private IgnoreCaseSet interwikiPrefixs;
77
78         /// <summary>
79         /// MediaWikiのウィキ間リンクのプレフィックス情報(APIから取得した値と設定値の集合)。
80         /// </summary>
81         private IgnoreCaseSet interwikiPrefixCaches;
82
83         /// <summary>
84         /// <see cref="InitializeByMetaApi"/>同期用ロックオブジェクト。
85         /// </summary>
86         private object lockLoadMetaApi = new object();
87
88         #endregion
89
90         #region コンストラクタ
91
92         /// <summary>
93         /// 指定された言語, サーバーのMediaWikiを表すインスタンスを作成。
94         /// </summary>
95         /// <param name="language">ウェブサイトの言語。</param>
96         /// <param name="location">ウェブサイトの場所。</param>
97         /// <exception cref="ArgumentNullException"><paramref name="language"/>または<paramref name="location"/>が<c>null</c>の場合。</exception>
98         /// <exception cref="ArgumentException"><paramref name="location"/>が空の文字列の場合。</exception>
99         public MediaWiki(Language language, string location)
100             : base(language, location)
101         {
102         }
103
104         /// <summary>
105         /// 指定された言語のWikipediaを表すインスタンスを作成。
106         /// </summary>
107         /// <param name="language">ウェブサイトの言語。</param>
108         /// <exception cref="ArgumentNullException"><c>null</c>が指定された場合。</exception>
109         public MediaWiki(Language language)
110         {
111             // 親で初期化していないのは、languageのnullチェックの前にnull参照でエラーになってしまうから
112             this.Language = language;
113             this.Location = string.Format(Settings.Default.WikipediaLocation, language.Code);
114         }
115
116         /// <summary>
117         /// 空のインスタンスを作成(シリアライズ or 拡張用)。
118         /// </summary>
119         protected MediaWiki()
120         {
121         }
122
123         #endregion
124
125         #region 設定ファイルに初期値を持つプロパティ
126         
127         /// <summary>
128         /// MediaWikiメタ情報取得用にアクセスするAPI。
129         /// </summary>
130         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
131         public string MetaApi
132         {
133             get
134             {
135                 if (string.IsNullOrEmpty(this.metaApi))
136                 {
137                     return Settings.Default.MediaWikiMetaApi;
138                 }
139
140                 return this.metaApi;
141             }
142
143             set
144             {
145                 this.metaApi = value;
146             }
147         }
148
149         /// <summary>
150         /// MediaWiki記事データ取得用にアクセスするAPI。
151         /// </summary>
152         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
153         public string ContentApi
154         {
155             get
156             {
157                 if (string.IsNullOrEmpty(this.contentApi))
158                 {
159                     return Settings.Default.MediaWikiContentApi;
160                 }
161
162                 return this.contentApi;
163             }
164
165             set
166             {
167                 this.contentApi = value;
168             }
169         }
170
171         /// <summary>
172         /// MediaWiki言語間リンク取得用にアクセスするAPI。
173         /// </summary>
174         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
175         public string InterlanguageApi
176         {
177             get
178             {
179                 if (string.IsNullOrEmpty(this.interlanguageApi))
180                 {
181                     return Settings.Default.MediaWikiInterlanguageApi;
182                 }
183
184                 return this.interlanguageApi;
185             }
186
187             set
188             {
189                 this.interlanguageApi = value;
190             }
191         }
192
193         /// <summary>
194         /// テンプレートの名前空間を示す番号。
195         /// </summary>
196         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
197         public int TemplateNamespace
198         {
199             get
200             {
201                 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;
202             }
203
204             set
205             {
206                 this.templateNamespace = value;
207             }
208         }
209
210         /// <summary>
211         /// カテゴリの名前空間を示す番号。
212         /// </summary>
213         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
214         public int CategoryNamespace
215         {
216             get
217             {
218                 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;
219             }
220
221             set
222             {
223                 this.categoryNamespace = value;
224             }
225         }
226
227         /// <summary>
228         /// 画像の名前空間を示す番号。
229         /// </summary>
230         /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
231         public int FileNamespace
232         {
233             get
234             {
235                 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;
236             }
237
238             set
239             {
240                 this.fileNamespace = value;
241             }
242         }
243
244         /// <summary>
245         /// MediaWiki書式のシステム定義変数。
246         /// </summary>
247         /// <remarks>
248         /// 値が指定されていない場合、デフォルト値を返す。
249         /// 大文字小文字を区別する。
250         /// </remarks>
251         public ISet<string> MagicWords
252         {
253             get
254             {
255                 if (this.magicWords == null)
256                 {
257                     // ※ 初期値は http://www.mediawiki.org/wiki/Help:Magic_words 等を参考に設定。
258                     //    APIからも取得できるが、2012年2月現在 #expr でなければ認識されないものが
259                     //    exprで返ってきたりとアプリで使うには情報が足りないため人力で対応。
260                     return new HashSet<string>(Settings.Default.MediaWikiMagicWords.Cast<string>());
261                 }
262
263                 return this.magicWords;
264             }
265
266             set
267             {
268                 this.magicWords = value;
269             }
270         }
271
272         #endregion
273
274         #region サーバーから値を取得するプロパティ
275
276         /// <summary>
277         /// MediaWikiの名前空間の情報。
278         /// </summary>
279         /// <remarks>
280         /// サーバーから情報を取得。大文字小文字を区別しない。
281         /// </remarks>
282         public IDictionary<int, IgnoreCaseSet> Namespaces
283         {
284             get
285             {
286                 // 値が設定されていない場合、サーバーから取得して初期化する
287                 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
288                 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
289                 if (this.namespaces != null)
290                 {
291                     return this.namespaces;
292                 }
293
294                 lock (this.lockLoadMetaApi)
295                 {
296                     if (this.namespaces != null)
297                     {
298                         return this.namespaces;
299                     }
300
301                     this.InitializeByMetaApi();
302                 }
303
304                 return this.namespaces;
305             }
306
307             protected set
308             {
309                 this.namespaces = value;
310             }
311         }
312
313         /// <summary>
314         /// MediaWikiのウィキ間リンクのプレフィックス情報。
315         /// </summary>
316         /// <remarks>
317         /// 値が設定されていない場合デフォルト値とサーバーから、
318         /// 設定されている場合その内容とサーバーから取得した情報を使用する。
319         /// 大文字小文字を区別しない。
320         /// </remarks>
321         public IgnoreCaseSet InterwikiPrefixs
322         {
323             get
324             {
325                 // 値が準備されていない場合、サーバーと設定ファイルから取得して初期化する
326                 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
327                 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
328                 if (this.interwikiPrefixCaches != null)
329                 {
330                     return this.interwikiPrefixCaches;
331                 }
332
333                 lock (this.lockLoadMetaApi)
334                 {
335                     if (this.interwikiPrefixCaches != null)
336                     {
337                         return this.interwikiPrefixCaches;
338                     }
339
340                     this.InitializeByMetaApi();
341                 }
342
343                 return this.interwikiPrefixCaches;
344             }
345
346             set
347             {
348                 // 値を代入しキャッシュを消去
349                 this.interwikiPrefixs = value;
350                 this.interwikiPrefixCaches = null;
351             }
352         }
353
354         #endregion
355
356         #region それ以外のプロパティ
357
358         /// <summary>
359         /// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
360         /// </summary>
361         /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
362         public string LinkInterwikiFormat
363         {
364             get;
365             set;
366         }
367
368         /// <summary>
369         /// Template:Langで書式化するためのフォーマット。
370         /// </summary>
371         /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
372         public string LangFormat
373         {
374             get;
375             set;
376         }
377
378         /// <summary>
379         /// 言語名の記事が存在するようなサイトか?
380         /// </summary>
381         /// <remarks>
382         /// そうした記事が存在する場合<c>true</c>。
383         /// Wikipediaには[[日本語]]といった記事が存在するが、Wikitravelであればそうした記事は存在し得ない。
384         /// 言語名をリンクにするかといった判断に用いる。
385         /// </remarks>
386         public bool HasLanguagePage
387         {
388             get;
389             set;
390         }
391
392         #endregion
393
394         #region 公開メソッド
395
396         /// <summary>
397         /// ページを取得。
398         /// </summary>
399         /// <param name="title">ページタイトル。</param>
400         /// <returns>取得したページ。</returns>
401         /// <exception cref="InvalidDataException">APIから取得したデータが想定外。</exception>
402         /// <exception cref="NullReferenceException">APIから取得したデータが想定外。</exception>
403         /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
404         /// <remarks>
405         /// ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。
406         /// このメソッドでは記事本文や日時といった情報は取得しない。
407         /// そうした情報は、実際にアクセスされたタイミングで動的に取得する。
408         /// </remarks>
409         public override Page GetPage(string title)
410         {
411             // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
412             // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
413             string escapeTitle = title;
414             if (new Uri(this.Location).IsFile)
415             {
416                 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
417             }
418
419             // URIを生成
420             Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.InterlanguageApi, escapeTitle));
421
422             // ページの言語間リンク情報XMLデータをMediaWikiサーバーから取得
423             XElement doc;
424             using (Stream reader = this.WebProxy.GetStream(uri))
425             {
426                 doc = XElement.Load(reader);
427             }
428
429             // クエリーエレメントを取得
430             // ※ エレメントは常に1件
431             XElement qe;
432             try
433             {
434                 qe = (from n in doc.Elements("query")
435                       select n).First();
436             }
437             catch (InvalidOperationException)
438             {
439                 throw new InvalidOperationException("parse failed : api/query element is not found");
440             }
441
442             // クエリーからページ情報を読み込み返す
443             // ※ ページが無い場合などは、例外が投げられる
444             return MediaWikiPage.GetFromQuery(this, uri, qe);
445         }
446
447         /// <summary>
448         /// ページを取得。
449         /// </summary>
450         /// <param name="title">ページタイトル。</param>
451         /// <returns>取得したページ。</returns>
452         /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
453         /// <exception cref="EndPeriodException">末尾がピリオドのページの場合(既知の不具合への対応)。</exception>
454         /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
455         public Page GetPageBodyAndTimestamp(string title)
456         {
457             // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
458             // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
459             string escapeTitle = title;
460             if (new Uri(this.Location).IsFile)
461             {
462                 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
463             }
464
465             // URIを生成
466             Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ContentApi, escapeTitle));
467
468             // ページのXMLデータをMediaWikiサーバーから取得
469             XElement doc;
470             using (Stream reader = this.WebProxy.GetStream(uri))
471             {
472                 doc = XElement.Load(reader);
473             }
474
475             // ページエレメントを取得
476             // ※ この問い合わせでは、ページが無い場合も要素自体は毎回ある模様
477             //    一件しか返らないはずなので先頭データを対象とする
478             XElement pe;
479             try
480             {
481                 pe = (from query in doc.Elements("query")
482                       from pages in query.Elements("pages")
483                       from n in pages.Elements("page")
484                       select n).First();
485             }
486             catch (InvalidOperationException)
487             {
488                 throw new InvalidOperationException("parse failed : query/pages/page element is not found");
489             }
490
491             // ページの解析
492             if (pe.Attribute("missing") != null)
493             {
494                 // missing属性が存在する場合、ページ無し
495                 throw new FileNotFoundException("page not found");
496             }
497
498             // ページ名、ページ本文、最終更新日時
499             var re = (from revisions in pe.Elements("revisions")
500                       from n in revisions.Elements("rev")
501                       select n).First();
502
503             // ページ情報を作成して返す
504             return new MediaWikiPage(
505                 this,
506                 XmlUtils.Value(pe.Attribute("title"), title),
507                 re.Value,
508                 new DateTime?(DateTime.Parse(re.Attribute("timestamp").Value)),
509                 uri);
510         }
511
512         /// <summary>
513         /// 指定された文字列がMediaWikiのシステム変数に相当かを判定。
514         /// </summary>
515         /// <param name="text">チェックする文字列。</param>
516         /// <returns>システム変数に相当する場合<c>true</c>。</returns>
517         /// <remarks>大文字小文字は区別する。</remarks>
518         public bool IsMagicWord(string text)
519         {
520             // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
521             string s = StringUtils.DefaultString(text);
522             foreach (string variable in this.MagicWords)
523             {
524                 if (s == variable || s.StartsWith(variable + ":"))
525                 {
526                     return true;
527                 }
528             }
529
530             return false;
531         }
532
533         /// <summary>
534         /// 指定されたリンク文字列がMediaWikiのウィキ間リンクかを判定。
535         /// </summary>
536         /// <param name="link">チェックするリンク文字列。</param>
537         /// <returns>ウィキ間リンクに該当する場合<c>true</c>。</returns>
538         /// <remarks>大文字小文字は区別しない。</remarks>
539         public bool IsInterwiki(string link)
540         {
541             // ※ ウィキ間リンクには入れ子もあるが、ここでは意識する必要はない
542             string s = StringUtils.DefaultString(link);
543
544             // 名前空間と被る場合はそちらが優先、ウィキ間リンクと判定しない
545             if (this.IsNamespace(link))
546             {
547                 return false;
548             }
549
550             // 文字列がいずれかのウィキ間リンクのプレフィックスで始まるか
551             int index = s.IndexOf(':');
552             if (index < 0)
553             {
554                 return false;
555             }
556
557             return this.InterwikiPrefixs.Contains(s.Remove(index));
558         }
559
560         /// <summary>
561         /// 指定されたリンク文字列がMediaWikiのいずれかの名前空間に属すかを判定。
562         /// </summary>
563         /// <param name="link">チェックするリンク文字列。</param>
564         /// <returns>いずれかの名前空間に該当する場合<c>true</c>。</returns>
565         /// <remarks>大文字小文字は区別しない。</remarks>
566         public bool IsNamespace(string link)
567         {
568             // 文字列がいずれかの名前空間のプレフィックスで始まるか
569             string s = StringUtils.DefaultString(link);
570             int index = s.IndexOf(':');
571             if (index < 0)
572             {
573                 return false;
574             }
575
576             string prefix = s.Remove(index);
577             foreach (IgnoreCaseSet prefixes in this.Namespaces.Values)
578             {
579                 if (prefixes.Contains(prefix))
580                 {
581                     return true;
582                 }
583             }
584
585             return false;
586         }
587
588         /// <summary>
589         /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
590         /// </summary>
591         /// <param name="title">記事名。</param>
592         /// <param name="lang">言語。</param>
593         /// <param name="langTitle">他言語版記事名。</param>
594         /// <param name="label">表示名。</param>
595         /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
596         public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
597         {
598             if (string.IsNullOrEmpty(this.LinkInterwikiFormat))
599             {
600                 return null;
601             }
602
603             return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
604         }
605
606         /// <summary>
607         /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
608         /// </summary>
609         /// <param name="lang">言語。</param>
610         /// <param name="text">文字列。</param>
611         /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
612         /// <remarks>
613         /// この<paramref name="lang"/>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
614         /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
615         /// </remarks>
616         public string FormatLang(string lang, string text)
617         {
618             if (string.IsNullOrEmpty(this.LangFormat))
619             {
620                 return null;
621             }
622
623             return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
624         }
625         
626         #endregion
627
628         #region XMLシリアライズ用メソッド
629
630         /// <summary>
631         /// シリアライズするXMLのスキーマ定義を返す。
632         /// </summary>
633         /// <returns>XML表現を記述する<see cref="System.Xml.Schema.XmlSchema"/>。</returns>
634         public System.Xml.Schema.XmlSchema GetSchema()
635         {
636             return null;
637         }
638
639         /// <summary>
640         /// XMLからオブジェクトをデシリアライズする。
641         /// </summary>
642         /// <param name="reader">デシリアライズ元の<see cref="XmlReader"/></param>
643         public void ReadXml(XmlReader reader)
644         {
645             XmlDocument xml = new XmlDocument();
646             xml.Load(reader);
647
648             // Webサイト
649             // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
650             XmlElement siteElement = xml.DocumentElement;
651             this.Location = siteElement.SelectSingleNode("Location").InnerText;
652
653             using (XmlReader r = XmlReader.Create(
654                 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
655             {
656                 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
657             }
658
659             this.MetaApi = XmlUtils.InnerText(siteElement.SelectSingleNode("MetaApi"));
660             this.ContentApi = XmlUtils.InnerText(siteElement.SelectSingleNode("ContentApi"));
661             this.InterlanguageApi = XmlUtils.InnerText(siteElement.SelectSingleNode("InterlanguageApi"));
662
663             int namespaceId;
664             if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace")), out namespaceId))
665             {
666                 this.TemplateNamespace = namespaceId;
667             }
668
669             if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace")), out namespaceId))
670             {
671                 this.CategoryNamespace = namespaceId;
672             }
673
674             if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace")), out namespaceId))
675             {
676                 this.FileNamespace = namespaceId;
677             }
678
679             // システム定義変数
680             ISet<string> variables = new HashSet<string>();
681             foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
682             {
683                 variables.Add(variableNode.InnerText);
684             }
685
686             if (variables.Count > 0)
687             {
688                 // 初期値の都合上、値がある場合のみ
689                 this.MagicWords = variables;
690             }
691
692             // ウィキ間リンク
693             IgnoreCaseSet prefixs = new IgnoreCaseSet();
694             foreach (XmlNode prefixNode in siteElement.SelectNodes("InterwikiPrefixs/Prefix"))
695             {
696                 prefixs.Add(prefixNode.InnerText);
697             }
698
699             if (prefixs.Count > 0)
700             {
701                 // 初期値の都合上、値がある場合のみ
702                 this.InterwikiPrefixs = prefixs;
703             }
704
705             this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
706             this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
707             bool hasLanguagePage;
708             if (bool.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("HasLanguagePage")), out hasLanguagePage))
709             {
710                 this.HasLanguagePage = hasLanguagePage;
711             }
712         }
713
714         /// <summary>
715         /// オブジェクトをXMLにシリアライズする。
716         /// </summary>
717         /// <param name="writer">シリアライズ先の<see cref="XmlWriter"/></param>
718         public void WriteXml(XmlWriter writer)
719         {
720             writer.WriteElementString("Location", this.Location);
721             new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
722
723             // MediaWiki固有の情報
724             // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
725             writer.WriteElementString("MetaApi", this.metaApi);
726             writer.WriteElementString("ContentApi", this.contentApi);
727             writer.WriteElementString("InterlanguageApi", this.interlanguageApi);
728             writer.WriteElementString(
729                 "TemplateNamespace",
730                 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : string.Empty);
731             writer.WriteElementString(
732                 "CategoryNamespace",
733                 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : string.Empty);
734             writer.WriteElementString(
735                 "FileNamespace",
736                 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : string.Empty);
737
738             // システム定義変数
739             writer.WriteStartElement("MagicWords");
740             if (this.magicWords != null)
741             {
742                 foreach (string variable in this.magicWords)
743                 {
744                     writer.WriteElementString("Variable", variable);
745                 }
746             }
747
748             // ウィキ間リンク
749             writer.WriteEndElement();
750             writer.WriteStartElement("InterwikiPrefixs");
751             if (this.interwikiPrefixs != null)
752             {
753                 foreach (string prefix in this.interwikiPrefixs)
754                 {
755                     writer.WriteElementString("Prefix", prefix);
756                 }
757             }
758
759             writer.WriteEndElement();
760             writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
761             writer.WriteElementString("LangFormat", this.LangFormat);
762             writer.WriteElementString("HasLanguagePage", this.HasLanguagePage.ToString());
763         }
764
765         #endregion
766
767         #region 内部処理用メソッド
768
769         /// <summary>
770         /// <see cref="MetaApi"/>を使用してサーバーからメタ情報を取得する。
771         /// </summary>
772         /// <exception cref="System.Net.WebException">通信エラー等が発生した場合。</exception>
773         /// <exception cref="InvalidDataException">APIから取得した情報が想定外のフォーマットの場合。</exception>
774         private void InitializeByMetaApi()
775         {
776             // APIのXMLデータをMediaWikiサーバーから取得
777             XmlDocument xml = new XmlDocument();
778             using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.MetaApi)))
779             {
780                 xml.Load(reader);
781             }
782
783             // ルートエレメントまで取得し、フォーマットをチェック
784             XmlElement rootElement = xml["api"];
785             if (rootElement == null)
786             {
787                 // XMLは取得できたが空 or フォーマットが想定外
788                 throw new InvalidDataException("parse failed : api element is not found");
789             }
790
791             // クエリーを取得
792             XmlElement queryElement = rootElement["query"];
793             if (queryElement == null)
794             {
795                 // フォーマットが想定外
796                 throw new InvalidDataException("parse failed : query element is not found");
797             }
798
799             // クエリー内のネームスペース・ネームスペースエイリアス、ウィキ間リンクを読み込み
800             this.namespaces = this.LoadNamespacesElement(queryElement);
801             this.interwikiPrefixCaches = this.LoadInterwikimapElement(queryElement);
802
803             // ウィキ間リンクは読み込んだ後に設定ファイルorプロパティの分をマージ
804             // ※ 設定ファイルの初期値は下記より作成。
805             //    http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/maintenance/interwiki.sql?view=markup
806             //    APIに加えて設定ファイルも持っているのは、2012年2月現在APIから返ってこない
807             //    項目(wikipediaとかcommonsとか)が存在するため。
808             this.interwikiPrefixCaches.UnionWith(
809                 this.interwikiPrefixs == null
810                 ? Settings.Default.MediaWikiInterwikiPrefixs.Cast<string>()
811                 : this.interwikiPrefixs);
812         }
813
814         /// <summary>
815         /// <see cref="MetaApi"/>から取得したXMLのうち、ネームスペースに関する部分を読み込む。
816         /// </summary>
817         /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
818         /// <returns>読み込んだネームスペース情報。</returns>
819         /// <exception cref="InvalidDataException">namespacesエレメントが存在しない場合。</exception>
820         private IDictionary<int, IgnoreCaseSet> LoadNamespacesElement(XmlElement queryElement)
821         {
822             // ネームスペースブロックを取得、ネームスペースブロックまでは必須
823             XmlElement namespacesElement = queryElement["namespaces"];
824             if (namespacesElement == null)
825             {
826                 // フォーマットが想定外
827                 throw new InvalidDataException("parse failed : namespaces element is not found");
828             }
829
830             // ネームスペースを取得
831             IDictionary<int, IgnoreCaseSet> namespaces = new Dictionary<int, IgnoreCaseSet>();
832             foreach (XmlNode node in namespacesElement.ChildNodes)
833             {
834                 XmlElement namespaceElement = node as XmlElement;
835                 if (namespaceElement != null)
836                 {
837                     try
838                     {
839                         int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
840                         IgnoreCaseSet values = new IgnoreCaseSet();
841                         values.Add(namespaceElement.InnerText);
842                         namespaces[id] = values;
843
844                         // あれば標準名も設定
845                         string canonical = namespaceElement.GetAttribute("canonical");
846                         if (!string.IsNullOrEmpty(canonical))
847                         {
848                             values.Add(canonical);
849                         }
850                     }
851                     catch (Exception e)
852                     {
853                         // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
854                         System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
855                     }
856                 }
857             }
858
859             // ネームスペースエイリアスブロックを取得、無い場合も想定
860             XmlElement aliasesElement = queryElement["namespacealiases"];
861             if (aliasesElement != null)
862             {
863                 // ネームスペースエイリアスを取得
864                 foreach (XmlNode node in aliasesElement.ChildNodes)
865                 {
866                     XmlElement namespaceElement = node as XmlElement;
867                     if (namespaceElement != null)
868                     {
869                         try
870                         {
871                             int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
872                             ISet<string> values = new HashSet<string>();
873                             if (namespaces.ContainsKey(id))
874                             {
875                                 values = namespaces[id];
876                             }
877
878                             values.Add(namespaceElement.InnerText);
879                         }
880                         catch (Exception e)
881                         {
882                             // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
883                             System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
884                         }
885                     }
886                 }
887             }
888
889             return namespaces;
890         }
891
892         /// <summary>
893         /// <see cref="MetaApi"/>から取得したXMLのうち、ウィキ間リンクに関する部分を読み込む。
894         /// </summary>
895         /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
896         /// <returns>読み込んだウィキ間リンク情報。</returns>
897         /// <exception cref="InvalidDataException">interwikimapエレメントが存在しない場合。</exception>
898         private IgnoreCaseSet LoadInterwikimapElement(XmlElement queryElement)
899         {
900             // ウィキ間リンクブロックを取得、ウィキ間リンクブロックまでは必須
901             XmlElement interwikimapElement = queryElement["interwikimap"];
902             if (interwikimapElement == null)
903             {
904                 // フォーマットが想定外
905                 throw new InvalidDataException("parse failed : interwikimap element is not found");
906             }
907
908             // ウィキ間リンクを取得
909             IgnoreCaseSet interwikiPrefixs = new IgnoreCaseSet();
910             foreach (XmlNode node in interwikimapElement.ChildNodes)
911             {
912                 XmlElement interwikiElement = node as XmlElement;
913                 if (interwikiElement != null)
914                 {
915                     string prefix = interwikiElement.GetAttribute("prefix");
916                     if (!string.IsNullOrWhiteSpace(prefix))
917                     {
918                         interwikiPrefixs.Add(prefix);
919                     }
920                 }
921             }
922
923             return interwikiPrefixs;
924         }
925
926         #endregion
927     }
928 }