/*
* Copyright (C) 2011 A. Horn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mcsoxford.rss;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Asynchronous loader for RSS feeds. RSS feeds can be loaded in FIFO order or
* based on priority. Objects of this type can be constructed with one of the
* provided static methods:
* <ul>
* <li>{@link #fifo()}</li>
* <li>{@link #fifo(int)}</li>
* <li>{@link #priority()}</li>
* <li>{@link #priority(int)}</li>
* </ul>
*
* Completed RSS feed loads can be retrieved with {@link RSSLoader#take()},
* {@link RSSLoader#poll()} or {@link RSSLoader#poll(long, TimeUnit)}.
*
* <p>
* <b>Usage Example</b>
*
* Suppose you want to load an array of RSS feed URIs concurrently before
* retrieving the results one at a time. You could write this as:
*
* <pre>
* {@code
* void fetchRSS(String[] uris) throws InterruptedException {
* RSSLoader loader = RSSLoader.fifo();
* for (String uri : uris) {
* loader.load(uri);
* }
*
* Future<RSSFeed> future;
* RSSFeed feed;
* for (int i = 0; i < uris.length; i++) {
* future = loader.take();
* try {
* feed = future.get();
* use(feed);
* } catch (ExecutionException ignore) {}
* }
* }}
* </pre>
*
* </p>
*
* @author A. Horn
*/
public class RSSLoader {
/**
* Human-readable name of the thread loading RSS feeds
*/
private final static String DEFAULT_THREAD_NAME = "Asynchronous RSS feed loader";
/**
* Arrange incoming load requests on this queue.
*/
private final BlockingQueue<RSSFuture> in;
/**
* Once the an RSS feed has completed loading, place the result on this queue.
*/
private final BlockingQueue<RSSFuture> out;
/**
* Flag changes are visible after operations on {@link #in} queue.
*/
private boolean stopped;
/**
* Create an object which can load RSS feeds asynchronously in FIFO order.
*
* @see #fifo(int)
*/
public static RSSLoader fifo() {
return new RSSLoader(new LinkedBlockingQueue<RSSFuture>());
}
/**
* Create an object which can load RSS feeds asynchronously in FIFO order.
*
* @param capacity
* expected number of URIs to be loaded at a given time
*/
public static RSSLoader fifo(int capacity) {
return new RSSLoader(new LinkedBlockingQueue<RSSFuture>(capacity));
}
/**
* Create an object which can load RSS feeds asynchronously based on priority.
*
* @see #priority(int)
*/
public static RSSLoader priority() {
return new RSSLoader(new PriorityBlockingQueue<RSSFuture>());
}
/**
* Create an object which can load RSS feeds asynchronously based on priority.
*
* @param capacity
* expected number of URIs to be loaded at a given time
*/
public static RSSLoader priority(int capacity) {
return new RSSLoader(new PriorityBlockingQueue<RSSFuture>(capacity));
}
/**
* Instantiate an object which can load RSS feeds asynchronously. The provided
* {@link BlockingQueue} implementation determines the load behaviour.
*
* @see LinkedBlockingQueue
* @see PriorityBlockingQueue
*/
RSSLoader(BlockingQueue<RSSFuture> in) {
this.in = in;
this.out = new LinkedBlockingQueue<RSSFuture>();
// start separate thread for loading of RSS feeds
new Thread(new Loader(new RSSReader()), DEFAULT_THREAD_NAME).start();
}
/**
* Returns {@code true} if RSS feeds are currently being loaded, {@code false}
* otherwise.
*/
public boolean isLoading() {
// order of conjuncts matters because of happens-before relationship
return !in.isEmpty() && !stopped;
}
/**
* Stop thread after finishing loading pending RSS feed URIs. If this loader
* has been constructed with {@link #priority()} or {@link #priority(int)},
* only RSS feed loads with priority strictly greater than seven (7) are going
* to be completed.
* <p>
* Subsequent invocations of {@link #load(String)} and
* {@link #load(String, int)} return {@code null}.
*/
public void stop() {
// flag writings happen-before enqueue
stopped = true;
in.offer(SENTINEL);
}
/**
* Loads the specified RSS feed URI asynchronously. If this loader has been
* constructed with {@link #priority()} or {@link #priority(int)}, then a
* default priority of three (3) is used. Otherwise, RSS feeds are loaded in
* FIFO order.
* <p>
* Returns {@code null} if the RSS feed URI cannot be scheduled for loading
* due to resource constraints or if {@link #stop()} has been previously
* called.
* <p>
* Completed RSS feed loads can be retrieved by calling {@link #take()}.
* Alternatively, non-blocking polling is possible with {@link #poll()}.
*
* @param uri
* RSS feed URI to be loaded
*
* @return Future representing the RSS feed scheduled for loading,
* {@code null} if scheduling failed
*/
public Future<RSSFeed> load(String uri) {
return load(uri, RSSFuture.DEFAULT_PRIORITY);
}
/**
* Loads the specified RSS feed URI asynchronously. For the specified priority
* to determine the relative loading order of RSS feeds, this loader must have
* been constructed with {@link #priority()} or {@link #priority(int)}.
* Otherwise, RSS feeds are loaded in FIFO order.
* <p>
* Returns {@code null} if the RSS feed URI cannot be scheduled for loading
* due to resource constraints or if {@link #stop()} has been previously
* called.
* <p>
* Completed RSS feed loads can be retrieved by calling {@link #take()}.
* Alternatively, non-blocking polling is possible with {@link #poll()}.
*
* @param uri
* RSS feed URI to be loaded
* @param priority
* larger integer gives higher priority
*
* @return Future representing the RSS feed scheduled for loading,
* {@code null} if scheduling failed
*/
public Future<RSSFeed> load(String uri, int priority) {
if (uri == null) {
throw new IllegalArgumentException("RSS feed URI must not be null.");
}
// optimization (after flag changes have become visible)
if (stopped) {
return null;
}
// flag readings happen-after enqueue
final RSSFuture future = new RSSFuture(uri, priority);
final boolean ok = in.offer(future);
if (!ok || stopped) {
return null;
}
return future;
}
/**
* Retrieves and removes the next Future representing the result of loading an
* RSS feed, waiting if none are yet present.
*
* @return the {@link Future} representing the loaded RSS feed
*
* @throws InterruptedException
* if interrupted while waiting
*/
public Future<RSSFeed> take() throws InterruptedException {
return out.take();
}
/**
* Retrieves and removes the next Future representing the result of loading an
* RSS feed or {@code null} if none are present.
*
* @return the {@link Future} representing the loaded RSS feed, or
* {@code null} if none are present
*
* @throws InterruptedException
* if interrupted while waiting
*/
public Future<RSSFeed> poll() {
return out.poll();
}
/**
* Retrieves and removes the Future representing the result of loading an RSS
* feed, waiting if necessary up to the specified wait time if none are yet
* present.
*
* @param timeout
* how long to wait before giving up, in units of {@code unit}
* @param unit
* a {@link TimeUnit} determining how to interpret the
* {@code timeout} parameter
* @return the {@link Future} representing the loaded RSS feed, or
* {@code null} if none are present within the specified time interval
* @throws InterruptedException
* if interrupted while waiting
*/
public Future<RSSFeed> poll(long timeout, TimeUnit unit) throws InterruptedException {
return out.poll(timeout, unit);
}
/**
* Internal consumer of RSS feed URIs stored in the blocking queue.
*/
class Loader implements Runnable {
private final RSSReader reader;
Loader(RSSReader reader) {
this.reader = reader;
}
/**
* Keep on loading RSS feeds by dequeuing incoming tasks until the sentinel
* is encountered.
*/
@Override
public void run() {
try {
RSSFuture future = null;
RSSFeed feed;
while ((future = in.take()) != SENTINEL) {
if (future.status.compareAndSet(RSSFuture.READY, RSSFuture.LOADING)) {
try {
// perform loading outside of locked region
feed = reader.load(future.uri);
// set successfully loaded RSS feed
future.set(feed, /* error */null);
// enable caller to consume the loaded RSS feed
out.add(future);
} catch (RSSException e) {
// throw ExecutionException when calling RSSFuture::get()
future.set(/* feed */null, e);
} catch (RSSFault e) {
// throw ExecutionException when calling RSSFuture::get()
future.set(/* feed */null, e);
} finally {
// RSSFuture::isDone() returns true even if an error occurred
future.status.compareAndSet(RSSFuture.LOADING, RSSFuture.LOADED);
}
}
}
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
/**
* Internal sentinel to stop the thread that is loading RSS feeds.
*/
private final static RSSFuture SENTINEL = new RSSFuture(null, /* priority */7);
/**
* Offer callers control over the asynchronous loading of an RSS feed.
*/
static class RSSFuture implements Future<RSSFeed>, Comparable<RSSFuture> {
static final int DEFAULT_PRIORITY = 3;
static final int READY = 0;
static final int LOADING = 1;
static final int LOADED = 2;
static final int CANCELLED = 4;
/** RSS feed URI */
final String uri;
/** Larger integer gives higher priority */
final int priority;
AtomicInteger status;
boolean waiting;
RSSFeed feed;
Exception cause;
RSSFuture(String uri, int priority) {
this.uri = uri;
this.priority = priority;
status = new AtomicInteger(READY);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return isCancelled() || status.compareAndSet(READY, CANCELLED);
}
@Override
public boolean isCancelled() {
return status.get() == CANCELLED;
}
@Override
public boolean isDone() {
return (status.get() & (LOADED | CANCELLED)) != 0;
}
@Override
public synchronized RSSFeed get() throws InterruptedException, ExecutionException {
if (feed == null && cause == null) {
try {
waiting = true;
// guard against spurious wakeups
while (waiting) {
wait();
}
} finally {
waiting = false;
}
}
if (cause != null) {
throw new ExecutionException(cause);
}
return feed;
}
@Override
public synchronized RSSFeed get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (feed == null && cause == null) {
try {
waiting = true;
final long timeoutMillis = unit.toMillis(timeout);
final long startMillis = System.currentTimeMillis();
// guard against spurious wakeups
while (waiting) {
wait(timeoutMillis);
// check timeout
if (System.currentTimeMillis() - startMillis > timeoutMillis) {
throw new TimeoutException("RSS feed loading timed out");
}
}
} finally {
waiting = false;
}
}
if (cause != null) {
throw new ExecutionException(cause);
}
return feed;
}
synchronized void set(RSSFeed feed, Exception cause) {
this.feed = feed;
this.cause = cause;
if (waiting) {
waiting = false;
notifyAll();
}
}
@Override
public int compareTo(RSSFuture other) {
// Note: head of PriorityQueue implementation is the least element
return other.priority - priority;
}
}
}