package edu.umd.rhsmith.diads.meater.modules.tweater.tlc; import java.util.HashSet; import twitter4j.RateLimitStatus; import twitter4j.Status; import twitter4j.TwitterException; 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.BaseMediaProcessor; import edu.umd.rhsmith.diads.meater.core.app.components.media.MediaProcessor; import edu.umd.rhsmith.diads.meater.core.app.components.media.MediaSource; import edu.umd.rhsmith.diads.meater.modules.tweater.TwitterManager; import edu.umd.rhsmith.diads.meater.modules.tweater.media.DefaultUserStatusData; 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.QueryFollow; import edu.umd.rhsmith.diads.meater.util.ControlException; import edu.umd.rhsmith.diads.meater.util.Util; import edu.umd.rhsmith.diads.tools.twitter.TimelineStream; import edu.umd.rhsmith.diads.tools.twitter.TimelineStreamListener; public class TimelineCollector extends Component implements TimelineStreamListener { public static final String SRCNAME_TWEETS = "tweets"; public static final String PNAME_USERS = "users"; // TODO better means of record-keeping here? this will *eventually* fill up. // may want to switch to an external filter component when those systems are // ready, so it becomes the user's responsibility to keep track of collected // ids if they want to. private final HashSet<QueryFollow> collectedUsers; private Thread tlcThread; private TimelineStream tlc; private final MediaSource<UserStatusData> statusSource; private final MediaProcessor<QueryFollow> userProcessor; private final String oAuthName; public TimelineCollector(TimelineCollectorInitializer init) throws MEaterConfigurationException { super(init); this.collectedUsers = new HashSet<QueryFollow>(); this.oAuthName = init.getoAuthConfigurationName(); // media source for outputting statuses this.statusSource = new MediaSource<UserStatusData>(SRCNAME_TWEETS, UserStatusData.class); this.registerMediaSource(this.statusSource); // media processor for adding new queries this.userProcessor = new BaseMediaProcessor<QueryFollow>(PNAME_USERS, QueryFollow.class) { @Override public boolean processMedia(QueryFollow media) { addUser(media); return true; } }; this.registerMediaProcessor(userProcessor); } /* * -------------------------------- * Control methods * -------------------------------- */ @Override protected void doInitRoutine() throws MEaterConfigurationException { // get oauth -> build twitter collection object 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.tlc = new TimelineStream(oAuth.getConsumerKey(), oAuth .getConsumerSecret(), oAuth.getAccessToken(), oAuth .getAccessTokenSecret()); this.tlc.setShutdownWhenEmpty(false); this.tlcThread = new Thread(this.tlc); } @Override protected void doStartupRoutine() throws ControlException { this.tlc.addListener(this); this.tlcThread.start(); } @Override protected void doShutdownRoutine() { this.tlc.setShouldShutdown(true); try { this.tlcThread.join(); } catch (InterruptedException e) { this.logSevere(MSG_ERR_SHUTDOWN_INTERRUPTED); } } /* * -------------------------------- * Timeline interaction * -------------------------------- */ // add a user for timeline collection public void addUser(QueryFollow userQuery) { // skip incoming queries that have already been processed synchronized (this.collectedUsers) { if (this.collectedUsers.contains(userQuery)) { return; } this.collectedUsers.add(userQuery); } this.tlc.addUser(userQuery.getUserId()); } @Override public void onUserStarted(long userId) { logFine(MSG_COLLECTING_ID_FMT, userId); } @Override public void onUserPageStarted(long userId, int page) { logFiner(MSG_COLLECTING_ID_PAGE_FMT, userId, page); } @Override public void onRateLimit(long userId, int page, RateLimitStatus rateLimitStatus) { logInfo(MSG_RATELIMIT_FMT, userId, page, rateLimitStatus .getSecondsUntilReset()); } @Override public void onException(long userId, int page, TwitterException ex) { logWarning(MSG_ERR_TWITEX_FMT, userId, page, Util.traceMessage(ex)); } @Override public void onStatus(Status status) { this.statusSource.sourceMedia(new DefaultUserStatusData(status)); } @Override public void onShutdown() { 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_ERR_SHUTDOWN_INTERRUPTED = "Interrupted while awaiting querier theread termination"; private static final String MSG_QUERYTHREAD_ENDED = "Collector shut down."; private static final String MSG_COLLECTING_ID_FMT = "Collecting user id %d"; private static final String MSG_COLLECTING_ID_PAGE_FMT = "Collecting user id %d page %d"; private static final String MSG_ERR_TWITEX_FMT = "Twitter exception while getting user %d timeline page %d: %s"; private static final String MSG_RATELIMIT_FMT = "Rate-limited; waiting on user %d page %d for %d seconds."; }