// MediaWikiのウェブサイト(システム)をあらわすモデルクラスソース</summary>
//
// <copyright file="MediaWiki.cs" company="honeplusのメモ帳">
-// Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
+// Copyright (C) 2013 Honeplus. All rights reserved.</copyright>
// <author>
// Honeplus</author>
// ================================================================================================
using System.IO;
using System.Linq;
using System.Xml;
+ using System.Xml.Linq;
using System.Xml.Serialization;
using Honememo.Models;
using Honememo.Utilities;
private string metaApi;
/// <summary>
- /// 記事のXMLデータが存在するパス。
+ /// MediaWiki記事データ取得用にアクセスするAPI。
/// </summary>
- private string exportPath;
+ private string contentApi;
/// <summary>
- /// リダイレクトの文字列。
+ /// MediaWiki言語間リンク取得用にアクセスするAPI。
/// </summary>
- private string redirect;
+ private string interlanguageApi;
/// <summary>
/// テンプレートの名前空間を示す番号。
/// </summary>
private object lockLoadMetaApi = new object();
- /// <summary>
- /// Template:Documentation(言語間リンク等を別ページに記述するためのテンプレート)に相当するページ名。
- /// </summary>
- private IList<string> documentationTemplates = new List<string>();
-
#endregion
#region コンストラクタ
}
/// <summary>
- /// 記事のXMLデータが存在するパス。
+ /// MediaWiki記事データ取得用にアクセスするAPI。
/// </summary>
/// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
- public string ExportPath
+ public string ContentApi
{
get
{
- if (string.IsNullOrEmpty(this.exportPath))
+ if (string.IsNullOrEmpty(this.contentApi))
{
- return Settings.Default.MediaWikiExportPath;
+ return Settings.Default.MediaWikiContentApi;
}
- return this.exportPath;
+ return this.contentApi;
}
set
{
- this.exportPath = value;
+ this.contentApi = value;
}
}
/// <summary>
- /// リダイレクトの文字列。
+ /// MediaWiki言語間リンク取得用にアクセスするAPI。
/// </summary>
/// <remarks>値が指定されていない場合、デフォルト値を返す。</remarks>
- public string Redirect
+ public string InterlanguageApi
{
get
{
- if (string.IsNullOrEmpty(this.redirect))
+ if (string.IsNullOrEmpty(this.interlanguageApi))
{
- return Settings.Default.MediaWikiRedirect;
+ return Settings.Default.MediaWikiInterlanguageApi;
}
- return this.redirect;
+ return this.interlanguageApi;
}
set
{
- this.redirect = value;
+ this.interlanguageApi = value;
}
}
#region それ以外のプロパティ
/// <summary>
- /// Template:Documentation(言語間リンク等を別ページに記述するためのテンプレート)に相当するページ名。
- /// </summary>
- /// <remarks>空の場合、その言語版にはこれに相当する機能は無いものとして扱う。</remarks>
- public IList<string> DocumentationTemplates
- {
- get
- {
- return this.documentationTemplates;
- }
-
- set
- {
- if (value == null)
- {
- // nullの場合、空のリストを代入
- // ※ 例外にしてもよいが、このクラスにはnullで初期化としている
- // プロパティが多数存在するので、動きをあわせる
- value = new List<string>();
- }
-
- this.documentationTemplates = value;
- }
- }
-
- /// <summary>
- /// Template:Documentationで指定が無い場合に参照するページ名。
- /// </summary>
- /// <remarks>
- /// ほとんどの言語では[[/Doc]]の模様。
- /// 空の場合、明示的な指定が無い場合は参照不能として扱う。
- /// </remarks>
- public string DocumentationTemplateDefaultPage
- {
- get;
- set;
- }
-
- /// <summary>
/// Template:仮リンク(他言語へのリンク)で書式化するためのフォーマット。
/// </summary>
/// <remarks>空の場合、その言語版にはこれに相当する機能は無いor使用しないものとして扱う。</remarks>
/// </summary>
/// <param name="title">ページタイトル。</param>
/// <returns>取得したページ。</returns>
+ /// <exception cref="InvalidDataException">APIから取得したデータが想定外。</exception>
+ /// <exception cref="NullReferenceException">APIから取得したデータが想定外。</exception>
/// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
- /// <exception cref="EndPeriodException">末尾がピリオドのページの場合(既知の不具合への対応)。</exception>
- /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
+ /// <remarks>
+ /// ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。
+ /// このメソッドでは記事本文や日時といった情報は取得しない。
+ /// そうした情報は、実際にアクセスされたタイミングで動的に取得する。
+ /// </remarks>
public override Page GetPage(string title)
{
// fileスキームの場合、記事名からファイルに使えない文字をエスケープ
}
// URIを生成
- Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ExportPath, escapeTitle));
- if (uri.OriginalString.EndsWith("."))
+ Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.InterlanguageApi, escapeTitle));
+
+ // ページの言語間リンク情報XMLデータをMediaWikiサーバーから取得
+ XElement doc;
+ using (Stream reader = this.WebProxy.GetStream(uri))
{
- // 末尾がピリオドのページが取得できない既知の不具合への暫定対応
- // 対処方法が不明なため、せめて例外を投げて検知する
- throw new EndPeriodException(title + " is not suppoted");
+ doc = XElement.Load(reader);
}
- // ページのXMLデータをMediaWikiサーバーから取得
- XmlDocument xml = new XmlDocument();
+ // クエリーエレメントを取得
+ // ※ エレメントは常に1件
+ XElement qe;
try
{
- using (Stream reader = this.WebProxy.GetStream(uri))
- {
- xml.Load(reader);
- }
+ qe = (from n in doc.Elements("query")
+ select n).First();
}
- catch (System.Net.WebException e)
+ catch (InvalidOperationException)
{
- // 404エラーによるページ取得失敗は詰め替えて返す
- if (this.IsNotFound(e))
- {
- throw new FileNotFoundException("page not found", e);
- }
+ throw new InvalidOperationException("parse failed : api/query element is not found");
+ }
+
+ // クエリーからページ情報を読み込み返す
+ // ※ ページが無い場合などは、例外が投げられる
+ return MediaWikiPage.GetFromQuery(this, uri, qe);
+ }
- throw e;
+ /// <summary>
+ /// ページを取得。
+ /// </summary>
+ /// <param name="title">ページタイトル。</param>
+ /// <returns>取得したページ。</returns>
+ /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
+ /// <exception cref="EndPeriodException">末尾がピリオドのページの場合(既知の不具合への対応)。</exception>
+ /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
+ public Page GetPageBodyAndTimestamp(string title)
+ {
+ // fileスキームの場合、記事名からファイルに使えない文字をエスケープ
+ // ※ 仕組み的な処理はWebsite側に置きたいが、向こうではタイトルだけを抽出できないので
+ string escapeTitle = title;
+ if (new Uri(this.Location).IsFile)
+ {
+ escapeTitle = FormUtils.ReplaceInvalidFileNameChars(title);
}
- // ルートエレメントまで取得し、フォーマットをチェック
- XmlElement rootElement = xml["mediawiki"];
- if (rootElement == null)
+ // URIを生成
+ Uri uri = new Uri(new Uri(this.Location), StringUtils.FormatDollarVariable(this.ContentApi, escapeTitle));
+
+ // ページのXMLデータをMediaWikiサーバーから取得
+ XElement doc;
+ using (Stream reader = this.WebProxy.GetStream(uri))
{
- // XMLは取得できたが空 or フォーマットが想定外
- throw new InvalidDataException("parse failed : mediawiki element is not found");
+ doc = XElement.Load(reader);
+ }
+
+ // ページエレメントを取得
+ // ※ この問い合わせでは、ページが無い場合も要素自体は毎回ある模様
+ // 一件しか返らないはずなので先頭データを対象とする
+ XElement pe;
+ try
+ {
+ pe = (from query in doc.Elements("query")
+ from pages in query.Elements("pages")
+ from n in pages.Elements("page")
+ select n).First();
+ }
+ catch (InvalidOperationException)
+ {
+ throw new InvalidOperationException("parse failed : query/pages/page element is not found");
}
// ページの解析
- XmlElement pageElement = rootElement["page"];
- if (pageElement == null)
+ if (pe.Attribute("missing") != null)
{
- // ページ無し
+ // missing属性が存在する場合、ページ無し
throw new FileNotFoundException("page not found");
}
// ページ名、ページ本文、最終更新日時
- // ※ 一応、各項目が無くても動作するようにする
- string pageTitle = XmlUtils.InnerText(pageElement["title"], title);
- string text = null;
- DateTime? time = null;
- XmlElement revisionElement = pageElement["revision"];
- if (revisionElement != null)
- {
- text = XmlUtils.InnerText(revisionElement["text"], null);
- XmlElement timeElement = revisionElement["timestamp"];
- if (timeElement != null)
- {
- time = new DateTime?(DateTime.Parse(timeElement.InnerText));
- }
- }
+ var re = (from revisions in pe.Elements("revisions")
+ from n in revisions.Elements("rev")
+ select n).First();
// ページ情報を作成して返す
- return new MediaWikiPage(this, pageTitle, text, time, uri);
+ return new MediaWikiPage(
+ this,
+ XmlUtils.Value(pe.Attribute("title"), title),
+ re.Value,
+ new DateTime?(DateTime.Parse(re.Attribute("timestamp").Value)),
+ uri);
}
/// <summary>
}
this.MetaApi = XmlUtils.InnerText(siteElement.SelectSingleNode("MetaApi"));
- this.ExportPath = XmlUtils.InnerText(siteElement.SelectSingleNode("ExportPath"));
- this.Redirect = XmlUtils.InnerText(siteElement.SelectSingleNode("Redirect"));
+ this.ContentApi = XmlUtils.InnerText(siteElement.SelectSingleNode("ContentApi"));
+ this.InterlanguageApi = XmlUtils.InnerText(siteElement.SelectSingleNode("InterlanguageApi"));
int namespaceId;
if (int.TryParse(XmlUtils.InnerText(siteElement.SelectSingleNode("TemplateNamespace")), out namespaceId))
this.InterwikiPrefixs = prefixs;
}
- // Template:Documentationの設定
- this.DocumentationTemplates = new List<string>();
- foreach (XmlNode docNode in siteElement.SelectNodes("DocumentationTemplates/DocumentationTemplate"))
- {
- this.DocumentationTemplates.Add(docNode.InnerText);
- XmlElement docElement = docNode as XmlElement;
- if (docElement != null)
- {
- // ※ XML上DefaultPageはテンプレートごとに異なる値を持てるが、
- // そうした例を見かけたことがないため、代表で一つの値のみ使用
- // (複数値が持てるのも、リダイレクトが存在するためその対策として)
- this.DocumentationTemplateDefaultPage = docElement.GetAttribute("DefaultPage");
- }
- }
-
this.LinkInterwikiFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LinkInterwikiFormat"));
this.LangFormat = XmlUtils.InnerText(siteElement.SelectSingleNode("LangFormat"));
bool hasLanguagePage;
// MediaWiki固有の情報
// ※ 設定ファイルに初期値を持つものは、プロパティではなく値から出力
writer.WriteElementString("MetaApi", this.metaApi);
- writer.WriteElementString("ExportPath", this.exportPath);
- writer.WriteElementString("Redirect", this.redirect);
+ writer.WriteElementString("ContentApi", this.contentApi);
+ writer.WriteElementString("InterlanguageApi", this.interlanguageApi);
writer.WriteElementString(
"TemplateNamespace",
this.templateNamespace.HasValue ? this.templateNamespace.ToString() : string.Empty);
}
}
- // Template:Documentationの設定
- writer.WriteEndElement();
- writer.WriteStartElement("DocumentationTemplates");
- foreach (string doc in this.DocumentationTemplates)
- {
- writer.WriteStartElement("DocumentationTemplate");
- writer.WriteAttributeString("DefaultPage", this.DocumentationTemplateDefaultPage);
- writer.WriteValue(doc);
- writer.WriteEndElement();
- }
-
writer.WriteEndElement();
writer.WriteElementString("LinkInterwikiFormat", this.LinkInterwikiFormat);
writer.WriteElementString("LangFormat", this.LangFormat);