package com.limegroup.gnutella.altlocs;
import java.io.IOException;
import java.util.Iterator;
import org.limewire.collection.FixedSizeSortedSet;
import org.limewire.service.ErrorService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.http.HTTPHeaderValue;
/**
* This class holds a collection of <tt>AlternateLocation</tt> instances,
* providing type safety for alternate location data.
* <p>
*
* @see AlternateLocation
*/
public class AlternateLocationCollection<T extends AlternateLocation> implements HTTPHeaderValue,
Iterable<T> {
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;
}
/**
* Returns a type-safe empty collection.
*/
@SuppressWarnings("unchecked")
public static <T extends AlternateLocation> AlternateLocationCollection<T> getEmptyCollection() {
return EMPTY;
}
/**
* 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.
* <p>
* 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<T> LOCATIONS = new FixedSizeSortedSet<T>(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 <T extends AlternateLocation> AlternateLocationCollection<T> create(URN sha1) {
return new AlternateLocationCollection<T>(sha1);
}
/**
* 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.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.
* <p>
* 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(T al) {
URN sha1 = al.getSHA1Urn();
if (!sha1.equals(SHA1))
throw new IllegalArgumentException("SHA1 does not match");
synchronized (this) {
T alt = 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();
if (alt instanceof AbstractAlternateLocation) {
AbstractAlternateLocation absAlt = (AbstractAlternateLocation) alt;
absAlt.promote();
absAlt.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(T 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) {
T loc = 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);
if (loc instanceof AbstractAlternateLocation) {
AbstractAlternateLocation absAlt = (AbstractAlternateLocation) loc;
absAlt.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(Object 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 = ", ";
StringBuilder writeBuffer = new StringBuilder();
boolean wrote = false;
synchronized (this) {
for (AlternateLocation current : LOCATIONS) {
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<T> 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
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Alternate Locations: ");
synchronized (this) {
for (T curLoc : LOCATIONS) {
sb.append(curLoc.toString());
sb.append("\n");
}
}
return sb.toString();
}
@Override
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<AlternateLocation> {
EmptyCollection() throws IOException {
super(URN.createSHA1Urn("urn:sha1:PLSTHIPQGSSZTS5FJUPAKUZWUGYQYPFB"));
}
@Override
public boolean add(AlternateLocation loc) {
throw new UnsupportedOperationException();
}
}
}