Package com.salas.bb.views.feeds

Source Code of com.salas.bb.views.feeds.FeedDisplayModel$UpdateModel

// 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);
                }
            }
        }
    }
}
TOP

Related Classes of com.salas.bb.views.feeds.FeedDisplayModel$UpdateModel

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.