RSSService.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /*
  2. * Copyright © 2011-2012 Nokia Corporation. All rights reserved.
  3. * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
  4. * Other product and company names mentioned herein may be trademarks
  5. * or trade names of their respective owners.
  6. * See LICENSE.TXT for license information.
  7. */
  8. using System;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Net;
  13. using System.ServiceModel.Syndication;
  14. using System.Text;
  15. using System.Text.RegularExpressions;
  16. using System.Windows;
  17. using System.Windows.Resources;
  18. using System.Xml;
  19. using System.Xml.Linq;
  20. namespace RSSReader.Model
  21. {
  22. /// <summary>
  23. /// RSS feed & item service, handles retrieving and caching of RSS items
  24. /// </summary>
  25. public static class RSSService
  26. {
  27. /// <summary>
  28. /// The cache expire time in minutes
  29. /// </summary>
  30. private static readonly double EXPIRE_TIME = 30;
  31. /// <summary>
  32. /// The main data model
  33. /// </summary>
  34. public static RSSCache CachedItems { get; set; }
  35. /// <summary>
  36. /// Currently selected RSSItem
  37. /// </summary>
  38. public static RSSItem SelectedItem { get; set; }
  39. /// <summary>
  40. /// Returns global instance of the RSS cache
  41. /// </summary>
  42. /// <returns>RSS cache instance</returns>
  43. public static RSSCache GetDataModel()
  44. {
  45. return (RSSCache)Application.Current.Resources["RSSPagesDataSource"];
  46. }
  47. /// <summary>
  48. /// Initializes the feeds. Called from App.xaml.cs
  49. /// when the application starts
  50. /// </summary>
  51. public static void InitializeFeeds()
  52. {
  53. RSSCache pages = GetDataModel();
  54. CachedItems = RSSCache.Load();
  55. foreach (RSSPage page in CachedItems.Cache)
  56. {
  57. pages.Cache.Add(page);
  58. }
  59. CachedItems = pages;
  60. if (CachedItems.Cache.Count == 0)
  61. {
  62. FirstLaunch();
  63. }
  64. }
  65. /// <summary>
  66. /// Persists the cache to disk
  67. /// </summary>
  68. public static void PersistCache()
  69. {
  70. CachedItems.Save();
  71. }
  72. /// <summary>
  73. /// Method for doing initialization related to first launch of the application
  74. /// </summary>
  75. private static void FirstLaunch()
  76. {
  77. // On the first launch, just add everything from the OPML file
  78. StreamResourceInfo xml = App.GetResourceStream(new Uri("/RSSReader;component/sample-opml.xml", UriKind.Relative));
  79. List<RSSPage> rssPages = ParseOPML(xml.Stream);
  80. // Add the pages to the data model
  81. CachedItems.AddPages(rssPages);
  82. GetFeedImages();
  83. }
  84. /// <summary>
  85. /// Return a RSS page from cache based on id
  86. /// </summary>
  87. /// <param name="pageId">Id of the page to return</param>
  88. /// <returns>RSS page</returns>
  89. public static RSSPage GetRSSPage(int pageId)
  90. {
  91. return CachedItems.Cache[pageId];
  92. }
  93. /// <summary>
  94. /// Returns a RSS feed from cache based on page id and feed id
  95. /// </summary>
  96. /// <param name="pageId">Id of the page where the feed resides</param>
  97. /// <param name="feedId">Id of the feed</param>
  98. /// <returns>RSS feed</returns>
  99. public static RSSFeed GetRSSFeed(int pageId, int feedId)
  100. {
  101. RSSFeed feed = null;
  102. int count = 0;
  103. foreach (RSSFeed f in CachedItems.Cache[pageId].Feeds)
  104. {
  105. if (!f.IsVisible)
  106. {
  107. continue;
  108. }
  109. else
  110. {
  111. if (count == feedId)
  112. {
  113. feed = f;
  114. break;
  115. }
  116. count++;
  117. }
  118. }
  119. return feed;
  120. }
  121. /// <summary>
  122. /// Method for retrieving RSS feed information, especially the image of the feed
  123. /// </summary>
  124. /// <param name="feed">Feed whose information to fetch</param>
  125. /// <param name="onGetRSSFeedImageUriCompleted">Action to take when information has been retrieved</param>
  126. /// <param name="onError">Action to take when an error occurs</param>
  127. public static void GetRSSFeedImageUri(RSSFeed feed, Action<Uri, RSSFeed> onGetRSSFeedImageUriCompleted = null, Action<Exception> onError = null)
  128. {
  129. WebClient webClient = new WebClient();
  130. webClient.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
  131. {
  132. try
  133. {
  134. if (e.Error != null)
  135. {
  136. if (onError != null)
  137. {
  138. onError(e.Error);
  139. }
  140. return;
  141. }
  142. XmlReader response = XmlReader.Create(e.Result);
  143. SyndicationFeed rssFeed = SyndicationFeed.Load(response);
  144. if (onGetRSSFeedImageUriCompleted != null)
  145. {
  146. onGetRSSFeedImageUriCompleted(rssFeed.ImageUrl, feed);
  147. }
  148. }
  149. catch (Exception error)
  150. {
  151. if (onError != null)
  152. {
  153. onError(error);
  154. }
  155. }
  156. };
  157. webClient.OpenReadAsync(new Uri(feed.URL));
  158. }
  159. /// <summary>
  160. /// Gets the RSS items
  161. /// </summary>
  162. /// <param name="feed">The RSS feed</param>
  163. /// <param name="onGetRSSItemsCompleted">Callback on complete</param>
  164. /// <param name="onError">Callback for errors</param>
  165. public static void GetRSSItems(int pageId, int feedId, bool useCache, Action<IEnumerable<RSSItem>> onGetRSSItemsCompleted = null, Action<Exception> onError = null)
  166. {
  167. DateTime validUntilThis = DateTime.Now;
  168. validUntilThis = validUntilThis.AddMinutes(-EXPIRE_TIME);
  169. RSSFeed feed = GetRSSFeed(pageId, feedId);
  170. bool feedExists = (feed != null);
  171. bool feedHasItems = (feedExists && feed.Items != null && feed.Items.Count > 0);
  172. bool cacheHasExpired = (DateTime.Compare(validUntilThis, feed.Timestamp) > 0);
  173. // First check if this valid items for this feed exist in the cache already
  174. if (feedExists &&
  175. feedHasItems &&
  176. useCache &&
  177. !cacheHasExpired &&
  178. onGetRSSItemsCompleted != null)
  179. {
  180. onGetRSSItemsCompleted(feed.Items);
  181. }
  182. // Items not found in cache, perform a web request
  183. else
  184. {
  185. WebClient webClient = new WebClient();
  186. webClient.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
  187. {
  188. try
  189. {
  190. if (e.Error != null)
  191. {
  192. if (onError != null)
  193. {
  194. onError(e.Error);
  195. }
  196. return;
  197. }
  198. List<RSSItem> rssItems = new List<RSSItem>();
  199. XmlReader response = XmlReader.Create(e.Result);
  200. SyndicationFeed rssFeed = SyndicationFeed.Load(response);
  201. foreach (SyndicationItem syndicationItem in rssFeed.Items)
  202. {
  203. // Clean the title in case it includes line breaks
  204. string title = syndicationItem.Title.Text;
  205. title = title.Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", " ");
  206. // Check for "enclosure" tag that references an image
  207. string image = "";
  208. foreach (SyndicationLink link in syndicationItem.Links)
  209. {
  210. if (link.RelationshipType == "enclosure" &&
  211. link.MediaType.StartsWith( "image/" ) )
  212. {
  213. image = link.Uri.ToString();
  214. break;
  215. }
  216. }
  217. RSSItem rssItem = new RSSItem(
  218. title,
  219. syndicationItem.Summary.Text,
  220. syndicationItem.Links[0].Uri.AbsoluteUri,
  221. syndicationItem.PublishDate,
  222. feed,
  223. image);
  224. rssItems.Add(rssItem);
  225. }
  226. // Cache the results
  227. feed.Items = rssItems;
  228. feed.Timestamp = DateTime.Now;
  229. // Call the callback with the list of RSSItems
  230. if (onGetRSSItemsCompleted != null)
  231. {
  232. onGetRSSItemsCompleted(rssItems);
  233. }
  234. }
  235. catch (Exception error)
  236. {
  237. if (onError != null)
  238. {
  239. onError(error);
  240. }
  241. }
  242. };
  243. webClient.OpenReadAsync(new Uri(feed.URL));
  244. }
  245. }
  246. /// <summary>
  247. /// Method to initialize fetching of data & images of RSS feeds
  248. /// </summary>
  249. private static void GetFeedImages()
  250. {
  251. bool error = false;
  252. // Get images
  253. foreach (RSSPage page in CachedItems.Cache)
  254. {
  255. foreach (RSSFeed feed in page.Feeds)
  256. {
  257. GetRSSFeedImageUri(
  258. feed,
  259. (uri, rssFeed) =>
  260. {
  261. // Assign the URI of the image to the feed if it exists
  262. // Otherwise just ignore it, and the default image will be used
  263. if (uri != null && Regex.IsMatch(uri.ToString(), "(jpg|jpeg|png)$"))
  264. {
  265. rssFeed.ImageURL = uri.ToString();
  266. }
  267. },
  268. (exception) =>
  269. {
  270. error = true;
  271. });
  272. }
  273. }
  274. if (error)
  275. {
  276. MessageBox.Show("Application failed to retrieve content from server. Please check your network connectivity.");
  277. }
  278. }
  279. /// <summary>
  280. /// Parses the OPML file and returns it as a list of RSSPages
  281. /// </summary>
  282. /// <param name="stream">The OPML file stream</param>
  283. /// <returns>List of RSSPages</returns>
  284. private static List<RSSPage> ParseOPML(Stream stream)
  285. {
  286. RSSCache cache = GetDataModel();
  287. List<RSSPage> rssCategories = null;
  288. if (cache.Cache.Count == 0)
  289. {
  290. XDocument xDocument = XDocument.Load(stream);
  291. // XML parsed using Linq
  292. rssCategories = (from outline in xDocument.Descendants("outline")
  293. where outline.Attribute("xmlUrl") == null
  294. select new RSSPage()
  295. {
  296. Title = outline.Attribute("title").Value,
  297. Feeds = (from o in outline.Descendants("outline")
  298. select new RSSFeed
  299. {
  300. URL = o.Attribute("xmlUrl").Value,
  301. ImageURL = "/Resources/rss-icon.jpg",
  302. Title = o.Attribute("title").Value,
  303. IsVisible = true
  304. }).ToList<RSSFeed>()
  305. }).ToList<RSSPage>();
  306. }
  307. return rssCategories;
  308. }
  309. /// <summary>
  310. /// Creates the primitive HTML page from the article text.
  311. /// Displays the first img-tag found (if any) and strips
  312. /// other HTML away.
  313. /// </summary>
  314. /// <param name="article">Article text</param>
  315. /// <returns>HTML page</returns>
  316. public static String CreateArticleHTML(RSSItem item)
  317. {
  318. String start = @"<html>
  319. <head>
  320. <meta name=""Viewport"" content=""width=480; user-scaleable=no; initial-scale=1.0"" />
  321. <style>
  322. * { background: #fff !important; color: #000 !important; width: auto !important; font-size: 1em !important; }
  323. h1 { font-size: 1.125em !important }
  324. img { max-width: 200px !important; display: block; margin: 0 auto; }
  325. .timestamp { font-size: 0.875em !important; font-style: italic; display: block; margin-top: 2em; }
  326. </style>
  327. </head>
  328. <body>
  329. <table>
  330. <tr><td>";
  331. String end = @"</td></tr></table></body></html>";
  332. String headerTemplate = @"<tr><td><h1>{0}</h1></td></tr>";
  333. String imageTemplate = @"<tr><td>{0}";
  334. String articleTemplate = @"<span class=""article"">{0}</span></td></tr>";
  335. String timestampTemplate = @"<tr><td><span class=""timestamp"">{0}</span></td></tr>";
  336. String header = String.Format(headerTemplate, item.Title);
  337. // If RSS item references an image in "enclosure" tag, use it. Otherwise try to extract the first image from the article
  338. // Note about images: if we're using an image retrieved from closure, or the first image we retrieve
  339. // from the article, doesn't specify image width or height, IE will not apply max-width property. This will
  340. // result in us showing larger pictures than intended. This could be worked around by using JavaScript resize
  341. // the images.
  342. String image = "";
  343. if (item.Image != null && item.Image != "")
  344. {
  345. // Image from enclosure
  346. String imgTemplate = @"<img src=""{0}"" />";
  347. image = String.Format( imageTemplate, String.Format(imgTemplate, item.Image ));
  348. }
  349. else
  350. {
  351. Regex img = new Regex("<img.*?>", RegexOptions.IgnoreCase);
  352. MatchCollection matches = img.Matches(item.Text);
  353. if (matches.Count > 0 && matches[0] != null)
  354. {
  355. // Image from article content
  356. image = String.Format(imageTemplate, matches[0].Groups[0].Value);
  357. }
  358. else
  359. {
  360. // No image
  361. image = String.Format(imageTemplate, "");
  362. }
  363. }
  364. String article = String.Format( articleTemplate, HttpUtility.HtmlDecode(Regex.Replace(item.Text, "<.*?>", "") ) );
  365. String timestamp = String.Format(timestampTemplate, item.Datestamp);
  366. StringBuilder builder = new StringBuilder();
  367. builder.Append(start);
  368. builder.Append(header);
  369. builder.Append(image);
  370. builder.Append(article);
  371. builder.Append(timestamp);
  372. builder.Append(end);
  373. return builder.ToString();
  374. }
  375. }
  376. }