OSDN Git Service

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