package edu.umd.rhsmith.diads.meater.modules.tweater.streaming;
import java.util.Date;
import java.util.TreeSet;
import twitter4j.StallWarning;
import twitter4j.Status;
import twitter4j.StatusDeletionNotice;
import twitter4j.StatusListener;
import twitter4j.TwitterStream;
import twitter4j.TwitterStreamFactory;
import twitter4j.auth.AccessToken;
import edu.umd.rhsmith.diads.meater.core.app.MEaterConfigurationException;
import edu.umd.rhsmith.diads.meater.core.app.components.Component;
import edu.umd.rhsmith.diads.meater.core.app.components.media.MediaSource;
import edu.umd.rhsmith.diads.meater.core.app.components.media.sets.SimpleMediaSetUpdater;
import edu.umd.rhsmith.diads.meater.core.app.components.media.sets.MediaSetUpdater;
import edu.umd.rhsmith.diads.meater.modules.tweater.TwitterManager;
import edu.umd.rhsmith.diads.meater.modules.tweater.media.DefaultStatusData;
import edu.umd.rhsmith.diads.meater.modules.tweater.media.DefaultUserData;
import edu.umd.rhsmith.diads.meater.modules.tweater.media.DefaultUserStatusData;
import edu.umd.rhsmith.diads.meater.modules.tweater.media.StatusData;
import edu.umd.rhsmith.diads.meater.modules.tweater.media.UserData;
import edu.umd.rhsmith.diads.meater.modules.tweater.media.UserStatusData;
import edu.umd.rhsmith.diads.meater.modules.tweater.oauth.OAuthInfo;
import edu.umd.rhsmith.diads.meater.modules.tweater.queries.QueryItem;
import edu.umd.rhsmith.diads.meater.util.Util;
/**
* The main point of contact with the Twitter Streaming API.
*
* This class periodically handles re-connecting to the Twitter Streaming API if
* the query is out of date.
*
* @author dmonner
*/
public class StreamQuerier extends Component implements Runnable,
StatusListener {
public static final String SRCNAME_TWEETS = "tweets";
public static final String PNAME_QADD = "addQueries";
public static final String PNAME_QRMV = "removeQueries";
/**
* The connection to Twitter
*/
private TwitterStream tw;
/**
* The minimum amount of time (ms) allowed between each request to Twitter
* for a change in the query
*/
private static final long MIN_UPDATE_INTERVAL_MS = 1 * 60 * 1000;
/**
* Collection of active query items
*/
private TreeSet<QueryItem> queryItems;
/**
* The time (in ms since the epoch) when the query was last sent to Twitter
*/
private long lastUpdate;
/**
* Whether or not we need to send a new query to Twitter at the next
* opportunity
*/
private boolean needsUpdate;
/**
* Whether or not we are shut down (permanently disconnected for this
* session)
*/
private boolean shutdownQuerierThread;
/**
* <code>Thread</code> handling the rebuilding of queries
*/
private final Thread querierThread;
private final MediaSource<UserStatusData> statusSource;
private final MediaSetUpdater<QueryItem> queryUpdater;
private final String oAuthName;
public StreamQuerier(StreamQuerierInitializer init)
throws MEaterConfigurationException {
super(init);
this.lastUpdate = Long.MIN_VALUE;
this.needsUpdate = true;
this.shutdownQuerierThread = false;
this.querierThread = new Thread(this);
this.statusSource = new MediaSource<UserStatusData>(SRCNAME_TWEETS,
UserStatusData.class);
this.registerMediaSource(this.statusSource);
this.queryItems = new TreeSet<QueryItem>();
this.queryUpdater = new SimpleMediaSetUpdater<QueryItem>(PNAME_QADD,
PNAME_QRMV, QueryItem.class) {
@Override
public boolean add(QueryItem item) {
addItem(item);
return true;
}
@Override
public boolean remove(QueryItem item) {
delItem(item);
return true;
}
};
this.registerMediaProcessor(this.queryUpdater.getMediaAdder());
this.registerMediaProcessor(this.queryUpdater.getMediaRemover());
this.oAuthName = init.getoAuthConfigurationName();
}
/**
* Adds a <code>QueryItem</code> to this querier
*
* @param item
*/
protected void addItem(final QueryItem item) {
synchronized (queryItems) {
this.queryItems.add(item);
this.needsUpdate = true;
}
}
/**
* Removes a specific <code>QueryItem</code> from this querier
*
* @param item
*/
protected void delItem(final QueryItem item) {
synchronized (queryItems) {
this.queryItems.add(item);
this.needsUpdate = true;
}
}
/*
* --------------------------------
* Status handling
* --------------------------------
*/
/**
* Connects to the Twitter Streaming API with the current query. If this
* querier has been marked as shut down, this method does nothing.
*/
protected void connect() {
if (!shutdownQuerierThread && queryItems.size() > 0) {
FilterQueryBuilder b;
synchronized (this.queryItems) {
b = new FilterQueryBuilder(this.queryItems);
}
logInfo(MSG_CONNECTING_FMT, b.toString());
tw.filter(b.getFilterQuery());
}
}
/**
* Disconnects from the Twitter Streaming API.
*/
protected void disconnect() {
tw.cleanUp();
}
@Override
public void onException(Exception ex) {
// FIXME
if (!ex.getMessage().contains("Stream closed")) {
logSevere(Util.traceMessage(ex));
}
}
@Override
public void onDeletionNotice(StatusDeletionNotice arg0) {
// TODO Auto-generated method stub
}
@Override
public void onScrubGeo(long userId, long upToStatusId) {
// TODO Auto-generated method stub
}
@Override
public void onStallWarning(StallWarning arg0) {
// TODO: Implement something to do on StallWarnings
// See also:
// https://dev.twitter.com/docs/streaming-apis/parameters#stall_warnings
}
@Override
public void onStatus(Status arg0) {
StatusData status = new DefaultStatusData(arg0);
UserData user = new DefaultUserData(arg0.getUser());
UserStatusData us = new DefaultUserStatusData(user, status);
this.statusSource.sourceMedia(us);
}
@Override
public void onTrackLimitationNotice(int arg0) {
// TODO Auto-generated method stub
}
/*
* --------------------------------
* Control methods
* --------------------------------
*/
/**
* Shuts down this querier, disconnecting it permanently from Twitter.
*/
public void shutdown() {
shutdownQuerierThread = true;
}
@Override
protected void doInitRoutine() throws MEaterConfigurationException {
TwitterManager mgr = this.getComponentManager().getMain()
.getRuntimeModule(TwitterManager.class);
if (mgr == null) {
throw new MEaterConfigurationException(MSG_ERR_NOTWMGR);
}
OAuthInfo oAuth = mgr.getOAuthInfo(oAuthName);
if (oAuth == null) {
throw new MEaterConfigurationException(this.messageString(
MSG_ERR_AUTH_FMT, oAuthName));
}
this.tw = new TwitterStreamFactory().getInstance();
tw.setOAuthConsumer(oAuth.getConsumerKey(), oAuth.getConsumerSecret());
tw.setOAuthAccessToken(new AccessToken(oAuth.getAccessToken(), oAuth
.getAccessTokenSecret()));
}
@Override
protected void doStartupRoutine() {
// start the builder thread
this.querierThread.start();
// start listening on twitter
tw.addListener(this);
}
@Override
protected void doShutdownRoutine() {
// disconnect from stream
disconnect();
// tell the thread to stop, and then wait for it to do so
shutdownQuerierThread = true;
try {
this.querierThread.join();
} catch (InterruptedException e) {
logSevere(MSG_ERR_INTERRUPTED);
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Thread#run()
*/
@Override
public void run() {
// Loop until we are shut down, periodically asking the QueryBuilder if
// the query has been updated
while (!shutdownQuerierThread) {
long now = new Date().getTime();
this.logFinest(MSG_RUNNING);
this.logFinest(MSG_RUNNING_QUERIES_FMT, queryItems);
// if we need to update, and it's been long enough since the
// last update
synchronized (queryItems) {
if (needsUpdate && now > lastUpdate + MIN_UPDATE_INTERVAL_MS) {
disconnect();
connect();
lastUpdate = now;
needsUpdate = false;
}
}
// Wait a while before starting the loop again
try {
Thread.sleep((int) (2000.0 + Math.random() * 1000.0));
} catch (final InterruptedException ex) {
logSevere(Util.traceMessage(ex));
}
}
tw.shutdown();
disconnect();
this.logInfo(MSG_QUERYTHREAD_ENDED);
}
/*
* --------------------------------
* Messages
* --------------------------------
*/
private static final String MSG_ERR_AUTH_FMT = "Unable to load oAuth configuration name '%s'";
private static final String MSG_ERR_NOTWMGR = "No twitter manager available for getting OAuth";
private static final String MSG_RUNNING_QUERIES_FMT = "active query: %s";
private static final String MSG_RUNNING = "Querier.run():";
private static final String MSG_QUERYTHREAD_ENDED = "Querier shut down.";
private static final String MSG_CONNECTING_FMT = "Querier connecting: %s";
private static final String MSG_ERR_INTERRUPTED = "Interrupted during shutdown while awaiting querier-thread termination";
}