OSDN Git Service

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