OSDN Git Service

#30840 画面のデザインが崩れていたのを修正,
[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="InvalidDataException">APIから取得したデータが想定外。</exception>
453         /// <exception cref="NullReferenceException">APIから取得したデータが想定外。</exception>
454         /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
455         /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
456         public Page GetPageBodyAndTimestamp(string title)
457         {
458             // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
459             // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
460             string escapeTitle = title;
461             if (new Uri(this.Location).IsFile)
462             {
463                 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
464             }
465
466             // URIを生成
467             Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ContentApi, escapeTitle));
468
469             // ページのXMLデータをMediaWikiサーバーから取得
470             XElement doc;
471             using (Stream reader = this.WebProxy.GetStream(uri))
472             {
473                 doc = XElement.Load(reader);
474             }
475
476             // ページエレメントを取得
477             // ※ この問い合わせでは、ページが無い場合も要素自体は毎回ある模様
478             //    一件しか返らないはずなので先頭データを対象とする
479             XElement pe;
480             try
481             {
482                 pe = (from query in doc.Elements("query")
483                       from pages in query.Elements("pages")
484                       from n in pages.Elements("page")
485                       select n).First();
486             }
487             catch (InvalidOperationException)
488             {
489                 throw new InvalidOperationException("parse failed : query/pages/page element is not found");
490             }
491
492             // ページの解析
493             if (pe.Attribute("missing") != null)
494             {
495                 // missing属性が存在する場合、ページ無し
496                 throw new FileNotFoundException("page not found");
497             }
498
499             // ページ名、ページ本文、最終更新日時
500             var re = (from revisions in pe.Elements("revisions")
501                       from n in revisions.Elements("rev")
502                       select n).First();
503
504             // ページ情報を作成して返す
505             return new MediaWikiPage(
506                 this,
507                 XmlUtils.Value(pe.Attribute("title"), title),
508                 re.Value,
509                 new DateTime?(DateTime.Parse(re.Attribute("timestamp").Value)),
510                 uri);
511         }
512
513         /// <summary>
514         /// 指定された文字列がMediaWikiのシステム変数に相当かを判定。
515         /// </summary>
516         /// <param name="text">チェックする文字列。</param>
517         /// <returns>システム変数に相当する場合<c>true</c>。</returns>
518         /// <remarks>大文字小文字は区別する。</remarks>
519         public bool IsMagicWord(string text)
520         {
521             // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
522             string s = StringUtils.DefaultString(text);
523             foreach (string variable in this.MagicWords)
524             {
525                 if (s == variable || s.StartsWith(variable + ":"))
526                 {
527                     return true;
528                 }
529             }
530
531             return false;
532         }
533
534         /// <summary>
535         /// 指定されたリンク文字列がMediaWikiのウィキ間リンクかを判定。
536         /// </summary>
537         /// <param name="link">チェックするリンク文字列。</param>
538         /// <returns>ウィキ間リンクに該当する場合<c>true</c>。</returns>
539         /// <remarks>大文字小文字は区別しない。</remarks>
540         public bool IsInterwiki(string link)
541         {
542             // ※ ウィキ間リンクには入れ子もあるが、ここでは意識する必要はない
543             string s = StringUtils.DefaultString(link);
544
545             // 名前空間と被る場合はそちらが優先、ウィキ間リンクと判定しない
546             if (this.IsNamespace(link))
547             {
548                 return false;
549             }
550
551             // 文字列がいずれかのウィキ間リンクのプレフィックスで始まるか
552             int index = s.IndexOf(':');
553             if (index < 0)
554             {
555                 return false;
556             }
557
558             return this.InterwikiPrefixs.Contains(s.Remove(index));
559         }
560
561         /// <summary>
562         /// 指定されたリンク文字列がMediaWikiのいずれかの名前空間に属すかを判定。
563         /// </summary>
564         /// <param name="link">チェックするリンク文字列。</param>
565         /// <returns>いずれかの名前空間に該当する場合<c>true</c>。</returns>
566         /// <remarks>大文字小文字は区別しない。</remarks>
567         public bool IsNamespace(string link)
568         {
569             // 文字列がいずれかの名前空間のプレフィックスで始まるか
570             string s = StringUtils.DefaultString(link);
571             int index = s.IndexOf(':');
572             if (index < 0)
573             {
574                 return false;
575             }
576
577             string prefix = s.Remove(index);
578             foreach (IgnoreCaseSet prefixes in this.Namespaces.Values)
579             {
580                 if (prefixes.Contains(prefix))
581                 {
582                     return true;
583                 }
584             }
585
586             return false;
587         }
588
589         /// <summary>
590         /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
591         /// </summary>
592         /// <param name="title">記事名。</param>
593         /// <param name="lang">言語。</param>
594         /// <param name="langTitle">他言語版記事名。</param>
595         /// <param name="label">表示名。</param>
596         /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
597         public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
598         {
599             if (string.IsNullOrEmpty(this.LinkInterwikiFormat))
600             {
601                 return null;
602             }
603
604             return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
605         }
606
607         /// <summary>
608         /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
609         /// </summary>
610         /// <param name="lang">言語。</param>
611         /// <param name="text">文字列。</param>
612         /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
613         /// <remarks>
614         /// この<paramref name="lang"/>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
615         /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
616         /// </remarks>
617         public string FormatLang(string lang, string text)
618         {
619             if (string.IsNullOrEmpty(this.LangFormat))
620             {
621                 return null;
622             }
623
624             return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
625         }
626         
627         #endregion
628
629         #region XMLシリアライズ用メソッド
630
631         /// <summary>
632         /// シリアライズするXMLのスキーマ定義を返す。
633         /// </summary>
634         /// <returns>XML表現を記述する<see cref="System.Xml.Schema.XmlSchema"/>。</returns>
635         public System.Xml.Schema.XmlSchema GetSchema()
636         {
637             return null;
638         }
639
640         /// <summary>
641         /// XMLからオブジェクトをデシリアライズする。
642         /// </summary>
643         /// <param name="reader">デシリアライズ元の<see cref="XmlReader"/></param>
644         public void ReadXml(XmlReader reader)
645         {
646             XmlDocument xml = new XmlDocument();
647             xml.Load(reader);
648
649             // Webサイト
650             // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
651             XmlElement siteElement = xml.DocumentElement;
652             this.Location = siteElement.SelectSingleNode("Location").InnerText;
653
654             using (XmlReader r = XmlReader.Create(
655                 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
656             {
657                 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
658             }
659
660             this.MetaApi = XmlUtils.InnerText(siteElement.SelectSingleNode("MetaApi"));
661             this.ContentApi = XmlUtils.InnerText(siteElement.SelectSingleNode("ContentApi"));
662             this.InterlanguageApi = XmlUtils.InnerText(siteElement.SelectSingleNode("InterlanguageApi"));
663
664             int namespaceId;
665             if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace")), out namespaceId))
666             {
667                 this.TemplateNamespace = namespaceId;
668             }
669
670             if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace")), out namespaceId))
671             {
672                 this.CategoryNamespace = namespaceId;
673             }
674
675             if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace")), out namespaceId))
676             {
677                 this.FileNamespace = namespaceId;
678             }
679
680             // システム定義変数
681             ISet<string> variables = new HashSet<string>();
682             foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
683             {
684                 variables.Add(variableNode.InnerText);
685             }
686
687             if (variables.Count > 0)
688             {
689                 // 初期値の都合上、値がある場合のみ
690                 this.MagicWords = variables;
691             }
692
693             // ウィキ間リンク
694             IgnoreCaseSet prefixs = new IgnoreCaseSet();
695             foreach (XmlNode prefixNode in siteElement.SelectNodes("InterwikiPrefixs/Prefix"))
696             {
697                 prefixs.Add(prefixNode.InnerText);
698             }
699
700             if (prefixs.Count > 0)
701             {
702                 // 初期値の都合上、値がある場合のみ
703                 this.InterwikiPrefixs = prefixs;
704             }
705
706             this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
707             this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
708             bool hasLanguagePage;
709             if (bool.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("HasLanguagePage")), out hasLanguagePage))
710             {
711                 this.HasLanguagePage = hasLanguagePage;
712             }
713         }
714
715         /// <summary>
716         /// オブジェクトをXMLにシリアライズする。
717         /// </summary>
718         /// <param name="writer">シリアライズ先の<see cref="XmlWriter"/></param>
719         public void WriteXml(XmlWriter writer)
720         {
721             writer.WriteElementString("Location", this.Location);
722             new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
723
724             // MediaWiki固有の情報
725             // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
726             writer.WriteElementString("MetaApi", this.metaApi);
727             writer.WriteElementString("ContentApi", this.contentApi);
728             writer.WriteElementString("InterlanguageApi", this.interlanguageApi);
729             writer.WriteElementString(
730                 "TemplateNamespace",
731                 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : string.Empty);
732             writer.WriteElementString(
733                 "CategoryNamespace",
734                 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : string.Empty);
735             writer.WriteElementString(
736                 "FileNamespace",
737                 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : string.Empty);
738
739             // システム定義変数
740             writer.WriteStartElement("MagicWords");
741             if (this.magicWords != null)
742             {
743                 foreach (string variable in this.magicWords)
744                 {
745                     writer.WriteElementString("Variable", variable);
746                 }
747             }
748
749             // ウィキ間リンク
750             writer.WriteEndElement();
751             writer.WriteStartElement("InterwikiPrefixs");
752             if (this.interwikiPrefixs != null)
753             {
754                 foreach (string prefix in this.interwikiPrefixs)
755                 {
756                     writer.WriteElementString("Prefix", prefix);
757                 }
758             }
759
760             writer.WriteEndElement();
761             writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
762             writer.WriteElementString("LangFormat", this.LangFormat);
763             writer.WriteElementString("HasLanguagePage", this.HasLanguagePage.ToString());
764         }
765
766         #endregion
767
768         #region 内部処理用メソッド
769
770         /// <summary>
771         /// <see cref="MetaApi"/>を使用してサーバーからメタ情報を取得する。
772         /// </summary>
773         /// <exception cref="System.Net.WebException">通信エラー等が発生した場合。</exception>
774         /// <exception cref="InvalidDataException">APIから取得した情報が想定外のフォーマットの場合。</exception>
775         private void InitializeByMetaApi()
776         {
777             // APIのXMLデータをMediaWikiサーバーから取得
778             XmlDocument xml = new XmlDocument();
779             using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.MetaApi)))
780             {
781                 xml.Load(reader);
782             }
783
784             // ルートエレメントまで取得し、フォーマットをチェック
785             XmlElement rootElement = xml["api"];
786             if (rootElement == null)
787             {
788                 // XMLは取得できたが空 or フォーマットが想定外
789                 throw new InvalidDataException("parse failed : api element is not found");
790             }
791
792             // クエリーを取得
793             XmlElement queryElement = rootElement["query"];
794             if (queryElement == null)
795             {
796                 // フォーマットが想定外
797                 throw new InvalidDataException("parse failed : query element is not found");
798             }
799
800             // クエリー内のネームスペース・ネームスペースエイリアス、ウィキ間リンクを読み込み
801             this.namespaces = this.LoadNamespacesElement(queryElement);
802             this.interwikiPrefixCaches = this.LoadInterwikimapElement(queryElement);
803
804             // ウィキ間リンクは読み込んだ後に設定ファイルorプロパティの分をマージ
805             // ※ 設定ファイルの初期値は下記より作成。
806             //    http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/maintenance/interwiki.sql?view=markup
807             //    APIに加えて設定ファイルも持っているのは、2012年2月現在APIから返ってこない
808             //    項目(wikipediaとかcommonsとか)が存在するため。
809             this.interwikiPrefixCaches.UnionWith(
810                 this.interwikiPrefixs == null
811                 ? Settings.Default.MediaWikiInterwikiPrefixs.Cast<string>()
812                 : this.interwikiPrefixs);
813         }
814
815         /// <summary>
816         /// <see cref="MetaApi"/>から取得したXMLのうち、ネームスペースに関する部分を読み込む。
817         /// </summary>
818         /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
819         /// <returns>読み込んだネームスペース情報。</returns>
820         /// <exception cref="InvalidDataException">namespacesエレメントが存在しない場合。</exception>
821         private IDictionary<int, IgnoreCaseSet> LoadNamespacesElement(XmlElement queryElement)
822         {
823             // ネームスペースブロックを取得、ネームスペースブロックまでは必須
824             XmlElement namespacesElement = queryElement["namespaces"];
825             if (namespacesElement == null)
826             {
827                 // フォーマットが想定外
828                 throw new InvalidDataException("parse failed : namespaces element is not found");
829             }
830
831             // ネームスペースを取得
832             IDictionary<int, IgnoreCaseSet> namespaces = new Dictionary<int, IgnoreCaseSet>();
833             foreach (XmlNode node in namespacesElement.ChildNodes)
834             {
835                 XmlElement namespaceElement = node as XmlElement;
836                 if (namespaceElement != null)
837                 {
838                     try
839                     {
840                         int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
841                         IgnoreCaseSet values = new IgnoreCaseSet();
842                         values.Add(namespaceElement.InnerText);
843                         namespaces[id] = values;
844
845                         // あれば標準名も設定
846                         string canonical = namespaceElement.GetAttribute("canonical");
847                         if (!string.IsNullOrEmpty(canonical))
848                         {
849                             values.Add(canonical);
850                         }
851                     }
852                     catch (Exception e)
853                     {
854                         // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
855                         System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
856                     }
857                 }
858             }
859
860             // ネームスペースエイリアスブロックを取得、無い場合も想定
861             XmlElement aliasesElement = queryElement["namespacealiases"];
862             if (aliasesElement != null)
863             {
864                 // ネームスペースエイリアスを取得
865                 foreach (XmlNode node in aliasesElement.ChildNodes)
866                 {
867                     XmlElement namespaceElement = node as XmlElement;
868                     if (namespaceElement != null)
869                     {
870                         try
871                         {
872                             int id = decimal.ToInt16(decimal.Parse(namespaceElement.GetAttribute("id")));
873                             ISet<string> values = new HashSet<string>();
874                             if (namespaces.ContainsKey(id))
875                             {
876                                 values = namespaces[id];
877                             }
878
879                             values.Add(namespaceElement.InnerText);
880                         }
881                         catch (Exception e)
882                         {
883                             // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
884                             System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
885                         }
886                     }
887                 }
888             }
889
890             return namespaces;
891         }
892
893         /// <summary>
894         /// <see cref="MetaApi"/>から取得したXMLのうち、ウィキ間リンクに関する部分を読み込む。
895         /// </summary>
896         /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
897         /// <returns>読み込んだウィキ間リンク情報。</returns>
898         /// <exception cref="InvalidDataException">interwikimapエレメントが存在しない場合。</exception>
899         private IgnoreCaseSet LoadInterwikimapElement(XmlElement queryElement)
900         {
901             // ウィキ間リンクブロックを取得、ウィキ間リンクブロックまでは必須
902             XmlElement interwikimapElement = queryElement["interwikimap"];
903             if (interwikimapElement == null)
904             {
905                 // フォーマットが想定外
906                 throw new InvalidDataException("parse failed : interwikimap element is not found");
907             }
908
909             // ウィキ間リンクを取得
910             IgnoreCaseSet interwikiPrefixs = new IgnoreCaseSet();
911             foreach (XmlNode node in interwikimapElement.ChildNodes)
912             {
913                 XmlElement interwikiElement = node as XmlElement;
914                 if (interwikiElement != null)
915                 {
916                     string prefix = interwikiElement.GetAttribute("prefix");
917                     if (!string.IsNullOrWhiteSpace(prefix))
918                     {
919                         interwikiPrefixs.Add(prefix);
920                     }
921                 }
922             }
923
924             return interwikiPrefixs;
925         }
926
927         #endregion
928     }
929 }