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 } }