Package com.salas.bb.utils.poller

Source Code of com.salas.bb.utils.poller.Poller

// 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: Poller.java,v 1.52 2008/02/15 09:08:44 spyromus Exp $
//
package com.salas.bb.utils.poller;

import EDU.oswego.cs.dl.util.concurrent.BoundedPriorityQueue;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import com.salas.bb.core.GlobalController;
import com.salas.bb.core.GlobalModel;
import com.salas.bb.domain.*;
import com.salas.bb.utils.ConnectionState;
import com.salas.bb.utils.concurrency.ExecutorFactory;
import com.salas.bb.utils.concurrency.NamingThreadFactory;
import com.salas.bb.utils.concurrency.SimpleLock;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.net.auth.AuthCancelException;
import com.salas.bb.utils.opml.Helper;
import com.salas.bb.utils.opml.ImporterAdv;
import com.salas.bbutilities.opml.ImporterException;
import com.salas.bbutilities.opml.objects.DefaultOPMLFeed;
import com.salas.bbutilities.opml.objects.DirectOPMLFeed;
import com.salas.bbutilities.opml.objects.OPMLGuide;
import com.salas.bbutilities.opml.objects.QueryOPMLFeed;

import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* <p>Poller takes care of periodical feeds updates. It makes regular scans
* of the feeds lists attempting to find the feeds requiring to be updated.
* The period of these scans is defined by <code>scanPeriod</code> property.</p>
*
* <p>Poller is always dedicated to some guides set and works only with it.
* This decision was made to simplify scanning of feeds and does not limit
* the functionality as the application always have only one active guide set.</p>
*
* <p>This class has convenient methods to force updates of the feeds out of
* the schedule. They are:</p>
* <ul>
<li><code>update()</code> - updates all feeds (indirectly) in all guides.</li>
<li><code>update(IGuide)</code> - updates all feeds (indirectly) in given guide.</li>
<li><code>update(IFeed)</code> - updates the feed. The feed can be updated directly
*      (meaning that the updating of this particular feed is required) or indirectly
*      (meaning that the updating of the feed happens as a part of bigger update:
*      guide or whole set).
* </ul>
*
* <p>Before actually doing the update Poller asks each of the feeds whether they
* can be updated or not. The feed decides, taking in account the fact of
* direct or indirect call, last poll date and other factors, if it would like to be
* updated. If it would like to then the Poller puts the feed in queue for updates.</p>
*
* <p>Poller owns some number of worker threads which are waiting for the tasks
* in the queue. Once they grab the task from the queue, they follow update procedure.
* After they finish they move back to the fetching of the next task.</p>
*/
public final class Poller implements Runnable
{
    private static final Logger LOG = Logger.getLogger(Poller.class.getName());

    /** Maximum time to live for worker thread in a pool (ms). */
    private static final int THREAD_KEEP_ALIVE_TIME = 15000;

    /** Number of worker threads. */
    private static final int WORKERS = 5;
    /** Polling queue size. */
    private static final int QUEUE_SIZE = 5000;

    /** Guides set which is under scan. */
    private GuidesSet guidesSet;

    /**
     * This lock is used to controll access to the feed update method to
     * avoid concurrency issues.
     */
    private final SimpleLock feedUpdateLock;

    /** Polling tasks executor. */
    private final Executor   executor;

    /** Connection state interface. */
    private final ConnectionState connectionState;

    /** <code>TRUE</code> when updates skipped due to being offline. */
    private boolean skippedWhenOffline;

    /** When <code>TRUE</code> feeds are allowed to be updated with manual commands. */
    private boolean updateFeedsManually;
    /** When <code>TRUE</code> reading lists are allowed to be updated with manual commands. */
    private boolean updateReadingListsManually;
    private boolean noFeedPolling;

    /**
     * Creates poller.
     *
     * @param aConnectionState  connection state interface.
     */
    public Poller(ConnectionState aConnectionState)
    {
        updateFeedsManually = true;
        updateReadingListsManually = true;

        connectionState = aConnectionState;
        connectionState.addPropertyChangeListener(ConnectionState.PROP_ONLINE,
            new ConnectionStateListener());

        feedUpdateLock = new SimpleLock();

        skippedWhenOffline = false;

        int threads = WORKERS;
        Integer cntProperty = Integer.getInteger("poller.workers");
        if (cntProperty != null) threads = cntProperty;
        noFeedPolling = System.getProperty("poller.noFeedPolling") != null;

        if (LOG.isLoggable(Level.CONFIG)) LOG.config("Number of worker threads: " + threads);

        // Create a pool of executors with minimum thread priority
        executor = ExecutorFactory.createPooledExecutor(
            new NamingThreadFactory("Poller", Thread.MIN_PRIORITY),
            threads, THREAD_KEEP_ALIVE_TIME,
            new BoundedPriorityQueue(QUEUE_SIZE, new PollerTaskPrioritizer()),
            ExecutorFactory.BlockedPolicy.DISCARD);

        // Note: The policy is "Discard" a requst if the queue is full
        // (will be retried in 10 seconds)
    }

    /**
     * Enables / disables updating feeds with manual commands.
     *
     * @param update <code>TRUE</code> to allow.
     */
    public void setUpdateFeedsManually(boolean update)
    {
        updateFeedsManually = update;
    }

    /**
     * Enables / disables updating reading lists with manual commands.
     *
     * @param update <code>TRUE</code> to allow.
     */
    public void setUpdateReadingListsManually(boolean update)
    {
        updateReadingListsManually = update;
    }

    /**
     * Sets the guides set to scan for updates.
     *
     * @param set guides set.
     */
    public void setGuidesSet(GuidesSet set)
    {
        guidesSet = set;
    }

    /**
     * Orders to stars full scan of the set immediately.
     */
    public void update()
    {
        update(true);
    }

    /**
     * Orders to stars full scan of the set immediately.
     *
     * @param manual TRUE if update was requested manually.
     */
    private void update(boolean manual)
    {
        if (guidesSet == null) return;

        StandardGuide[] guides = guidesSet.getStandardGuides(null);
        for (StandardGuide guide : guides) update(guide, manual);
    }

    /**
     * Orders to scan the specified guide.
     *
     * @param guide guide to scan for updates.
     *
     * @throws NullPointerException if guide isn't specified.
     */
    public void update(IGuide guide)
    {
        update(guide, true);
    }

    /**
     * Orders to scan the specified guide.
     *
     * @param guide guide to scan for updates.
     * @param manual TRUE if update was requested manually.
     *
     * @throws NullPointerException if guide isn't specified.
     */
    private void update(IGuide guide, boolean manual)
    {
        if (guide == null) throw new NullPointerException(Strings.error("unspecified.guide"));

        if (!manual || updateFeedsManually)
        {
            IFeed[] feeds = guide.getFeeds();
            for (IFeed feed : feeds)
            {
                if (feed instanceof DataFeed) update((DataFeed)feed, manual);
            }
        }

        // Update reading lists
        if (guide instanceof StandardGuide && (!manual || updateReadingListsManually))
        {
            ReadingList[] readingLists = ((StandardGuide)guide).getReadingLists();
            for (ReadingList list : readingLists) update(list, manual);
        }
    }

    /**
     * Orders to perform update of the selected feed.
     *
     * @param feed    feed to update.
     * @param manual  <code>TRUE</code> if it's manual update request.
     *
     * @throws NullPointerException if feed isn't specified.
     */
    public void update(DataFeed feed, boolean manual)
    {
        update(feed, manual, false);
    }

    /**
     * Orders to perform update of the selected feed.
     *
     * @param feed    feed to update.
     * @param manual  <code>TRUE</code> if it's manual update request.
     * @param allowInvisible <code>TRUE</code> if invisible feed is allowed for update.
     *
     * @throws NullPointerException if feed isn't specified.
     */
    public void update(DataFeed feed, boolean manual, boolean allowInvisible)
    {
        if (noFeedPolling) return;
        if (feed == null) throw new NullPointerException(Strings.error("unspecified.feed"));

        feedUpdateLock.lock();
        try
        {
            // If feed wishes to be updated schedule the update
            if (feed.isUpdatable(manual, allowInvisible))
            {
                // Starting processing (will finish in PollerTask.finishPolling())
                feed.processingStarted();

                PollerTask pollerTask = new PollerTask(feed);

                scheduleTask(pollerTask);
            }
        } finally
        {
            feedUpdateLock.unlock();
        }
    }

    /**
     * Checks if update of a reading list is required and carries on
     * with update if it is.
     *
     * @param list      list to check and update.
     * @param manual    <code>TRUE</code> if user requested immediate update.
     */
    private void update(ReadingList list, boolean manual)
    {
        if ((manual || list.isUpdatable()) && !list.isUpdating())
        {
            list.setUpdating(true);
            scheduleTask(new ReadingListUpdatePollerTask(list));
        }
    }

    /**
     * Schedule or execute task immediately.
     *
     * @param aPollerTask task.
     */
    private void scheduleTask(Runnable aPollerTask)
    {
        try
        {
            executor.execute(aPollerTask);
        } catch (InterruptedException e)
        {
            LOG.severe(Strings.error("interrupted"));
            aPollerTask.run();
        }
    }

    /**
     * Simple initiation of the scan.
     */
    public void run()
    {
        if (connectionState.isOnline())
        {
            update(false);
        } else
        {
            skippedWhenOffline = true;
        }
    }

    /**
     * Listens for connection to go online.
     */
    private class ConnectionStateListener implements PropertyChangeListener
    {
        /**
         * Called when connection state changes.
         *
         * @param evt property change event.
         */
        public void propertyChange(PropertyChangeEvent evt)
        {
            if (connectionState.isOnline() && skippedWhenOffline) run();
        }
    }

    /**
     * The task for updating reading list.
     */
    private static class ReadingListUpdatePollerTask implements Runnable
    {
        private final ReadingList list;

        /**
         * Creates task for updating reading list.
         *
         * @param aList list to update.
         */
        public ReadingListUpdatePollerTask(ReadingList aList)
        {
            list = aList;
        }

        /**
         * Invoked when it's time to run updates.
         */
        public void run()
        {
            boolean setPollTime = true;
            try
            {
                final OPMLGuide newGuide = fetchGuide();

                if (newGuide != null)
                {
                    updateListInfo(newGuide);
                    updateFeedsList(newGuide);
                }

                list.setMissing(false);
            } catch (FileNotFoundException e)
            {
                list.setMissing(true);
                setPollTime = true;
            } catch (Throwable e)
            {
                setPollTime = false;
                if (!(e.getCause() instanceof AuthCancelException))
                {
                    LOG.log(Level.SEVERE, Strings.error("unhandled.exception"), e);
                }
            } finally
            {
                list.setUpdating(false);
            }

            if (setPollTime) list.setLastPollTime(System.currentTimeMillis());
        }

        /**
         * Fetches new version of guide for the given list.
         *
         * @return new version of guide.
         *
         * @throws FileNotFoundException when reading list is no loger there.
         */
        private OPMLGuide fetchGuide()
            throws FileNotFoundException
        {
            OPMLGuide[] opmlGuide;

            try
            {
                opmlGuide = new ImporterAdv().process(list.getURL(), true).getGuides();
            } catch (ImporterException e)
            {
                if (e.getCause() instanceof FileNotFoundException)
                {
                    throw (FileNotFoundException)e.getCause();
                }

                LOG.log(Level.WARNING, MessageFormat.format(
                    Strings.error("failed.to.fetch.the.reading.list.list.0"),
                    list.getURL()), e);
                opmlGuide = null;
            }

            return opmlGuide == null ? null
                : opmlGuide.length == 0
                    ? new OPMLGuide("", "", false, null, null, false, 0, false, false, false)
                    : opmlGuide[0];
        }

        /**
         * Updates reading list information.
         *
         * @param aGuide OPML guide parsed from the source.
         */
        private void updateListInfo(OPMLGuide aGuide)
        {
            list.setTitle(aGuide.getTitle());
        }

        /**
         * Updates list of associated feeds.
         *
         * @param aGuide OPML guide parsed from the source.
         *
         * @throws InterruptedException         when interrupted.
         * @throws InvocationTargetException    when invocation failed.
         */
        private void updateFeedsList(OPMLGuide aGuide)
            throws InvocationTargetException, InterruptedException
        {
            URL baseURL = list.getURL();
            List feeds = aGuide.getFeeds();
            Set<String> urls = new HashSet<String>();

            GlobalModel model = GlobalController.SINGLETON.getModel();
            int limit = model.getUserPreferences().getFeedImportLimit();

            // Collect all XML URL's of all direct feeds
            List<DirectFeed> feedsList = new ArrayList<DirectFeed>(feeds.size());
            for (int i = 0; limit > 0 && i < feeds.size(); i++)
            {
                DefaultOPMLFeed feed = (DefaultOPMLFeed)feeds.get(i);

                // Convert query feed to normal direct feed
                if (feed instanceof QueryOPMLFeed)
                {
                    QueryFeed qFeed = Helper.createQueryFeed((QueryOPMLFeed)feed);
                    feed = new DirectOPMLFeed(feed.getTitle(), qFeed.getXmlURL().toString(), null,
                        feed.getRating(), feed.getReadArticlesKeys(), feed.getPinnedArticlesKeys(), feed.getLimit(),
                        null, null, null, null, null, null, false, qFeed.getType().getType(), false, 0, null,
                        qFeed.getHandlingType().toInteger());
                }

                if (feed instanceof DirectOPMLFeed)
                {
                    DirectOPMLFeed doFeed = (DirectOPMLFeed)feed;
                    String feedURL = doFeed.getXmlURL();

                    try
                    {
                        URL url = new URL(baseURL, feedURL);
                        if (!urls.contains(url.toString()))
                        {
                            urls.add(url.toString());

                            DirectFeed dFeed = new DirectFeed();
                            dFeed.setXmlURL(url);
                            Helper.populateDirectFeedProperties(baseURL, dFeed, doFeed);

                            feedsList.add(dFeed);
                            limit--;
                        }
                    } catch (MalformedURLException e)
                    {
                        // Ignore malformed URLs from the list
                    }
                }
            }

            DirectFeed[] directFeeds = feedsList.toArray(new DirectFeed[feedsList.size()]);

            final List<DirectFeed> addFeeds = new LinkedList<DirectFeed>();
            final List<DirectFeed> removeFeeds = new LinkedList<DirectFeed>();
            list.collectDifferences(directFeeds, addFeeds, removeFeeds);

            // After this stage we have the list of feeds to add and feeds to remove.
            // When we face the problem of redirected feeds when a feed redirects itself
            // after adding to the list, the existing feed appears in the "to remove" list,
            // and it's an indicator of that we have this situation.

            // So if there's anything to remove, we need to check if any of feeds we are
            // adding match these
            if (removeFeeds.size() > 0)
            {
                List<DirectFeed> dontAddFeeds = new LinkedList<DirectFeed>();
                List<DirectFeed> dontRemoveFeeds = new LinkedList<DirectFeed>();

                for (DirectFeed addFeed : addFeeds)
                {
                    URL oldURL = addFeed.getXmlURL();

                    try
                    {
                        URL newURL = getRedirectionURL(oldURL, new LinkedList<String>());
                        if (newURL != null && !newURL.toString().equals(oldURL.toString()))
                        {
                            String newURLS = newURL.toString();

                            // See if a feed with a resolved URL is among those for removal
                            // and don't remove it if so.
                            for (DirectFeed removeFeed : removeFeeds)
                            {
                                if (removeFeed.getXmlURL().toString().equals(newURLS))
                                {
                                    dontRemoveFeeds.add(removeFeed);
                                    dontAddFeeds.add(addFeed);
                                    break;
                                }
                            }
                        }
                    } catch (IOException e)
                    {
                        if (GlobalController.getConnectionState().isOnline())
                        {
                            LOG.log(Level.INFO, "Failed to resolve redirection for " + oldURL, e);
                        }
                    }
                }

                // Rebuild an add-list if there's something we don't need to add
                if (dontAddFeeds.size() > 0) addFeeds.removeAll(dontAddFeeds);

                // Rebuild a remove-list if there's something we don't need to remove
                if (dontRemoveFeeds.size() > 0) removeFeeds.removeAll(dontRemoveFeeds);
            }

            if (addFeeds.size() > 0 || removeFeeds.size() > 0)
            {
                // Call updates in EDT
                SwingUtilities.invokeAndWait(new Runnable()
                {
                    public void run()
                    {
                        GlobalController.updateReadingList(list, addFeeds, removeFeeds);
                    }
                });
            }
        }

        /**
         * Checks for the redirection from the given URL to somewhere else and
         * returns the new URL. If it's looped, the NULL is returned.
         *
         * @param url source URL.
         * @param visited the list of visited URL's.
         *
         * @return new URL.
         *
         * @throws IOException in case of network I/O problem.
         */
        private static URL getRedirectionURL(URL url, List<String> visited)
            throws IOException
        {
            if (url == null) return null;
            if (visited == null) visited = new ArrayList<String>(); else
            if (visited.contains(url.toString())) return null;

            URLConnection con = url.openConnection();
            if (con instanceof HttpURLConnection)
            {
                HttpURLConnection hcon = (HttpURLConnection)con;
                String newLocation = getNewPermanentLocation(hcon);

                // If the redirection takes place and it's permanent,
                // follow the new location
                if (newLocation != null)
                {
                    visited.add(url.toString());
                    url = getRedirectionURL(new URL(url, newLocation), visited);
                }
            }

            return url;
        }

        /**
         * Connects using the connection and reads the headers. If there's a permanent
         * redirection instruction, returns new location.
         *
         * @param hcon  connection to use.
         *
         * @return new location if there was the permanent redirection instruction in
         *         the headers.
         *
         * @throws IOException in case of network I/O problem.
         */
        private static String getNewPermanentLocation(HttpURLConnection hcon)
            throws IOException
        {
            String newLocation = null;

            hcon.setInstanceFollowRedirects(false);
            hcon.setAllowUserInteraction(false);
            hcon.connect();
            try
            {
                if (isRedirectionCode(hcon.getResponseCode()))
                {
                    newLocation = hcon.getHeaderField("Location");
                }
            } finally
            {
                hcon.disconnect();
            }

            return newLocation;
        }

        /**
         * Returns <code>TRUE</code> if the code is one of the redirection codes.
         *
         * @param responseCode code.
         *
         * @return <code>TRUE</code> if the code is one of the redirection codes.
         */
        private static boolean isRedirectionCode(int responseCode)
        {
            return responseCode == HttpURLConnection.HTTP_MULT_CHOICE ||
                responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
                responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||
                responseCode == HttpURLConnection.HTTP_SEE_OTHER ||
                responseCode == HttpURLConnection.HTTP_USE_PROXY ||
                responseCode == 307;
        }
    }

    /**
     * Prioritizes tasks so that reading list updates go first.
     */
    private static class PollerTaskPrioritizer implements Comparator
    {
        /**
         * Compares its two arguments for order.  Returns a negative integer,
         * zero, or a positive integer as the first argument is less than, equal
         * to, or greater than the second.<p>
         *
         * @param o1 the first object to be compared.
         * @param o2 the second object to be compared.
         *
         * @return a negative integer, zero, or a positive integer as the
         *         first argument is less than, equal to, or greater than the
         *         second.
         *
         * @throws ClassCastException if the arguments' types prevent them from
         *                            being compared by this comparator.
         */
        public int compare(Object o1, Object o2)
        {
            boolean rl1 = o1 instanceof ReadingListUpdatePollerTask;
            boolean rl2 = o2 instanceof ReadingListUpdatePollerTask;

            return (rl1 && rl2) || (!rl1 && !rl2) ? 0 :
                   (rl1 && !rl2) ? -1 : 1;
        }
    }
}
TOP

Related Classes of com.salas.bb.utils.poller.Poller

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.