// 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: AbstractFeed.java,v 1.43 2007/11/07 17:16:48 spyromus Exp $
//
package com.salas.bb.domain;
import com.salas.bb.utils.CommonUtils;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.views.feeds.IFeedDisplayConstants;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import java.text.MessageFormat;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Abstract implementation of <code>IFeed</code> interface.
*/
public abstract class AbstractFeed implements IFeed
{
private static final Logger LOG = Logger.getLogger(AbstractFeed.class.getName());
/** ID of the corresponding record in the database. Default is -1. */
private long id;
/** List of event listeners. */
private final List<IFeedListener> listeners;
/** Incremented when someone doing something lengthy. */
private AtomicInteger processingCount;
/** Holders of this feed. */
private IGuide[] guides;
private final Object guidesLock = new Object();
/** Reason of this feed for being invalid. */
private String invalidnessReason;
/** User-set rating of the feed. */
protected int rating;
/** Time when feed was visited for last time */
private long lastVisitTime = 0;
/** Type of the feed. */
private FeedType type;
/** Feed handling type. */
private FeedHandlingType handlingType;
/** When <code>TRUE</code> the feed has custom view mode assigned. */
private boolean customViewModeEnabled;
private int customViewMode;
/**
* Time time indicates the last time when the last of the properties involved in
* synchronization routines was updated.
*/
private long lastUpdateTime;
/** Number of views of this feed. */
private int views;
/** Number of times articles of this feed were opened in the browser. */
private int clickthroughs;
/** Sorting override. */
private Boolean ascendingSorting;
private boolean autoSaveArticles;
private String autoSaveArticlesFolder;
private String autoSaveArticlesNameFormat;
private boolean autoSaveEnclosures;
private String autoSaveEnclosuresFolder;
private String autoSaveEnclosuresNameFormat;
/**
* Constructs new feed.
*/
public AbstractFeed()
{
guides = new IGuide[0];
processingCount = new AtomicInteger(0);
listeners = new CopyOnWriteArrayList<IFeedListener>();
invalidnessReason = null;
id = -1;
rating = RATING_NOT_SET;
type = FeedType.TEXT;
handlingType = FeedHandlingType.DEFAULT;
customViewModeEnabled = false;
customViewMode = IFeedDisplayConstants.MODE_BRIEF;
lastUpdateTime = -1;
views = 0;
clickthroughs = 0;
ascendingSorting = null;
autoSaveArticles = false;
autoSaveEnclosures = false;
}
/**
* Returns ID of the feed. This ID is used by persistence layer to identify record in database.
*
* @return ID of the feed.
*/
public long getID()
{
return id;
}
/**
* Sets the ID of the feed.
*
* @param aId ID of the feed.
*/
public void setID(long aId)
{
id = aId;
}
/**
* Adds a guide currently holding this feed.
*
* @param aGuide parent guide.
*/
public void addParentGuide(IGuide aGuide)
{
synchronized (guidesLock)
{
if (indexOfParentGuide(aGuide) == -1)
{
IGuide[] newList = new IGuide[guides.length + 1];
for (int i = 0; i < guides.length; i++) newList[i] = guides[i];
newList[guides.length] = aGuide;
guides = newList;
}
}
}
/**
* Removes a guide that no longer holds this feed.
*
* @param aGuide guide.
*/
public void removeParentGuide(IGuide aGuide)
{
synchronized (guidesLock)
{
int index = indexOfParentGuide(aGuide);
if (index != -1)
{
IGuide[] newList = new IGuide[guides.length - 1];
for (int i = 0; i < index; i++) newList[i] = guides[i];
for (int i = index + 1; i < guides.length; i++) newList[i - 1] = guides[i];
guides = newList;
}
}
}
/**
* Returns index of the guide in the list of parent guides.
*
* @param aGuide guide to look for.
*
* @return index or -1 if not found.
*/
private int indexOfParentGuide(IGuide aGuide)
{
int index = -1;
for (int i = 0; index == -1 && i < guides.length; i++)
{
if (guides[i] == aGuide) index = i;
}
return index;
}
/**
* Returns <code>TRUE</code> if the feed belongs to the guide.
*
* @param guide guide to check.
*
* @return <code>TRUE</code> if the feed belongs to the guide.
*/
public boolean belongsTo(IGuide guide)
{
return indexOfParentGuide(guide) != -1;
}
/**
* Returns <code>TRUE</code> if this feed is assigned to some reading list.
*
* @return <code>TRUE</code> if this feed is assigned to some reading list.
*/
public boolean isDynamic()
{
return false;
}
/**
* Returns guides currently holding this feed.
*
* @return parent guides.
*/
public IGuide[] getParentGuides()
{
// Dangerous!!! Possible modifications to the guides list.
return guides;
}
/**
* Notifies feed that processing of it started.
*/
public void processingStarted()
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Start Processing : " + getTitle());
}
int cnt = processingCount.getAndIncrement();
if (cnt == 1) firePropertyChanged(PROP_PROCESSING, FALSE, TRUE, false, true);
}
/**
* Notifies feed that processing of it finished.
*/
public void processingFinished()
{
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("Finish Processing : " + getTitle());
}
int cnt = processingCount.decrementAndGet();
if (cnt <= 0)
{
if (cnt < 0)
{
processingCount.compareAndSet(cnt, 0);
if (LOG.isLoggable(Level.WARNING))
{
LOG.warning(MessageFormat.format(Strings.error("feed.processing.counter.got.below.0"), getTitle()));
}
}
firePropertyChanged(PROP_PROCESSING, TRUE, FALSE, false, true);
}
}
/**
* Returns TRUE if the feed is under some lengthy processing.
*
* @return TRUE if the feed is under some lengthy processing.
*/
public boolean isProcessing()
{
return processingCount.get() > 0;
}
/**
* TRUE when all articles are read in this feed.
*
* @return TRUE when everything is read.
*/
public synchronized boolean isRead()
{
return getUnreadArticlesCount() == 0;
}
/**
* Sets the whole feed read / unread.
*
* @param read TRUE if mark as read.
*/
public synchronized void setRead(boolean read)
{
int count = getArticlesCount();
for (int i = 0; i < count; i++)
{
IArticle article = getArticleAt(count - i - 1);
article.setRead(read);
}
}
/**
* Returns unread articles count.
*
* @return count.
*/
public int getUnreadArticlesCount()
{
int unread = 0;
// I intentionally don't use method-level sync
// to keep the synchronized keyword out of the signature
synchronized (this)
{
int count = getArticlesCount();
for (int i = 0; i < count; i++)
{
IArticle article = getArticleAt(i);
if (!article.isRead()) unread++;
}
}
return unread;
}
/**
* Returns <code>true</code> if this feed is invalid.
*
* @return <code>true</code> if invalid.
*/
public boolean isInvalid()
{
return getInvalidnessReason() != null;
}
/**
* Sets the reason of invalidness of this feed. If reason is set to <code>NULL</code> the feed
* is considered valid.
*
* @param reason reason of invalidness or <code>NULL</code>.
*/
public void setInvalidnessReason(String reason)
{
String oldReason = invalidnessReason;
invalidnessReason = StringUtils.intern(reason);
firePropertyChanged(PROP_INVALIDNESS_REASON, oldReason, invalidnessReason, false, true);
}
/**
* Returns reason for being invalid.
*
* @return reason.
*/
public String getInvalidnessReason()
{
return invalidnessReason;
}
/**
* Adds listener to the list.
*
* @param l listener.
*/
public void addListener(IFeedListener l)
{
if (!listeners.contains(l)) listeners.add(l);
}
/**
* Removes listener from the list.
*
* @param l listener.
*/
public void removeListener(IFeedListener l)
{
listeners.remove(l);
}
/**
* Fires event about that the feed information has been changed.
*
* @param property property of the feed.
* @param oldValue old property value.
* @param newValue new property value.
*
* @throws NullPointerException if property isn't specified.
*/
protected void firePropertyChanged(String property, Object oldValue, Object newValue)
{
firePropertyChanged(property, oldValue, newValue, false, false);
}
/**
* Fires event about that the feed information has been changed.
*
* @param property property of the feed.
* @param oldValue old property value.
* @param newValue new property value.
*
* @throws NullPointerException if property isn't specified.
*/
protected void firePropertyChanged(String property, int oldValue, int newValue)
{
firePropertyChanged(property, new Integer(oldValue), new Integer(newValue));
}
/**
* Fires event about that the feed information has been changed.
*
* @param property property of the feed.
* @param oldValue old property value.
* @param newValue new property value.
* @param syncProperty <code>TRUE</code> if this property is involved in synchronization.
* @param visibilityProperty <code>TRUE</code> if this property possibly affects visibility.
*
* @throws NullPointerException if property isn't specified.
*/
protected void firePropertyChanged(String property, Object oldValue, Object newValue,
boolean syncProperty, boolean visibilityProperty)
{
if (property == null) throw new NullPointerException(Strings.error("unspecified.property"));
// Do not fire the event if property values are identical
if (!CommonUtils.areDifferent(oldValue, newValue)) return;
for (IFeedListener listener : listeners) listener.propertyChanged(this, property, oldValue, newValue);
if (syncProperty) registerUpdate();
if (visibilityProperty) invalidateVisibilityCache();
}
/**
* Fires event about new article has been added.
*
* @param article article which was added.
*
* @throws NullPointerException if article isn't specified.
*/
protected void fireArticleAdded(IArticle article)
{
for (IFeedListener listener : listeners) listener.articleAdded(this, article);
}
/**
* Fires event about new article has been removed.
*
* @param article article which was removed.
*/
protected void fireArticleRemoved(IArticle article)
{
for (IFeedListener listener : listeners) listener.articleRemoved(this, article);
}
/**
* Returns string representation of this feed object.
*
* @return string representation.
*/
public String toString()
{
return MessageFormat.format(Strings.message("feed.string.representation"), getTitle(),
getUnreadArticlesCount(), getArticlesCount());
}
/**
* Returns the rating of this feed set by user.
*
* @return rating of the feed or (-1) if not set.
*/
public int getRating()
{
return rating;
}
/**
* Sets new rating for the feed.
*
* @param aRating new rating in range [0;4] or (-1) to reset.
*
* @throws IllegalArgumentException if rating is not within range or (-1).
*
* @see #RATING_NOT_SET
*/
public void setRating(int aRating)
{
if (aRating != RATING_NOT_SET && (aRating < RATING_MIN || aRating > RATING_MAX))
throw new IllegalArgumentException(MessageFormat.format(Strings.error("incorrect.rating.value"), aRating));
int oldRating = rating;
rating = aRating;
firePropertyChanged(PROP_RATING, oldRating, rating, true, true);
}
/**
* Returns time when feed was visited for the last time.
*
* @return time last visit time
*/
public long getLastVisitTime()
{
return lastVisitTime;
}
/**
* Sets time when feed was visited for the last time.
*
* @param time last visit time
*/
public void setLastVisitTime(final long time)
{
final long oldLastVisitTime = lastVisitTime;
lastVisitTime = time;
firePropertyChanged(PROP_LAST_VISIT_TIME, oldLastVisitTime, lastVisitTime);
}
/**
* Returns the type of the feed.
*
* @return feed type.
*/
public FeedType getType()
{
return type;
}
/**
* Sets the type of the feed.
*
* @param aType type.
*/
public void setType(FeedType aType)
{
FeedType oldType = type;
type = aType;
firePropertyChanged(PROP_TYPE, oldType, type, true, false);
}
/**
* Gets feed handling type.
*
* @return type.
*/
public FeedHandlingType getHandlingType()
{
return handlingType;
}
/**
* Sets feed handling type.
*
* @param type type.
*/
public void setHandlingType(FeedHandlingType type)
{
if (type == null) type = FeedHandlingType.DEFAULT;
FeedHandlingType oldType = handlingType;
handlingType = type;
firePropertyChanged(PROP_HANDLING_TYPE, oldType, handlingType, true, false);
}
/**
* When <code>TRUE</code> the feed has preferred view mode set.
*
* @return <code>TRUE</code> when feed has its own view mode.
*/
public boolean isCustomViewModeEnabled()
{
return customViewModeEnabled;
}
/**
* Sets the custom view mode enabled / disabled.
*
* @param enabled <code>TRUE</code> to enable.
*/
public void setCustomViewModeEnabled(boolean enabled)
{
boolean oldValue = customViewModeEnabled;
customViewModeEnabled = enabled;
firePropertyChanged(PROP_CUSTOM_VIEW_MODE_ENABLED, oldValue, enabled, true, false);
}
/**
* Returns custom view mode.
*
* @return view mode.
*
* @see IFeedDisplayConstants#MODE_MINIMAL
* @see IFeedDisplayConstants#MODE_BRIEF
* @see IFeedDisplayConstants#MODE_FULL
*/
public int getCustomViewMode()
{
return customViewMode;
}
/**
* Sets the custom view mode.
*
* @param mode mode.
*
* @see IFeedDisplayConstants#MODE_MINIMAL
* @see IFeedDisplayConstants#MODE_BRIEF
* @see IFeedDisplayConstants#MODE_FULL
*/
public void setCustomViewMode(int mode)
{
if (mode == -1) return;
int oldValue = customViewMode;
customViewMode = mode;
firePropertyChanged(PROP_CUSTOM_VIEW_MODE, oldValue, customViewMode, true, false);
}
/**
* Returns the time of last properties update. This time is necessary for the synchronization
* engine to learn what object is newer.
*
* @return the time or <code>-1</code> if not updated yet.
*/
public long getLastUpdateTime()
{
return lastUpdateTime;
}
/**
* Sets the time of last properties update. When the user changes some property this time is set
* automatically. This method is necessary for persistence layer to init the object with what is
* currently in the database.
*
* @param time time.
*/
public void setLastUpdateTime(long time)
{
long oldValue = lastUpdateTime;
lastUpdateTime = time;
firePropertyChanged(PROP_LAST_UPDATE_TIME, oldValue, time);
}
/**
* Registers last update time.
*/
protected void registerUpdate()
{
setLastUpdateTime(System.currentTimeMillis());
}
/**
* Returns the number of views of this feed.
*
* @return views count.
*/
public int getViews()
{
return views;
}
/**
* Sets the number of views of this feed.
*
* @param views views count.
*/
public void setViews(int views)
{
int oldVal = this.views;
this.views = views;
firePropertyChanged(PROP_VIEWS, oldVal, views);
}
/**
* Returns the number of times articles from this feed were opened in the browser.
*
* @return times.
*/
public int getClickthroughs()
{
return clickthroughs;
}
/**
* Sets the number of times articles from this feed have been opened in the browser.
*
* @param times times.
*/
public void setClickthroughs(int times)
{
int oldVal = clickthroughs;
clickthroughs = times;
firePropertyChanged(PROP_CLICKTHROUGHS, oldVal, clickthroughs);
}
/**
* Returns <code>TRUE</code> if an article has matching words from <code>from</code> to
* <code>to</code> of the title.
*
* @param article article.
* @param from the first word.
* @param to the last word.
* @param articles articles to check against.
*
* @return <code>TRUE</code> if duplicate.
*/
protected static boolean isDuplicate(IArticle article, int from, int to, List<IArticle> articles)
{
// Decrement to map from human to computer indexes
from--;
to--;
String[] words = article.getTitleWords();
// String words = StringUtils.getWordsInRange(article.getTitle(), from - 1, to - 1);
if (words.length == 0) return false;
for (IArticle art : articles)
{
if (art == article) continue;
String[] aw = art.getTitleWords();
// String aw = StringUtils.getWordsInRange(art.getTitle(), from - 1, to - 1);
if (wordsEqual(words, aw, from, to)) return true;
}
return false;
}
/**
* Compares two sets of words starting from one index and moving on to another. If
* there are not enough words in one of the sets, they aren't equal.
*
* @param words1 first set.
* @param words2 second set.
* @param from index to start from.
* @param to the last index (inclusive).
*
* @return <code>TRUE</code> if the words in range are equal (ignoring the case) in both sets.
*/
static boolean wordsEqual(String[] words1, String[] words2, int from, int to)
{
if (words1.length <= to || words2.length <= to) return false;
boolean match = false;
while (from <= to && (match = words1[from].equalsIgnoreCase(words2[from]))) from++;
return match;
}
/**
* Returns the mask of a feed meta-classes.
*
* @return mask.
*/
public int getClassesMask()
{
return FeedClassifier.classify(this);
}
// ------------------------------------------------------------------------
// Feeds visibility
// ------------------------------------------------------------------------
private static IFeedVisibilityResolver feedVisibilityResolver;
private static final long VISIBILITY_CACHE_EXPIRE_PERIOD = 600000; // 10 minutes
private volatile long visibilityCacheExpires = 0;
private volatile boolean visibilityCache = true;
private final ReentrantReadWriteLock visibilityCL = new ReentrantReadWriteLock();
/**
* Registers feed visibility resolver.
*
* @param fvis feed visibility resolver.
*/
public static void setFeedVisibilityResolver(IFeedVisibilityResolver fvis)
{
AbstractFeed.feedVisibilityResolver = fvis;
}
/**
* Returns <code>TRUE</code> if feed is visible.
*
* @return <code>TRUE</code> if feed is visible.
*/
public boolean isVisible()
{
boolean visible;
long time = System.currentTimeMillis();
visibilityCL.readLock().lock();
if (visibilityCacheExpires < time)
{
// Upgrade the lock
visibilityCL.readLock().unlock();
visibilityCL.writeLock().lock();
try
{
// Check if is still expired
if (visibilityCacheExpires < time)
{
// We set the expiration time first to be sure
// if the invalidation comes while we are computing isVisibleNoCache
// the next time we ask for this flag, it will be calculated again.
visibilityCacheExpires = time + VISIBILITY_CACHE_EXPIRE_PERIOD;
visibilityCache = isVisibleNoCache();
}
} finally
{
// Downgrade the lock
visibilityCL.readLock().lock();
visibilityCL.writeLock().unlock();
}
}
visible = visibilityCache;
visibilityCL.readLock().unlock();
return visible;
}
/**
* Invalidates visibility cache immediately.
*/
public void invalidateVisibilityCache()
{
// visibilityCL.writeLock().lock();
visibilityCacheExpires = 0;
// visibilityCL.writeLock().unlock();
}
/**
* Returns <code>TRUE</code> if feed is visible.
*
* @return <code>TRUE</code> if feed is visible.
*/
protected boolean isVisibleNoCache()
{
return feedVisibilityResolver == null || isProcessing() || feedVisibilityResolver.isVisible(this);
}
/**
* Returns the state of the articles sort override flag.
*
* @return <code>NULL</code> for no override, <code>TRUE / FALSE</code> as a value.
*/
public Boolean getAscendingSorting()
{
return ascendingSorting;
}
/**
* Sets the state of the articles sort override flag.
*
* @param asc <code>NULL</code> to clear override, <code>TRUE</code> to sort in ascending order.
*/
public void setAscendingSorting(Boolean asc)
{
Boolean old = ascendingSorting;
ascendingSorting = asc;
firePropertyChanged(PROP_ASCENDING_SORTING, old, asc, true, false);
}
// ------------------------------------------------------------------------
// Articles auto-saving
// ------------------------------------------------------------------------
/**
* Enables / disables automatic articles saving.
*
* @param en <code>TRUE</code> to enable.
*/
public void setAutoSaveArticles(boolean en)
{
boolean old = autoSaveArticles;
autoSaveArticles = en;
firePropertyChanged(PROP_AUTO_SAVE_ARTICLES, old, en);
}
/**
* Returns <code>TRUE</code> if auto-saving of articles is enabled.
*
* @return <code>TRUE</code> if auto-saving of articles is enabled.
*/
public boolean isAutoSaveArticles()
{
return autoSaveArticles;
}
/**
* Sets the path to the folder to save articles to.
*
* @param folder the folder path.
*/
public void setAutoSaveArticlesFolder(String folder)
{
String old = autoSaveArticlesFolder;
autoSaveArticlesFolder = folder;
firePropertyChanged(PROP_AUTO_SAVE_ARTICLES_FOLDER, old, folder);
}
/**
* Returns the path to the folder to save articles to.
*
* @return folder path.
*/
public String getAutoSaveArticlesFolder()
{
return autoSaveArticlesFolder;
}
/**
* Sets the format for the file name.
*
* @param nameFormat name format.
*/
public void setAutoSaveArticlesNameFormat(String nameFormat)
{
String old = autoSaveArticlesNameFormat;
autoSaveArticlesNameFormat = nameFormat;
firePropertyChanged(PROP_AUTO_SAVE_ARTICLES_NAME_FORMAT, old, nameFormat);
}
/**
* Returns the format for the file name.
*
* @return name format.
*/
public String getAutoSaveArticlesNameFormat()
{
return autoSaveArticlesNameFormat;
}
// ------------------------------------------------------------------------
// Enclosure auto-saving
// ------------------------------------------------------------------------
/**
* Enables / disables automatic enclosures saving.
*
* @param en <code>TRUE</code> to enable.
*/
public void setAutoSaveEnclosures(boolean en)
{
boolean old = autoSaveEnclosures;
autoSaveEnclosures = en;
firePropertyChanged(PROP_AUTO_SAVE_ENCLOSURES, old, en);
}
/**
* Returns <code>TRUE</code> if auto-saving of enclosures is enabled.
*
* @return <code>TRUE</code> if enabled.
*/
public boolean isAutoSaveEnclosures()
{
return autoSaveEnclosures;
}
/**
* Sets the path to the folder to save enclosures to.
*
* @param folder the folder path.
*/
public void setAutoSaveEnclosuresFolder(String folder)
{
String old = autoSaveEnclosuresFolder;
autoSaveEnclosuresFolder = folder;
firePropertyChanged(PROP_AUTO_SAVE_ENCLOSURES_FOLDER, old, folder);
}
/**
* Returns the path to the folder to save enclosures to.
*
* @return folder path.
*/
public String getAutoSaveEnclosuresFolder()
{
return autoSaveEnclosuresFolder;
}
/**
* Sets the format for the file name.
*
* @param nameFormat name format.
*/
public void setAutoSaveEnclosuresNameFormat(String nameFormat)
{
String old = autoSaveEnclosuresNameFormat;
autoSaveEnclosuresNameFormat = nameFormat;
firePropertyChanged(PROP_AUTO_SAVE_ENCLOSURES_NAME_FORMAT, old, nameFormat);
}
/**
* Returns the format for the file name.
*
* @return name format.
*/
public String getAutoSaveEnclosuresNameFormat()
{
return autoSaveEnclosuresNameFormat;
}
}