1 // ================================================================================================
3 // MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>
5 // <copyright file="MediaWiki.cs" company="honeplusのメモ帳">
6 // Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
9 // ================================================================================================
11 namespace Honememo.Wptscs.Websites
14 using System.Collections.Generic;
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;
26 /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。
28 public class MediaWiki : Website, IXmlSerializable
33 /// 名前空間情報取得用にアクセスするAPI。
35 private string metaApi;
40 private string exportPath;
45 private string redirect;
50 private int? templateNamespace;
55 private int? categoryNamespace;
60 private int? fileNamespace;
63 /// MediaWiki書式のシステム定義変数。
65 private ISet<string> magicWords;
68 /// MediaWikiの名前空間の情報。
70 private IDictionary<int, IgnoreCaseSet> namespaces;
73 /// MediaWikiのウィキ間リンクのプレフィックス情報。
75 private IgnoreCaseSet interwikiPrefixs;
78 /// MediaWikiのウィキ間リンクのプレフィックス情報(APIから取得した値と設定値の集合)。
80 private IgnoreCaseSet interwikiPrefixCaches;
83 /// <see cref="InitializeByMetaApi"/>同期用ロックオブジェクト。
85 private object lockLoadMetaApi = new object();
92 /// コンストラクタ(MediaWiki全般)。
94 /// <param name="language">ウェブサイトの言語。</param>
95 /// <param name="location">ウェブサイトの場所。</param>
96 public MediaWiki(Language language, string location) : this()
99 this.Language = language;
100 this.Location = location;
104 /// コンストラクタ(Wikipedia用)。
106 /// <param name="language">ウェブサイトの言語。</param>
107 public MediaWiki(Language language) : this()
110 // ※ オーバーロードメソッドを呼んでいないのは、languageがnullのときに先にエラーになるから
111 this.Language = language;
112 this.Location = String.Format(Settings.Default.WikipediaLocation, language.Code);
116 /// コンストラクタ(シリアライズ or 拡張用)。
118 protected MediaWiki()
120 this.WebProxy = new AppDefaultWebProxy();
121 this.DocumentationTemplates = new List<string>();
126 #region 設定ファイルに初期値を持つプロパティ
129 /// MediaWikiメタ情報取得用にアクセスするAPI。
131 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
132 public string MetaApi
136 if (String.IsNullOrEmpty(this.metaApi))
138 return Settings.Default.MediaWikiMetaApi;
146 this.metaApi = value;
151 /// 記事のXMLデータが存在するパス。
153 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
154 public string ExportPath
158 if (String.IsNullOrEmpty(this.exportPath))
160 return Settings.Default.MediaWikiExportPath;
163 return this.exportPath;
168 this.exportPath = value;
175 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
176 public string Redirect
180 if (String.IsNullOrEmpty(this.redirect))
182 return Settings.Default.MediaWikiRedirect;
185 return this.redirect;
190 this.redirect = value;
195 /// テンプレートの名前空間を示す番号。
197 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
198 public int TemplateNamespace
202 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;
207 this.templateNamespace = value;
214 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
215 public int CategoryNamespace
219 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;
224 this.categoryNamespace = value;
231 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
232 public int FileNamespace
236 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;
241 this.fileNamespace = value;
246 /// MediaWiki書式のシステム定義変数。
249 /// 値が指定されていない場合、デフォルト値を返す。
252 public ISet<string> MagicWords
256 if (this.magicWords == null)
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>());
264 return this.magicWords;
269 this.magicWords = value;
275 #region サーバーから値を取得するプロパティ
278 /// MediaWikiの名前空間の情報。
281 /// サーバーから情報を取得。大文字小文字を区別しない。
283 public IDictionary<int, IgnoreCaseSet> Namespaces
287 // 値が設定されていない場合、サーバーから取得して初期化する
288 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
289 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
290 if (this.namespaces != null)
292 return this.namespaces;
295 lock (this.lockLoadMetaApi)
297 if (this.namespaces != null)
299 return this.namespaces;
302 this.InitializeByMetaApi();
305 return this.namespaces;
310 this.namespaces = value;
315 /// MediaWikiのウィキ間リンクのプレフィックス情報。
318 /// 値が設定されていない場合デフォルト値とサーバーから、
319 /// 設定されている場合その内容とサーバーから取得した情報を使用する。
322 public IgnoreCaseSet InterwikiPrefixs
326 // 値が準備されていない場合、サーバーと設定ファイルから取得して初期化する
327 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
328 // ※ 余計なロック・通信をしないよう、ロックの前後に値のチェックを行う
329 if (this.interwikiPrefixCaches != null)
331 return this.interwikiPrefixCaches;
334 lock (this.lockLoadMetaApi)
336 if (this.interwikiPrefixCaches != null)
338 return this.interwikiPrefixCaches;
341 this.InitializeByMetaApi();
344 return this.interwikiPrefixCaches;
350 this.interwikiPrefixs = value;
351 this.interwikiPrefixCaches = null;
360 /// Template:Documentation(言語間リンク等を別ページに記述するためのテンプレート)に相当するページ名。
362 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いものとして扱う。</remarks>
363 public IList<string> DocumentationTemplates
370 /// Template:Documentationで指定が無い場合に参照するページ名。
373 /// ほとんどの言語では[[/Doc]]の模様。
374 /// 空の場合、明示的な指定が無い場合は参照不能として扱う。
376 public string DocumentationTemplateDefaultPage
383 /// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
385 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
386 public string LinkInterwikiFormat
393 /// Template:Langで書式化するためのフォーマット。
395 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
396 public string LangFormat
403 /// このクラスで使用するWebアクセス用Proxyインスタンス。
405 /// <remarks>setterはユニットテスト用に公開。</remarks>
406 public IWebProxy WebProxy
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)
426 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
427 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
428 string escapeTitle = title;
429 if (new Uri(this.Location).IsFile)
431 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
435 Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ExportPath, escapeTitle));
436 if (uri.OriginalString.EndsWith("."))
438 // 末尾がピリオドのページが取得できない既知の不具合への暫定対応
439 // 対処方法が不明なため、せめて例外を投げて検知する
440 throw new EndPeriodException(title + " is not suppoted");
443 // ページのXMLデータをMediaWikiサーバーから取得
444 XmlDocument xml = new XmlDocument();
447 using (Stream reader = this.WebProxy.GetStream(uri))
452 catch (System.Net.WebException e)
454 // 404エラーによるページ取得失敗は詰め替えて返す
455 if (this.IsNotFound(e))
457 throw new FileNotFoundException("page not found", e);
463 // ルートエレメントまで取得し、フォーマットをチェック
464 XmlElement rootElement = xml["mediawiki"];
465 if (rootElement == null)
467 // XMLは取得できたが空 or フォーマットが想定外
468 throw new InvalidDataException("parse failed : mediawiki element is not found");
472 XmlElement pageElement = rootElement["page"];
473 if (pageElement == null)
476 throw new FileNotFoundException("page not found");
480 // ※ 一応、各項目が無くても動作するようにする
481 string pageTitle = XmlUtils.InnerText(pageElement["title"], title);
483 DateTime? time = null;
484 XmlElement revisionElement = pageElement["revision"];
485 if (revisionElement != null)
487 text = XmlUtils.InnerText(revisionElement["text"], null);
488 XmlElement timeElement = revisionElement["timestamp"];
489 if (timeElement != null)
491 time = new DateTime?(DateTime.Parse(timeElement.InnerText));
496 return new MediaWikiPage(this, pageTitle, text, time);
500 /// 指定された文字列がMediaWikiのシステム変数に相当かを判定。
502 /// <param name="text">チェックする文字列。</param>
503 /// <returns>システム変数に相当する場合<c>true</c>。</returns>
504 /// <remarks>大文字小文字は区別する。</remarks>
505 public bool IsMagicWord(string text)
507 // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
508 string s = StringUtils.DefaultString(text);
509 foreach (string variable in this.MagicWords)
511 if (s == variable || s.StartsWith(variable + ":"))
521 /// 指定されたリンク文字列がMediaWikiのウィキ間リンクかを判定。
523 /// <param name="link">チェックするリンク文字列。</param>
524 /// <returns>ウィキ間リンクに該当する場合<c>true</c>。</returns>
525 /// <remarks>大文字小文字は区別しない。</remarks>
526 public bool IsInterwiki(string link)
528 // ※ ウィキ間リンクには入れ子もあるが、ここでは意識する必要はない
529 string s = StringUtils.DefaultString(link);
531 // 名前空間と被る場合はそちらが優先、ウィキ間リンクと判定しない
532 if (this.IsNamespace(link))
537 // 文字列がいずれかのウィキ間リンクのプレフィックスで始まるか
538 int index = s.IndexOf(':');
544 return this.InterwikiPrefixs.Contains(s.Remove(index));
548 /// 指定されたリンク文字列がMediaWikiのいずれかの名前空間に属すかを判定。
550 /// <param name="link">チェックするリンク文字列。</param>
551 /// <returns>いずれかの名前空間に該当する場合<c>true</c>。</returns>
552 /// <remarks>大文字小文字は区別しない。</remarks>
553 public bool IsNamespace(string link)
555 // 文字列がいずれかの名前空間のプレフィックスで始まるか
556 string s = StringUtils.DefaultString(link);
557 int index = s.IndexOf(':');
563 string prefix = s.Remove(index);
564 foreach (IgnoreCaseSet prefixes in this.Namespaces.Values)
566 if (prefixes.Contains(prefix))
576 /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
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)
585 if (String.IsNullOrEmpty(this.LinkInterwikiFormat))
590 return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
594 /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
596 /// <param name="lang">言語。</param>
597 /// <param name="text">文字列。</param>
598 /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
600 /// この<paramref name="lang"/>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
601 /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
603 public string FormatLang(string lang, string text)
605 if (String.IsNullOrEmpty(this.LangFormat))
610 return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
615 #region XMLシリアライズ用メソッド
618 /// シリアライズするXMLのスキーマ定義を返す。
620 /// <returns>XML表現を記述するXmlSchema。</returns>
621 public System.Xml.Schema.XmlSchema GetSchema()
627 /// XMLからオブジェクトをデシリアライズする。
629 /// <param name="reader">デシリアライズ元のXmlReader</param>
630 public void ReadXml(XmlReader reader)
632 XmlDocument xml = new XmlDocument();
636 // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
637 XmlElement siteElement = xml.DocumentElement;
638 this.Location = siteElement.SelectSingleNode("Location").InnerText;
640 using (XmlReader r = XmlReader.Create(
641 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
643 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
646 this.MetaApi = XmlUtils.InnerText(siteElement.SelectSingleNode("MetaApi"));
647 this.ExportPath = XmlUtils.InnerText(siteElement.SelectSingleNode("ExportPath"));
648 this.Redirect = XmlUtils.InnerText(siteElement.SelectSingleNode("Redirect"));
650 string text = XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace"));
651 if (!String.IsNullOrEmpty(text))
653 this.TemplateNamespace = int.Parse(text);
656 text = XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace"));
657 if (!String.IsNullOrEmpty(text))
659 this.CategoryNamespace = int.Parse(text);
662 text = XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace"));
663 if (!String.IsNullOrEmpty(text))
665 this.FileNamespace = int.Parse(text);
669 ISet<string> variables = new HashSet<string>();
670 foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
672 variables.Add(variableNode.InnerText);
675 if (variables.Count > 0)
678 this.MagicWords = variables;
682 IgnoreCaseSet prefixs = new IgnoreCaseSet();
683 foreach (XmlNode prefixNode in siteElement.SelectNodes("InterwikiPrefixs/Prefix"))
685 prefixs.Add(prefixNode.InnerText);
688 if (prefixs.Count > 0)
691 this.InterwikiPrefixs = prefixs;
694 // Template:Documentationの設定
695 this.DocumentationTemplates = new List<string>();
696 foreach (XmlNode docNode in siteElement.SelectNodes("DocumentationTemplates/DocumentationTemplate"))
698 this.DocumentationTemplates.Add(docNode.InnerText);
699 XmlElement docElement = docNode as XmlElement;
700 if (docElement != null)
702 // ※ XML上DefaultPageはテンプレートごとに異なる値を持てるが、
703 // そうした例を見かけたことがないため、代表で一つの値のみ使用
704 // (複数値が持てるのも、リダイレクトが存在するためその対策として)
705 this.DocumentationTemplateDefaultPage = docElement.GetAttribute("DefaultPage");
709 this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
710 this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
714 /// オブジェクトをXMLにシリアライズする。
716 /// <param name="writer">シリアライズ先のXmlWriter</param>
717 public void WriteXml(XmlWriter writer)
719 writer.WriteElementString("Location", this.Location);
720 new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
723 // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
724 writer.WriteElementString("MetaApi", this.metaApi);
725 writer.WriteElementString("ExportPath", this.exportPath);
726 writer.WriteElementString("Redirect", this.redirect);
727 writer.WriteElementString(
729 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : String.Empty);
730 writer.WriteElementString(
732 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : String.Empty);
733 writer.WriteElementString(
735 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : String.Empty);
738 writer.WriteStartElement("MagicWords");
739 if (this.magicWords != null)
741 foreach (string variable in this.magicWords)
743 writer.WriteElementString("Variable", variable);
748 writer.WriteEndElement();
749 writer.WriteStartElement("InterwikiPrefixs");
750 if (this.interwikiPrefixs != null)
752 foreach (string prefix in this.interwikiPrefixs)
754 writer.WriteElementString("Prefix", prefix);
758 // Template:Documentationの設定
759 writer.WriteEndElement();
760 writer.WriteStartElement("DocumentationTemplates");
761 foreach (string doc in this.DocumentationTemplates)
763 writer.WriteStartElement("DocumentationTemplate");
764 writer.WriteAttributeString("DefaultPage", this.DocumentationTemplateDefaultPage);
765 writer.WriteValue(doc);
766 writer.WriteEndElement();
769 writer.WriteEndElement();
770 writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
771 writer.WriteElementString("LangFormat", this.LangFormat);
779 /// <see cref="MetaApi"/>を使用してサーバーからメタ情報を取得する。
781 /// <exception cref="System.Net.WebException">通信エラー等が発生した場合。</exception>
782 /// <exception cref="InvalidDataException">APIから取得した情報が想定外のフォーマットの場合。</exception>
783 private void InitializeByMetaApi()
785 // APIのXMLデータをMediaWikiサーバーから取得
786 XmlDocument xml = new XmlDocument();
787 using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.MetaApi)))
792 // ルートエレメントまで取得し、フォーマットをチェック
793 XmlElement rootElement = xml["api"];
794 if (rootElement == null)
796 // XMLは取得できたが空 or フォーマットが想定外
797 throw new InvalidDataException("parse failed : api element is not found");
801 XmlElement queryElement = rootElement["query"];
802 if (queryElement == null)
805 throw new InvalidDataException("parse failed : query element is not found");
808 // クエリー内のネームスペース・ネームスペースエイリアス、ウィキ間リンクを読み込み
809 this.namespaces = this.LoadNamespacesElement(queryElement);
810 this.interwikiPrefixCaches = this.LoadInterwikimapElement(queryElement);
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);
824 /// <see cref="MetaApi"/>から取得したXMLのうち、ネームスペースに関する部分を読み込む。
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)
831 // ネームスペースブロックを取得、ネームスペースブロックまでは必須
832 XmlElement namespacesElement = queryElement["namespaces"];
833 if (namespacesElement == null)
836 throw new InvalidDataException("parse failed : namespaces element is not found");
840 IDictionary<int, IgnoreCaseSet> namespaces = new Dictionary<int, IgnoreCaseSet>();
841 foreach (XmlNode node in namespacesElement.ChildNodes)
843 XmlElement namespaceElement = node as XmlElement;
844 if (namespaceElement != null)
848 int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));
849 IgnoreCaseSet values = new IgnoreCaseSet();
850 values.Add(namespaceElement.InnerText);
851 namespaces[id] = values;
854 string canonical = namespaceElement.GetAttribute("canonical");
855 if (!String.IsNullOrEmpty(canonical))
857 values.Add(canonical);
862 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
863 System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
868 // ネームスペースエイリアスブロックを取得、無い場合も想定
869 XmlElement aliasesElement = queryElement["namespacealiases"];
870 if (aliasesElement != null)
873 foreach (XmlNode node in aliasesElement.ChildNodes)
875 XmlElement namespaceElement = node as XmlElement;
876 if (namespaceElement != null)
880 int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));
881 ISet<string> values = new HashSet<string>();
882 if (namespaces.ContainsKey(id))
884 values = namespaces[id];
887 values.Add(namespaceElement.InnerText);
891 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
892 System.Diagnostics.Debug.WriteLine("MediaWiki.LoadNamespacesElement > 例外発生 : " + e);
902 /// <see cref="MetaApi"/>から取得したXMLのうち、ウィキ間リンクに関する部分を読み込む。
904 /// <param name="queryElement">APIから取得したXML要素のうち、api→query部分のエレメント。</param>
905 /// <returns>読み込んだウィキ間リンク情報。</returns>
906 /// <exception cref="InvalidDataException">interwikimapエレメントが存在しない場合。</exception>
907 private IgnoreCaseSet LoadInterwikimapElement(XmlElement queryElement)
909 // ウィキ間リンクブロックを取得、ウィキ間リンクブロックまでは必須
910 XmlElement interwikimapElement = queryElement["interwikimap"];
911 if (interwikimapElement == null)
914 throw new InvalidDataException("parse failed : interwikimap element is not found");
918 IgnoreCaseSet interwikiPrefixs = new IgnoreCaseSet();
919 foreach (XmlNode node in interwikimapElement.ChildNodes)
921 XmlElement interwikiElement = node as XmlElement;
922 if (interwikiElement != null)
924 string prefix = interwikiElement.GetAttribute("prefix");
925 if (!String.IsNullOrWhiteSpace(prefix))
927 interwikiPrefixs.Add(prefix);
932 return interwikiPrefixs;