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;
17 using System.Xml.Serialization;
18 using Honememo.Utilities;
19 using Honememo.Wptscs.Models;
20 using Honememo.Wptscs.Properties;
21 using Honememo.Wptscs.Utilities;
24 /// MediaWikiのウェブサイト(システム)をあらわすモデルクラスです。
26 public class MediaWiki : Website, IXmlSerializable
31 /// 名前空間情報取得用にアクセスするAPI。
33 private string namespacePath;
38 private string exportPath;
43 private string redirect;
48 private int? templateNamespace;
53 private int? categoryNamespace;
58 private int? fileNamespace;
61 /// Wikipedia書式のシステム定義変数。
63 /// <remarks>初期値は http://www.mediawiki.org/wiki/Help:Magic_words を参照</remarks>
64 private IList<string> magicWords;
67 /// MediaWikiの名前空間の情報。
69 private IDictionary<int, IList<string>> namespaces = new Dictionary<int, IList<string>>();
76 /// コンストラクタ(MediaWiki全般)。
78 /// <param name="language">ウェブサイトの言語。</param>
79 /// <param name="location">ウェブサイトの場所。</param>
80 public MediaWiki(Language language, string location) : this()
83 this.Language = language;
84 this.Location = location;
88 /// コンストラクタ(Wikipedia用)。
90 /// <param name="language">ウェブサイトの言語。</param>
91 public MediaWiki(Language language) : this()
94 // ※ オーバーロードメソッドを呼んでいないのは、languageがnullのときに先にエラーになるから
95 this.Language = language;
96 this.Location = String.Format(Settings.Default.WikipediaLocation, language.Code);
100 /// コンストラクタ(シリアライズ or 拡張用)。
102 protected MediaWiki()
104 this.WebProxy = new AppDefaultWebProxy();
105 this.DocumentationTemplates = new List<string>();
110 #region 設定ファイルに初期値を持つプロパティ
113 /// MediaWiki名前空間情報取得用にアクセスするAPI。
115 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
116 public string NamespacePath
120 if (String.IsNullOrEmpty(this.namespacePath))
122 return Settings.Default.MediaWikiNamespacePath;
125 return this.namespacePath;
130 this.namespacePath = value;
135 /// 記事のXMLデータが存在するパス。
137 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
138 public string ExportPath
142 if (String.IsNullOrEmpty(this.exportPath))
144 return Settings.Default.MediaWikiExportPath;
147 return this.exportPath;
152 this.exportPath = value;
159 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
160 public string Redirect
164 if (String.IsNullOrEmpty(this.redirect))
166 return Settings.Default.MediaWikiRedirect;
169 return this.redirect;
174 this.redirect = value;
179 /// テンプレートの名前空間を示す番号。
181 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
182 public int TemplateNamespace
186 return this.templateNamespace ?? Settings.Default.MediaWikiTemplateNamespace;
191 this.templateNamespace = value;
198 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
199 public int CategoryNamespace
203 return this.categoryNamespace ?? Settings.Default.MediaWikiCategoryNamespace;
208 this.categoryNamespace = value;
215 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
216 public int FileNamespace
220 return this.fileNamespace ?? Settings.Default.MediaWikiFileNamespace;
225 this.fileNamespace = value;
230 /// Wikipedia書式のシステム定義変数。
232 /// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
233 public IList<string> MagicWords
237 if (this.magicWords == null)
239 string[] w = new string[Settings.Default.MediaWikiMagicWords.Count];
240 Settings.Default.MediaWikiMagicWords.CopyTo(w, 0);
244 return this.magicWords;
249 this.magicWords = value;
258 /// MediaWikiの名前空間の情報。
260 /// <remarks>値が指定されていない場合、サーバーから情報を取得。</remarks>
261 public IDictionary<int, IList<string>> Namespaces
265 lock (this.namespaces)
267 // 値が設定されていない場合、サーバーから取得して初期化する
268 // ※ コンストラクタ等で初期化していないのは、通信の準備が整うまで行えないため
269 // ※ MagicWordsがnullでこちらが空で若干条件が違うのは、あちらは設定ファイルに
270 // 保存する設定だが、こちらは設定ファイルに保存しない基本的に読み込み用の設定だから。
271 if (this.namespaces.Count > 0)
273 return this.namespaces;
276 // APIのXMLデータをMediaWikiサーバーから取得
277 XmlDocument xml = new XmlDocument();
278 using (Stream reader = this.WebProxy.GetStream(new Uri(new Uri(this.Location), this.NamespacePath)))
283 // ルートエレメントまで取得し、フォーマットをチェック
284 XmlElement rootElement = xml["api"];
285 if (rootElement == null)
287 // XMLは取得できたが空 or フォーマットが想定外
288 throw new InvalidDataException("parse failed");
292 XmlElement queryElement = rootElement["query"];
293 if (queryElement == null)
296 throw new InvalidDataException("parse failed");
299 // ネームスペースブロックを取得、ネームスペースブロックまでは必須
300 XmlElement namespacesElement = queryElement["namespaces"];
301 if (namespacesElement == null)
304 throw new InvalidDataException("parse failed");
308 foreach (XmlNode node in namespacesElement.ChildNodes)
310 XmlElement namespaceElement = node as XmlElement;
311 if (namespaceElement != null)
315 int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));
316 IList<string> values = new List<string>();
317 values.Add(namespaceElement.InnerText);
318 this.namespaces[id] = values;
321 string canonical = namespaceElement.GetAttribute("canonical");
322 if (!String.IsNullOrEmpty(canonical))
324 values.Add(canonical);
329 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
330 System.Diagnostics.Debug.WriteLine("MediaWiki.Namespaces > 例外発生 : " + e);
335 // ネームスペースエイリアスブロックを取得、無い場合も想定
336 XmlElement aliasesElement = queryElement["namespacealiases"];
337 if (aliasesElement != null)
340 foreach (XmlNode node in aliasesElement.ChildNodes)
342 XmlElement namespaceElement = node as XmlElement;
343 if (namespaceElement != null)
347 int id = Decimal.ToInt16(Decimal.Parse(namespaceElement.GetAttribute("id")));
348 IList<string> values = new List<string>();
349 if (this.namespaces.ContainsKey(id))
351 values = this.namespaces[id];
354 values.Add(namespaceElement.InnerText);
358 // キャッチしているのは、万が一想定外の書式が返された場合に、完璧に動かなくなるのを防ぐため
359 System.Diagnostics.Debug.WriteLine("MediaWiki.Namespaces > 例外発生 : " + e);
366 return this.namespaces;
371 // ※必須な情報が設定されていない場合、ArgumentNullExceptionを返す
374 throw new ArgumentNullException("namespaces");
377 this.namespaces = value;
382 /// Template:Documentation(言語間リンク等を別ページに記述するためのテンプレート)に相当するページ名。
384 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いものとして扱う。</remarks>
385 public IList<string> DocumentationTemplates
392 /// Template:Documentationで指定が無い場合に参照するページ名。
395 /// ほとんどの言語では[[/Doc]]の模様。
396 /// 空の場合、明示的な指定が無い場合は参照不能として扱う。
398 public string DocumentationTemplateDefaultPage
405 /// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
407 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
408 public string LinkInterwikiFormat
415 /// Template:Langで書式化するためのフォーマット。
417 /// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
418 public string LangFormat
425 /// このクラスで使用するWebアクセス用Proxyインスタンス。
427 /// <remarks>setterはユニットテスト用に公開。</remarks>
428 public IWebProxy WebProxy
441 /// <param name="title">ページタイトル。</param>
442 /// <returns>取得したページ。</returns>
443 /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
444 /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
445 public override Page GetPage(string title)
447 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
448 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
449 string escapeTitle = title;
450 if (new Uri(this.Location).Scheme == "file")
452 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
455 // ページのXMLデータをMediaWikiサーバーから取得
456 XmlDocument xml = new XmlDocument();
459 using (Stream reader = this.WebProxy.GetStream(
460 new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ExportPath, escapeTitle))))
465 catch (System.Net.WebException e)
467 // 404エラーによるページ取得失敗は詰め替えて返す
468 if (this.IsNotFound(e))
470 throw new FileNotFoundException("page not found", e);
476 // ルートエレメントまで取得し、フォーマットをチェック
477 XmlElement rootElement = xml["mediawiki"];
478 if (rootElement == null)
480 // XMLは取得できたが空 or フォーマットが想定外
481 throw new InvalidDataException("parse failed");
485 XmlElement pageElement = rootElement["page"];
486 if (pageElement == null)
489 throw new FileNotFoundException("page not found");
493 // ※ 一応、各項目が無くても動作するようにする
494 string pageTitle = XmlUtils.InnerText(pageElement["title"], title);
496 DateTime? time = null;
497 XmlElement revisionElement = pageElement["revision"];
498 if (revisionElement != null)
500 text = XmlUtils.InnerText(revisionElement["text"], null);
501 XmlElement timeElement = revisionElement["timestamp"];
502 if (timeElement != null)
504 time = new DateTime?(DateTime.Parse(timeElement.InnerText));
509 return new MediaWikiPage(this, pageTitle, text, time);
513 /// 指定された文字列がWikipediaのシステム変数に相当かを判定。
515 /// <param name="text">チェックする文字列。</param>
516 /// <returns><c>true</c> システム変数に相当。</returns>
517 public bool IsMagicWord(string text)
519 string s = text != null ? text : String.Empty;
521 // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
522 foreach (string variable in this.MagicWords)
524 if (s == variable || s.StartsWith(variable + ":"))
534 /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
536 /// <param name="title">記事名。</param>
537 /// <param name="lang">言語。</param>
538 /// <param name="langTitle">他言語版記事名。</param>
539 /// <param name="label">表示名。</param>
540 /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
541 public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
543 if (String.IsNullOrEmpty(this.LinkInterwikiFormat))
548 return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
552 /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
554 /// <param name="lang">言語。</param>
555 /// <param name="text">文字列。</param>
556 /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
558 /// この<para>lang</para>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
559 /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
561 public string FormatLang(string lang, string text)
563 if (String.IsNullOrEmpty(this.LangFormat))
568 return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
573 #region XMLシリアライズ用メソッド
576 /// シリアライズするXMLのスキーマ定義を返す。
578 /// <returns>XML表現を記述するXmlSchema。</returns>
579 public System.Xml.Schema.XmlSchema GetSchema()
585 /// XMLからオブジェクトをデシリアライズする。
587 /// <param name="reader">デシリアライズ元のXmlReader</param>
588 public void ReadXml(XmlReader reader)
590 XmlDocument xml = new XmlDocument();
594 // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
595 XmlElement siteElement = xml.DocumentElement;
596 this.Location = siteElement.SelectSingleNode("Location").InnerText;
598 using (XmlReader r = XmlReader.Create(
599 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
601 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
604 this.NamespacePath = XmlUtils.InnerText(siteElement.SelectSingleNode("NamespacePath"));
605 this.ExportPath = XmlUtils.InnerText(siteElement.SelectSingleNode("ExportPath"));
606 this.Redirect = XmlUtils.InnerText(siteElement.SelectSingleNode("Redirect"));
608 string text = XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace"));
609 if (!String.IsNullOrEmpty(text))
611 this.TemplateNamespace = int.Parse(text);
614 text = XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace"));
615 if (!String.IsNullOrEmpty(text))
617 this.CategoryNamespace = int.Parse(text);
620 text = XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace"));
621 if (!String.IsNullOrEmpty(text))
623 this.FileNamespace = int.Parse(text);
627 IList<string> variables = new List<string>();
628 foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
630 variables.Add(variableNode.InnerText);
633 if (variables.Count > 0)
636 this.MagicWords = variables;
639 // Template:Documentationの設定
640 this.DocumentationTemplates = new List<string>();
641 foreach (XmlNode docNode in siteElement.SelectNodes("DocumentationTemplates/DocumentationTemplate"))
643 this.DocumentationTemplates.Add(docNode.InnerText);
644 XmlElement docElement = docNode as XmlElement;
645 if (docElement != null)
647 // ※ XML上DefaultPageはテンプレートごとに異なる値を持てるが、
648 // そうした例を見かけたことがないため、代表で一つの値のみ使用
649 // (複数値が持てるのも、リダイレクトが存在するためその対策として)
650 this.DocumentationTemplateDefaultPage = docElement.GetAttribute("DefaultPage");
654 this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
655 this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
659 /// オブジェクトをXMLにシリアライズする。
661 /// <param name="writer">シリアライズ先のXmlWriter</param>
662 public void WriteXml(XmlWriter writer)
664 writer.WriteElementString("Location", this.Location);
665 new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
668 // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
669 writer.WriteElementString("NamespacePath", this.namespacePath);
670 writer.WriteElementString("ExportPath", this.exportPath);
671 writer.WriteElementString("Redirect", this.redirect);
672 writer.WriteElementString(
674 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : String.Empty);
675 writer.WriteElementString(
677 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : String.Empty);
678 writer.WriteElementString(
680 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : String.Empty);
683 writer.WriteStartElement("MagicWords");
684 if (this.magicWords != null)
686 foreach (string variable in this.magicWords)
688 writer.WriteElementString("Variable", variable);
692 // Template:Documentationの設定
693 writer.WriteEndElement();
694 writer.WriteStartElement("DocumentationTemplates");
695 foreach (string doc in this.DocumentationTemplates)
697 writer.WriteStartElement("DocumentationTemplate");
698 writer.WriteAttributeString("DefaultPage", this.DocumentationTemplateDefaultPage);
699 writer.WriteValue(doc);
700 writer.WriteEndElement();
703 writer.WriteEndElement();
704 writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
705 writer.WriteElementString("LangFormat", this.LangFormat);