package org.limewire.core.impl.search; import java.util.List; import java.util.Set; import org.limewire.bittorrent.Torrent; 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.file.CategoryManager; import org.limewire.core.api.search.SearchResult; import org.limewire.core.impl.TorrentFactory; import org.limewire.core.impl.friend.GnutellaPresence; import org.limewire.core.impl.util.FilePropertyKeyPopulator; 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.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; 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). */ @SuppressWarnings("deprecation") public class RemoteFileDescAdapter implements SearchResult { public interface Factory { /** * Constructs {@link RemoteFileDescAdapter} with an anonymous Gnutella presence based on the rfd's * address and a set of altlocs. */ RemoteFileDescAdapter create(RemoteFileDesc rfd, Set<? extends IpPort> locs); /** * Constructs {@link RemoteFileDescAdapter} with a specific and possibly non anonymous presence * and a set of altlocs. */ RemoteFileDescAdapter create(RemoteFileDesc rfd, Set<? extends IpPort> locs, FriendPresence friendPresence); } /** * Non anonymous sources, XMPP friends, are considered very important. */ private static int FRIENDLY_PEER_FACTOR = 20; /** * Browseable sources are considered more important than unbrowseable ones. */ private static int BROWSEABLE_ANONYMOUS_PEER_FACTOR = 6; private static int NON_BROWSEABLE_ANONYMOUS_PEER_FACTOR = 1; 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 float relevance = -1; private final Torrent torrent; /** * Constructs {@link RemoteFileDescAdapter} with an anonymous Gnutella presence based on the rfd's * address and a set of altlocs. */ // we're using the old AssistedInject because it works with multiple constructors @AssistedInject RemoteFileDescAdapter(@Assisted RemoteFileDesc rfd, @Assisted Set<? extends IpPort> locs, CategoryManager categoryManager, TorrentFactory torrentFactory) { this(rfd, locs, new GnutellaPresence.GnutellaPresenceWithGuid(rfd.getAddress(), rfd.getClientGUID()), categoryManager, torrentFactory); } /** * Constructs {@link RemoteFileDescAdapter} with a specific and possibly non anonymous presence * and a set of altlocs. */ // we're using the old AssistedInject because it works with multiple constructors @AssistedInject RemoteFileDescAdapter(@Assisted RemoteFileDesc rfd, @Assisted Set<? extends IpPort> locs, @Assisted FriendPresence friendPresence, CategoryManager categoryManager, TorrentFactory torrentFactory) { this.rfd = rfd; this.locs = ImmutableList.copyOf(locs); this.friendPresence = friendPresence; this.extension = FileUtils.getFileExtension(rfd.getFileName()); this.category = categoryManager.getCategoryForExtension(extension); this.quality = FilePropertyKeyPopulator.calculateQuality(category, extension, rfd.getSize(), rfd.getXMLDocument()); this.torrent = torrentFactory.createTorrentFromXML(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; this.torrent = copy.torrent; // and other items too, if they were constructed.. this.relevance = copy.relevance; } /** * Returns a score that indicates the quality of the sources and the degree * to which the result matches the query. Non-anonymous sources with active * capabilities (ie. browseable) are given greatest weight. */ @Override public float getRelevance(String query) { // Consider whether the result matches the query if(!StringUtils.isEmpty(query) && !rfd.matchesQuery(query)) return 0; // Ignore alt locs for relevance ranking if(friendPresence.getFriend().isAnonymous()) { if(rfd.isBrowseHostEnabled()) return BROWSEABLE_ANONYMOUS_PEER_FACTOR; else return NON_BROWSEABLE_ANONYMOUS_PEER_FACTOR; } else { return FRIENDLY_PEER_FACTOR; } } /** * @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); // TODO: what was going on here?? why do we need to recreate? case TORRENT: return torrent; default: return FilePropertyKeyPopulator.get(category, property, rfd.getXMLDocument()); } } @Override public long getSize() { if (torrent != null) { return torrent.getTotalPayloadSize(); } return rfd.getSize(); } /** * @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 whether the rfd has been marked as spam or not. */ @Override public boolean isSpam() { return rfd.isSpam(); } @Override public RemoteHost getSource() { return new RfdRemoteHost(friendPresence, rfd); } /** * @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 RemoteHost { 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; } @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 RemoteHost { 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 String toString() { return "AltLoc Host For: " + presence; } } }