package com.limegroup.gnutella.search;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.util.ProcessingQueue;
/**
* Manages dynamic querying for Ultrapeers.
*
* This maintains the data for all active queries for this Ultrapeer and any
* of its leaves, also providing an interface for removing active queries.
* Queries may be removed, for example, when a leaf node with an active query
* disconnects from the Ultrapeer.
*/
public final class QueryDispatcher implements Runnable {
/**
* <tt>Map</tt> of outstanding queries.
*/
private final Map QUERIES = new HashMap(); // GUID -> QueryHandler
/**
* <tt>List</tt> of new queries to add.
* LOCKING: Thread-safe, although you must obtain a lock on NEW_QUERIES if
* it's ever iterated over.
*/
private final List NEW_QUERIES = Collections.synchronizedList(new LinkedList());
/**
* <tt>QueryDispatcher</tt> instance following singleton.
*/
private static final QueryDispatcher INSTANCE = new QueryDispatcher();
/**
* The ProcessingQueue that handles sending queries out.
*
* Items are added to this only if it's not already processing anything.
*/
private final ProcessingQueue PROCESSOR = new ProcessingQueue("QueryDispatcher");
/**
* Whether or not processing is already active. If it is, we don't start it up again
* when adding new queries.
*/
private boolean _active;
/**
* Instance accessor for the <tt>QueryDispatcher</tt>.
*
* @return the <tt>QueryDispatcher</tt> instance
*/
public static QueryDispatcher instance() {
return INSTANCE;
}
/**
* Creates a new <tt>QueryDispatcher</tt> instance.
*/
private QueryDispatcher() {}
/**
* Adds the specified <tt>QueryHandler</tt> to the list of queries to
* process.
*
* @param handler the <tt>QueryHandler</tt> instance to add
*/
public void addQuery(QueryHandler handler) {
handler.sendQuery(); // immediately send out one query.
synchronized(NEW_QUERIES) {
NEW_QUERIES.add(handler);
if(NEW_QUERIES.size() == 1 && !_active) {
_active = true;
PROCESSOR.add(this);
}
}
}
/**
* This method removes all queries for the given <tt>ReplyHandler</tt>
* instance.
*
* @param handler the handler that should have it's queries removed
*/
public void removeReplyHandler(ReplyHandler handler) {
// if it's not a leaf connection, we don't care that it's closed
if(!handler.isSupernodeClientConnection())
return;
remove(handler);
}
/** Updates the relevant QueryHandler with result stats from the leaf.
*/
public void updateLeafResultsForQuery(GUID queryGUID, int numResults) {
synchronized (QUERIES) {
QueryHandler qh = (QueryHandler) QUERIES.get(queryGUID);
if (qh != null)
qh.updateLeafResults(numResults);
}
}
/** Gets the number of results the Leaf has reported so far.
* @return a non-negative number if the guid exists, else -1.
*/
public int getLeafResultsForQuery(GUID queryGUID) {
synchronized (QUERIES) {
QueryHandler qh = (QueryHandler) QUERIES.get(queryGUID);
if (qh == null)
return -1;
else
return qh.getNumResultsReportedByLeaf();
}
}
/**
* Removes all queries using the specified <tt>ReplyHandler</tt>
* from NEW_QUERIES & QUERIES.
*
* @param handler the <tt>ReplyHandler</tt> to remove
*/
private void remove(ReplyHandler handler) {
synchronized(NEW_QUERIES) {
Iterator iter = NEW_QUERIES.iterator();
while(iter.hasNext()) {
QueryHandler qh = (QueryHandler)iter.next();
if(qh.getReplyHandler() == handler)
iter.remove();
}
}
synchronized(QUERIES) {
Iterator iter = QUERIES.values().iterator();
while(iter.hasNext()) {
QueryHandler qh = (QueryHandler)iter.next();
if(qh.getReplyHandler() == handler)
iter.remove();
}
}
}
/**
* Removes the specified <tt>ReplyHandler</tt> from NEW_QUERIES & QUERIES.
*
* @param handler the <tt>ReplyHandler</tt> to remove
*/
private void remove(GUID guid) {
synchronized(NEW_QUERIES) {
Iterator iter = NEW_QUERIES.iterator();
while(iter.hasNext()) {
QueryHandler qh = (QueryHandler)iter.next();
if(qh.getGUID().equals(guid))
iter.remove();
}
}
synchronized(QUERIES) {
Iterator iter = QUERIES.values().iterator();
while(iter.hasNext()) {
QueryHandler qh = (QueryHandler)iter.next();
if(qh.getGUID().equals(guid))
iter.remove();
}
}
}
/**
* Processes queries until there is nothing left to process,
* or there are no new queries to process.
*/
public void run() {
while(true) {
try {
Thread.sleep(400);
} catch(InterruptedException ignored) {}
try {
// If there are no more queries to process...
if(!processQueries()) {
synchronized(NEW_QUERIES) {
// If there are no new queries to add,
// set active to false & leave.
if(NEW_QUERIES.isEmpty()) {
_active = false;
return;
}
// else, loop.
}
}
// else, loop.
} catch(Throwable t) {
ErrorService.error(t);
}
}
}
/**
* Processes current queries.
*/
private boolean processQueries() {
synchronized(NEW_QUERIES) {
synchronized(QUERIES) {
Iterator iter = NEW_QUERIES.iterator();
while (iter.hasNext()) {
QueryHandler qh = (QueryHandler) iter.next();
QUERIES.put(qh.getGUID(), qh);
}
}
NEW_QUERIES.clear();
}
synchronized(QUERIES) {
Iterator iter = QUERIES.values().iterator();
while(iter.hasNext()) {
QueryHandler handler = (QueryHandler)iter.next();
handler.sendQuery();
if(handler.hasEnoughResults())
iter.remove();
}
return !QUERIES.isEmpty();
}
}
/**
* Removes all queries that match this GUID.
*
* @param g the <tt>GUID</tt> of the search to remove
*/
public void addToRemove(GUID g) {
remove(g);
}
}