package com.limegroup.gnutella.guess;
import java.net.InetAddress;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.messages.QueryRequest;
/** Utility class for sending GUESS queries.
*/
public class OnDemandUnicaster {
private static final int CLEAR_TIME = 5 * 60 * 1000; // 5 minutes
/** GUESSEndpoints => QueryKey.
*/
private static final Map _queryKeys;
/** Access to UDP traffic.
*/
private static final UDPService _udp;
/** Short term store for queries waiting for query keys.
* GUESSEndpoints => URNs
*/
private static final Map _bufferedURNs;
static {
// static initializers are only called once, right?
_queryKeys = new Hashtable(); // need sychronization
_bufferedURNs = new Hashtable(); // synchronization handy
_udp = UDPService.instance();
// schedule a runner to clear various data structures
RouterService.schedule(new Expirer(), CLEAR_TIME, CLEAR_TIME);
}
/** Feed me QueryKey pongs so I can query people....
* pre: pr.getQueryKey() != null
*/
public static void handleQueryKeyPong(PingReply pr)
throws NullPointerException, IllegalArgumentException {
// validity checks
// ------
if (pr == null)
throw new NullPointerException("null pong");
QueryKey qk = pr.getQueryKey();
if (qk == null)
throw new IllegalArgumentException("no key in pong");
// ------
// create guess endpoint
// ------
InetAddress address = pr.getInetAddress();
int port = pr.getPort();
GUESSEndpoint endpoint = new GUESSEndpoint(address, port);
// ------
// store query key
_queryKeys.put(endpoint, qk);
// if a buffered query exists, send it...
// -----
SendLaterBundle bundle =
(SendLaterBundle) _bufferedURNs.remove(endpoint);
if (bundle != null) {
QueryRequest query =
QueryRequest.createQueryKeyQuery(bundle._queryURN, qk);
RouterService.getMessageRouter().originateQueryGUID(query.getGUID());
_udp.send(query, endpoint.getAddress(),
endpoint.getPort());
}
// -----
}
/** Sends out a UDP query with the specified URN to the specified host.
* @throws IllegalArgumentException if ep or queryURN are null.
* @param ep the location you want to query.
* @param queryURN the URN you are querying for.
*/
public static void query(GUESSEndpoint ep, URN queryURN)
throws IllegalArgumentException {
// validity checks
// ------
if (ep == null)
throw new IllegalArgumentException("No Endpoint!");
if (queryURN == null)
throw new IllegalArgumentException("No urn to look for!");
// ------
// see if you have a QueryKey - if not, request one
// ------
QueryKey key = (QueryKey) _queryKeys.get(ep);
if (key == null) {
GUESSEndpoint endpoint = new GUESSEndpoint(ep.getAddress(),
ep.getPort());
SendLaterBundle bundle = new SendLaterBundle(queryURN);
_bufferedURNs.put(endpoint, bundle);
PingRequest pr = PingRequest.createQueryKeyRequest();
_udp.send(pr, ep.getAddress(), ep.getPort());
}
// ------
// if possible send query, else buffer
// ------
else {
QueryRequest query = QueryRequest.createQueryKeyQuery(queryURN, key);
RouterService.getMessageRouter().originateQueryGUID(query.getGUID());
_udp.send(query, ep.getAddress(), ep.getPort());
}
// ------
}
private static class SendLaterBundle {
private static final int MAX_LIFETIME = 60 * 1000;
public final URN _queryURN;
private final long _creationTime;
public SendLaterBundle(URN urn) {
_queryURN = urn;
_creationTime = System.currentTimeMillis();
}
public boolean shouldExpire() {
return ((System.currentTimeMillis() - _creationTime) >
MAX_LIFETIME);
}
}
/** @return true if the Query Key data structure was cleared.
* @param lastQueryKeyClearTime The last time query keys were cleared.
* @param queryKeyClearInterval how often you like query keys to be
* cleared.
* This method has been disaggregated from the Expirer class for ease of
* testing.
*/
private static boolean clearDataStructures(long lastQueryKeyClearTime,
long queryKeyClearInterval)
throws Throwable {
boolean clearedQueryKeys = false;
// Clear the QueryKeys if needed
// ------
if ((System.currentTimeMillis() - lastQueryKeyClearTime) >
queryKeyClearInterval) {
clearedQueryKeys = true;
// we just indiscriminately clear all the query keys - we
// could just expire 'old' ones, but the benefit is marginal
_queryKeys.clear();
}
// ------
// Get rid of all the buffered URNs that should be expired
// ------
synchronized (_bufferedURNs) {
Iterator iter = _bufferedURNs.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
SendLaterBundle bundle =
(SendLaterBundle) entry.getValue();
if (bundle.shouldExpire())
iter.remove();
}
}
// ------
return clearedQueryKeys;
}
/** This is run to clear various data structures used.
* Made package access for easy test access.
*/
private static class Expirer implements Runnable {
// 24 hours
private static final int QUERY_KEY_CLEAR_TIME = 24 * 60 * 60 * 1000;
private long _lastQueryKeyClearTime;
public Expirer() {
_lastQueryKeyClearTime = System.currentTimeMillis();
}
public void run() {
try {
if (clearDataStructures(_lastQueryKeyClearTime,
QUERY_KEY_CLEAR_TIME))
_lastQueryKeyClearTime = System.currentTimeMillis();
}
catch(Throwable t) {
ErrorService.error(t);
}
}
}
}