package org.limewire.core.impl.search; import java.util.AbstractList; import java.util.List; import java.util.Set; import org.limewire.core.api.Category; import org.limewire.core.api.FilePropertyKey; import org.limewire.core.api.URN; import org.limewire.core.api.endpoint.RemoteHost; import org.limewire.core.api.search.SearchResult; import org.limewire.core.impl.friend.GnutellaPresence; import org.limewire.core.impl.util.FilePropertyKeyPopulator; import org.limewire.core.settings.SearchSettings; import org.limewire.friend.api.FriendPresence; import org.limewire.friend.api.feature.LimewireFeature; import org.limewire.io.Connectable; import org.limewire.io.ConnectableImpl; import org.limewire.io.IpPort; import org.limewire.util.FileUtils; import org.limewire.util.StringUtils; import com.google.common.collect.ImmutableList; import com.limegroup.gnutella.CategoryConverter; import com.limegroup.gnutella.RemoteFileDesc; import com.limegroup.gnutella.browser.MagnetOptions; import com.limegroup.gnutella.xml.LimeXMLDocument; /** * A class to generate a compatible {@link SearchResult} for the ui using an anonymous or non * anonymous {@link FriendPresence} a {@link RemoteFileDesc} and a list of alternate locations (AltLocs). */ public class RemoteFileDescAdapter implements SearchResult { /** * AltLocs are given a weight of one since we do not really know anything about them but * they will improve the quality of the result if they work or are not spam. */ private static int ALTLOC_FACTOR = 1; private static int BROWSABLE_ANONYMOUS_PEER_FACTOR = 6; private static int NON_BROWSABLE_ANONYMOUS_PEER_FACTOR = 1; /** * Non anonymous sources, XMPP friends, are considered very important. */ private static int FRIENDLY_PEER_FACTOR = 20; private final FriendPresence friendPresence; private final RemoteFileDesc rfd; private final String extension; private final List<IpPort> locs; private final Category category; private final int quality; /** The cached relevance value from {@link #getRelevance()}, -1 is unset */ private int relevance = -1; /** * Constructs {@link RemoteFileDescAdapter} with an anonymous Gnutella presence based on the rfd's * address and a set of altlocs. */ public RemoteFileDescAdapter(RemoteFileDesc rfd, Set<? extends IpPort> locs) { this(rfd, locs, new GnutellaPresence.GnutellaPresenceWithGuid(rfd.getAddress(), rfd.getClientGUID())); } /** * Constructs {@link RemoteFileDescAdapter} with a specific and possibly non anonymous presence * and a set of altlocs. */ public RemoteFileDescAdapter(RemoteFileDesc rfd, Set<? extends IpPort> locs, FriendPresence friendPresence) { this.rfd = rfd; this.locs = ImmutableList.copyOf(locs); this.friendPresence = friendPresence; this.extension = FileUtils.getFileExtension(rfd.getFileName()); this.category = CategoryConverter.categoryForExtension(extension); this.quality = FilePropertyKeyPopulator.calculateQuality(category, extension, rfd.getSize(), rfd.getXMLDocument()); } /** A copy constructor for a RemoteFileDescAdapter, except it changes the presence. */ public RemoteFileDescAdapter(RemoteFileDescAdapter copy, FriendPresence presence) { this.rfd = copy.rfd; this.locs = copy.locs; this.friendPresence = presence; this.extension = copy.extension; this.category = copy.category; this.quality = copy.quality; // and other items too, if they were constructed.. this.relevance = copy.relevance; } /** * Calculates a rough "relevance", which is a measure of the quality of the sources. * Non anonymous sources with active capabilities (ie. browseable) are given greatest weight. */ @Override public int getRelevance() { // If the value has already been calculated take that one, since it can not change // during the lifecycle of this object if (relevance != -1) { return relevance; } relevance = 0; // Calculate the relevance based on the (truncated) sources list for(RemoteHost remoteHost : getSources()) { if (remoteHost instanceof RelevantRemoteHost) { relevance += ((RelevantRemoteHost) remoteHost).getRelevance(); } } return relevance; } /** * @returns the complete list of AltLocs. */ public List<IpPort> getAlts() { return locs; } /** * @return the full filename of the rfd. */ @Override public String getFileName() { return rfd.getFileName(); } /** * @return the extension for the sourced rfd. */ @Override public String getFileExtension() { return extension; } @Override public String getFileNameWithoutExtension() { return FileUtils.getFilenameNoExtension(rfd.getFileName()); } /** * @return if the file has licence info in its xml doc. */ @Override public boolean isLicensed() { LimeXMLDocument doc = rfd.getXMLDocument(); return (doc != null) && (doc.getLicenseString() != null); } /** * @return the specified property using the file key provided. */ @Override public Object getProperty(FilePropertyKey property) { switch(property) { case NAME: return getFileNameWithoutExtension(); case DATE_CREATED: return rfd.getCreationTime() == -1 ? null : rfd.getCreationTime(); case FILE_SIZE: return rfd.getSize(); case QUALITY: return quality == -1 ? null : Long.valueOf(quality); default: return FilePropertyKeyPopulator.get(category, property, rfd.getXMLDocument()); } } /** * @return the category the rfd filetype falls into. */ @Override public Category getCategory() { return category; } /** * @return the rfd used to generate this adapter. */ public RemoteFileDesc getRfd() { return rfd; } /** * @return the {@link FriendPresence} used to generate this adaptor. */ public FriendPresence getFriendPresence() { return friendPresence; } /** * @return the file size of the rfd. */ @Override public long getSize() { return rfd.getSize(); } /** * @return whether the rfd has been marked as spam or not. */ @Override public boolean isSpam() { return rfd.isSpam(); } /** * @return the sources that should be shown in the UI. Includes a limited * number of alt-locs. */ @Override public List<RemoteHost> getSources() { // This returns a pass-through list for memory optimizations. // Do not cache this or the contents of this without doing // memory profiling and ensuring things are OK. return new AbstractList<RemoteHost>() { @Override public void clear() { throw new UnsupportedOperationException(); } @Override public RemoteHost get(int index) { if(index == 0) { return new RfdRemoteHost(friendPresence, rfd); } else if(index - 1 < SearchSettings.ALT_LOCS_TO_DISPLAY.getValue()) { return new AltLocRemoteHost(locs.get(index-1)); } else { throw new IndexOutOfBoundsException("index: " + index + ", size: " + size()); } } @Override public boolean isEmpty() { return false; } @Override public RemoteHost remove(int index) { throw new UnsupportedOperationException(); } @Override public int size() { return Math.min(SearchSettings.ALT_LOCS_TO_DISPLAY.getValue(), locs.size()) + 1; } }; } /** * @return the {@link URNImpl} for the rfd. */ @Override public URN getUrn() { return rfd.getSHA1Urn(); } /** * @return the a magnet link URL string for the rfd. */ @Override public String getMagnetURL() { MagnetOptions magnet = MagnetOptions.createMagnet(rfd, null, rfd.getClientGUID()); return magnet.toExternalForm(); } @Override public String toString() { return StringUtils.toString(this); } /** * An adapter that creates a compatible {@link RemoteHost} from the {@link RemoteFileDesc} and anonymous * or non anonymous {@link FriendPresence} that the main {@link RemoteFileDescAdapter} was constructed with. */ static class RfdRemoteHost implements RelevantRemoteHost { private final FriendPresence friendPresence; private final boolean browseHostEnabled; public RfdRemoteHost(FriendPresence presence, RemoteFileDesc rfd) { this.friendPresence = presence; this.browseHostEnabled = rfd.isBrowseHostEnabled(); } @Override public boolean isBrowseHostEnabled() { if(friendPresence.getFriend().isAnonymous()) { return browseHostEnabled; } else { //ensure friend/user still logged in through LW return friendPresence.hasFeatures(LimewireFeature.ID); } } @Override public boolean isChatEnabled() { if(friendPresence.getFriend().isAnonymous()) { return false; } else { //ensure friend/user still logged in through LW return friendPresence.hasFeatures(LimewireFeature.ID); } } @Override public boolean isSharingEnabled() { if(friendPresence.getFriend().isAnonymous()) { return false; } else { //ensure friend/user still logged in through LW return friendPresence.hasFeatures(LimewireFeature.ID); } } @Override public FriendPresence getFriendPresence() { return friendPresence; } /** * @return the relevance for a primary host calculated from its * capabilities. */ @Override public int getRelevance() { if(friendPresence.getFriend().isAnonymous()) { if (browseHostEnabled) { return BROWSABLE_ANONYMOUS_PEER_FACTOR; } else { return NON_BROWSABLE_ANONYMOUS_PEER_FACTOR; } } else { return FRIENDLY_PEER_FACTOR; } } @Override public String toString() { return "RFD Host for: " + friendPresence; } } /** * An adapter class for an AltLoc based on {@link IpPort} and translated to a {@link RemoteHost}. */ static class AltLocRemoteHost implements RelevantRemoteHost { private final FriendPresence presence; AltLocRemoteHost(IpPort ipPort) { if(ipPort instanceof Connectable) { this.presence = new GnutellaPresence.GnutellaPresenceWithConnectable((Connectable)ipPort); } else { this.presence = new GnutellaPresence.GnutellaPresenceWithConnectable(new ConnectableImpl(ipPort, false)); } } /** * Indicates that a browse host is possible, however, in this case, it actually * may not be 100% of the time. Returning true allows a browse host attempts * to be started. */ @Override public boolean isBrowseHostEnabled() { return true; } /** * Chat is unsupported for Gnutella/anonymous sources so it will * never be supported in an altloc. */ @Override public boolean isChatEnabled() { return false; } /** * Share is unsupported for Gnutella/anonymous sources so it will * never be supported in an AltLoc. */ @Override public boolean isSharingEnabled() { return false; } /** * @return the anonymous {@link FriendPresence} associated with this altloc. */ @Override public FriendPresence getFriendPresence() { return presence; } @Override public int getRelevance() { return ALTLOC_FACTOR; } @Override public String toString() { return "AltLoc Host For: " + presence; } } /** * Defines a relevance calculation unique to a specific {@link RemoteHost} type. */ protected static interface RelevantRemoteHost extends RemoteHost { public int getRelevance(); } }