// 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: MDDiscoverer.java,v 1.15 2007/04/10 10:33:45 spyromus Exp $
//
package com.salas.bb.discovery;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.FJTaskRunnerGroup;
import com.salas.bb.domain.FeedMetaDataHolder;
import com.salas.bb.utils.ConnectionState;
import com.salas.bb.utils.concurrency.ExecutorFactory;
import com.salas.bb.utils.i18n.Strings;
import java.net.URL;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Meta-data discoverer. Uses external services and own local means to discover
* information about given URL's.
*/
class MDDiscoverer
{
private static final Logger LOG = Logger.getLogger(MDDiscoverer.class.getName());
// The name of discoverer threads group
private static final String THREAD_NAME = "Discoverer";
// Maximum number of threads to spawn
private static final int MAX_THREADS = 5;
// Time to wait before rescheduling task
private static final int RESCHEDULE_DELAY_MS = 15000;
// Time to keep discovery threads in pool active
private static final int THREAD_KEEP_ALIVE_TIME_MS = 60000;
// The list of protocols allowed to discovery
private static final List ALLOWED_PROTOCOLS;
private final FJTaskRunnerGroup eventsRunner;
private final Executor executor;
private final Set<String> schedule;
private final Timer rescheduleTimer;
private final List<IDiscoveryListener> listeners;
private final ConnectionState connectionState;
static
{
ALLOWED_PROTOCOLS = Arrays.asList("http", "file", "https", "ftp");
}
/**
* Creates discoverer.
*
* @param aConnectionState connection state interface.
*/
public MDDiscoverer(ConnectionState aConnectionState)
{
this(ExecutorFactory.createPooledExecutor(THREAD_NAME,
MAX_THREADS, THREAD_KEEP_ALIVE_TIME_MS), aConnectionState);
}
/**
* Creates discoverer with given executor.
*
* @param anExecutor executor.
* @param aConnectionState connection state interface.
*/
MDDiscoverer(Executor anExecutor, ConnectionState aConnectionState)
{
connectionState = aConnectionState;
eventsRunner = new FJTaskRunnerGroup(1);
executor = anExecutor;
schedule = new HashSet<String>();
rescheduleTimer = new Timer(true);
listeners = new CopyOnWriteArrayList<IDiscoveryListener>();
}
/**
* Scedules discovery of the given URL. Discovered meta-data will be put into
* specified holder.
*
* @param url URL to discover if <code>NULL</code> then URL will be taken from XML URL of
* holder.
* @param holder holder of resulting meta-data.
*
* @throws NullPointerException if holder isn't specified or URL is not specified.
*/
public void scheduleDiscovery(URL url, FeedMetaDataHolder holder)
{
if (holder == null) throw new NullPointerException(Strings.error("unspecified.holder"));
if (url == null) url = holder.getXmlURL();
if (url == null) throw new NullPointerException(Strings.error("unspecified.url"));
String protocol = url.getProtocol().toLowerCase();
if (!ALLOWED_PROTOCOLS.contains(protocol)) return;
if (LOG.isLoggable(Level.FINE)) LOG.fine("Schedule discovery: " + url);
boolean schedulingRequired = false;
String urlString = url.toString();
synchronized (schedule)
{
if (!schedule.contains(urlString))
{
schedule.add(urlString);
schedulingRequired = true;
}
}
if (schedulingRequired)
{
MDDiscoveryRequest request = new MDDiscoveryRequest(url, holder);
schedule(request);
}
}
/**
* Schedules discovery request.
*
* @param aRequest request.
*/
private void schedule(MDDiscoveryRequest aRequest)
{
try
{
executor.execute(new DiscoverTask(aRequest));
} catch (InterruptedException e)
{
LOG.log(Level.WARNING, Strings.error("interrupted"), e);
}
}
/**
* Reschedules discovery after some delay defined at {@link #RESCHEDULE_DELAY_MS}.
* The call isn't blocking. It puts the timer task and continues.
*
* @param request request to reschedule.
*/
private void rescheduleDiscovery(MDDiscoveryRequest request)
{
if (LOG.isLoggable(Level.FINE)) LOG.fine("Discovery rescheduled: " + request.getUrl());
request.addAttemptCount();
rescheduleTimer.schedule(new ScheduleRequestTimerTask(request), RESCHEDULE_DELAY_MS);
}
/**
* Marking URL as no longer scheduled.
*
* @param url URL to mark.
*/
private void markAsNotScheduled(URL url)
{
synchronized (schedule)
{
schedule.remove(url.toString());
}
}
/**
* Invoked when some discovery starts.
*
* @param request discovery request.
*/
private void discoveryStarted(MDDiscoveryRequest request)
{
URL url = request.getUrl();
if (LOG.isLoggable(Level.FINE)) LOG.fine("Discovery started: " + url);
if (request.getAttempts() == 0) fireDiscoveryStarted(url);
}
/**
* Invoked when some discovery successfully finishes.
*
* @param request discovery request.
*/
private void discoveryFinished(MDDiscoveryRequest request)
{
FeedMetaDataHolder holder = request.getHolder();
boolean reschedule = !holder.isComplete() ||
(!request.isServiceDiscoveryComplete() && !request.isLocal());
if (holder.isComplete())
{
URL url = request.getUrl();
if (LOG.isLoggable(Level.FINE)) LOG.fine("Discovery finished: " + url);
if (!reschedule) markAsNotScheduled(url);
}
fireDiscoveryFinished(request.getUrl(), holder.isComplete());
if (reschedule) rescheduleDiscovery(request);
}
/**
* Invoked when discovery fails for some reason.
*
* @param request discovery request.
* @param cause cause of the error.
*/
private void discoveryFailed(MDDiscoveryRequest request, Exception cause)
{
URL url = request.getUrl();
LOG.log(Level.WARNING, MessageFormat.format(
Strings.error("discovery.failed.0"), url), cause);
markAsNotScheduled(url);
fireDiscoveryFailed(url);
}
/**
* Adds discovery listener.
*
* @param aListener listener.
*/
public void addListener(IDiscoveryListener aListener)
{
if (!listeners.contains(aListener)) listeners.add(aListener);
}
/**
* Fires event about started discovery.
*
* @param url URL.
*/
private void fireDiscoveryStarted(URL url)
{
FireEventTask task = new FireEventTask(listeners, url, false)
{
protected void fireEvent(IDiscoveryListener listener, URL url, boolean complete)
{
listener.discoveryStarted(url);
}
};
runFireEventTask(task);
}
/**
* Fires event about finished discovery.
*
* @param url URL.
* @param complete <code>TRUE</code> when discovery is complete.
*/
private void fireDiscoveryFinished(URL url, boolean complete)
{
FireEventTask task = new FireEventTask(listeners, url, complete)
{
protected void fireEvent(IDiscoveryListener listener, URL url, boolean complete)
{
listener.discoveryFinished(url, complete);
}
};
runFireEventTask(task);
}
/**
* Fires event about failed discovery.
*
* @param url URL.
*/
private void fireDiscoveryFailed(URL url)
{
FireEventTask task = new FireEventTask(listeners, url, false)
{
protected void fireEvent(IDiscoveryListener listener, URL url, boolean complete)
{
listener.discoveryFailed(url);
}
};
runFireEventTask(task);
}
/**
* Calls fire event task in events thread or runs in directly.
*
* @param aTask task.
*/
private void runFireEventTask(FireEventTask aTask)
{
try
{
eventsRunner.execute(aTask);
} catch (InterruptedException e)
{
aTask.run();
}
}
/**
* Task to deal with discovery.
*/
private class DiscoverTask implements Runnable
{
private MDDiscoveryRequest request;
/**
* Creates task to discover given URL and put information into given holder.
*
* @param aRequest discovery request.
*/
public DiscoverTask(MDDiscoveryRequest aRequest)
{
request = aRequest;
}
/**
* Runs discovery task.
*/
public void run()
{
try
{
discoveryStarted(request);
MDDiscoveryLogic.processDiscovery(request, connectionState);
discoveryFinished(request);
} catch (Exception e)
{
discoveryFailed(request, e);
}
}
}
/**
* Task to schedule discovery request.
*/
private class ScheduleRequestTimerTask extends TimerTask
{
private final MDDiscoveryRequest request;
/**
* Creates task.
*
* @param aRequest request to schedule.
*/
public ScheduleRequestTimerTask(MDDiscoveryRequest aRequest)
{
request = aRequest;
}
/**
* Invoked on execution.
*/
public void run()
{
schedule(request);
}
}
private abstract static class FireEventTask implements Runnable
{
private final List<IDiscoveryListener> listeners;
private final URL url;
private final boolean complete;
public FireEventTask(List<IDiscoveryListener> aListeners, URL aUrl, boolean aComplete)
{
complete = aComplete;
listeners = aListeners;
url = aUrl;
}
public void run()
{
for (IDiscoveryListener listener : listeners) fireEvent(listener, url, complete);
}
protected abstract void fireEvent(IDiscoveryListener listener, URL url, boolean complete);
}
}