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 /// <exception cref="NotSupportedException">末尾がピリオドのページの場合(既知の不具合への対応)。</exception>
445 /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
446 public override Page GetPage(string title)
448 // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
449 // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
450 string escapeTitle = title;
451 if (new Uri(this.Location).IsFile)
453 escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
457 Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ExportPath, escapeTitle));
458 if (uri.OriginalString.EndsWith("."))
460 // 末尾がピリオドのページが取得できない既知の不具合への暫定対応
461 // 対処方法が不明なため、せめて例外を投げて検知する
462 throw new NotSupportedException(title + " is not suppoted");
465 // ページのXMLデータをMediaWikiサーバーから取得
466 XmlDocument xml = new XmlDocument();
469 using (Stream reader = this.WebProxy.GetStream(uri))
474 catch (System.Net.WebException e)
476 // 404エラーによるページ取得失敗は詰め替えて返す
477 if (this.IsNotFound(e))
479 throw new FileNotFoundException("page not found", e);
485 // ルートエレメントまで取得し、フォーマットをチェック
486 XmlElement rootElement = xml["mediawiki"];
487 if (rootElement == null)
489 // XMLは取得できたが空 or フォーマットが想定外
490 throw new InvalidDataException("parse failed");
494 XmlElement pageElement = rootElement["page"];
495 if (pageElement == null)
498 throw new FileNotFoundException("page not found");
502 // ※ 一応、各項目が無くても動作するようにする
503 string pageTitle = XmlUtils.InnerText(pageElement["title"], title);
505 DateTime? time = null;
506 XmlElement revisionElement = pageElement["revision"];
507 if (revisionElement != null)
509 text = XmlUtils.InnerText(revisionElement["text"], null);
510 XmlElement timeElement = revisionElement["timestamp"];
511 if (timeElement != null)
513 time = new DateTime?(DateTime.Parse(timeElement.InnerText));
518 return new MediaWikiPage(this, pageTitle, text, time);
522 /// 指定された文字列がWikipediaのシステム変数に相当かを判定。
524 /// <param name="text">チェックする文字列。</param>
525 /// <returns><c>true</c> システム変数に相当。</returns>
526 public bool IsMagicWord(string text)
528 string s = text != null ? text : String.Empty;
530 // {{CURRENTYEAR}}や{{ns:1}}みたいなパターンがある
531 foreach (string variable in this.MagicWords)
533 if (s == variable || s.StartsWith(variable + ":"))
543 /// <see cref="LinkInterwikiFormat"/> を渡された記事名, 言語, 他言語版記事名, 表示名で書式化した文字列を返す。
545 /// <param name="title">記事名。</param>
546 /// <param name="lang">言語。</param>
547 /// <param name="langTitle">他言語版記事名。</param>
548 /// <param name="label">表示名。</param>
549 /// <returns>書式化した文字列。<see cref="LinkInterwikiFormat"/>が未設定の場合<c>null</c>。</returns>
550 public string FormatLinkInterwiki(string title, string lang, string langTitle, string label)
552 if (String.IsNullOrEmpty(this.LinkInterwikiFormat))
557 return StringUtils.FormatDollarVariable(this.LinkInterwikiFormat, title, lang, langTitle, label);
561 /// <see cref="LangFormat"/> を渡された言語, 文字列で書式化した文字列を返す。
563 /// <param name="lang">言語。</param>
564 /// <param name="text">文字列。</param>
565 /// <returns>書式化した文字列。<see cref="LangFormat"/>が未設定の場合<c>null</c>。</returns>
567 /// この<para>lang</para>と<see cref="Language"/>のコードは、厳密には一致しないケースがあるが
568 /// (例、simple→en)、2012年2月現在の実装ではそこまで正確さは要求していない。
570 public string FormatLang(string lang, string text)
572 if (String.IsNullOrEmpty(this.LangFormat))
577 return StringUtils.FormatDollarVariable(this.LangFormat, lang, text);
582 #region XMLシリアライズ用メソッド
585 /// シリアライズするXMLのスキーマ定義を返す。
587 /// <returns>XML表現を記述するXmlSchema。</returns>
588 public System.Xml.Schema.XmlSchema GetSchema()
594 /// XMLからオブジェクトをデシリアライズする。
596 /// <param name="reader">デシリアライズ元のXmlReader</param>
597 public void ReadXml(XmlReader reader)
599 XmlDocument xml = new XmlDocument();
603 // ※ 以下、基本的に無かったらNGの部分はいちいちチェックしない。例外飛ばす
604 XmlElement siteElement = xml.DocumentElement;
605 this.Location = siteElement.SelectSingleNode("Location").InnerText;
607 using (XmlReader r = XmlReader.Create(
608 new StringReader(siteElement.SelectSingleNode("Language").OuterXml), reader.Settings))
610 this.Language = new XmlSerializer(typeof(Language)).Deserialize(r) as Language;
613 this.NamespacePath = XmlUtils.InnerText(siteElement.SelectSingleNode("NamespacePath"));
614 this.ExportPath = XmlUtils.InnerText(siteElement.SelectSingleNode("ExportPath"));
615 this.Redirect = XmlUtils.InnerText(siteElement.SelectSingleNode("Redirect"));
617 string text = XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace"));
618 if (!String.IsNullOrEmpty(text))
620 this.TemplateNamespace = int.Parse(text);
623 text = XmlUtils.InnerText(siteElement.SelectSingleNode("CategoryNamespace"));
624 if (!String.IsNullOrEmpty(text))
626 this.CategoryNamespace = int.Parse(text);
629 text = XmlUtils.InnerText(siteElement.SelectSingleNode("FileNamespace"));
630 if (!String.IsNullOrEmpty(text))
632 this.FileNamespace = int.Parse(text);
636 IList<string> variables = new List<string>();
637 foreach (XmlNode variableNode in siteElement.SelectNodes("MagicWords/Variable"))
639 variables.Add(variableNode.InnerText);
642 if (variables.Count > 0)
645 this.MagicWords = variables;
648 // Template:Documentationの設定
649 this.DocumentationTemplates = new List<string>();
650 foreach (XmlNode docNode in siteElement.SelectNodes("DocumentationTemplates/DocumentationTemplate"))
652 this.DocumentationTemplates.Add(docNode.InnerText);
653 XmlElement docElement = docNode as XmlElement;
654 if (docElement != null)
656 // ※ XML上DefaultPageはテンプレートごとに異なる値を持てるが、
657 // そうした例を見かけたことがないため、代表で一つの値のみ使用
658 // (複数値が持てるのも、リダイレクトが存在するためその対策として)
659 this.DocumentationTemplateDefaultPage = docElement.GetAttribute("DefaultPage");
663 this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
664 this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
668 /// オブジェクトをXMLにシリアライズする。
670 /// <param name="writer">シリアライズ先のXmlWriter</param>
671 public void WriteXml(XmlWriter writer)
673 writer.WriteElementString("Location", this.Location);
674 new XmlSerializer(this.Language.GetType()).Serialize(writer, this.Language);
677 // ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
678 writer.WriteElementString("NamespacePath", this.namespacePath);
679 writer.WriteElementString("ExportPath", this.exportPath);
680 writer.WriteElementString("Redirect", this.redirect);
681 writer.WriteElementString(
683 this.templateNamespace.HasValue ? this.templateNamespace.ToString() : String.Empty);
684 writer.WriteElementString(
686 this.templateNamespace.HasValue ? this.categoryNamespace.ToString() : String.Empty);
687 writer.WriteElementString(
689 this.templateNamespace.HasValue ? this.fileNamespace.ToString() : String.Empty);
692 writer.WriteStartElement("MagicWords");
693 if (this.magicWords != null)
695 foreach (string variable in this.magicWords)
697 writer.WriteElementString("Variable", variable);
701 // Template:Documentationの設定
702 writer.WriteEndElement();
703 writer.WriteStartElement("DocumentationTemplates");
704 foreach (string doc in this.DocumentationTemplates)
706 writer.WriteStartElement("DocumentationTemplate");
707 writer.WriteAttributeString("DefaultPage", this.DocumentationTemplateDefaultPage);
708 writer.WriteValue(doc);
709 writer.WriteEndElement();
712 writer.WriteEndElement();
713 writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
714 writer.WriteElementString("LangFormat", this.LangFormat);