OSDN Git Service

#30840 画面のデザインが崩れていたのを修正,
[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         /// <remarks>ページの取得に失敗した場合(通信エラーなど)は、その状況に応じた例外を投げる。</remarks>
326         protected void SetPageBodyAndTimestamp()
327         {
328             Page body = this.Website.GetPageBodyAndTimestamp(this.Title);
329             this.Text = body.Text;
330             this.Timestamp = body.Timestamp;
331             this.Uri = body.Uri;
332         }
333
334         /// <summary>
335         /// ページが指定された番号の名前空間に所属するかをチェック。
336         /// </summary>
337         /// <param name="id">名前空間のID。</param>
338         /// <returns>所属する場合<c>true</c>。</returns>
339         /// <remarks>大文字小文字は区別しない。</remarks>
340         protected bool IsNamespacePage(int id)
341         {
342             // 指定された記事名がカテゴリー(Category:等で始まる)かをチェック
343             int index = this.Title.IndexOf(':');
344             if (index < 0)
345             {
346                 return false;
347             }
348
349             string title = this.Title.Remove(index);
350             IgnoreCaseSet prefixes = this.Website.Namespaces[id];
351             return prefixes != null && prefixes.Contains(title);
352         }
353
354         /// <summary>
355         /// このページ内のリンクのサブページ形式の記事名を完全な記事名にする。
356         /// </summary>
357         /// <param name="subpage">サブページ形式の記事名。</param>
358         /// <returns>変換した記事名。</returns>
359         private string NormalizeSubpage(string subpage)
360         {
361             string title = subpage;
362             if (subpage.StartsWith("/"))
363             {
364                 // サブページ(子)へのリンクの場合、親の記事名を補填
365                 title = this.Title + subpage;
366             }
367             else if (subpage.StartsWith("../"))
368             {
369                 // サブページ(親・兄弟・おじ)へのリンクの場合、各階層の記事名を補填
370                 string subtitle = subpage;
371                 int count = 0;
372                 while (subtitle.StartsWith("../"))
373                 {
374                     // 階層をカウント
375                     ++count;
376                     subtitle = subtitle.Substring("../".Length);
377                 }
378
379                 // 指定された階層の記事名を補填
380                 string parent = this.Title;
381                 for (int i = 0; i < count; i++)
382                 {
383                     int index = parent.LastIndexOf('/');
384                     if (index < 0)
385                     {
386                         // 階層が足りない場合、補填できないので元の記事名を返す
387                         return subpage;
388                     }
389
390                     parent = parent.Remove(index);
391                 }
392
393                 // 親記事名と子記事名(あれば)を結合して完了
394                 title = parent;
395                 if (!string.IsNullOrEmpty(subtitle))
396                 {
397                     title += "/" + subtitle;
398                 }
399             }
400
401             // 末尾に / が付いている場合、表示名に関する指定なので除去
402             return title.TrimEnd('/');
403         }
404
405         /// <summary>
406         /// このページ内のテンプレート形式のリンクの記事名を完全な記事名にする。
407         /// </summary>
408         /// <param name="template">このページ内のテンプレート形式のリンク。</param>
409         /// <returns>変換した記事名。</returns>
410         private string NormalizeTemplate(MediaWikiTemplate template)
411         {
412             if (template.IsColon || this.Website.IsNamespace(template.Title)
413                 || this.Website.IsMagicWord(template.Title))
414             {
415                 // 標準名前空間が指定されている(先頭にコロン)
416                 // または何かしらの名前空間が指定されている、
417                 // またはテンプレート呼び出しではなくマジックナンバーの場合、補完不要
418                 return template.Title;
419             }
420
421             // 補完する必要がある場合、名前空間のプレフィックス(Template等)を取得
422             string prefix = this.GetTemplatePrefix();
423             if (string.IsNullOrEmpty(prefix))
424             {
425                 // 名前空間の設定が存在しない場合、何も出来ないため終了
426                 return template.Title;
427             }
428
429             // 頭にプレフィックスを付けた記事名を返す
430             return prefix + ":" + template.Title;
431         }
432
433         /// <summary>
434         /// テンプレート名前空間のプレフィックスを取得。
435         /// </summary>
436         /// <returns>プレフィックス。取得できない場合<c>null</c></returns>
437         private string GetTemplatePrefix()
438         {
439             ISet<string> prefixes = this.Website.Namespaces[this.Website.TemplateNamespace];
440             if (prefixes != null)
441             {
442                 return prefixes.FirstOrDefault();
443             }
444
445             return null;
446         }
447
448         #endregion
449     }
450 }