///
/// Copyright © 2003-2008 JetBrains s.r.o.
/// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder).
///
using System;
using System.Net;
using System.Windows.Forms;
using JetBrains.Omea.Base;
using JetBrains.Omea.Containers;
using JetBrains.Omea.HttpTools;
using JetBrains.Omea.OpenAPI;
namespace JetBrains.Omea.RSSPlugin
{
///
/// Queue for updating RSS feeds, allowing to limit the number of feeds updated at the same time.
///
internal class FeedUpdateQueue
{
private int _updatingCount = 0;
private int _maxCount = 10;
private PriorityQueue _pendingFeeds = new PriorityQueue();
internal event ResourceEventHandler FeedUpdated;
///
/// Fires when either all the feeds queued for update are thru,
/// or a single feed has been updated without ever getting into the queue.
///
internal event EventHandler QueueGotEmpty;
internal FeedUpdateQueue()
{
// if we're using a proxy, all the connections will go through one ServicePoint,
// so it does not make sense to set a queue size larger than the maximum connection
// limit on the proxy
WebProxy defaultProxy = GlobalProxySelection.Select as WebProxy;
if ( defaultProxy != null && defaultProxy.Address != null )
{
_maxCount = ServicePointManager.DefaultConnectionLimit;
}
}
///
/// Initiates an update of the selected feed at a time determined by its
/// last update time and update frequency.
///
public void ScheduleFeedUpdate( IResource feed )
{
string updatePeriod = feed.GetStringProp( Props.UpdatePeriod );
int updateFrequency = feed.GetIntProp( Props.UpdateFrequency );
if ( updateFrequency > 0 )
{
DateTime dt = feed.GetDateProp( Props.LastUpdateTime );
switch ( updatePeriod )
{
case UpdatePeriods.Daily:
dt = dt.AddDays( updateFrequency );
break;
case UpdatePeriods.Weekly:
dt = dt.AddDays( 7 * updateFrequency );
break;
case UpdatePeriods.Minutely:
dt = dt.AddMinutes( updateFrequency );
break;
default:
dt = dt.AddHours( updateFrequency );
break;
}
Core.NetworkAP.QueueJobAt( dt, new ResourceDelegate( QueueFeedUpdate ), feed );
}
}
private delegate void QueueFeedUpdateDelegate( IResource feed, int attempt );
///
/// Queues the specified feed for an immediate update.
///
public void QueueFeedUpdate( IResource feed, JobPriority jobPriority )
{
QueueFeedUpdate( feed, 0, jobPriority );
}
public void QueueFeedUpdate( IResource feed )
{
QueueFeedUpdate( feed, JobPriority.Normal );
}
private class PendingFeed
{
private int _attempt;
private IResource _feed;
public PendingFeed( IResource feed, int attempt )
{
_feed = feed;
_attempt = attempt;
}
public IResource Feed { get { return _feed; } }
public int Attempt { get { return _attempt; } }
}
private void QueueFeedUpdate( IResource feed, int attempt )
{
QueueFeedUpdate( feed, attempt, JobPriority.Normal );
}
private void QueueFeedUpdate( IResource feed, int attempt, JobPriority jobPriority )
{
if ( feed.Type != "RSSFeed" )
{
throw new ArgumentException( "Invalid resource type for QueueFeedUpdate: " + feed.Type );
}
if ( !HttpReader.IsSupportedProtocol( feed.GetPropText( Props.URL ) ) )
{
return;
}
// Do not update feeds which were manually set into
// hybernating state.
if( feed.HasProp( Props.IsPaused ) )
{
return;
}
lock ( this )
{
if ( _updatingCount >= _maxCount )
{
_pendingFeeds.Push( (int) jobPriority, new PendingFeed( feed, attempt ) );
}
else
{
RSSUnitOfWork uow = new RSSUnitOfWork( feed, true, false );
uow.Attempts = attempt;
uow.ParseDone += new EventHandler( OnRSSParseDone );
Core.NetworkAP.QueueJob( jobPriority, uow );
_updatingCount++;
}
}
if ( feed.HasProp( Props.AutoUpdateComments ) )
{
foreach ( IResource commentFeed in feed.GetLinksTo( null, Props.FeedComment2Feed ) )
{
if ( NeedUpdate( commentFeed ) )
{
QueueFeedUpdate( commentFeed );
}
}
}
}
private bool NeedUpdate( IResource commentFeed )
{
Guard.NullArgument( commentFeed, "commentFeed" );
IResourceList rssItems = commentFeed.GetLinksOfType( "RSSItem", Props.RSSItem );
rssItems.Sort( new SortSettings( Core.Props.Date, false ) );
IResource lastItem;
if ( rssItems.Count == 0 )
{
lastItem = commentFeed.GetLinkProp( Props.ItemCommentFeed );
}
else
{
lastItem = rssItems[ 0 ];
}
DateTime dt = lastItem.GetDateProp( Core.Props.Date );
string updatePeriod = Settings.StopUpdatePeriod;
int updateFrequency = Settings.StopUpdateFrequency;
if ( updateFrequency > 0 )
{
switch ( updatePeriod )
{
case UpdatePeriods.Daily:
dt = dt.AddDays( updateFrequency );
break;
case UpdatePeriods.Weekly:
dt = dt.AddDays( 7 * updateFrequency );
break;
case UpdatePeriods.Minutely:
dt = dt.AddMinutes( updateFrequency );
break;
default:
dt = dt.AddHours( updateFrequency );
break;
}
}
return DateTime.Now < dt;
}
public static void CleanupCommentFeed( RSSUnitOfWork uow )
{
IResource commentItem = uow.Feed.GetLinkProp( Props.ItemCommentFeed );
if ( commentItem == null )
{
return;
}
IResourceList comments = commentItem.GetLinksTo( "RSSItem", Props.ItemComment );
int commentCount = comments.Count;
foreach ( IResource existingComment in comments )
{
if ( existingComment.HasProp( Props.Transient ) )
{
--commentCount;
new ResourceProxy( existingComment ).Delete();
}
else if ( commentItem.HasProp( Core.Props.IsDeleted ) )
{
new ResourceProxy( existingComment ).SetProp( Core.Props.IsDeleted, true );
}
}
new ResourceProxy( commentItem ).SetProp( Props.CommentCount, commentCount );
new ResourceProxy( uow.Feed ).DeleteProp( Props.UpdateStatus );
}
private void OnRSSParseDone( object sender, EventArgs e )
{
RSSUnitOfWork uow = (RSSUnitOfWork)sender;
uow.ParseDone -= new EventHandler( OnRSSParseDone );
if ( uow.Status == RSSWorkStatus.FeedDeleted )
{
return;
}
ResourceProxy proxy = new ResourceProxy( uow.Feed );
proxy.BeginUpdate();
proxy.SetProp( Props.LastUpdateTime, DateTime.Now );
if ( !uow.Feed.HasProp( Core.Props.Parent ) && !uow.Feed.HasProp( Props.ItemCommentFeed ) )
{
proxy.SetProp( Core.Props.Parent, RSSPlugin.RootFeedGroup );
}
if ( uow.Status == RSSWorkStatus.HTTPError || uow.Status == RSSWorkStatus.XMLError )
{
proxy.SetProp( Props.UpdateStatus, "(error)" );
proxy.SetProp( Core.Props.LastError, uow.LastException.Message );
}
else
{
proxy.DeleteProp( Props.UpdateStatus );
proxy.DeleteProp( Core.Props.LastError );
}
if ( uow.LastException is HttpDecompressException )
{
proxy.SetProp( Props.DisableCompression, true );
}
if ( uow.Status == RSSWorkStatus.HTTPError && uow.HttpStatus == HttpStatusCode.Gone )
{
proxy.SetProp( Props.UpdateFrequency, -1 );
}
else if ( !uow.Feed.HasProp( Props.UpdateFrequency ) )
{
proxy.SetProp( Props.UpdateFrequency, (int)Settings.UpdateFrequency );
}
if ( !uow.Feed.HasProp( Props.UpdatePeriod ) )
{
proxy.SetProp( Props.UpdatePeriod, (string)Settings.UpdatePeriod );
}
proxy.EndUpdate();
if ( uow.Status == RSSWorkStatus.HTTPError && uow.Attempts < 3 )
{
Core.NetworkAP.QueueJobAt(
DateTime.Now.AddMinutes( 5 ),
new QueueFeedUpdateDelegate( QueueFeedUpdate ), uow.Feed, uow.Attempts + 1 );
}
if( uow.Status == RSSWorkStatus.Success && uow.Feed.HasProp( Props.AutoDownloadEnclosure ))
ScheduleEnclosures( uow.Feed );
CleanupCommentFeed( uow );
ScheduleFeedUpdate( uow.Feed );
if ( FeedUpdated != null )
{
FeedUpdated( this, new ResourceEventArgs( uow.Feed ) );
}
lock ( this )
{
_updatingCount--;
while ( _pendingFeeds.Count > 0 )
{
PendingFeed feed = (PendingFeed)_pendingFeeds.Pop();
if ( feed.Feed.IsDeleted )
{
continue;
}
QueueFeedUpdate( feed.Feed, feed.Attempt );
break;
}
// Has queue gotten empty?
if((_pendingFeeds.Count == 0) && (_updatingCount == 0))
Core.UserInterfaceAP.QueueJob( "Feeds Update Queue has Gotten Empty.", new MethodInvoker(FireQueueGotEmpty) );
}
}
///
/// Fires the event async.
///
private void FireQueueGotEmpty()
{
try
{
if(QueueGotEmpty != null)
QueueGotEmpty(this, EventArgs.Empty);
}
catch(Exception ex)
{
Core.ReportException( ex, ExceptionReportFlags.AttachLog );
}
}
///
/// Queue for download those enclosures of the feed's unread items which
/// are not yet downloaded or queued.
///
/// A feed resource which items are to be analyzed.
private void ScheduleEnclosures( IResource feed )
{
IResourceList items = feed.GetLinksOfType( Props.RSSItemResource, Props.RSSItem );
items = items.Intersect( Core.ResourceStore.FindResourcesWithProp( Props.RSSItemResource, Core.Props.IsUnread ), true );
items = items.Minus( Core.ResourceStore.FindResourcesWithProp( Props.RSSItemResource, Core.Props.IsDeleted ) );
IResourceList itemsWithEncls = Core.ResourceStore.FindResources( null, Props.EnclosureDownloadingState, (int)EnclosureDownloadState.NotDownloaded );
items = items.Intersect( itemsWithEncls, true );
foreach( IResource item in items )
{
EnclosureDownloadManager.PlanToDownload( item );
}
}
}
}