Package com.salas.bb.domain

Source Code of com.salas.bb.domain.DataFeed$ArticlesListener

// 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: DataFeed.java,v 1.87 2008/03/17 10:53:21 spyromus Exp $
//

package com.salas.bb.domain;

import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock;
import EDU.oswego.cs.dl.util.concurrent.ReaderPreferenceReadWriteLock;
import com.salas.bb.core.FeedDisplayModeManager;
import com.salas.bb.domain.utils.ArticleDateComparator;
import com.salas.bb.utils.Constants;
import com.salas.bb.utils.StringUtils;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.parser.Channel;
import com.salas.bb.utils.parser.Item;
import com.salas.bb.utils.swinghtml.TextProcessor;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Abstract feed acting as a base for any feed whith own (non-virtual) articles.
*/
public abstract class DataFeed extends AbstractFeed
{
    private static final Logger LOG = Logger.getLogger(DataFeed.class.getName());

    public static final String PROP_PURGE_LIMIT             = "purgeLimit";
    public static final String PROP_LAST_POLL_TIME          = "lastPollTime";
    public static final String PROP_LAST_UPDATE_SERVER_TIME = "lastUpdateServerTime";
    public static final String PROP_RETRIEVALS              = "retrievals";
    public static final String PROP_TOTAL_POLLED_ARTICLES   = "totalPolledArticles";
    public static final String PROP_INITIALIZED             = "initialized";
    public static final String PROP_FORMAT                  = "format";
    public static final String PROP_LANGUAGE                = "language";
    public static final String PROP_MARK_READ_WHEN_NO_KEYWORDS = "markReadWhenNoKeywords";
    public static final String PROP_UPDATE_PERIOD           = "updatePeriod";
    public static final String PROP_LAST_FETCH_ARTICLE_KEYS = "lastFetchArticleKeys";

    static final int DEFAULT_LAST_UPDATE_SERVER_TIME        = -1;
    static final int INIT_TIME_UNINITIALIZED                = -1;
    static final int DEFAULT_LAST_POLL_TIME                 = 0;
    static final int DEFAULT_TOTAL_POLLED_ARTICLES          = 0;
    static final int DEFAULT_RETRIEVALS                     = 0;
    static final boolean DEFAULT_MARK_READ_WHEN_NO_KEYWORDS = false;
    public static final int DEFAULT_PURGE_LIMIT             = 30;

    static final int PURGE_LIMIT_INHERITED                  = -1;
    static final int UPDATE_PERIOD_INHERITED                = -1;

    /**
     * Flag, showing if it's allowed to purge unread records or they should
     * be preserved during cleanup. TRUE to allow removal.
     */
    private static boolean globalPurgeUnread = true;

    /**
     * Global purge limit. If purge limit of feed isn't set this value is taken.
     * @see #purgeLimit
     */
    private static int globalPurgeLimit = 30;

    /**
     * Global update period. If update period of feed isn't set this value is taken.
     * @see #updatePeriod
     */
    private static long globalUpdatePeriod = Constants.MILLIS_IN_HOUR;

    /**
     * Time of initialization of the feed.
     */
    private long initTime;

    /**
     * List of all articles in the feed.
     */
    private final List<IArticle> articles;

    /**
     * List of simple match keys of all articles which should be marked as read on addition.
     * This list is a part of a trick called "marking read articles as read after import".
     */
    protected List readArticlesKeys;

    /**
     * List of simple match keys of all articles which should be marked as pinned on addition.
     * This list is a part of a trick called "marking pinned articles as pinned after import".
     */
    protected List pinnedArticlesKeys;

    /**
     * Maximum number of articles to have. This value holds the specific limit to current
     * feed. If it's equal to (-1) the value will be taken from parent guide.
     */
    private int purgeLimit;

    /**
     * The time of last successful update operation. In conjunction with
     * <code>getUpdatePeriod()</code> call it's necessary to determine if it's time
     * to update the feed.
     */
    private long lastPollTime;

    /**
     * Time of last update according to the server. The time is given in the
     * server's time-zone which allows us to query server for changes since this
     * last update time. Note, that if server has no last update time reporting
     * facility this field will always be equal to <code>-1</code> and a feed
     * will always be fetched fully for further analysis.
     */
    private long lastUpdateServerTime;

    /**
     * Total number of articles pased through the feed since initialization. In conjuntion
     * with initialization time this number is used to determine the activity of the
     * feed.
     */
    private int totalPolledArticles;

    /**
     * Total number of retrievals -- the successful update attempts.
     */
    private int retrievals;

    /**
     * Format of the feed. It's a simple string message in free format holding the
     * information about the format of the feed. In most cases it will be something, like
     * RSS 2.0 or Atom 0.3 and etc.
     */
    private String format;

    /**
     * Language in which the feed is written. Free-format string with language information.
     */
    private String language;

    /**
     * Update period of the feed in ms. It can be inherited (-1) or specified exclusively
     * for every feed.
     */
    private long updatePeriod;

    /**
     * Counter of unread articles.
     */
    private int unreadArticlesCount;
    private final ReadWriteLock unreadArticlesCountLock;

    /**
     * Listener for changes in articles.
     */
    private ArticlesListener articlesListener;

    /**
     * The match keys of all articles seen in the feed during last fetch.
     */
    private String[] lastFetchArticleKeys;

    /**
     * Creates a feed.
     */
    protected DataFeed()
    {
        articles = new ArrayList<IArticle>();

        unreadArticlesCount = 0;
        unreadArticlesCountLock = new ReaderPreferenceReadWriteLock();

        readArticlesKeys = new ArrayList();
        pinnedArticlesKeys = new ArrayList();

        articlesListener = new ArticlesListener();

        purgeLimit = PURGE_LIMIT_INHERITED;
        updatePeriod = UPDATE_PERIOD_INHERITED;

        resetFeedStatistics();

        format = null;
        language = null;
    }

    /**
     * Clears initialization and last poll times, number of retievals and total polled articles
     * count.
     */
    protected void resetFeedStatistics()
    {
        initTime = INIT_TIME_UNINITIALIZED;
        lastPollTime = DEFAULT_LAST_POLL_TIME;
        lastUpdateServerTime = DEFAULT_LAST_UPDATE_SERVER_TIME;
        totalPolledArticles = DEFAULT_TOTAL_POLLED_ARTICLES;
        retrievals = DEFAULT_RETRIEVALS;
    }

    /**
     * Returns the Article at the specified index.
     *
     * @param index index of article in feed.
     *
     * @return article object.
     */
    public synchronized IArticle getArticleAt(int index)
    {
        return articles.get(index);
    }

    /**
     * Returns number of articles in feed.
     *
     * @return number of articles.
     */
    public synchronized int getArticlesCount()
    {
        return articles.size();
    }

    /**
     * Returns number of articles this feed owns.
     *
     * @return number of articles.
     */
    public int getOwnArticlesCount()
    {
        return getArticlesCount();
    }

    /**
     * Returns the list of all articles which are currently in the feed.
     *
     * @return all articles at this moment.
     */
    public synchronized IArticle[] getArticles()
    {
        return articles.toArray(new IArticle[articles.size()]);
    }

    /**
     * Returns the articles list to the child.
     *
     * @return articles list.
     */
    protected List<IArticle> getArticlesList()
    {
        return articles;
    }

    /**
     * Adds article to the list. Duplicates aren't added.
     *
     * @param article article to add.
     *
     * @return TRUE if added article.
     *
     * @throws NullPointerException if article is null.
     * @throws IllegalStateException if article belongs to the other feed already.
     */
    public synchronized boolean appendArticle(IArticle article)
    {
        if (article == null) throw new NullPointerException(Strings.error("unspecified.article"));

        // Register this feed as candidate to have proper key checks
        article.setCandidateFeed(this);

        boolean result = insertArticle(getArticlesCount(), article);

        // Release candidate feed
        article.setCandidateFeed(null);

        return result;
    }

    /**
     * Insert article to the list at a given position. Duplicates aren't added.
     *
     * @param index     index to add article at.
     * @param article   article to add.
     *
     * @return TRUE if added article.
     *
     * @throws NullPointerException      if article is null.
     * @throws IndexOutOfBoundsException if article index points to missing position.
     * @throws IllegalStateException     if article belongs to the other feed already.
     */
    public synchronized boolean insertArticle(int index, IArticle article)
    {
        if (article == null) throw new NullPointerException(Strings.error("unspecified.article"));
        if (article.getFeed() != null)
            throw new IllegalStateException(Strings.error("article.belongs.to.the.feed.already"));

        int count = getArticlesCount();
        if (index < 0 || index > count)
            throw new IndexOutOfBoundsException(Strings.error("index.is.out.of.bounds.of.article.list"));

        boolean added = false;

        if (article.getID() > 0 || !articles.contains(article))
        {
            articles.add(index, article);
            article.setFeed(this);
            added = true;

            // Automatically mark as read if we have the key of this article in our
            // read-list
            if (isArticleOnReadList(article)) article.setRead(true);

            // Automaticall mark as pinned if we have the key of this article in our
            // pinned-list
            if (isArticleOnPinnedList(article)) article.setPinned(true);

            // Increment counter of unread articles if the article is not read
            article.addListener(articlesListener);
            if (!article.isRead()) setUnreadArticlesCount(unreadArticlesCount + 1);

            article.setNew(true);
            try
            {
                fireArticleAdded(article);
            } finally
            {
                article.setNew(false);
            }
        }

        return added;
    }

    /**
     * Removes article from the list.
     *
     * @param article article to remove.
     *
     * @return <code>TRUE</code> if article has been removed.
     *
     * @throws NullPointerException if article is null.
     */
    public synchronized boolean removeArticle(IArticle article)
    {
        if (article == null) throw new NullPointerException(Strings.error("unspecified.article"));

        boolean removed;

        removed = articles.remove(article);
        if (removed)
        {
            article.removeListener(articlesListener);
            if (!article.isRead()) setUnreadArticlesCount(unreadArticlesCount - 1);

            fireArticleRemoved(article);
        }

        return removed;
    }

    /**
     * Returns unread articles count.
     *
     * @return count.
     */
    public int getUnreadArticlesCount()
    {
        int count = 0;
        try
        {
            unreadArticlesCountLock.readLock().acquire();
            count = unreadArticlesCount;
            unreadArticlesCountLock.readLock().release();
        } catch (InterruptedException e)
        {
            LOG.log(Level.SEVERE, Strings.error("interrupted"), e);
        }

        return count;
    }

    /**
     * Returns the number of pinned articles.
     *
     * @return number of pinned articles.
     */
    private int getPinnedArticlesCount()
    {
        int count = 0;
        for (IArticle article : articles) if (article.isPinned()) count++;
        return count;
    }

    /**
     * Sets new unread articles count.
     *
     * @param count new unread articles count.
     *
     * @throws IllegalArgumentException if count less than 0 or greater than number of articles.
     */
    private void setUnreadArticlesCount(int count)
        throws IllegalArgumentException
    {
        if (count < 0 || count > getArticlesCount())
            throw new IllegalArgumentException(MessageFormat.format(
                Strings.error("unread.articles.count.ran.out.of.allowed.range"),
                count, getArticlesCount()));

        try
        {
            unreadArticlesCountLock.writeLock().acquire();
            int oldValue = unreadArticlesCount;
            unreadArticlesCount = count;
            unreadArticlesCountLock.writeLock().release();

            firePropertyChanged(PROP_UNREAD_ARTICLES_COUNT, oldValue, count, false, true);
        } catch (InterruptedException e)
        {
            LOG.log(Level.SEVERE, Strings.error("interrupted"), e);
        }
    }

    /**
     * Returns comma-delimetered list of keys of read articles. This method returns not the same
     * value which might be set by appropriate setter. This is because this method returns current
     * situation representationa and setter only suggests what articles should be marked as read.
     *
     * @return list of keys.
     */
    public synchronized String getReadArticlesKeys()
    {
        String keys;

        if (!isInitialized())
        {
            // If the feed was received with SyncIn (on its own or as part of SyncFull)
            // and isn't polled yet, then it will have no articles and SyncOut (SyncFull)
            // will save empty list of read articles keys, which isn't right. So, we reuse
            // what we have got.
            if (readArticlesKeys != null)
            {
                keys = StringUtils.join(readArticlesKeys.iterator(), ",");
            } else
            {
                keys = Constants.EMPTY_STRING;
            }
        } else
        {
            List<String> keysList = new ArrayList<String>(getUnreadArticlesCount());
            for (int i = 0; i < getArticlesCount(); i++)
            {
                final IArticle a = getArticleAt(i);
                if (a.isRead()) keysList.add(a.getSimpleMatchKey());
            }

            keys = StringUtils.join(keysList.iterator(), ",");
        }

        return keys;
    }

    /**
     * Returns a comma-delimetered list of keys of pinned articles. This method returns not the same
     * value which might be set by appropriate setter. This is because this method returns current
     * situation representationa and setter only suggests what articles should be marked as pinned.
     *
     * @return list of keys.
     */
    public synchronized String getPinnedArticlesKeys()
    {
        String keys;

        if (!isInitialized())
        {
            // If the feed was received with SyncIn (on its own or as part of SyncFull)
            // and isn't polled yet, then it will have no articles and SyncOut (SyncFull)
            // will save empty list of pinned articles keys, which isn't right. So, we reuse
            // what we have got.
            if (pinnedArticlesKeys != null)
            {
                keys = StringUtils.join(pinnedArticlesKeys.iterator(), ",");
            } else
            {
                keys = Constants.EMPTY_STRING;
            }
        } else
        {
            List<String> keysList = new ArrayList<String>();
            for (int i = 0; i < getArticlesCount(); i++)
            {
                final IArticle a = getArticleAt(i);
                if (a.isPinned()) keysList.add(a.getSimpleMatchKey());
            }

            keys = StringUtils.join(keysList.iterator(), ",");
        }

        return keys;
    }

    /**
     * Sets comma-delimetered list of articles' keys. This list will be taken in account when new
     * articles will be added. If article has the key mentioned in the list then it will be
     * selected.
     *
     * @param keys list of keys. Null is ok.
     */
    public synchronized void setReadArticlesKeys(String keys)
    {
        readArticlesKeys.clear();

        if (keys != null)
        {
            // parse list of keys
            readArticlesKeys = parseKeysToList(keys);

            // check if we have articles to mark already
            for (int i = 0; i < getArticlesCount(); i++)
            {
                IArticle article = getArticleAt(i);
                if (isArticleOnReadList(article)) article.setRead(true);
            }
        }
    }

    /**
     * Sets a comma-delimetered list of articles' keys. This list will be taken in account when new
     * articles will be added. If article has the key mentioned in the list then it will be
     * selected.
     *
     * @param keys list of keys. Null is ok.
     */
    public synchronized void setPinnedArticlesKeys(String keys)
    {
        pinnedArticlesKeys.clear();

        if (keys != null)
        {
            // parse list of keys
            pinnedArticlesKeys = parseKeysToList(keys);

            // check if we have articles to mark already
            for (int i = 0; i < getArticlesCount(); i++)
            {
                IArticle article = getArticleAt(i);
                if (isArticleOnPinnedList(article)) article.setPinned(true);
            }
        }
    }

    /**
     * Parses the list of keys into the Java list.
     *
     * @param keys keys string.
     *
     * @return array.
     */
    private static ArrayList<String> parseKeysToList(String keys)
    {
        StringTokenizer st = new StringTokenizer(keys, ",");
        ArrayList<String> list = new ArrayList<String>(st.countTokens());
        while (st.hasMoreTokens())
        {
            String key = st.nextToken();

            // Special treatment for old 8-byte keys based on links
            // (convert to positive 4-byte equivalents)
            if (key.length() == 16)
            {
                long positive = 0x100000000L - Long.parseLong(key.substring(8), 16);
                key = Long.toHexString(positive);
            }

            list.add(key);
        }
        return list;
    }

    /**
     * Returns <code>TRUE</code> if article should be marked as pinned because of being on the
     * pinned articles list.
     *
     * @param article article to check.
     *
     * @return <code>TRUE</code> if it is.
     */
    private boolean isArticleOnPinnedList(IArticle article)
    {
        return pinnedArticlesKeys.size() > 0 && pinnedArticlesKeys.contains(article.getSimpleMatchKey());
    }

    /**
     * Returns <code>TRUE</code> if article should be marked as Read because of being on the
     * read articles list.
     *
     * @param article article to check.
     *
     * @return <code>TRUE</code> if it is.
     */
    private boolean isArticleOnReadList(IArticle article)
    {
        return readArticlesKeys.size() > 0 && readArticlesKeys.contains(article.getSimpleMatchKey());
    }

    /**
     * Returns the format of this feed.
     *
     * @return format.
     */
    public String getFormat()
    {
        return format;
    }

    /**
     * Sets the format of the feed.
     *
     * @param aFormat new format.
     */
    public void setFormat(String aFormat)
    {
        String oldFormat = format;
        format = aFormat;
        firePropertyChanged(PROP_FORMAT, oldFormat, format);
    }

    /**
     * Returns language of feed.
     *
     * @return language.
     */
    public String getLanguage()
    {
        return language;
    }

    /**
     * Sets the language of the feed.
     *
     * @param aLanguage language.
     */
    public void setLanguage(String aLanguage)
    {
        String oldLanguage = language;
        language = aLanguage;
        firePropertyChanged(PROP_LANGUAGE, oldLanguage, language);
    }

    /**
     * Returns TRUE if automatic new articles scanning is enabled.
     *
     * @return TRUE if automatic new articles scanning is enabled.
     */
    public boolean isAutoFeedsDiscovery()
    {
        return isAutoFeedsDiscoveryInParentGuides();
    }

    /**
     * Returns <code>TRUE</code> if auto-feed discovery is enabled in at least one parent guide.
     *
     * @return <code>TRUE</code> if auto-feed discovery is enabled in at least one parent guide.
     */
    private boolean isAutoFeedsDiscoveryInParentGuides()
    {
        boolean guideAutoFeedsDiscovery = false;

        // WARNING: Synchronization of parentGuides required !!!
        IGuide[] parentGuides = getParentGuides();
        for (int i = 0; !guideAutoFeedsDiscovery && i < parentGuides.length; i++)
        {
            guideAutoFeedsDiscovery = parentGuides[i].isAutoFeedsDiscovery();
        }

        return guideAutoFeedsDiscovery;
    }

    /**
     * Returns purge limit setting for this particular feed.
     *
     * @return limit setting or <code>PURGE_LIMIT_INHERITED</code>.
     *
     * @see #PURGE_LIMIT_INHERITED
     */
    public int getPurgeLimit()
    {
        return purgeLimit;
    }

    /**
     * Returns currently set purge limit. If specific purge limit isn't set the global is taken.
     *
     * @return purge limit.
     */
    public int getPurgeLimitCombined()
    {
        return purgeLimit == PURGE_LIMIT_INHERITED ? getGlobalPurgeLimit() : purgeLimit;
    }

    /**
     * Returns global value of purge limit. It will be used when the feed has no own
     * limit setting.
     *
     * @return default limit.
     */
    public static int getGlobalPurgeLimit()
    {
        return globalPurgeLimit;
    }

    /**
     * Sets new value for global purge limit. This value is used when the feed has no own
     * limit setting. Note that setting this value will not induce cleanup.
     *
     * @param limit new limit value.
     */
    public static void setGlobalPurgeLimit(int limit)
    {
        globalPurgeLimit = limit;
    }

    /**
     * Sets purge limit.
     *
     * @param aPurgeLimit new purge limit.
     */
    public void setPurgeLimit(int aPurgeLimit)
    {
        int oldLimit = getPurgeLimitCombined();
        purgeLimit = aPurgeLimit;
        int newLimit = getPurgeLimitCombined();

        firePropertyChanged(PROP_PURGE_LIMIT, oldLimit, newLimit, true, false);

        // if purge limit became lower then cleaning may be necessary
        if (oldLimit > newLimit) clean();
    }

    /**
     * Returns TRUE if the feed is in the Manual update mode.
     *
     * @return TRUE if in manual mode.
     */
    protected boolean isOnlyManual()
    {
        return getUpdatePeriod() == 0;
    }

    /**
     * Returns TRUE if this feed is updatable, meaning that it's not invalid for some reason
     * and it's proper time to call <code>update()</code> method. The behaviod may differ
     * if the update operation was called manually to this particular feed and not as a part
     * of a bigger update operation (update guide or update all).
     *
     * @param manual if <code>TRUE</code> then the update was requested manually (not through periodic check).
     * @param allowInvisible <code>TRUE</code> if invisible feed is allowed for update.
     *
     * @return <code>TRUE</code> if it's updatable.
     */
    public final boolean isUpdatable(boolean manual, boolean allowInvisible)
    {
        return getID() != -1 &&
            !isProcessing() &&
            (allowInvisible || canBenefitFromUpdate()) &&
            isUpdatable(manual);
    }

    /**
     * Returns <code>TRUE</code> if considering current feed classes set,
     * the feed can benefit for the update and it is going to change its status.
     *
     * @return <code>TRUE</code> if update could help this feed become visible.
     */
    private boolean canBenefitFromUpdate()
    {
        int m = getClassesMask() & FeedClass.MASK_UNUPDATABLE;
        return FeedDisplayModeManager.getInstance().isVisible(m);
    }

    /**
     * Returns TRUE if this feed is updatable, meaning that it's not invalid for some reason
     * and it's proper time to call <code>update()</code> method. The behaviod may differ
     * if the update operation was called manually to this particular feed and not as a part
     * of a bigger update operation (update guide or update all).
     *
     * @param manual if TRUE then the update was requested manually (not through periodic check).
     *
     * @return <code>TRUE</code> if it's updatable.
     */
    protected boolean isUpdatable(boolean manual)
    {
        return manual || (!isInvalid() && !isOnlyManual() && (getLastPollTime() + getUpdatePeriodCombined()) < System.currentTimeMillis());
    }

    /**
     * Updates the feed contents using internal algorithms specific to each feed.
     */
    public void update()
    {
        long updateTime = System.currentTimeMillis();

        // Fetch the feed data
        Channel channel = null;
        try
        {
            channel = fetchFeed();
        } catch (Exception e)
        {
            setInvalidnessReason(Strings.message("feed.invalidness.reason.bad.data"));
            LOG.log(Level.SEVERE, MessageFormat.format(Strings.error("feed.fetching.errored.internal.error"),
                toString()), e);
        }

        // Convert the list of items into articles before entering the
        // synchronized block to minimize locking
        StandardArticle[] articles = null;
        if (channel != null)
        {
            articles = new StandardArticle[channel.getItemsCount()];
            for (int i = 0; i < articles.length; i++) articles[i] = createArticle(channel.getItemAt(i));
        }

        synchronized (this)
        {
            if (channel != null)
            {
                updateFeed(channel);
                updateArticles(articles);
                clean();
            }

            setLastPollTime(updateTime);
            setInitTime(updateTime);
            setRetrievals(getRetrievals() + 1);
        }

        // Release candidate feed references after the checks
        if (articles != null)
        {
            for (StandardArticle article : articles) article.setCandidateFeed(null);
        }
    }

    /**
     * Removes the articles from the feed to fit into limit parameter.
     * If removal of unread articles isn't allowed then the read articles may be
     * removed even if they are in the head of the feed to match the limit setting.
     *
     * @param limit               clean limit.
     * @param overridePurgeUnread <code>TRUE</code> to override purge unread setting
     *                            and allow removing of unread articles.
     */
    public synchronized void clean(final int limit, boolean overridePurgeUnread)
    {
        // Calculate number of articles we need to remove
        boolean purgeUnread = overridePurgeUnread || isPurgeUnread();
        int total = getArticlesCount();
        int unread = getUnreadArticlesCount();
        int pinned = getPinnedArticlesCount();
        int toRemove = calcArticlesToRemove(total, unread, pinned, limit, purgeUnread);

        if (toRemove > 0)
        {
            processingStarted();

            // Choose articles that can potentially be deleted
            List<IArticle> canBeDeleted = new ArrayList<IArticle>();
            for (int i = 0; i < total; i++)
            {
                IArticle article = getArticleAt(i);
                if ((purgeUnread || article.isRead()) && !article.isPinned()) canBeDeleted.add(article);
            }

            if (canBeDeleted.size() > 0)
            {
                // Sort article by pubdate
                Collections.sort(canBeDeleted, new ArticleDateComparator());

                // Move on from the tail and remove only allowed articles
                toRemove = Math.min(toRemove, canBeDeleted.size());
                for (int i = 0; i < toRemove; i++)
                {
                    IArticle article = canBeDeleted.get(i);
                    removeArticle(article);
                }
            }

            processingFinished();
        }
    }

    /**
     * Removes the articles from the feed to fit into the current purge limit setting.
     * If removal of unread articles isn't allowed then the read articles may be
     * removed even if they are in the head of the feed to match the limit setting.
     */
    public synchronized void clean()
    {
        clean(getPurgeLimitCombined(), false);
    }

    /**
     * Calculates number of articles to remove from the tail depending on the
     * settings and counts.
     *
     * @param total         total number of articles.
     * @param unread        number of unread articles.
     * @param pinned        number of pinned articles.
     * @param limit         limit (desired number of articles to have).
     * @param purgeUnread   TRUE to allow removal of unread articles.
     *
     * @return number of articles which can be safely removed from the tail (respecting
     *         <code>purgeUnread</code> flag, of course).
     */
    static int calcArticlesToRemove(int total, int unread, int pinned, int limit, boolean purgeUnread)
    {
        // Pinned articles never count
        total = total - pinned;

        // Calculate number of articles, free to be removed
        int dynamic = purgeUnread ? total : total - unread;

        // Calculate number of articles we need to leave untouched
        int leave = Math.max(limit, total - dynamic);

        // Return number of articles we need to remove [0;inf)
        return Math.max(0, total - leave);
    }

    /**
     * Returns TRUE if purging is allowed to remove unread articles.
     *
     * @return TRUE if purging is allowed to remove unread articles.
     */
    public boolean isPurgeUnread()
    {
        return isGlobalPurgeUnread();
    }

    /**
     * Returns TRUE if purgin of unread articles is enabled globally.
     *
     * @return TRUE if purgin of unread articles is enabled globally.
     */
    public static boolean isGlobalPurgeUnread()
    {
        return globalPurgeUnread;
    }

    /**
     * Changes the state of default flag (for all feeds) showing whether it's
     * allowed to purge unread articles or no.
     *
     * @param value TRUE to allow.
     */
    public static void setGlobalPurgeUnread(boolean value)
    {
        globalPurgeUnread = value;
    }

    /**
     * Fetches the feed by some specific means.
     *
     * @return the feed or NULL if there was an error or no updates required.
     */
    protected abstract Channel fetchFeed()
        throws IOException;

    /**
     * Updates the feed properties from the channel object.
     *
     * @param channel channel object.
     */
    protected void updateFeed(Channel channel)
    {
        String channelFormat = channel.getFormat();
        if (channelFormat != null) setFormat(channelFormat);

        String channelLanguage = channel.getLanguage();
        if (channelLanguage != null) setLanguage(channelLanguage);

        long newUpdatePeriod = channel.getUpdatePeriod();
        if (getUpdatePeriod() == -1 && newUpdatePeriod != -1) setUpdatePeriod(newUpdatePeriod);
        setLastUpdateServerTime(channel.getLastUpdateServerTime());
    }

    /**
     * Updates the list of articles from the list of items taken from the channel object.
     * This implementation analyzes the incoming list of articles and carefully selects
     * the items for addition to the feed.
     *
     * @param incomingArticles articles to get updates from.
     */
    protected void updateArticles(StandardArticle[] incomingArticles)
    {
        boolean canAddMore = true;
        int added = 0;

        // In this version of feed update logic we scan through all articles
        // and see if they were seen during the previous fetch. If they were
        // then they are ignored. If they weren't we are trying to add them
        // to the local feed. In case when some article is reposted it may
        // come that the article wasn't seen during the last fetch attempt,
        // but it's still in the local feed version -- it will also be ignored.
        // After all articles are scanned through we save the list of all
        // article match keys for the future use.

        int purgeLimit = getPurgeLimitCombined();
        List<String> keys = new ArrayList<String>(incomingArticles.length);
        try
        {
            for (StandardArticle article : incomingArticles)
            {
                String matchKey = article.getSimpleMatchKey();
                keys.add(matchKey);

                if (canAddMore &&
                    !isArticleSeen(matchKey) &&
                    !isDuplicate(article) &&
                    insertArticle(0, article))
                {
                    added++;
                    if (added == purgeLimit) canAddMore = false;
                }
            }
        } finally
        {
            setTotalPolledArticles(getTotalPolledArticles() + added);
            setLastFetchArticleKeys(keys.toArray(new String[keys.size()]));
        }
    }

    /**
     * Returns <code>TRUE</code> if an article is duplicate of some other already registered.
     *
     * @param article article.
     *
     * @return <code>TRUE</code> if an article is duplicate of some other already registered.
     */
    protected boolean isDuplicate(IArticle article)
    {
        // This method is overriden by QueryFeed to control the duplicates
        return false;
    }

    /**
     * Returns <code>TRUE</code> if the article with such match key was seen during last fetch.
     *
     * @param aKey key.
     *
     * @return <code>TRUE</code> if the article with such match key was seen during last fetch.
     */
    private boolean isArticleSeen(String aKey)
    {
        if (lastFetchArticleKeys == null || aKey == null) return false;
        boolean res = false;

        for (int i = 0; !res && i < lastFetchArticleKeys.length; i++)
        {
            res = aKey.equals(lastFetchArticleKeys[i]);
        }

        return res;
    }

    /**
     * Sets the list of last seen article keys.
     *
     * @param aKeys keys.
     */
    public void setLastFetchArticleKeys(String[] aKeys)
    {
        String[] old = lastFetchArticleKeys;
        lastFetchArticleKeys = aKeys;

        if (!Arrays.equals(old, lastFetchArticleKeys))
        {
            firePropertyChanged(PROP_LAST_FETCH_ARTICLE_KEYS, old, lastFetchArticleKeys);
        }
    }

    /**
     * Returns the list of last seen article keys.
     *
     * @return the list.
     */
    public String[] getLastFetchArticleKeys()
    {
        return lastFetchArticleKeys;
    }

// Below is an old version
//    /**
//     * Updates the list of articles from the list of items taken from the channel object.
//     * This implementation analyzes the incoming list of articles and carefully selects
//     * the items for addition to the feed.
//     *
//     * @param channel channel object.
//     */
//    protected void updateArticles(Channel channel)
//    {
//        int insertionIndex = 0;
//        boolean canAddMore = true;
//        int added = 0;
//
//        // We take the time of update start as a base for articles without publication
//        // date set. We will create our own dates for them starting from now to the
//        // past (as we assume that the recent articles go before the old ones in the feed).
//        long baseTime = System.currentTimeMillis();
//
//        int purgeLimit = getPurgeLimitCombined();
//        int count = channel.getItemsCount();
//        for (int i = 0; canAddMore && i < count; i++)
//        {
//            Item item = channel.getItemAt(i);
//
//            StandardArticle article = createArticle(item);
//
//            // Create our own date for article if it has no own publication date
//            // NOTE: There are pieces of code which count on that they
//            //       never see NULL as publication date. Review carefully this
//            //       if you are going to remove these lines.
//            if (article.getPublicationDate() == null)
//            {
//                article.setPublicationDate(new Date(baseTime--));
//            }
//
//            boolean addedArticle = insertArticle(insertionIndex, article);
//            insertionIndex = articles.indexOf(article) + 1;
//
//            // If we have detected the old article (existing already in the list)
//            // then we may need to stop reading following articles or continue
//            // depending on the publication date of this one. If the date is in
//            // the future then most probably it was a mistake and we continue
//            // reading.
//            if (addedArticle)
//            {
//                added++;
//            } else
//            {
//                Date pubDate = article.getPublicationDate();
//                if (pubDate.after(new Date())) addedArticle = true;
//            }
//
//            if (!addedArticle || added == purgeLimit) canAddMore = false;
//        }
//
//        setTotalPolledArticles(getTotalPolledArticles() + added);
//    }

    /**
     * Creates article from the given parsed item.
     *
     * @param item item to create article from.
     *
     * @return article.
     */
    private StandardArticle createArticle(Item item)
    {
        String text = TextProcessor.filterText(item.getText());
        String title = TextProcessor.filterTitle(item.getTitle(), text);

        StandardArticle article = new LazyArticle(text);
        article.setRead(false);
        article.setTitle(title);
        article.setAuthor(item.getAuthor());
        article.setLink(item.getLink());
        article.setPublicationDate(item.getPublicationDate());
        article.setSubject(StringUtils.unescape(item.getSubject()));
        article.getPlainText(); // stimulate plain text creation

        // Register this feed as candidate to have proper key checks
        article.setCandidateFeed(this);

        article.computeSimpleMatchKey();

        return article;
    }

    /**
     * Returns <code>true</code> if this feed has valid XML url and was successfully initialized.
     *
     * @return <code>true</code> if initialized.
     */
    public boolean isInitialized()
    {
        return initTime != INIT_TIME_UNINITIALIZED;
    }

    /**
     * Returns time of initialization.
     *
     * @return time of initialization or (-1) if still not initialized.
     */
    public long getInitTime()
    {
        return initTime;
    }

    /**
     * Sets the time of initialization. The setting will work out only if the feed
     * isn't initialized.
     *
     * @param aInitTime time of initialization.
     */
    public void setInitTime(long aInitTime)
    {
        if (initTime == INIT_TIME_UNINITIALIZED && aInitTime != INIT_TIME_UNINITIALIZED)
        {
            initTime = aInitTime;
            firePropertyChanged(PROP_INITIALIZED, false, true, false, true);
        }
    }

    /**
     * Returns timestamp of last successful poll.
     *
     * @return timestamp or DEFAULT_LAST_POLL_TIME if not polled yet.
     *
     * @see #DEFAULT_LAST_POLL_TIME
     */
    public long getLastPollTime()
    {
        return lastPollTime;
    }

    /**
     * Sets last successful poll time.
     *
     * @param time timestamp.
     */
    public void setLastPollTime(long time)
    {
        long old = lastPollTime;
        lastPollTime = time;
        firePropertyChanged(PROP_LAST_POLL_TIME, old, time);
    }

    /**
     * Returns last feed update time (in a server time-zone).
     *
     * @return last feed update time.
     */
    public long getLastUpdateServerTime()
    {
        return lastUpdateServerTime;
    }

    /**
     * Sets last feed update time (in a server time-zone).
     *
     * @param time timestamp.
     */
    public void setLastUpdateServerTime(long time)
    {
        long old = lastUpdateServerTime;
        lastUpdateServerTime = time;
        firePropertyChanged(PROP_LAST_UPDATE_SERVER_TIME, old, time);
    }

    /**
     * Returns update period of this feed. The period may be specific to feed or taken
     * from parent guide.
     *
     * @return update period in ms.
     */
    public long getUpdatePeriodCombined()
    {
        return updatePeriod < 0 ? getGlobalUpdatePeriod() : updatePeriod;
    }

    /**
     * Returns update period of this feed.
     *
     * @return update period in ms or <code>UPDATE_PERIOD_INHERITED</code>.
     *
     * @see #UPDATE_PERIOD_INHERITED
     */
    public long getUpdatePeriod()
    {
        return updatePeriod;
    }

    /**
     * Sets the period of updates to the feed. The period can be inherited, meaning that
     * defaul global value will be taken, or specific to the feed.
     *
     * @param period period in ms.
     *
     * @see #UPDATE_PERIOD_INHERITED
     */
    public void setUpdatePeriod(long period)
    {
        long oldPeriod = getUpdatePeriod();
        updatePeriod = period;
        long newPeriod = getUpdatePeriod();

        firePropertyChanged(PROP_UPDATE_PERIOD, oldPeriod, newPeriod, true, false);
    }

    /**
     * Returns global update period.
     *
     * @return update period in ms.
     */
    public static long getGlobalUpdatePeriod()
    {
        return globalUpdatePeriod;
    }

    /**
     * Sets new global update period. The setting of update period will not induce
     * immediate update.
     *
     * @param period period in ms.
     */
    public static void setGlobalUpdatePeriod(long period)
    {
        globalUpdatePeriod = period;
    }

    /**
     * Returns number of retrievals.
     *
     * @return number of retrievals.
     */
    public int getRetrievals()
    {
        return retrievals;
    }

    /**
     * Sets number of retrievals.
     *
     * @param aRetrievals number of retrievals.
     */
    public void setRetrievals(int aRetrievals)
    {
        int old = retrievals;
        retrievals = aRetrievals;
        firePropertyChanged(PROP_RETRIEVALS, new Integer(old), new Integer(retrievals));
    }

    /**
     * Total number of articles passed through this feed since its first initialization.
     *
     * @return articles count.
     */
    public int getTotalPolledArticles()
    {
        return totalPolledArticles;
    }

    /**
     * Sets total number of articles passed through this feed.
     *
     * @param value count.
     */
    public void setTotalPolledArticles(int value)
    {
        int old = totalPolledArticles;
        totalPolledArticles = value;
        firePropertyChanged(PROP_TOTAL_POLLED_ARTICLES, new Integer(old), new Integer(value));
    }

    /**
     * Listens to changes in contained articles and updates own state.
     */
    private class ArticlesListener implements IArticleListener
    {
        /**
         * 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(IArticle article, String property, Object oldValue,
                                    Object newValue)
        {
            if (IArticle.PROP_READ.equals(property))
            {
                synchronized (DataFeed.this)
                {
                    boolean readNow = (Boolean)newValue;
                    setUnreadArticlesCount(unreadArticlesCount + (readNow ? -1 : 1));
                }
            }
        }
    }
}
TOP

Related Classes of com.salas.bb.domain.DataFeed$ArticlesListener

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.