package com.limegroup.gnutella.altlocs; import java.io.IOException; import java.util.Iterator; import java.util.StringTokenizer; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.http.HTTPHeaderValue; import com.limegroup.gnutella.util.FixedSizeSortedSet; /** * This class holds a collection of <tt>AlternateLocation</tt> instances, * providing type safety for alternate location data. * <p> * @see AlternateLocation */ public class AlternateLocationCollection implements HTTPHeaderValue { private static final int MAX_SIZE = 100; public static final AlternateLocationCollection EMPTY; static { AlternateLocationCollection col = null; try { col = new EmptyCollection(); } catch (IOException bad) { ErrorService.error(bad); } EMPTY = col; } /** * This uses a <tt>FixedSizeSortedSet</tt> so that the highest * entry * inserted is removed when the limit is reached. * <p> * LOCKING: obtain this' monitor when iterating. Note that all modifications * to LOCATIONS are synchronized on this. * * LOCKING: Never grab the lock on AlternateLocationCollection.class if you * have this' monitor. If both locks are needed, always lock on * AlternateLocationCollection.class first, never the other way around. */ private final FixedSizeSortedSet LOCATIONS=new FixedSizeSortedSet(MAX_SIZE); /** * SHA1 <tt>URN</tt> for this collection. */ private final URN SHA1; /** * Factory constructor for creating a new * <tt>AlternateLocationCollection</tt> for this <tt>URN</tt>. * * @param sha1 the SHA1 <tt>URN</tt> for this collection * @return a new <tt>AlternateLocationCollection</tt> instance for * this SHA1 */ public static AlternateLocationCollection create(URN sha1) { return new AlternateLocationCollection(sha1); } /** * Creates a new <tt>AlternateLocationCollection</tt> with all alternate * locations contained in the given comma-delimited HTTP header value * string. The returned <tt>AlternateLocationCollection</tt> may be empty. * * @param value the HTTP header value containing alternate locations * @return a new <tt>AlternateLocationCollection</tt> with any valid * <tt>AlternateLocation</tt>s from the HTTP string, or <tt>null</tt> * if no valid locations could be found * @throws <tt>NullPointerException</tt> if <tt>value</tt> is <tt>null</tt> * * Note: this method requires the full altloc syntax (including the SHA1 in it) * In other words, you cannot use the httpStringValue() output as an input to * this method if you want to recreate the collection. It seems to be used only * in downloader.HeadRequester */ public static AlternateLocationCollection createCollectionFromHttpValue(final String value) { if(value == null) { throw new NullPointerException("cannot create an "+ "AlternateLocationCollection "+ "from a null value"); } StringTokenizer st = new StringTokenizer(value, ","); AlternateLocationCollection alc = null; while(st.hasMoreTokens()) { String curTok = st.nextToken(); try { AlternateLocation al = AlternateLocation.create(curTok); if(alc == null) alc = new AlternateLocationCollection(al.getSHA1Urn()); if(al.getSHA1Urn().equals(alc.getSHA1Urn())) alc.add(al); } catch(IOException e) { continue; } } return alc; } /** * Creates a new <tt>AlternateLocationCollection</tt> for the specified * <tt>URN</tt>. * * @param sha1 the SHA1 <tt>URN</tt> for this alternate location collection */ private AlternateLocationCollection(URN sha1) { if(sha1 == null) throw new NullPointerException("null URN"); if( sha1 != null && !sha1.isSHA1()) throw new IllegalArgumentException("URN must be a SHA1"); SHA1 = sha1; } /** * Returns the SHA1 for this AlternateLocationCollection. */ public URN getSHA1Urn() { return SHA1; } /** * Adds a new <tt>AlternateLocation</tt> to the list. If the * alternate location is already present in the collection, * it's count will be incremented. * * Implements the <tt>AlternateLocationCollector</tt> interface. * * @param al the <tt>AlternateLocation</tt> to add * * @throws <tt>IllegalArgumentException</tt> if the * <tt>AlternateLocation</tt> being added does not have a SHA1 urn or if * the SHA1 urn does not match the urn for this collection * * @return true if added, false otherwise. */ public boolean add(AlternateLocation al) { URN sha1 = al.getSHA1Urn(); if(!sha1.equals(SHA1)) throw new IllegalArgumentException("SHA1 does not match"); synchronized(this) { AlternateLocation alt = (AlternateLocation)LOCATIONS.get(al); boolean ret = false; if(alt==null) {//it was not in collections. ret = true; LOCATIONS.add(al); } else { LOCATIONS.remove(alt); alt.increment(); alt.promote(); alt.resetSent(); ret = false; LOCATIONS.add(alt); //add incremented version } return ret; } } /** * Removes this <tt>AlternateLocation</tt> from the active locations * and adds it to the removed locations. */ public boolean remove(AlternateLocation al) { URN sha1 = al.getSHA1Urn(); if(!sha1.equals(SHA1)) return false; //it cannot be in this list if it has a different SHA1 synchronized(this) { AlternateLocation loc = (AlternateLocation)LOCATIONS.get(al); if(loc==null) //it's not in locations, cannot remove return false; if(loc.isDemoted()) {//if its demoted remove it LOCATIONS.remove(loc); return true; } else { LOCATIONS.remove(loc); loc.demote(); //one more strike and you are out... LOCATIONS.add(loc); //make it replace the older loc return false; } } } public synchronized void clear() { LOCATIONS.clear(); } // implements the AlternateLocationCollector interface public synchronized boolean hasAlternateLocations() { return !LOCATIONS.isEmpty(); } /** * @return true is this contains loc */ public synchronized boolean contains(AlternateLocation loc) { return LOCATIONS.contains(loc); } /** * Implements the <tt>HTTPHeaderValue</tt> interface. * * @return an HTTP-compliant string of alternate locations, delimited * by commas, or the empty string if there are no alternate locations * to report */ public String httpStringValue() { final String commaSpace = ", "; StringBuffer writeBuffer = new StringBuffer(); boolean wrote = false; synchronized(this) { Iterator iter = LOCATIONS.iterator(); while(iter.hasNext()) { AlternateLocation current = (AlternateLocation)iter.next(); writeBuffer.append( current.httpStringValue()); writeBuffer.append(commaSpace); wrote = true; } } // Truncate the last comma from the buffer. // This is arguably quicker than rechecking hasNext on the iterator. if ( wrote ) writeBuffer.setLength(writeBuffer.length()-2); return writeBuffer.toString(); } // Implements AlternateLocationCollector interface -- // inherit doc comment public synchronized int getAltLocsSize() { return LOCATIONS.size(); } public Iterator iterator() { return LOCATIONS.iterator(); } /** * Overrides Object.toString to print out all of the alternate locations * for this collection of alternate locations. * * @return the string representation of all alternate locations in * this collection */ public String toString() { StringBuffer sb = new StringBuffer(); sb.append("Alternate Locations: "); synchronized(this) { Iterator iter = LOCATIONS.iterator(); while(iter.hasNext()) { AlternateLocation curLoc = (AlternateLocation)iter.next(); sb.append(curLoc.toString()); sb.append("\n"); } } return sb.toString(); } public boolean equals(Object o) { if(o == this) return true; if(!(o instanceof AlternateLocationCollection)) return false; AlternateLocationCollection alc = (AlternateLocationCollection)o; boolean ret = SHA1.equals(alc.SHA1); if ( !ret ) return false; // This must be synchronized on both LOCATIONS and alc.LOCATIONS // because we not using the SynchronizedMap versions, and equals // will inherently call methods that would have been synchronized. synchronized(AlternateLocationCollection.class) { synchronized(this) { synchronized(alc) { ret = LOCATIONS.equals(alc.LOCATIONS); } } } return ret; } private static class EmptyCollection extends AlternateLocationCollection { EmptyCollection() throws IOException { super(URN.createSHA1Urn("urn:sha1:PLSTHIPQGSSZTS5FJUPAKUZWUGYQYPFB")); } public boolean add(AlternateLocation loc) { throw new UnsupportedOperationException(); } } }