package com.limegroup.gnutella;
import java.io.IOException;
import java.io.Writer;
import java.text.ParseException;
import java.util.Comparator;
import java.util.Iterator;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.ApplicationSettings;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.Buffer;
import com.limegroup.gnutella.util.StringUtils;
import com.limegroup.gnutella.util.NetworkUtils;
/**
* An endpoint with additional history information used to prioritize
* HostCatcher's permanent list:
* <ul>
* <li>The average daily uptime in seconds, as reported by the "DU" GGEP
* extension.
* <li>The system time in milliseconds that this was added to the cache
* <li>The system times in milliseconds when was successfully connected
* to this host.
* <li>The system times in milliseconds when we tried but failed to connect
* to this host.
* </ul>
*
* ExtendedEndpoint has methods to read and write information to a single line
* of text, e.g.:
* <pre>
* "18.239.0.144:6347,3043,1039939393,529333939;3343434;23433,3934223"
* </pre>
* This "poor man's serialization" is used to help HostCatcher implement the
* reading and writing of gnutella.net files.<p>
*
* ExtendedEndpoint does not override the compareTo method because
* it creates confusion between compareTo and equals.
* </ul>
* For comparing by priority, users should use the return value of
* priorityComparator()
*/
public class ExtendedEndpoint extends Endpoint {
/** The value to use for timeRecorded if unknown. Doesn't really matter. */
static final long DEFAULT_TIME_RECORDED=0;
/** The system time that dailyUptime was encountered, typically when this
* was added to the system, or -1 if we don't know (in which case we'll use
* DEFAULT_UPTIME_RECORDED for calculations) */
private long timeRecorded=-1;
/** The value to use for dailyUptime if not reported by the user.
* Rationale: by looking at version logs, we find that the average session
* uptime for a host is about 8.1 minutes. A study of connection uptimes
* (http://www.limewire.com/developer/lifetimes/) confirms this.
* Furthermore, we estimate that users connect to the network about 0.71
* times per day, for a total of 8.1*60*0.71=345 seconds of uptime per day.
*
* Why not use 0? If you have to choose between a node with an unknown
* uptime and one with a confirmed low uptime, you'll gamble on the
* former; it's unlikely to be worse! */
static final int DEFAULT_DAILY_UPTIME=345;
/** The average daily uptime in seconds, as reported by the "DU" GGEP
* extension, or -1 if we don't know (in which case we'll use
* DEFAULT_DAILY_UPTIME for calculations) */
private int dailyUptime=-1;
/** The number of connection attempts (failures) to record. */
static final int HISTORY_SIZE=3;
/** Never record two connection attempts (failures) within this many
* milliseconds. Package access for testing. */
static final long WINDOW_TIME=24*60*60*1000; //1 day
/** The system times (each a Long) that when I successfully connected to the
* given address. Sorted by time with the most recent at the head.
* Bounded in size, so only the most recent HISTORY_SIZE times are
* recorded. Also, only one entry is recorded for any two connection
* successes within a WINDOW_TIME millisecond window. */
private Buffer /* of Long */ connectSuccesses=new Buffer(HISTORY_SIZE);
/** Same as connectSuccesses, but for failed connections. */
private Buffer /* of Long */ connectFailures=new Buffer(HISTORY_SIZE);
/** the locale of the client that this endpoint represents */
private String _clientLocale =
ApplicationSettings.DEFAULT_LOCALE.getValue();
/**
* The number of times this has failed while attempting to connect
* to a UDP host cache.
* If -1, this is NOT a udp host cache.
*/
private int udpHostCacheFailures = -1;
/** locale of this client */
private final static String ownLocale =
ApplicationSettings.LANGUAGE.getValue();
/**
* Creates a new ExtendedEndpoint with uptime data read from a ping reply.
* The creation time is set to the current system time. It is assumed that
* that we have not yet attempted a connection to this.
*/
public ExtendedEndpoint(String host, int port, int dailyUptime) {
super(host, port);
this.dailyUptime=dailyUptime;
this.timeRecorded=now();
}
/**
* Creates a new ExtendedEndpoint without extended uptime information. (The
* default will be used.) The creation time is set to the current system
* time. It is assumed that we have not yet attempted a connection to this.
*/
public ExtendedEndpoint(String host, int port) {
super(host, port);
this.timeRecorded=now();
}
/**
* Creates a new ExtendedEndpoint without extended uptime information. (The
* default will be used.) The creation time is set to the current system
* time. It is assumed that we have not yet attempted a connection to this.
* Does not valid the host address.
*/
public ExtendedEndpoint(String host, int port, boolean strict) {
super(host, port, strict);
this.timeRecorded=now();
}
/**
* creates a new ExtendedEndpoint with the specified locale.
*/
public ExtendedEndpoint(String host, int port, int dailyUptime,
String locale) {
super(host, port);
this.dailyUptime = dailyUptime;
this.timeRecorded = now();
_clientLocale = locale;
}
/**
* creates a new ExtendedEndpoint with the specified locale
*/
public ExtendedEndpoint(String host, int port, String locale) {
this(host, port);
_clientLocale = locale;
}
////////////////////// Mutators and Accessors ///////////////////////
/** Returns the system time (in milliseconds) when this' was created. */
public long getTimeRecorded() {
if (timeRecorded<0)
return DEFAULT_TIME_RECORDED; //don't know
else
return timeRecorded;
}
/** Returns the average daily uptime (in seconds per day) reported in this'
* pong. */
public int getDailyUptime() {
if (dailyUptime<0)
return DEFAULT_DAILY_UPTIME; //don't know
else
return dailyUptime;
}
/**
* a setter for the daily uptime.
*/
public void setDailyUptime(int uptime) {
dailyUptime = uptime;
}
/** Records that we just successfully connected to this. */
public void recordConnectionSuccess() {
recordConnectionAttempt(connectSuccesses, now());
}
/** Records that we just failed to connect to this. */
public void recordConnectionFailure() {
recordConnectionAttempt(connectFailures, now());
}
/** Returns the last few times we successfully connected to this.
* @return an Iterator of system times in milliseconds, each as
* a Long, in descending order. */
public Iterator /* Long */ getConnectionSuccesses() {
return connectSuccesses.iterator();
}
/** Returns the last few times we successfully connected to this.
* @return an Iterator of system times in milliseconds, each as
* a Long, in descending order. */
public Iterator /* Long */ getConnectionFailures() {
return connectFailures.iterator();
}
/**
* accessor for the locale of this endpoint
*/
public String getClientLocale() {
return _clientLocale;
}
/**
* set the locale
*/
public void setClientLocale(String l) {
_clientLocale = l;
}
/**
* Determines if this is an ExtendedEndpoint for a UDP Host Cache.
*/
public boolean isUDPHostCache() {
return udpHostCacheFailures != -1;
}
/**
* Records a UDP Host Cache failure.
*/
public void recordUDPHostCacheFailure() {
Assert.that(isUDPHostCache());
udpHostCacheFailures++;
}
/**
* Decrements the failures for this UDP Host Cache.
*
* This is intended for use when the network has died and
* we really don't want to consider the host a failure.
*/
public void decrementUDPHostCacheFailure() {
Assert.that(isUDPHostCache());
// don't go below 0.
udpHostCacheFailures = Math.max(0, udpHostCacheFailures-1);
}
/**
* Records a UDP Host Cache success.
*/
public void recordUDPHostCacheSuccess() {
Assert.that(isUDPHostCache());
udpHostCacheFailures = 0;
}
/**
* Determines how many failures this UDP host cache had.
*/
public int getUDPHostCacheFailures() {
return udpHostCacheFailures;
}
/**
* Sets if this a UDP host cache endpoint.
*/
public ExtendedEndpoint setUDPHostCache(boolean cache) {
if(cache == true)
udpHostCacheFailures = 0;
else
udpHostCacheFailures = -1;
return this;
}
private void recordConnectionAttempt(Buffer buf, long now) {
if (buf.isEmpty()) {
//a) No attempts; just add it.
buf.addFirst(new Long(now));
} else if (now - ((Long)buf.first()).longValue() >= WINDOW_TIME) {
//b) Attempt more than WINDOW_TIME milliseconds ago. Add.
buf.addFirst(new Long(now));
} else {
//c) Attempt within WINDOW_TIME. Coalesce.
buf.removeFirst();
buf.addFirst(new Long(now));
}
}
/** Returns the current system time in milliseconds. Exists solely
* as a hook for testing. */
protected long now() {
return System.currentTimeMillis();
}
///////////////////////// Reading and Writing ///////////////////////
/** The separator for list elements (connection successes) */
private static final String LIST_SEPARATOR=";";
/** The separator for fields in the gnutella.net file. */
private static final String FIELD_SEPARATOR=",";
/** We've always used "\n" for the record separator in our gnutella.net
* files, even on systems that normally use "\r\n" for end-of-line. This
* has the nice advantage of making gnutella.net files portable across
* platforms. */
public static final String EOL="\n";
/**
* Writes this' state to a single line of out. Does not flush out.
* @exception IOException some problem writing to out
* @see read
*/
public void write(Writer out) throws IOException {
out.write(getAddress());
out.write(":");
out.write(getPort() + "");
out.write(FIELD_SEPARATOR);
if (dailyUptime>=0)
out.write(dailyUptime + "");
out.write(FIELD_SEPARATOR);
if (timeRecorded>=0)
out.write(timeRecorded + "");
out.write(FIELD_SEPARATOR);
write(out, getConnectionSuccesses());
out.write(FIELD_SEPARATOR);
write(out, getConnectionFailures());
out.write(FIELD_SEPARATOR);
out.write(_clientLocale);
out.write(FIELD_SEPARATOR);
if(isUDPHostCache())
out.write(udpHostCacheFailures + "");
out.write(EOL);
}
/** Writes Objects to 'out'. */
private void write(Writer out, Iterator objects)
throws IOException {
while (objects.hasNext()) {
out.write(objects.next().toString());
if (objects.hasNext())
out.write(LIST_SEPARATOR);
}
}
/**
* Parses a new ExtendedEndpoint. Strictly validates all data. For
* example, addresses MUST be in dotted quad format.
*
* @param line a single line read from the stream
* @return the endpoint constructed from the line
* @exception IOException problem reading from in, e.g., EOF reached
* prematurely
* @exception ParseException data not in proper format. Does NOT
* necessarily set the offset of the exception properly.
* @see write
*/
public static ExtendedEndpoint read(String line) throws ParseException {
//Break the line into fields. Skip if badly formatted. Note that
//subsequent delimiters are NOT coalesced.
String[] linea=StringUtils.splitNoCoalesce(line, FIELD_SEPARATOR);
if (linea.length==0)
throw new ParseException("Empty line", 0);
//1. Host and port. As a dirty trick, we use existing code in Endpoint.
//Note that we strictly validate the address to work around corrupted
//gnutella.net files from an earlier version
boolean pureNumeric;
String host;
int port;
try {
Endpoint tmp=new Endpoint(linea[0], true); // require numeric.
host=tmp.getAddress();
port=tmp.getPort();
pureNumeric = true;
} catch (IllegalArgumentException e) {
// Alright, pure numeric failed -- let's try constructing without
// numeric & without requiring a DNS lookup.
try {
Endpoint tmp = new Endpoint(linea[0], false, false);
host = tmp.getAddress();
port = tmp.getPort();
pureNumeric = false;
} catch(IllegalArgumentException e2) {
ParseException e3 = new ParseException("Couldn't extract address and port from: " + linea[0], 0);
e3.initCause(e2);
throw e3;
}
}
//Build endpoint without any optional data. (We'll set it if possible
//later.)
ExtendedEndpoint ret=new ExtendedEndpoint(host, port, false);
//2. Average uptime (optional)
if (linea.length>=2) {
try {
ret.dailyUptime=Integer.parseInt(linea[1].trim());
} catch (NumberFormatException e) { }
}
//3. Time of pong (optional). Do NOT use current system tome
// if not set.
ret.timeRecorded=DEFAULT_TIME_RECORDED;
if (linea.length>=3) {
try {
ret.timeRecorded=Long.parseLong(linea[2].trim());
} catch (NumberFormatException e) { }
}
//4. Time of successful connects (optional)
if (linea.length>=4) {
try {
String times[]=StringUtils.split(linea[3], LIST_SEPARATOR);
for (int i=times.length-1; i>=0; i--)
ret.recordConnectionAttempt(ret.connectSuccesses,
Long.parseLong(times[i].trim()));
} catch (NumberFormatException e) { }
}
//5. Time of failed connects (optional)
if (linea.length>=5) {
try {
String times[]=StringUtils.split(linea[4], LIST_SEPARATOR);
for (int i=times.length-1; i>=0; i--)
ret.recordConnectionAttempt(ret.connectFailures,
Long.parseLong(times[i].trim()));
} catch (NumberFormatException e) { }
}
//6. locale of the connection (optional)
if(linea.length>=6) {
ret.setClientLocale(linea[5]);
}
//7. udp-host
if(linea.length>=7) {
try {
int i = Integer.parseInt(linea[6]);
if(i >= 0)
ret.udpHostCacheFailures = i;
} catch(NumberFormatException nfe) {}
}
// validate address if numeric.
if(pureNumeric && !NetworkUtils.isValidAddress(host))
throw new ParseException("invalid dotted addr: " + ret, 0);
// validate that non UHC addresses were numeric.
if(!ret.isUDPHostCache() && !pureNumeric)
throw new ParseException("illegal non-UHC endpoint: " + ret, 0);
return ret;
}
////////////////////////////// Other /////////////////////////////
/**
* Returns a Comparator that compares ExtendedEndpoint's by priority, where
* ExtendedEndpoint's with higher priority are more likely to be
* available. Currently this is implemented as follows, though the
* heuristic may change in the future:
* <ul>
* <li>Whether the last connection attempt was a success (good), no
* connections have been attempted yet (ok), or the last connection
* attempt was a failure (bad)
* <li>Average daily uptime (higher is better)
* </ul>
*/
public static Comparator priorityComparator() {
return PRIORITY_COMPARATOR;
}
/**
* The sole priority comparator.
*/
private static final Comparator PRIORITY_COMPARATOR = new PriorityComparator();
static class PriorityComparator implements Comparator {
public int compare(Object extEndpoint1, Object extEndpoint2) {
ExtendedEndpoint a=(ExtendedEndpoint)extEndpoint1;
ExtendedEndpoint b=(ExtendedEndpoint)extEndpoint2;
int ret=a.connectScore()-b.connectScore();
if(ret != 0)
return ret;
ret = a.localeScore() - b.localeScore();
if(ret != 0)
return ret;
return a.getDailyUptime() - b.getDailyUptime();
}
}
/**
* Returns +1 if their locale matches our, -1 otherwise.
* Returns 0 if locale preferencing isn't enabled.
*/
private int localeScore() {
if(!ConnectionSettings.USE_LOCALE_PREF.getValue())
return 0;
if(ownLocale.equals(_clientLocale))
return 1;
else
return -1;
}
/** Returns +1 (last connection attempt was a success), 0 (no connection
* attempts), or -1 (last connection attempt was a failure). */
private int connectScore() {
if (connectSuccesses.isEmpty() && connectFailures.isEmpty())
return 0; //no attempts
else if (connectSuccesses.isEmpty())
return -1; //only failures
else if (connectFailures.isEmpty())
return 1; //only successes
else {
long success=((Long)connectSuccesses.last()).longValue();
long failure=((Long)connectFailures.last()).longValue();
//Can't use success-failure because of overflow/underflow.
if (success>failure)
return 1;
else if (success<failure)
return -1;
else
return 0;
}
}
public boolean equals(Object other) {
return super.equals(other);
//TODO: implement
}
}