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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.SecureMessage; import com.limegroup.gnutella.messages.vendor.QueryStatusResponse; import com.limegroup.gnutella.settings.SearchSettings; import com.limegroup.gnutella.spam.SpamManager; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.xml.LimeXMLDocument; /** * 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 { private static final Log LOG = LogFactory.getLog(SearchResultHandler.class); /** * 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 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. * Also adds the query to the Spam Manager to adjust percentages. * * @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)"); if (!qr.isBrowseHostQuery() && !qr.isWhatIsNewRequest()) SpamManager.instance().startedQuery(qr); GuidCount gc = new GuidCount(qr); GUID_COUNTS.add(gc); } /** * 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> */ public void handleQueryReply(final QueryReply qr) { HostData data; try { data = qr.getHostData(); } catch(BadPacketException bpe) { LOG.debug("bad packet reading qr", bpe); return; } // 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.getValue()) { LOG.debug("Ignoring because low quality"); return; } if(data.getSpeed() < SearchSettings.MINIMUM_SEARCH_SPEED.getValue()) { LOG.debug("Ignoring because low speed"); return; } // 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.debug("Ignoring from firewall funkiness"); return; } } List results = null; try { results = qr.getResultsAsList(); } catch (BadPacketException e) { LOG.debug("Error gettig results", e); return; } // throw away results that aren't secure. int secureStatus = qr.getSecureStatus(); if(secureStatus == SecureMessage.FAILED) { return; } boolean skipSpam = isWhatIsNew(qr) || qr.isBrowseHostReply(); int numGoodSentToFrontEnd = 0; double numBadSentToFrontEnd = 0; for (Iterator iter = results.iterator(); iter.hasNext();) { Response response = (Response) iter.next(); if (!qr.isBrowseHostReply() && secureStatus != SecureMessage.SECURE) { if (!RouterService.matchesType(data.getMessageGUID(), response)) { continue; } if (!RouterService.matchesQuery(data.getMessageGUID(), response)) { continue; } } // Throw away results from Mandragore Worm if (RouterService.isMandragoreWorm(data.getMessageGUID(), response)) { continue; } // If there was an action, only allow it if it's a secure message. LimeXMLDocument doc = response.getDocument(); if(doc != null && !"".equals(doc.getAction()) && secureStatus != SecureMessage.SECURE) { continue; } RemoteFileDesc rfd = response.toRemoteFileDesc(data); rfd.setSecureStatus(secureStatus); Set alts = response.getLocations(); RouterService.getCallback().handleQueryResult(rfd, data, alts); if (skipSpam || !SpamManager.instance().isSpam(rfd)) numGoodSentToFrontEnd++; else numBadSentToFrontEnd++; } //end of response loop numBadSentToFrontEnd = Math.ceil(numBadSentToFrontEnd * SearchSettings.SPAM_RESULT_RATIO.getValue()); accountAndUpdateDynamicQueriers(qr, numGoodSentToFrontEnd + (int)numBadSentToFrontEnd); } private void accountAndUpdateDynamicQueriers(final QueryReply qr, final int numGoodSentToFrontEnd) { 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 (numGoodSentToFrontEnd > 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(numGoodSentToFrontEnd); // inform proxying Ultrapeers.... if (RouterService.isShieldedLeaf()) { if (!gc.isFinished() && (gc.getNumResults() > gc.getNextReportNum())) { LOG.trace("SRH.accountAndUpdateDynamicQueriers(): telling UPs."); gc.tallyReport(); if (gc.getNumResults() > QueryHandler.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; } private boolean isWhatIsNew(QueryReply reply) { GuidCount gc = retrieveGuidCount(new GUID(reply.getGUID())); return gc != null && gc.getQueryRequest().isWhatIsNewRequest(); } /** * 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() < QueryHandler.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 _numGoodResults; private int _nextReportNum = REPORT_INTERVAL; private boolean markAsFinished = false; public GuidCount(QueryRequest qr) { _qr = qr; _guid = new GUID(qr.getGUID()); _time = System.currentTimeMillis(); } public GUID getGUID() { return _guid; } public int getNumResults() { return _numGoodResults ; } public int getNextReportNum() { return _nextReportNum; } public long getTime() { return _time; } public QueryRequest getQueryRequest() { return _qr; } public boolean isFinished() { return markAsFinished; } public void tallyReport() { _nextReportNum = _numGoodResults + REPORT_INTERVAL; } public void increment(int good) { _numGoodResults += good; } public void markAsFinished() { markAsFinished = true; } public String toString() { return "" + _guid + ":" + _numGoodResults + ":" + _nextReportNum; } } }