package com.limegroup.gnutella.search;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import com.util.LOG;
import com.limegroup.gnutella.Const;
import com.limegroup.gnutella.Endpoint;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.Response;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.vendor.QueryStatusResponse;
import com.limegroup.gnutella.settings.SearchSettings;
import com.limegroup.gnutella.util.NetworkUtils;
/**
* Handles incoming search results from the network. This class parses the
* results from <tt>QueryReply</tt> instances and performs the logic
* necessary to pass those results up to the UI.
*/
public final class SearchResultHandler {
/**
* The maximum amount of time to allow a query's processing
* to pass before giving up on it as an 'old' query.
*/
private static final int QUERY_EXPIRE_TIME = 30 * 1000; // 30 seconds.
/**
* The "delay" between responses to wait to send a QueryStatusResponse.
*/
public static final int REPORT_INTERVAL = 15;
/**
* The maximum number of results to send in a QueryStatusResponse -
* basically sent to say 'shut off query'.
*/
public static final int MAX_RESULTS = 65535;
/** Used to keep track of the number of non-filtered responses per GUID.
* I need synchronization for every call I make, so a Vector is fine.
*/
private final List GUID_COUNTS = new Vector();
/*---------------------------------------------------
PUBLIC INTERFACE METHODS
----------------------------------------------------*/
/**
* Adds the query reply, immediately processing it and passing
* it off to the GUI.
*
* @param qr the <tt>QueryReply</tt> to add
*/
public void handleQueryReply(QueryReply qr) {
handleReply(qr);
}
/**
* Adds the Query to the list of queries kept track of. You should do this
* EVERY TIME you start a query so we can leaf guide it when possible.
*
* @param qr The query that has been started. We really just acces the guid.
*/
public void addQuery(QueryRequest qr) {
LOG.trace("entered SearchResultHandler.addQuery(QueryRequest)");
GuidCount gc = new GuidCount(qr);
GUID_COUNTS.add(gc);
}
public QueryRequest getCurQuery() {
int len = GUID_COUNTS.size();
if (len > 0) {
GuidCount gc = (GuidCount)GUID_COUNTS.get(len-1);
return gc._qr;
} else {
return null;
}
}
/**
* Removes the Query frome the list of queries kept track of. You should do
* this EVERY TIME you stop a query.
*
* @param guid the guid of the query that has been removed.
*/
public void removeQuery(GUID guid) {
LOG.trace("entered SearchResultHandler.removeQuery(GUID)");
GuidCount gc = removeQueryInternal(guid);
if ((gc != null) && (!gc.isFinished())) {
// shut off the query at the UPs - it wasn't finished so it hasn't
// been shut off - at worst we may shut it off twice, but that is
// a timing issue that has a small probability of happening, no big
// deal if it does....
QueryStatusResponse stat = new QueryStatusResponse(guid,
MAX_RESULTS);
RouterService.getConnectionManager().updateQueryStatus(stat);
}
}
/**
* Returns a <tt>List</tt> of queries that require replanting into
* the network, based on the number of results they've had and/or
* whether or not they're new enough.
*/
public List getQueriesToReSend() {
LOG.trace("entered SearchResultHandler.getQueriesToSend()");
List reSend = null;
synchronized (GUID_COUNTS) {
long now = System.currentTimeMillis();
Iterator iter = GUID_COUNTS.iterator();
while (iter.hasNext()) {
GuidCount currGC = (GuidCount) iter.next();
if( isQueryStillValid(currGC, now) ) {
if(LOG.isDebugEnabled())
LOG.debug("adding " + currGC +
" to list of queries to resend");
if( reSend == null )
reSend = new LinkedList();
reSend.add(currGC.getQueryRequest());
}
}
}
if( reSend == null )
return Collections.EMPTY_LIST;
else
return reSend;
}
/**
* Use this to see how many results have been displayed to the user for the
* specified query.
*
* @param guid the guid of the query.
*
* @return the number of non-filtered results for query with guid guid. -1
* is returned if the guid was not found....
*/
public int getNumResultsForQuery(GUID guid) {
GuidCount gc = retrieveGuidCount(guid);
if (gc != null)
return gc.getNumResults();
else
return -1;
}
/**
* Determines whether or not the specified
/*---------------------------------------------------
END OF PUBLIC INTERFACE METHODS
----------------------------------------------------*/
/*---------------------------------------------------
PRIVATE INTERFACE METHODS
----------------------------------------------------*/
/**
* Handles the given query reply. Only one thread may call it at a time.
*
* @return <tt>true</tt> if the GUI will (probably) display the results,
* otherwise <tt>false</tt>
*/
private static int totalResult = 0;
private boolean handleReply(final QueryReply qr) {
LOG.logSp("enter SearchResultsHander.handleReply " + qr.getResultCount());
HostData data;
try {
data = qr.getHostData();
} catch(BadPacketException bpe) {
LOG.logSp("bad packet reading qr", bpe);
return false;
}
// always handle reply to multicast queries.
if( !data.isReplyToMulticastQuery() && !qr.isBrowseHostReply() ) {
// note that the minimum search quality will always be greater
// than -1, so -1 qualities (the impossible case) are never
// displayed
if(data.getQuality() < SearchSettings.MINIMUM_SEARCH_QUALITY) {
LOG.logSp("Ignoring because low quality");
return false;
}
if(data.getSpeed() < SearchSettings.MINIMUM_SEARCH_SPEED) {
LOG.logSp("Ignoring because low speed");
return false;
}
// if the other side is firewalled AND
// we're not on close IPs AND
// (we are firewalled OR we are a private IP) AND
// no chance for FW transfer then drop the reply.
if(data.isFirewalled() &&
!NetworkUtils.isVeryCloseIP(qr.getIPBytes()) &&
(!RouterService.acceptedIncomingConnection() ||
NetworkUtils.isPrivateAddress(RouterService.getAddress())) &&
!(UDPService.instance().canDoFWT() &&
qr.getSupportsFWTransfer())
) {
LOG.logSp("Ignoring from firewall funkiness");
return false;
}
}
List results = null;
try {
results = qr.getResultsAsList();
} catch (BadPacketException e) {
LOG.logSp("Error gettig results", e);
return false;
}
int numSentToFrontEnd = 0;
//int resultSize = qr.getResultCount();
for(Iterator iter = results.iterator(); iter.hasNext();) {
Response response = (Response)iter.next();
if (!RouterService.matchesType(data.getMessageGUID(), response)) {
if (totalResult == 0) {
LOG.logxml("we hit spammer " + response.getName());
// TODO: add ip to blacklist
// we hit spammer, need to close and reconnect, also research
RouterService.restart();
RouterService.getCallback().retryQueryAfterConnect();
return false;
}
continue;
}
//Throw away results from Mandragore Worm
if (RouterService.isMandragoreWorm(data.getMessageGUID(),response)) {
LOG.logSp("isMandragoreWorm");
continue;
}
RemoteFileDesc rfd = response.toRemoteFileDesc(data);
Set<Endpoint> alts = response.getLocations();
RouterService.getCallback().handleQueryResult(rfd, data, alts);
numSentToFrontEnd++;
} //end of response loop
// ok - some responses may have got through to the GUI, we should account
// for them....
accountAndUpdateDynamicQueriers(qr, numSentToFrontEnd);
totalResult += numSentToFrontEnd;
return (numSentToFrontEnd > 0);
}
private void accountAndUpdateDynamicQueriers(final QueryReply qr,
final int numSentToFrontEnd) {
LOG.trace("SRH.accountAndUpdateDynamicQueriers(): entered.");
// we should execute if results were consumed
// technically Ultrapeers don't use this info, but we are keeping it
// around for further use
if (numSentToFrontEnd > 0) {
// get the correct GuidCount
GuidCount gc = retrieveGuidCount(new GUID(qr.getGUID()));
if (gc == null)
// 0. probably just hit lag, or....
// 1. we could be under attack - hits not meant for us
// 2. programmer error - ejected a query we should not have
return;
// update the object
LOG.trace("SRH.accountAndUpdateDynamicQueriers(): incrementing.");
gc.increment(numSentToFrontEnd);
// inform proxying Ultrapeers....
if (!gc.isFinished() &&
(gc.getNumResults() > gc.getNextReportNum())) {
LOG.trace("SRH.accountAndUpdateDynamicQueriers(): telling UPs.");
gc.tallyReport();
if (gc.getNumResults() > Const.ULTRAPEER_RESULTS)
gc.markAsFinished();
// if you think you are done, then undeniably shut off the
// query.
final int numResultsToReport = (gc.isFinished() ?
MAX_RESULTS :
gc.getNumResults()/4);
QueryStatusResponse stat =
new QueryStatusResponse(gc.getGUID(),
numResultsToReport);
RouterService.getConnectionManager().updateQueryStatus(stat);
}
}
LOG.trace("SRH.accountAndUpdateDynamicQueriers(): returning.");
}
private GuidCount removeQueryInternal(GUID guid) {
synchronized (GUID_COUNTS) {
Iterator iter = GUID_COUNTS.iterator();
while (iter.hasNext()) {
GuidCount currGC = (GuidCount) iter.next();
if (currGC.getGUID().equals(guid)) {
iter.remove(); // get rid of this dude
return currGC; // and return it...
}
}
}
return null;
}
private GuidCount retrieveGuidCount(GUID guid) {
synchronized (GUID_COUNTS) {
Iterator iter = GUID_COUNTS.iterator();
while (iter.hasNext()) {
GuidCount currGC = (GuidCount) iter.next();
if (currGC.getGUID().equals(guid))
return currGC;
}
}
return null;
}
/**
* Determines whether or not the query contained in the
* specified GuidCount is still valid.
* This depends on values such as the time the query was
* created and the amount of results we've received so far
* for this query.
*/
private boolean isQueryStillValid(GuidCount gc, long now) {
LOG.trace("entered SearchResultHandler.isQueryStillValid(GuidCount)");
return (now < (gc.getTime() + QUERY_EXPIRE_TIME)) &&
(gc.getNumResults() < Const.ULTRAPEER_RESULTS);
}
/*---------------------------------------------------
END OF PRIVATE INTERFACE METHODS
----------------------------------------------------*/
/** A container that simply pairs a GUID and an int. The int should
* represent the number of non-filtered results for the GUID.
*/
private static class GuidCount {
private final long _time;
private final GUID _guid;
private final QueryRequest _qr;
private int _numResults;
private int _nextReportNum = REPORT_INTERVAL;
private boolean markAsFinished = false;
public GuidCount(QueryRequest qr) {
_qr = qr;
_guid = new GUID(qr.getGUID());
_numResults = 0;
_time = System.currentTimeMillis();
}
public GUID getGUID() { return _guid; }
public int getNumResults() { return _numResults; }
public int getNextReportNum() { return _nextReportNum; }
public long getTime() { return _time; }
public QueryRequest getQueryRequest() { return _qr; }
public boolean isFinished() { return markAsFinished; }
public void tallyReport() {
_nextReportNum = _numResults + REPORT_INTERVAL;
}
public void increment(int incr) { _numResults += incr; }
public void markAsFinished() { markAsFinished = true; }
public String toString() {
return "" + _guid + ":" + _numResults + ":" + _nextReportNum;
}
}
}