package edu.umd.rhsmith.diads.meater.modules.tweater.queries.legacy; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import java.util.TreeSet; 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.sets.SimpleMediaSetUpdateViewer; import edu.umd.rhsmith.diads.meater.core.app.components.media.sets.MediaSetUpdateViewer; import edu.umd.rhsmith.diads.meater.modules.tweater.queries.QueryItem; import edu.umd.rhsmith.diads.meater.modules.tweater.queries.QueryItemTime; import edu.umd.rhsmith.diads.meater.util.Util; /** * This class builds a set of query phrases that are currently active based on * an external data * source. The data source should specify what all query phrases to be * collected, as well as their * start and end times. This class runs the <code>update</code> method once per * hour in order to * re-sync with the data source, as that is where query changes should be made. * * @author dmonner */ public abstract class QuerySource extends Component implements Runnable { /** * The interval (ms) between updates from the data source */ protected final long buildIntervalMs; /** * Parallel-indexed with <code>times</code>. The idea is that the * <code>times</code> list contains * all the times at which the query changes. The first index is when the * first query begins, and * the last index is when the last query ends. If the current time is * between <code>times[i]</code> and <code>times[i+1]</code>, the current * set of query phrases will be in <code>queries[i+1]</code>. * <code>queries[0]</code> should always contain an empty set of * queries, to be returned before/after the time bounds. * * Both of these data structures should be built from the data source, from * scratch, every time * the <code>update</code> method is called. */ protected final ArrayList<TreeSet<QueryItem>> queries; protected final TreeSet<QueryItem> prevQueries; /** * Parallel-indexed with <code>queries</code>; see that variable's * description. */ protected final ArrayList<Long> times; /** * If <code>true</code>, we should stop updating */ private boolean shutdownBuilderThread; /** * <code>Thread</code> handling the rebuilding of queries */ private final Thread builderThread; private final MediaSetUpdateViewer<QueryItem> updater; public static final String PNAME_QADDED = "addedQueries"; public static final String PNAME_QRMVED = "removedQueries"; public QuerySource(QuerySourceInitializer init) throws MEaterConfigurationException { super(init); this.buildIntervalMs = init.getRebuildIntervalMs(); this.times = new ArrayList<Long>(); this.queries = new ArrayList<TreeSet<QueryItem>>(); this.prevQueries = new TreeSet<QueryItem>(); this.shutdownBuilderThread = false; this.updater = new SimpleMediaSetUpdateViewer<QueryItem>(PNAME_QADDED, PNAME_QRMVED, QueryItem.class); this.registerMediaSource(this.updater.getAddedMedia()); this.registerMediaSource(this.updater.getRemovedMedia()); // building happens in its own thread this.builderThread = new Thread(this); } /* * -------------------------------- * Query building * -------------------------------- */ /** * Reads all query information from the data source and returns it, without * regard to the * information that the query builder currently knows; this will be computed * by <code>set</code> separately. * * @return A list of query items and associated times, fresh from the data * source. */ protected abstract List<QueryItemTime> getQueriesFromSource(); /** * Based on the information from the data source, constructs the query that * would be active at the * specified time. * * @param time * @return The query active at the given time */ public TreeSet<QueryItem> at(final long time) { synchronized (queries) { if (times.isEmpty()) return new TreeSet<QueryItem>(); // if the time is less than the first entry in the array, query is // empty (queries[0]) if (time < times.get(0)) return queries.get(0); // otherwise, search array for the appropriate slot by finding first // time greater for (int i = 1; i < times.size(); i++) if (time < times.get(i)) return queries.get(i); // otherwise, time > biggest time, so query is empty (queries[0]) return queries.get(0); } } /** * Uses the most recent query information from the data source to * intelligently update the query builder's timeline. * * @param all * The most recent query information from the data source */ private void set(List<QueryItemTime> all) { synchronized (queries) { final TreeSet<Long> alltimes = new TreeSet<Long>(); for (final QueryItemTime qpt : all) { alltimes.add(qpt.startTime); alltimes.add(qpt.endTime); } times.clear(); times.addAll(alltimes); queries.clear(); for (int i = 0; i < times.size(); i++) queries.add(new TreeSet<QueryItem>()); for (final QueryItemTime qpt : all) for (int i = 0; i < times.size(); i++) if (qpt.startTime <= times.get(i) && times.get(i) < qpt.endTime) queries.get(i + 1).add(qpt.item); } } /* * -------------------------------- * Control methods * -------------------------------- */ @Override protected void doInitRoutine() throws MEaterConfigurationException { } @Override protected void doStartupRoutine() { // start the builder thread this.builderThread.start(); } @Override protected void doShutdownRoutine() { // instruct the builder thread to stop, and then wait for it to do so shutdownBuilderThread = true; try { this.builderThread.join(); } catch (InterruptedException e) { logSevere(MSG_ERR_SHUTDOWN_INTERRUPTED); } } // task executed by the builder thread @Override public void run() { // Main loop periodically updates the query from the data source while (!shutdownBuilderThread) { long now = new Date().getTime(); this.logInfo(MSG_REBUILDING); // rebuild query listing from source List<QueryItemTime> all = getQueriesFromSource(); if (all != null) { this.set(all); } // then send updates as queries become active, inactive refreshActiveQueries(now); // Wait a while before starting the loop again try { Thread.sleep(this.buildIntervalMs); } catch (InterruptedException ex) { logSevere(MSG_ERR_BUILDER_INTERRUPTED, Util.traceMessage(ex)); } } logInfo(MSG_THREAD_ENDED); } private void refreshActiveQueries(long now) { Set<QueryItem> newQueries = this.at(now); // compare the "current" tree to the "active" tree final TreeSet<QueryItem> toAdd = new TreeSet<QueryItem>(newQueries); toAdd.removeAll(this.prevQueries); final TreeSet<QueryItem> toRemove = new TreeSet<QueryItem>( this.prevQueries); toRemove.removeAll(newQueries); // if there are differences if (!toAdd.isEmpty() || !toRemove.isEmpty()) { // send the change deltas to all servers for (QueryItem qi : toAdd) { addItem(qi); } for (QueryItem qi : toRemove) { delItem(qi); } this.prevQueries.clear(); this.prevQueries.addAll(newQueries); } } private void delItem(QueryItem qi) { this.updater.getRemovedMedia().sourceMedia(qi); } private void addItem(QueryItem qi) { this.updater.getAddedMedia().sourceMedia(qi); } /* * -------------------------------- * Messages * -------------------------------- */ private static final String MSG_ERR_BUILDER_INTERRUPTED = "Interrupted while sleeping between rebuilds - %s"; private static final String MSG_ERR_SHUTDOWN_INTERRUPTED = "Interrupted during shutdown while awaiting builder-thread termination"; private static final String MSG_THREAD_ENDED = "Builder-thread shut down."; private static final String MSG_REBUILDING = "Rebuilding query set"; }