// 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: FeedDisplayModel.java,v 1.31 2008/02/28 15:59:46 spyromus Exp $
//
package com.salas.bb.views.feeds;
import com.jgoodies.binding.value.ValueHolder;
import com.jgoodies.binding.value.ValueModel;
import com.salas.bb.domain.FeedAdapter;
import com.salas.bb.domain.IArticle;
import com.salas.bb.domain.IArticleListener;
import com.salas.bb.domain.IFeed;
import com.salas.bb.domain.utils.ArticleDateComparator;
import com.salas.bb.utils.IdentityList;
import com.salas.bb.utils.TimeRange;
import com.salas.bb.utils.uif.UifUtilities;
import static com.salas.bb.views.feeds.IFeedDisplayConstants.*;
import javax.swing.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/**
* Simple model for groupping articles by dates and presenting them in the list.
* This implementation is thread-unsafe and it means that it's single-threaded.
* If this model will be used (and it is going to) as the backend for some visual
* component, all operations should be invoked in EDT thread only.
*/
public class FeedDisplayModel
{
private static final Logger LOG = Logger.getLogger(FeedDisplayModel.class.getName());
private static final ArticleDateComparator COMPARATOR_DESC =
new ArticleDateComparator(true, false);
private static final ArticleDateComparator COMPARATOR_ASC =
new ArticleDateComparator(false, false);
private static final IArticle[] EMPTY_GROUP = new IArticle[0];
/**
* TRUE to hide articles when they are marked as read while the
* unread-only filter is applied.
*/
private boolean hideArticlesWhenRead;
private final ValueModel pageCountModel;
private final IArticleListener listener;
private IFeed feed;
private boolean ascending;
private List<IFeedDisplayModelListener> listeners;
private FeedListener feedListener;
/** The list of sorted articles. */
private IArticle[] sortedArticles;
/** Groupped articles */
private IArticle[][] articlesGroups;
/** Current filtering mode. */
private int filter;
/** List of currently visible articles. */
private List<IArticle> visibleArticles;
/** Maximum article age to be displayed. */
private long maxArticleAge;
/** This is the article to show even if it doesn't match the filter. */
private IArticle alwaysVisibleArticle;
/** The time when a feed was changed for the last time. */
private volatile long feedChangeTime;
/** Current page number. */
private int page;
/** The number of articles on the page (allowed). */
private int pageSize;
/** Articles we show on the selected page. */
private List<IArticle> pageArticles;
/**
* Creates model w/o highlights advisor.
*/
public FeedDisplayModel()
{
this(new ValueHolder(0));
}
/**
* Creates model.
*
* @param pageCountModel model to update with new number of pages.
*/
public FeedDisplayModel(ValueModel pageCountModel)
{
this.pageCountModel = pageCountModel;
listener = new ArticleListener();
ascending = false;
listeners = new CopyOnWriteArrayList<IFeedDisplayModelListener>();
feedListener = new FeedListener();
visibleArticles = new IdentityList<IArticle>();
feed = null;
filter = IFeedDisplayConstants.FILTER_ALL;
maxArticleAge = -1;
page = 0;
pageSize = 10; // TODO: test only!
pageArticles = new IdentityList<IArticle>();
recalcModel();
}
/**
* Releases listeners and resources.
*/
public void prepareToDismiss()
{
alwaysVisibleArticle = null;
for (IArticle article : sortedArticles) article.removeListener(listener);
if (feed != null) feed.removeListener(feedListener);
fireArticlesRemoved();
}
/**
* Sets the feed to display.
*
* @param aFeed feed.
*/
public void setFeed(IFeed aFeed)
{
if (feed != aFeed)
{
alwaysVisibleArticle = null;
if (feed != null) feed.removeListener(feedListener);
feed = aFeed;
feedChangeTime = System.currentTimeMillis();
if (feed != null) feed.addListener(feedListener);
// Reset the page and recalculate the model
page = 0;
recalcModel();
}
}
/**
* Returns currently loaded feed.
*
* @return feed.
*/
public IFeed getFeed()
{
return feed;
}
/**
* Recalculate model.
*/
private void recalcModel()
{
// Unsubscribe from events
if (sortedArticles != null)
{
for (IArticle article : sortedArticles) article.removeListener(listener);
}
sortedArticles = EMPTY_GROUP;
visibleArticles.clear();
if (feed != null)
{
IArticle[] articles = feed.getArticles();
for (IArticle article : articles) addArticle(article);
// Calculate pages
updatePageCount();
loadPage();
} else
{
clearPage();
pageCountModel.setValue(0);
}
}
/**
* Updates the page count basing on visible articles.
*/
private void updatePageCount()
{
int numberOfPages = (int)Math.ceil(visibleArticles == null ? 0 : visibleArticles.size() / (float)pageSize);
pageCountModel.setValue(numberOfPages);
}
/**
* Clears the page and lets the view kmow.
*/
private void clearPage()
{
articlesGroups = new IArticle[GroupsSetup.getGroupsCount()][];
pageArticles.clear();
fireArticlesRemoved();
}
/**
* Loads the page with new articles.
*/
private void loadPage()
{
clearPage();
// Calculate the offset of the page
int pageOffset = page * pageSize;
// We can load something because there are visible articles in the buffer
for (int i = pageOffset; i < pageOffset + pageSize && i < visibleArticles.size(); i++)
{
IArticle article = visibleArticles.get(i);
addArticleToPage(article);
}
}
private void updatePage()
{
// Calculate the offset of the page
int pageOffset = page * pageSize;
// We can load something because there are visible articles in the buffer
IdentityList<IArticle> displayed = new IdentityList<IArticle>();
for (int i = pageOffset; i < pageOffset + pageSize && i < visibleArticles.size(); i++)
{
IArticle article = visibleArticles.get(i);
displayed.add(article);
if (!pageArticles.contains(article))
{
addArticleToPage(article);
}
}
// Place articles to remove into the removal array to avoid concurrent modification
List<IArticle> toRemove = null;
for (IArticle article : pageArticles)
{
if (!displayed.contains(article))
{
if (toRemove == null) toRemove = new LinkedList<IArticle>();
toRemove.add(article);
}
}
// Do actual removal
if (toRemove != null)
{
for (IArticle article : toRemove)
{
pageArticles.remove(article);
// Remove from the page only
int groupIndex = findGroupIndex(article);
if (groupIndex == -1) groupIndex = 0;
IArticle[] group = getRawGroup(groupIndex);
int indexWithinGroup = indexOf(article, group);
if (indexWithinGroup > -1)
{
articlesGroups[groupIndex] = removeArticle(group, article);
fireArticleRemoved(article, applySorting(groupIndex), indexWithinGroup);
}
}
}
}
private void addArticleToPage(IArticle article)
{
// Find a group for the article
int groupIndex = findGroupIndex(article);
if (groupIndex == -1) groupIndex = 0;
// Find a place in the group for the article
IArticle[] group = getRawGroup(groupIndex);
int indexWithinGroup = Arrays.binarySearch(group, article, getArticlesComparator());
if (indexWithinGroup < 0)
{
indexWithinGroup = -indexWithinGroup - 1;
// Insert it there and to the pagearticles list
articlesGroups[groupIndex] = insertArticle(group, article, indexWithinGroup);
pageArticles.add(article);
// Let the view know about this article
fireArticleAdded(article, applySorting(groupIndex), indexWithinGroup);
}
}
/**
* Called when new article should be added to the model.
* The model will find appropriate place for it in the lists and update
* them. After that new article event will be fired to notify the listeners.
*
* @param aArticle article to add.
*/
void onArticleAdded(IArticle aArticle)
{
if (addArticle(aArticle))
{
updatePageCount();
updatePage();
}
}
private boolean addArticle(IArticle aArticle)
{
if (contains(aArticle)) return false;
boolean reviewed = false;
int index = Arrays.binarySearch(sortedArticles, aArticle, getArticlesComparator());
// If index is positive, we have articles with the same timestamp and we reuse
// their index for upcoming insertion, otherwise -- we convert to insertion index.
if (index < 0)
{
index = -index - 1;
sortedArticles = insertArticle(sortedArticles, aArticle, index);
reviewed = reviewArticle(aArticle);
aArticle.addListener(listener);
}
return reviewed;
}
/**
* Called when some article should be removed from the model. The model will find
* it and remove from the lists. Also it will fire necessary events to report this
* fact to the listeners.
*
* @param aArticle article to remove.
*/
void onArticleRemoved(IArticle aArticle)
{
if (!contains(aArticle)) return;
sortedArticles = removeArticle(sortedArticles, aArticle);
if (isVisible(aArticle)) hideArticle(aArticle);
aArticle.removeListener(listener);
updatePageCount();
updatePage();
}
/**
* Returns number of articles in the model.
*
* @return articles count.
*/
public int getArticlesCount()
{
return pageArticles.size();
}
/**
* Returns the article at a given index.
*
* @param index index.
*
* @return article.
*/
public IArticle getArticle(int index)
{
return pageArticles.get(index);
}
/**
* Sets the order of sorting. Default is descending (latest first).
*
* @param asc <code>TRUE</code> for ascending order, <code>FALSE</code> for descending.
*/
public void setAscending(boolean asc)
{
if (ascending != asc)
{
ascending = asc;
rebuild();
}
}
/**
* Makes full model rebuild as if the feed was selected again.
*/
private void rebuild()
{
IFeed oldFeed = feed;
feed = null;
setFeed(oldFeed);
}
/**
* Returns number of groups in this model. Number of groups doesn't change over
* the time.
*
* @return groups count.
*/
public int getGroupsCount()
{
return GroupsSetup.getGroupsCount();
}
/**
* Returns the group of articles. Sorting order affects the order of groups.
*
* @param index group index.
*
* @return group index.
*
* @see #setAscending(boolean)
*/
public IArticle[] getGroup(int index)
{
return getRawGroup(applySorting(index));
}
/**
* Returns the group of articles.
*
* @param index group index.
*
* @return group index.
*
* @see #setAscending(boolean)
*/
private IArticle[] getRawGroup(int index)
{
IArticle[] group = articlesGroups[index];
if (group == null) group = EMPTY_GROUP;
return group;
}
/**
* Returns the name of group. Sorting order affects the order of groups.
*
* @param group group index.
*
* @return group name.
*
* @see #setAscending(boolean)
*/
public String getGroupName(int group)
{
return GroupsSetup.getGroupTitle(applySorting(group));
}
/**
* Applies sorting direction to the group index.
*
* @param group group index.
*
* @return group index from the head if sorting mode is descending, and
* from the tail if otherwise.
*/
private int applySorting(int group)
{
return ascending ? getGroupsCount() - group - 1: group;
}
/**
* Inserts another article into the given array.
*
* @param aArticles source articles array.
* @param aArticle article to insert.
* @param aIndex index to insert at.
*
* @return new array.
*/
static IArticle[] insertArticle(IArticle[] aArticles, IArticle aArticle, int aIndex)
{
IArticle[] newArticlesList = new IArticle[aArticles.length + 1];
copyObjects(aArticles, 0, newArticlesList, 0, aIndex);
copyObjects(aArticles, aIndex, newArticlesList, aIndex + 1, aArticles.length - aIndex);
newArticlesList[aIndex] = aArticle;
return newArticlesList;
}
/**
* Removes the article from given articles array.
*
* @param aArticles source articles array.
* @param aArticle article to remove.
*
* @return new array.
*/
static IArticle[] removeArticle(IArticle[] aArticles, IArticle aArticle)
{
IArticle[] newArticlesList = aArticles;
int index = indexOf(aArticle, aArticles);
if (index > -1)
{
newArticlesList = new IArticle[aArticles.length - 1];
copyObjects(aArticles, 0, newArticlesList, 0, index);
copyObjects(aArticles, index + 1, newArticlesList, index, aArticles.length - index - 1);
}
return newArticlesList;
}
/**
* Copies objects from source array to the destination array.
*
* @param src source array.
* @param srcIndex start index in source array.
* @param dest destination array.
* @param destIndex start index in destination array.
* @param len length of block.
*/
static void copyObjects(Object[] src, int srcIndex, Object[] dest, int destIndex, int len)
{
for (int a = 0; srcIndex < src.length && destIndex < dest.length && a < len; a++)
{
dest[destIndex++] = src[srcIndex++];
}
}
/**
* Returns correct comparator depending on current sorting mode.
*
* @return comparator.
*/
private ArticleDateComparator getArticlesComparator()
{
return ascending ? COMPARATOR_ASC : COMPARATOR_DESC;
}
/**
* Breaks the list of articles into groups corresponding to given ranges.
*
* @param articles list of articles.
* @param ranges ranges to assign articles to.
*
* @return structured articles.
*/
static IArticle[][] groupArticles(IArticle[] articles, TimeRange[] ranges)
{
IArticle[][] groups = new IArticle[ranges.length][];
for (IArticle article : articles)
{
int groupIndex = findGroupIndex(article, ranges);
if (groupIndex > -1)
{
IArticle[] group = groups[groupIndex];
if (group == null) group = EMPTY_GROUP;
groups[groupIndex] = insertArticle(group, article, group.length);
}
}
return groups;
}
/**
* Finds the index of the time range the article belongs to.
*
* @param aArticle article.
*
* @return index of the range or <code>-1</code>, if doesn't belong to any of them.
*/
static int findGroupIndex(IArticle aArticle)
{
return findGroupIndex(aArticle, GroupsSetup.getGroupTimeRanges());
}
/**
* Finds the index of the time range the article belongs to.
*
* @param aArticle article.
* @param aRanges list of ranges to check.
*
* @return index of the range or <code>-1</code>, if doesn't belong to any of them.
*/
static int findGroupIndex(IArticle aArticle, TimeRange[] aRanges)
{
long time = aArticle.getPublicationDate().getTime();
int index = -1;
for (int i = 0; index == -1 && i < aRanges.length; i++)
{
TimeRange range = aRanges[i];
if (range.isInRange(time)) index = i;
}
return index;
}
/**
* Returns <code>TRUE</code>, if exectly this article object is on the list.
*
* @param aArticle article to look for.
*
* @return <code>TRUE</code>, if exectly this article object is on the list.
*/
private boolean contains(IArticle aArticle)
{
return indexOf(aArticle) > -1;
}
/**
* Finds the index of article.
*
* @param aArticle article.
*
* @return index or <code>-1</code>, if not on the list.
*/
private int indexOf(IArticle aArticle)
{
return indexOf(aArticle, sortedArticles);
}
/**
* Finds the index of article.
*
* @param aArticle article.
* @param aArticles list of articles to scan.
*
* @return index or <code>-1</code>, if not on the list.
*/
private static int indexOf(IArticle aArticle, IArticle[] aArticles)
{
int index = -1;
for (int i = 0; index == -1 && i < aArticles.length; i++)
{
if (aArticles[i] == aArticle) index = i;
}
return index;
}
// --------------------------------------------------------------------------------------------
// Events
// --------------------------------------------------------------------------------------------
/**
* Adds new listener.
*
* @param aListener listener.
*/
public void addListener(IFeedDisplayModelListener aListener)
{
if (!listeners.contains(aListener)) listeners.add(aListener);
}
private void fireArticlesRemoved()
{
for (IFeedDisplayModelListener l : listeners) l.articlesRemoved();
}
/**
* Fires event to all listeners when new article gets added into the group.
*
* @param aArticle article has been added.
* @param aGroupIndex index of the group.
* @param aIndexWithinGroup index within the group.
*/
private void fireArticleAdded(IArticle aArticle, int aGroupIndex, int aIndexWithinGroup)
{
for (IFeedDisplayModelListener l : listeners)
{
l.articleAdded(aArticle, aGroupIndex, aIndexWithinGroup);
}
}
/**
* Fires event to all listeners when new article gets removed from the group.
*
* @param aArticle article has been removed.
* @param aGroupIndex index of the group.
* @param aIndexWithinGroup index within the group.
*/
private void fireArticleRemoved(IArticle aArticle, int aGroupIndex, int aIndexWithinGroup)
{
for (IFeedDisplayModelListener l : listeners)
{
l.articleRemoved(aArticle, aGroupIndex, aIndexWithinGroup);
}
}
/**
* Sets filter for articles filtering.
*
* @param aFilter filter.
*
* @see IFeedDisplayConstants#FILTER_ALL
* @see IFeedDisplayConstants#FILTER_UNREAD
*/
public void setFilter(int aFilter)
{
if (filter != aFilter)
{
// Also reset the always visible article
alwaysVisibleArticle = null;
filter = aFilter;
reviewArticles();
}
}
/**
* Ensures that the article is visible until the next filter mode
* change or setting different feed.
*
* @param article article.
*
* @return new page or <code>-1</code> if hasn't changed.
*/
public int ensureArticleVisibility(IArticle article)
{
int newPage = -1;
alwaysVisibleArticle = article;
if (article != null)
{
reviewArticle(article);
updatePageCount();
int articlePage = findPageFor(article);
if (articlePage != page && articlePage != -1)
{
setPage(articlePage);
newPage = articlePage;
} else updatePage();
}
return newPage;
}
/**
* Returns the page number for a given article if it's among the visible
* articles.
*
* @param article article.
*
* @return page number or '-1' if invisible.
*/
private int findPageFor(IArticle article)
{
int page = -1;
int i = visibleArticles.indexOf(article);
if (i >= 0 && pageSize > 0) page = i / pageSize;
return page;
}
/**
* Invoked when article changes.
*
* @param article article
*/
private void onArticleChanged(IArticle article)
{
if ((filter == FILTER_UNREAD && (hideArticlesWhenRead || !article.isRead())) ||
filter == FILTER_NEGATIVE || filter == FILTER_NON_NEGATIVE || filter == FILTER_POSITIVE)
{
if (reviewArticle(article))
{
updatePageCount();
updatePage();
}
}
}
/**
* Reviews all articles in this feed.
*/
private void reviewArticles()
{
boolean updated = false;
for (IArticle article : sortedArticles) updated |= reviewArticle(article);
if (updated)
{
updatePageCount();
updatePage();
}
}
/**
* Reviews given article.
*
* @param aArticle article.
*
* @return <code>TRUE</code> if updated the article state.
*/
private boolean reviewArticle(IArticle aArticle)
{
boolean updated;
if (shouldBeVisible(aArticle))
{
updated = !isVisible(aArticle) && showArticle(aArticle);
} else
{
updated = isVisible(aArticle) && hideArticle(aArticle);
}
return updated;
}
/**
* Hides the article.
*
* @param aArticle article to hide.
*
* @return <code>TRUE</code> if something was hidden.
*/
private boolean hideArticle(IArticle aArticle)
{
boolean hidden = false;
int groupIndex = findGroupIndex(aArticle);
if (groupIndex == -1) groupIndex = 0;
visibleArticles.remove(aArticle);
IArticle[] group = getRawGroup(groupIndex);
int indexWithinGroup = indexOf(aArticle, group);
if (indexWithinGroup > -1)
{
articlesGroups[groupIndex] = removeArticle(group, aArticle);
if (pageArticles.remove(aArticle))
{
hidden = true;
fireArticleRemoved(aArticle, applySorting(groupIndex), indexWithinGroup);
}
}
return hidden;
}
/**
* Shows the article (exposes outside the view).
*
* @param aArticle article to show.
*
* @return <code>TRUE</code> if article was shown.
*/
private boolean showArticle(IArticle aArticle)
{
boolean shown = false;
int index = Collections.binarySearch(visibleArticles, aArticle, getArticlesComparator());
// If index is positive, we have articles with the same timestamp and we reuse
// their index for upcoming insertion, otherwise -- we convert to insertion index.
if (index < 0)
{
index = -index - 1;
visibleArticles.add(index, aArticle);
shown = true;
}
// Disabled because articles never enter the paged view
// fireArticleAdded(aArticle, applySorting(groupIndex), indexWithinGroup);
return shown;
}
/**
* Returns <code>TRUE</code> if there are some visible articles.
*
* @return <code>TRUE</code> if there are some visible articles.
*/
public boolean hasVisibleArticles()
{
return visibleArticles.size() > 0;
}
/**
* Returns <code>TRUE</code> if article is currently visible.
*
* @param aArticle article.
*
* @return <code>TRUE</code> if article is currently visible.
*/
private boolean isVisible(IArticle aArticle)
{
return visibleArticles.contains(aArticle);
}
/**
* Returns <code>TRUE</code> if article should be visible taking current
* model mode in account.
*
* @param aArticle article to review.
*
* @return <code>TRUE</code> if should be visible.
*/
private boolean shouldBeVisible(IArticle aArticle)
{
if (alwaysVisibleArticle == aArticle) return true;
boolean filtered =
filter == FILTER_ALL ||
(filter == FILTER_PINNED && aArticle.isPinned()) ||
(filter == FILTER_UNREAD && !aArticle.isRead()) ||
(filter == FILTER_POSITIVE && aArticle.isPositive()) ||
(filter == FILTER_NEGATIVE && aArticle.isNegative()) ||
(filter == FILTER_NON_NEGATIVE && !aArticle.isNegative());
return filtered && (maxArticleAge == -1 || isNotOld(aArticle));
}
/**
* Returns <code>TRUE</code> if article is not older than max defined age.
*
* @param aArticle article to check.
*
* @return <code>TRUE</code> if article isn't older.
*/
private boolean isNotOld(IArticle aArticle)
{
long age = System.currentTimeMillis() - aArticle.getPublicationDate().getTime();
return age < maxArticleAge;
}
/**
* Suppresses the articles older than given number of millis.
*
* @param millis number of millis or <code>-1</code> to turn feature off.
*/
public void setMaxArticleAge(long millis)
{
if (maxArticleAge != millis)
{
maxArticleAge = millis;
reviewArticles();
}
}
// --------------------------------------------------------------------------------------------
// Paging
// --------------------------------------------------------------------------------------------
/**
* Changes the page to a given.
*
* @param page new page.
*/
public void setPage(int page)
{
if (this.page != page)
{
this.page = page;
loadPage();
}
}
/**
* Returns current page number.
*
* @return current page.
*/
public int getPage()
{
return page;
}
/**
* Returns the number of pages possible.
*
* @return pages.
*/
public int getPagesCount()
{
return (Integer)pageCountModel.getValue();
}
// --------------------------------------------------------------------------------------------
// DEBUG
// --------------------------------------------------------------------------------------------
/**
* Dumps the state to the console.
*/
public void dump()
{
LOG.warning("--- Model Dump ---");
LOG.warning("Number of articles: " + getArticlesCount());
LOG.warning("Articles:");
for (IArticle article : sortedArticles) LOG.warning(" " + article.getTitle());
}
/**
* Sets the size of the page.
*
* @param size size.
*/
public void setPageSize(int size)
{
if (pageSize != size)
{
pageSize = size;
recalcModel();
}
}
// --------------------------------------------------------------------------------------------
// Feed listener
// --------------------------------------------------------------------------------------------
/**
* Listener of feed events.
*/
private class FeedListener extends FeedAdapter
{
/**
* Called when some article is added to the feed.
*
* @param feed feed.
* @param article article.
*/
public void articleAdded(IFeed feed, final IArticle article)
{
if (UifUtilities.isEDT())
{
onArticleAdded(article);
} else SwingUtilities.invokeLater(new UpdateModel(article, UpdateAction.ADDED));
}
/**
* Called when some article is removed from the feed.
*
* @param feed feed.
* @param article article.
*/
public void articleRemoved(IFeed feed, final IArticle article)
{
if (UifUtilities.isEDT())
{
onArticleRemoved(article);
} else SwingUtilities.invokeLater(new UpdateModel(article, UpdateAction.REMOVED));
}
}
/**
* Listener for article read/unread state changes.
*/
private class ArticleListener implements IArticleListener
{
private List<String> interestingProperties;
/**
* Creates the listener.
*/
private ArticleListener()
{
interestingProperties = new LinkedList<String>();
interestingProperties.add(IArticle.PROP_READ);
interestingProperties.add(IArticle.PROP_POSITIVE);
interestingProperties.add(IArticle.PROP_NEGATIVE);
}
/**
* Invoked when the property of the article has been changed.
*
* @param article article.
* @param property property of the article.
* @param oldValue old property value.
* @param newValue new property value.
*/
public void propertyChanged(final IArticle article, String property, Object oldValue,
Object newValue)
{
if (interestingProperties.contains(property))
{
if (UifUtilities.isEDT())
{
onArticleChanged(article);
} else SwingUtilities.invokeLater(new UpdateModel(article, UpdateAction.CHANGED));
}
}
}
/**
* The set of possible update actions.
*/
enum UpdateAction { ADDED, REMOVED, CHANGED }
/**
* Model update command that respects the time of feed selection
* and skips the updates that are delivered for the previous feed.
*/
private class UpdateModel implements Runnable
{
private final IArticle article;
private final UpdateAction action;
private final long timestamp;
/**
* Creates the model update operation.
*
* @param article article.
* @param action update action.
*/
public UpdateModel(IArticle article, UpdateAction action)
{
this.article = article;
this.action = action;
timestamp = System.currentTimeMillis();
}
/**
* Invoked when it's time to update the model.
*/
public void run()
{
if (timestamp >= feedChangeTime)
{
switch (action)
{
case ADDED:
onArticleAdded(article);
break;
case REMOVED:
onArticleRemoved(article);
break;
default:
onArticleChanged(article);
}
}
}
}
}