// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: ReadingList.java,v 1.16 2007/03/13 11:47:24 spyromus Exp $
//
package com.salas.bb.domain;
import com.salas.bb.utils.i18n.Strings;
import java.net.URL;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This object holds information about some reading list -- an external
* OPML file with the list of feeds.
*
* It also has an association with some feeds in the guide it belongs too,
* indicating their correspondence to this list.
*/
public class ReadingList
{
private static final Logger LOG = Logger.getLogger(ReadingList.class.getName());
/** Title property. */
public static final String PROP_TITLE = "title";
/** Last poll time property. */
public static final String PROP_LAST_POLL_TIME = "lastPollTime";
/** Last update server time property. */
public static final String PROP_LAST_UPDATE_SERVER_TIME = "lastUpdateServerTime";
/** Name of the last time of sync property. */
public static final String PROP_LAST_SYNC_TIME = "lastSyncTime";
/**
* Time of initialization. This time is used to detect the necessety for an
* update when in once-per-run mode.
*/
private static final long INIT_TIME = System.currentTimeMillis();
/** Updating list(s) once per run. */
public static final long PERIOD_ONCE_PER_RUN = -1;
/** Never updating list(s) automatically. */
public static final long PERIOD_NEVER = 0;
/** Updating list(s) hourly. */
public static final long PERIOD_HOURLY = 60 * 60 * 1000;
/** Updating list(s) daily. */
public static final long PERIOD_DAILY = 24 * 60 * 60 * 1000;
/** Default period. */
public static final long DEFAULT_PERIOD = PERIOD_ONCE_PER_RUN;
/**
* Global update period. If update period of a list isn't set this value is taken.
*/
private static long globalUpdatePeriod = ReadingList.DEFAULT_PERIOD;
/** Identity. */
private long id;
/** Source URL. */
private final URL url;
/** Title of a list. */
private String title;
/** Time of last synchronization (either in or out). */
private long lastSyncTime;
/** Local time of last poll (to calculate the next poll time). */
private long lastPollTime;
/** Server time of last update (to ask for changes). */
private long lastUpdateServerTime;
/** A guide which has this list among its associated reading lists. */
private StandardGuide parentGuide;
/** A list of an associated feeds. */
private List<DirectFeed> feeds;
/** The list of listeners. */
private CopyOnWriteArrayList<IReadingListListener> listeners;
/** Shows <code>TRUE</code> when the list is being updated. */
private boolean updating;
/** Missing reading list flag. Over time the reading list may disappear. */
private boolean missing;
/**
* Creates Reading List.
*
* @param anURL URL of the source OPML.
*
* @throws NullPointerException if URL is not set.
*/
public ReadingList(URL anURL)
{
url = anURL;
id = -1;
lastPollTime = -1;
lastSyncTime = -1;
lastUpdateServerTime = -1;
feeds = new CopyOnWriteArrayList<DirectFeed>();
listeners = new CopyOnWriteArrayList<IReadingListListener>();
missing = false;
updating = false;
}
/**
* Returns URL of this reading list.
*
* @return URL.
*/
public URL getURL()
{
return url;
}
/**
* Returns ID of the list.
*
* @return ID.
*/
public long getID()
{
return id;
}
/**
* Sets ID of the list.
*
* @param aId ID.
*/
public void setID(long aId)
{
id = aId;
}
/**
* Returns title of the list.
*
* @return title.
*/
public String getTitle()
{
return title;
}
/**
* Sets new title of the list.
*
* @param aTitle title.
*/
public void setTitle(String aTitle)
{
String old = title;
title = aTitle;
firePropertyChanged(PROP_TITLE, old, title);
}
/**
* Returns last poll time.
*
* @return time of last poll.
*/
public long getLastPollTime()
{
return lastPollTime;
}
/**
* Sets last poll time.
*
* @param time time of last poll.
*/
public void setLastPollTime(long time)
{
long old = lastPollTime;
lastPollTime = time;
firePropertyChanged(PROP_LAST_POLL_TIME, old, lastPollTime);
}
/**
* Returns last server update time.
*
* @return time of last resource update according to a server.
*/
public long getLastUpdateServerTime()
{
return lastUpdateServerTime;
}
/**
* Sets last server update time.
*
* @param time time of last resource update according to a server.
*/
public void setLastUpdateServerTime(long time)
{
long old = lastUpdateServerTime;
lastUpdateServerTime = time;
firePropertyChanged(PROP_LAST_UPDATE_SERVER_TIME, old, time);
}
/**
* Adds some feed to this reading list.
*
* @param feed feed to add.
*/
public void add(DirectFeed feed)
{
if (feed == null) return;
if (!feeds.contains(feed))
{
feed.addReadingList(this);
feeds.add(feed);
fireFeedAdded(feed);
}
}
/**
* Remove association between this reading list and a feed.
*
* @param feed feed to remove association with.
*/
public void remove(DirectFeed feed)
{
if (feed == null) return;
if (feeds.contains(feed))
{
feed.removeReadingList(this);
if (feeds.remove(feed)) fireFeedRemoved(feed);
} else
{
// It's basically not a problem. Sometimes the reading list is removed and its updating
// is still in progress.
if (LOG.isLoggable(Level.WARNING)) LOG.warning(MessageFormat.format(
Strings.error("feed.is.not.associated.with.this.reading.list"),
feed));
}
}
/**
* Returns <code>TRUE</code> if reading list has associated feeds.
*
* @return <code>TRUE</code> if reading list has associated feeds.
*/
boolean hasAssociations()
{
return feeds.size() > 0;
}
/**
* Returns all associated feeds.
*
* @return all associated feeds.
*/
public DirectFeed[] getFeeds()
{
return feeds.toArray(new DirectFeed[0]);
}
/**
* Collects the differences between new list of feeds and what's actually associated
* with this reading list. If there's a feed on the list but it isn't associated
* with the local reading list yet, it goes to the list for addition. If in
* the end of scan we found that some feeds weren't mentioned in the new list of feeds
* they go to the list for removal.
*
* @param newFeeds array of direct feeds of a remote list.
* @param addFeeds the list will be populated with direct feeds to add.
* @param removeFeeds the list will be populated with direct feeds to remove.
*/
public void collectDifferences(DirectFeed[] newFeeds, List<DirectFeed> addFeeds, List<DirectFeed> removeFeeds)
{
if (newFeeds == null) return;
// Convert the new list of feeds into map xmlURL->Object
Map<String, DirectFeed> newFeedsMap = new HashMap<String, DirectFeed>(newFeeds.length);
for (DirectFeed feed : newFeeds)
{
newFeedsMap.put(feed.getXmlURL().toString(), feed);
}
// Put old feeds no longer on the new list to remove-list
// and remove from the new feeds map those present in both lists
// to leave only new feeds in that lists after this check
DirectFeed[] associatedFeeds = getFeeds();
for (DirectFeed feed : associatedFeeds)
{
String existingURL = feed.getXmlURL().toString();
if (!newFeedsMap.containsKey(existingURL))
{
removeFeeds.add(feed);
} else
{
newFeedsMap.remove(existingURL);
}
}
// Everything what's left in this map is new to the local reading list copy
addFeeds.addAll(newFeedsMap.values());
}
/**
* Sets <code>TRUE</code> when the list is being updated.
*
* @param flag <code>TRUE</code> when the list if being updated.
*/
public void setUpdating(boolean flag)
{
updating = flag;
}
/**
* Returns <code>TRUE</code> when the list if being updated at the moment.
*
* @return <code>TRUE</code> when the list if being updated at the moment.
*/
public boolean isUpdating()
{
return updating;
}
/**
* Returns <code>TRUE</code> if the list can be updated right now.
*
* @return <code>TRUE</code> if the list can be updated right now.
*/
public boolean isUpdatable()
{
boolean updatable = false;
long period = getGlobalUpdatePeriod();
if (period == PERIOD_ONCE_PER_RUN)
{
updatable = lastPollTime < INIT_TIME;
} else if (period > PERIOD_NEVER)
{
updatable = lastPollTime < (System.currentTimeMillis() - period);
}
return updatable;
}
/**
* Returns <code>TRUE</code> when reading list is not found during the last polling.
*
* @return <code>TRUE</code> when reading list is not found during the last polling.
*/
public boolean isMissing()
{
return missing;
}
/**
* Sets the value of missing flag.
*
* @param missing new value.
*/
public void setMissing(boolean missing)
{
this.missing = missing;
}
/**
* Assign new parent guide.
*
* @param guide parent guide.
*/
public void setParentGuide(StandardGuide guide)
{
parentGuide = guide;
}
/**
* Returns assigned parent guide.
*
* @return parent guide.
*/
public StandardGuide getParentGuide()
{
return parentGuide;
}
/**
* Returns global update period.
*
* @return global update period.
*
* @see #PERIOD_ONCE_PER_RUN
* @see #PERIOD_NEVER
* @see #PERIOD_DAILY
* @see #PERIOD_DAILY
*/
public static long getGlobalUpdatePeriod()
{
return globalUpdatePeriod;
}
/**
* Sets new global update period.
*
* @param period new period in milliseconds.
*
* @see #PERIOD_ONCE_PER_RUN
* @see #PERIOD_NEVER
* @see #PERIOD_DAILY
* @see #PERIOD_DAILY
*/
public static void setGlobalUpdatePeriod(long period)
{
globalUpdatePeriod = period;
}
/**
* Returns the time of last synchronization (sync-in or sync-out).
*
* @return time.
*/
public long getLastSyncTime()
{
return lastSyncTime;
}
/**
* Sets the time of last synchronization (sync-in or sync-out).
*
* @param time time.
*/
public void setLastSyncTime(long time)
{
long old = lastSyncTime;
lastSyncTime = time;
firePropertyChanged(PROP_LAST_SYNC_TIME, old, lastSyncTime);
}
/**
* Compares this list to the other.
*
* @param o other list.
*
* @return <code>TRUE</code> if equal.
*/
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ReadingList that = (ReadingList)o;
return url.toString().equals(that.url.toString());
}
/**
* Returns hash code of this list.
*
* @return has code.
*/
public int hashCode()
{
return url.toString().hashCode();
}
// ---------------------------------------------------------------------------------------------
/**
* Adds listener.
*
* @param l listener.
*/
public void addListener(IReadingListListener l)
{
listeners.add(l);
}
/**
* Removes listener.
*
* @param l listener.
*/
public void removeListener(IReadingListListener l)
{
listeners.remove(l);
}
/**
* Fires feed addition event.
*
* @param feed feed added.
*/
public void fireFeedAdded(IFeed feed)
{
for (IReadingListListener listener : listeners) listener.feedAdded(this, feed);
}
/**
* Fires feed removal event.
*
* @param feed feed removed.
*/
public void fireFeedRemoved(IFeed feed)
{
for (IReadingListListener listener : listeners) listener.feedRemoved(this, feed);
}
/**
* Fires property change event.
*
* @param property property.
* @param oldValue old value.
* @param newValue new value.
*/
public void firePropertyChanged(String property, Object oldValue, Object newValue)
{
for (IReadingListListener listener : listeners)
listener.propertyChanged(this, property, oldValue, newValue);
}
/**
* Returns <code>TRUE</code> if the specified feed is on the list.
*
* @param aFeed feed.
*
* @return <code>TRUE</code> if contains.
*/
public boolean contains(IFeed aFeed)
{
return feeds.contains(aFeed);
}
}