OSDN Git Service

ca88d06b54382b3ebb6bb05ddc21e11e7237714f
[wptscs/wpts.git] / Wptscs / Websites / MediaWikiPage.cs
1 // ================================================================================================
2 // <summary>
3 //      MediaWikiのページをあらわすモデルクラスソース</summary>
4 //
5 // <copyright file="MediaWikiPage.cs" company="honeplusのメモ帳">
6 //      Copyright (C) 2013 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.Linq;
18     using Honememo.Models;
19     using Honememo.Utilities;
20     using Honememo.Wptscs.Parsers;
21
22     /// <summary>
23     /// MediaWikiのページをあらわすモデルクラスです。
24     /// </summary>
25     public class MediaWikiPage : Page
26     {
27         #region コンストラクタ
28
29         /// <summary>
30         /// 指定されたMediaWikiの渡されたタイトル, 本文, タイムスタンプのページを作成。
31         /// </summary>
32         /// <param name="website">ページが所属するウェブサイト。</param>
33         /// <param name="title">ページタイトル。</param>
34         /// <param name="text">ページの本文。</param>
35         /// <param name="timestamp">ページのタイムスタンプ。</param>
36         /// <param name="uri">ページのURI。</param>
37         /// <exception cref="ArgumentNullException"><paramref name="website"/>または<paramref name="title"/>が<c>null</c>の場合。</exception>
38         /// <exception cref="ArgumentException"><paramref name="title"/>が空の文字列の場合。</exception>
39         public MediaWikiPage(MediaWiki website, string title, string text, DateTime? timestamp, Uri uri)
40             : base(website, title, text, timestamp, uri)
41         {
42             this.Interlanguages = new Dictionary<string, string>();
43         }
44
45         /// <summary>
46         /// 指定されたMediaWikiの渡されたタイトル, 本文のページを作成。
47         /// </summary>
48         /// <param name="website">ページが所属するウェブサイト。</param>
49         /// <param name="title">ページタイトル。</param>
50         /// <param name="text">ページの本文。</param>
51         /// <remarks>ページのタイムスタンプ, URIには<c>null</c>を設定。</remarks>
52         /// <exception cref="ArgumentNullException"><paramref name="website"/>または<paramref name="title"/>が<c>null</c>の場合。</exception>
53         /// <exception cref="ArgumentException"><paramref name="title"/>が空の文字列の場合。</exception>
54         public MediaWikiPage(MediaWiki website, string title, string text)
55             : base(website, title, text)
56         {
57             this.Interlanguages = new Dictionary<string, string>();
58         }
59
60         /// <summary>
61         /// 指定されたMediaWikiの渡されたタイトルのページを作成。
62         /// </summary>
63         /// <param name="website">ページが所属するウェブサイト。</param>
64         /// <param name="title">ページタイトル。</param>
65         /// <remarks>ページの本文, タイムスタンプ, URIには<c>null</c>を設定。</remarks>
66         /// <exception cref="ArgumentNullException"><paramref name="website"/>または<paramref name="title"/>が<c>null</c>の場合。</exception>
67         /// <exception cref="ArgumentException"><paramref name="title"/>が空の文字列の場合。</exception>
68         public MediaWikiPage(MediaWiki website, string title)
69             : base(website, title)
70         {
71             this.Interlanguages = new Dictionary<string, string>();
72         }
73
74         #endregion
75
76         #region プロパティ
77
78         /// <summary>
79         /// ページが所属するウェブサイト。
80         /// </summary>
81         public new MediaWiki Website
82         {
83             get
84             {
85                 return base.Website as MediaWiki;
86             }
87
88             protected set
89             {
90                 base.Website = value;
91             }
92         }
93
94         /// <summary>
95         /// ページの本文。
96         /// </summary>
97         /// <remarks>
98         /// get時に値が設定されていない場合、サーバーから本文を取得する。
99         /// ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。
100         /// </remarks>
101         public override string Text
102         {
103             get
104             {
105                 if (base.Text == null)
106                 {
107                     this.SetPageBodyAndTimestamp();
108                 }
109
110                 return base.Text;
111             }
112
113             protected set
114             {
115                 base.Text = value;
116             }
117         }
118
119         /// <summary>
120         /// ページのタイムスタンプ。
121         /// </summary>
122         /// <remarks>
123         /// get時に値が設定されていない場合、サーバーからタイムスタンプを取得する。
124         /// ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。
125         /// </remarks>
126         public override DateTime? Timestamp
127         {
128             get
129             {
130                 if (base.Timestamp == null)
131                 {
132                     this.SetPageBodyAndTimestamp();
133                 }
134
135                 return base.Timestamp;
136             }
137
138             protected set
139             {
140                 base.Timestamp = value;
141             }
142         }
143
144         /// <summary>
145         /// リダイレクト元の記事名。
146         /// </summary>
147         public string Redirect
148         {
149             get;
150             set;
151         }
152
153         /// <summary>
154         /// 言語間リンクの対応表。
155         /// </summary>
156         protected IDictionary<string, string> Interlanguages
157         {
158             get;
159             private set;
160         }
161
162         #endregion
163
164         #region 静的メソッド
165
166         /// <summary>
167         /// APIから取得した言語間リンク情報から、ページを取得する。
168         /// </summary>
169         /// <param name="website">ページが所属するウェブサイト。</param>
170         /// <param name="uri">クエリーを取得したURI。</param>
171         /// <param name="query">
172         /// MediaWiki APIから取得した言語間リンク情報。
173         /// <c>pages/page (ns="0"), redirects/r</c> を使用する。
174         /// </param>
175         /// <returns>言語間リンク情報から取得したページ。</returns>
176         /// <exception cref="InvalidDataException">XMLのフォーマットが想定外。</exception>
177         /// <exception cref="NullReferenceException">XMLのフォーマットが想定外。</exception>
178         /// <exception cref="FileNotFoundException">ページが存在しない場合。</exception>
179         public static MediaWikiPage GetFromQuery(MediaWiki website, Uri uri, XElement query)
180         {
181             // ページエレメントを取得
182             // ※ この問い合わせでは、ページが無い場合も要素自体は毎回ある模様
183             //    一件しか返らないはずなので先頭データを対象とする
184             XElement pe;
185             try
186             {
187                 pe = (from pages in query.Elements("pages")
188                       from n in pages.Elements("page")
189                       select n).First();
190             }
191             catch (InvalidOperationException)
192             {
193                 throw new InvalidOperationException("parse failed : pages/page element is not found");
194             }
195
196             // ページの解析
197             if (pe.Attribute("missing") != null)
198             {
199                 // missing属性が存在する場合、ページ無し
200                 throw new FileNotFoundException("page not found");
201             }
202
203             // ページ名、URI、リダイレクト、言語間リンク情報を詰めたオブジェクトを返す
204             // ※ ページ名以外はデータがあれば格納
205             MediaWikiPage page = new MediaWikiPage(website, pe.Attribute("title").Value);
206             page.Uri = uri;
207             var le = from links in pe.Elements("langlinks")
208                      from n in links.Elements("ll")
209                      select n;
210             foreach (var ll in le)
211             {
212                 page.Interlanguages.Add(ll.Attribute("lang").Value, ll.Value);
213             }
214
215             var re = from redirects in query.Elements("redirects")
216                      from n in redirects.Elements("r")
217                      select n;
218             foreach (var r in re)
219             {
220                 page.Redirect = r.Attribute("from").Value;
221             }
222
223             return page;
224         }
225
226         #endregion
227
228         #region 公開メソッド
229
230         /// <summary>
231         /// 指定された言語コードへの言語間リンクを返す。
232         /// </summary>
233         /// <param name="code">言語コード。</param>
234         /// <returns>言語間リンク。見つからない場合は<c>null</c>。</returns>
235         public virtual string GetInterlanguage(string code)
236         {
237             // 対応表から返す
238             string interlanguage;
239             if (this.Interlanguages.TryGetValue(code, out interlanguage))
240             {
241                 return interlanguage;
242             }
243
244             return null;
245         }
246
247         /// <summary>
248         /// ページがリダイレクトかをチェック。
249         /// </summary>
250         /// <returns><c>true</c> リダイレクト。</returns>
251         public bool IsRedirect()
252         {
253             return this.Redirect != null;
254         }
255
256         /// <summary>
257         /// ページがテンプレートかをチェック。
258         /// </summary>
259         /// <returns><c>true</c> テンプレート。</returns>
260         public bool IsTemplate()
261         {
262             // ページ名がカテゴリー(Category:等で始まる)かをチェック
263             return this.IsNamespacePage(this.Website.TemplateNamespace);
264         }
265
266         /// <summary>
267         /// ページがカテゴリーかをチェック。
268         /// </summary>
269         /// <returns><c>true</c> カテゴリー。</returns>
270         public bool IsCategory()
271         {
272             // ページ名がカテゴリー(Category:等で始まる)かをチェック
273             return this.IsNamespacePage(this.Website.CategoryNamespace);
274         }
275
276         /// <summary>
277         /// ページが画像かをチェック。
278         /// </summary>
279         /// <returns><c>true</c> 画像。</returns>
280         public bool IsFile()
281         {
282             // ページ名がファイル(Image:等で始まる)かをチェック
283             return this.IsNamespacePage(this.Website.FileNamespace);
284         }
285
286         /// <summary>
287         /// ページが標準名前空間かをチェック。
288         /// </summary>
289         /// <returns><c>true</c> 標準名前空間。</returns>
290         public bool IsMain()
291         {
292             // ページ名が標準名前空間以外のなんらかの名前空間かをチェック
293             return !this.Website.IsNamespace(this.Title);
294         }
295
296         /// <summary>
297         /// このページ内のリンクの記事名(サブページ等)を完全な記事名にする。
298         /// </summary>
299         /// <param name="link">このページ内のリンク。</param>
300         /// <returns>変換した記事名。</returns>
301         public virtual string Normalize(MediaWikiLink link)
302         {
303             string title = StringUtils.DefaultString(link.Title);
304             if (link.IsSubpage())
305             {
306                 // サブページ関連の正規化
307                 title = this.NormalizeSubpage(title);
308             }
309             else if (link is MediaWikiTemplate)
310             {
311                 // テンプレート関連の正規化(サブページの場合は不要)
312                 title = this.NormalizeTemplate((MediaWikiTemplate)link);
313             }
314
315             return title;
316         }
317
318         #endregion
319
320         #region 内部処理用メソッド
321
322         /// <summary>
323         /// ページの本文・タイムスタンプをサーバーから取得。
324         /// </summary>
325         /// <exception cref="Honememo.Wptscs.Utilities.EndPeriodException">
326         /// 末尾がピリオドのページの場合(既知の不具合への対応)。
327         /// </exception>
328         /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
329         protected void SetPageBodyAndTimestamp()
330         {
331             Page body = this.Website.GetPageBodyAndTimestamp(this.Title);
332             this.Text = body.Text;
333             this.Timestamp = body.Timestamp;
334             this.Uri = body.Uri;
335         }
336
337         /// <summary>
338         /// ページが指定された番号の名前空間に所属するかをチェック。
339         /// </summary>
340         /// <param name="id">名前空間のID。</param>
341         /// <returns>所属する場合<c>true</c>。</returns>
342         /// <remarks>大文字小文字は区別しない。</remarks>
343         protected bool IsNamespacePage(int id)
344         {
345             // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
346             int index = this.Title.IndexOf(':');
347             if (index < 0)
348             {
349                 return false;
350             }
351
352             string title = this.Title.Remove(index);
353             IgnoreCaseSet prefixes = this.Website.Namespaces[id];
354             return prefixes != null && prefixes.Contains(title);
355         }
356
357         /// <summary>
358         /// このページ内のリンクのサブページ形式の記事名を完全な記事名にする。
359         /// </summary>
360         /// <param name="subpage">サブページ形式の記事名。</param>
361         /// <returns>変換した記事名。</returns>
362         private string NormalizeSubpage(string subpage)
363         {
364             string title = subpage;
365             if (subpage.StartsWith("/"))
366             {
367                 // サブページ(子)へのリンクの場合、親の記事名を補填
368                 title = this.Title + subpage;
369             }
370             else if (subpage.StartsWith("../"))
371             {
372                 // サブページ(親・兄弟・おじ)へのリンクの場合、各階層の記事名を補填
373                 string subtitle = subpage;
374                 int count = 0;
375                 while (subtitle.StartsWith("../"))
376                 {
377                     // 階層をカウント
378                     ++count;
379                     subtitle = subtitle.Substring("../".Length);
380                 }
381
382                 // 指定された階層の記事名を補填
383                 string parent = this.Title;
384                 for (int i = 0; i < count; i++)
385                 {
386                     int index = parent.LastIndexOf('/');
387                     if (index < 0)
388                     {
389                         // 階層が足りない場合、補填できないので元の記事名を返す
390                         return subpage;
391                     }
392
393                     parent = parent.Remove(index);
394                 }
395
396                 // 親記事名と子記事名(あれば)を結合して完了
397                 title = parent;
398                 if (!string.IsNullOrEmpty(subtitle))
399                 {
400                     title += "/" + subtitle;
401                 }
402             }
403
404             // 末尾に / が付いている場合、表示名に関する指定なので除去
405             return title.TrimEnd('/');
406         }
407
408         /// <summary>
409         /// このページ内のテンプレート形式のリンクの記事名を完全な記事名にする。
410         /// </summary>
411         /// <param name="template">このページ内のテンプレート形式のリンク。</param>
412         /// <returns>変換した記事名。</returns>
413         private string NormalizeTemplate(MediaWikiTemplate template)
414         {
415             if (template.IsColon || this.Website.IsNamespace(template.Title)
416                 || this.Website.IsMagicWord(template.Title))
417             {
418                 // 標準名前空間が指定されている(先頭にコロン)
419                 // または何かしらの名前空間が指定されている、
420                 // またはテンプレート呼び出しではなくマジックナンバーの場合、補完不要
421                 return template.Title;
422             }
423
424             // 補完する必要がある場合、名前空間のプレフィックス(Template等)を取得
425             string prefix = this.GetTemplatePrefix();
426             if (string.IsNullOrEmpty(prefix))
427             {
428                 // 名前空間の設定が存在しない場合、何も出来ないため終了
429                 return template.Title;
430             }
431
432             // 頭にプレフィックスを付けた記事名を返す
433             return prefix + ":" + template.Title;
434         }
435
436         /// <summary>
437         /// テンプレート名前空間のプレフィックスを取得。
438         /// </summary>
439         /// <returns>プレフィックス。取得できない場合<c>null</c></returns>
440         private string GetTemplatePrefix()
441         {
442             ISet<string> prefixes = this.Website.Namespaces[this.Website.TemplateNamespace];
443             if (prefixes != null)
444             {
445                 return prefixes.FirstOrDefault();
446             }
447
448             return null;
449         }
450
451         #endregion
452     }
453 }