package com.limegroup.gnutella.downloader;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.impl.client.DefaultHttpClient;
import com.util.LOG;
import com.limegroup.gnutella.ActivityCallback;
import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.ResponseVerifier;
import com.limegroup.gnutella.SpeedConstants;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.StringUtils;
/**
* A ManagedDownloader for MAGNET URIs. Unlike a ManagedDownloader, a
* MagnetDownloader need not have an initial RemoteFileDesc. Instead it can be
* started with various combinations of the following:
* <ul>
* <li>initial URL (exact source)
* <li>hash/URN (exact topic)
* <li>file name (display name)
* <li>search keywords (keyword topic)
* </ul>
* Names in parentheses are those given by the MAGNET specification at
* http://magnet-uri.sourceforge.net/magnet-draft-overview.txt
* <p>
* Implementation note: this uses ManagedDownloader to try the initial download
* location. Unfortunately ManagedDownloader requires RemoteFileDesc's. We can
* fake up most of the RFD fields, but size presents problems.
* ManagedDownloader depends on size for swarming purposes. It is possible to
* redesign the swarming algorithm to work around the lack of size, but this is
* complex, especially with regard to HTTP/1.1 swarming. For this reason, we
* simply make a HEAD request to get the content length before starting the
* download.
*/
public class MagnetDownloader extends ManagedDownloader implements Serializable {
/** Prevent versioning problems. */
static final long serialVersionUID = 9092913030585214105L;
/** The string to prefix download files with in the rare case that we don't
* have a download name and can't calculate one from the URN. */
static final String DOWNLOAD_PREFIX="MAGNET download from ";
/** The string to use for requery attempts, or null if not provided.
* INVARIANT: _textQuery!=null || _urn!=null */
private String _textQuery;
/** The URN of the file we're looking for, or null if not provided. */
private URN _urn;
/** The download filename, or null if not provided. */
private String _filename;
/** The default location, or null if not provided. Not currently used, but
* may be useful later. */
private String[] _defaultURLs;
/**
* Creates a new MAGNET downloader. Immediately tries to download from
* <tt>defaultURLs</tt>, if specified. If that fails, or if defaultURLs does
* not provide alternate locations, issues a requery with <tt>textQuery</tt>
* and </tt>urn</tt>, as provided. (Note that at least one must be
* non-null.) If <tt>filename</tt> is specified, it will be used as the
* name of the complete file; otherwise it will be taken from any search
* results or guessed from <tt>defaultURLs</tt>.
*
* @param manager controls download queuing; passed to superclass
* @param filemanager shares saved files; passed to superclass
* @param ifm maintains blocks stored on disk; passed to superclass
* @param callback notifies GUI of updates; passed to superclass
* @param urn the hash of the file (exact topic), or null if unknown
* @param textQuery requery keywords (keyword topic), or null if unknown
* @param filename the final file name, or null if unknown
* @param defaultURLs the initial locations to try (exact source), or null
* if unknown
*/
public MagnetDownloader(IncompleteFileManager ifm,
URN urn,
String textQuery,
String filename,
String [] defaultURLs) {
//Initialize superclass with no locations. We'll add the default
//location when the download control thread calls tryAllDownloads.
super(new RemoteFileDesc[0], ifm, null);
this._textQuery=textQuery;
this._urn=urn;
this._filename=filename;
this._defaultURLs=defaultURLs;
}
public void initialize(DownloadManager manager,
ActivityCallback callback) {
downloadSHA1 = _urn;
super.initialize(manager,callback);
}
/**
* Overrides ManagedDownloader to ensure that the default location is tried.
*/
protected void performDownload() {
for (int i = 0; _defaultURLs != null && i < _defaultURLs.length; i++) {
//Send HEAD request to default location (if present)to get its size.
//This can block, so it must be done here instead of in constructor.
//See class overview and ManagedDownloader.tryAllDownloads.
RemoteFileDesc defaultRFD =
createRemoteFileDesc(_defaultURLs[i], _filename, _urn);
if (defaultRFD!=null) {
//Add the faked up location before starting download. Note that
//we must force ManagedDownloader to accept this RFD in case
//it has no hash and a name that doesn't match the search
//keywords.
boolean added=super.addDownloadForced(defaultRFD,true);
Assert.that(added, "Download rfd not accepted "+defaultRFD);
} else {
if(LOG.isWarnEnabled())
LOG.warn("Ignoring magnet url: " + _defaultURLs[i]);
}
}
//Start the downloads for real.
super.performDownload();
}
/**
* Creates a faked-up RemoteFileDesc to pass to ManagedDownloader. If a URL
* is provided, issues a HEAD request to get the file size. If this fails,
* returns null. Package-access and static for easy testing.
*/
private static RemoteFileDesc createRemoteFileDesc(String defaultURL,
String filename, URN urn) {
if (defaultURL==null) {
LOG.debug("createRemoteFileDesc called with null URL");
return null;
}
URL url = null;
try {
// Use the URL class to do a little parsing for us.
url = new URL(defaultURL);
int port = url.getPort();
if (port<0)
port=80; //assume default for HTTP (not 6346)
Set urns=new HashSet(1);
if (urn!=null)
urns.add(urn);
return new URLRemoteFileDesc(
url.getHost(),
port,
0l, //index--doesn't matter since we won't push
filename(filename, url),
contentLength(url),
new byte[16], //GUID--doesn't matter since we won't push
SpeedConstants.T3_SPEED_INT,
false, //no chat support
3, //four [sic] star quality
false, //no browse host
urns,
false, //not a reply to a multicast query
false,"",0l, //not firewalled, no vendor, timestamp=0 (OK?)
url, //url for GET request
null, //no push proxies
0); //assume no firewall transfer
} catch (IOException e) {
if(LOG.isWarnEnabled())
LOG.warn("IOException while processing magnet URL: " + url, e);
return null;
}
}
/** Returns the filename to use for the download, guessed if necessary.
* Package-access and static for easy testing.
* @param filename the filename to use if non-null
* @param url the URL for the resource, which must not be null */
static String filename(String filename, URL url) {
//If the URI specified a download name, use that.
if (filename!=null)
return filename;
//If the URL has a filename, return that. Remember that URL.getFile()
//may include directory information, e.g., "/path/file.txt" or "/path/".
//It also returns "" if no file part.
String path=url.getFile();
if (path.length()>0) {
int i=path.lastIndexOf('/');
if (i<0)
return path; //e.g., "file.txt"
if (i>=0 && i<(path.length()-1))
return path.substring(i+1); //e.g., "/path/to/file"
}
//In the rare case of no filename ("http://www.limewire.com" or
//"http://www.limewire.com/path/"), just make something up.
return DOWNLOAD_PREFIX+url.getHost();
}
/** Returns the length of the content at the given URL.
* @exception IOException couldn't find the length for some reason */
private static int contentLength(URL url) throws IOException {
try {
// Verify that the URL is valid.
new URI(url.toExternalForm());
} catch(Exception e) {
//invalid URI, don't allow this URL.
throw new IOException("invalid url: " + url);
}
HttpHead head = new HttpHead(url.toExternalForm());
head.addHeader("User-Agent",
CommonUtils.getHttpServer());
DefaultHttpClient client = new DefaultHttpClient();
HttpResponse res = client.execute(head);
//Extract Content-length, but only if the response was 200 OK.
//Generally speaking any 2xx response is ok, but in this situation
//we expect only 200.
if (res.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
throw new IOException("Got " + res.getStatusLine());
long length = res.getEntity().getContentLength();
if (length<0)
throw new IOException("No content length");
return (int)length;
}
////////////////////////////// Requery Logic ///////////////////////////
/**
* Overrides ManagedDownloader to use the query words
* specified by the MAGNET URI.
*/
protected QueryRequest newRequery(int numRequeries)
throws CantResumeException {
if (_textQuery != null) {
String q = StringUtils.createQueryString(_textQuery);
return QueryRequest.createQuery(q);
} else if (_filename != null) {
String q = StringUtils.createQueryString(_filename);
return QueryRequest.createQuery(q);
} else
throw new CantResumeException("no keywords or filename");
/* //TODO: if we ever add back URN query support
boolean isRequery = numRequeries!=0;
if(isRequery && (_urn != null)) {
if (_filename == null)
return QueryRequest.createRequery(_urn);
else
return QueryRequest.createRequery(_urn, _filename);
} else if(isRequery) {
return QueryRequest.createRequery(_textQuery);
} else if(_urn != null) {
if (_filename == null)
return QueryRequest.createQuery(_urn);
else
return QueryRequest.createQuery(_urn, _filename);
}
if (_urn != null)
return QueryRequest.createQuery(_urn, _textQuery);
return QueryRequest.createQuery(_textQuery);
*/
}
/**
* Overrides ManagedDownloader to allow any files with the right
* hash/keywords, even if this doesn't currently have any download
* locations.
*/
protected boolean allowAddition(RemoteFileDesc other) {
//Allow if we have a hash and other matches it.
if (_urn!=null) {
Set urns=other.getUrns();
if (urns!=null && urns.contains(_urn))
return true;
}
//Allow if we specified query keywords and the filename matches. TODO3:
//this tokenizes the query keyword every time. Would it be better to
//make ResponseVerifier.getSearchTerms/score(keywords[], name) public?
if (_textQuery!=null) {
int score=ResponseVerifier.score(_textQuery, null, other);
if (score==100)
return true;
}
//No match? Error.
return false;
}
/**
* Overrides ManagedDownloader to display a reasonable file name even
* when no locations have been found.
*/
public synchronized String getFileName() {
if (_filename!=null)
return _filename;
else {
String fname = null;
// Check the super name if I have an RFD
if ( hasRFD() )
fname = super.getFileName();
// If I still don't have a good name, resort to whatever I have.
if ( fname == null || fname.equals(UNKNOWN_FILENAME) )
fname = getFileNameHint();
return fname;
}
}
/**
* Overrides ManagedDownloader to display a reasonable file name
* when neither it or we have an idea of what the filename is.
*/
private String getFileNameHint() {
if ( _urn != null )
return _urn.toString();
else if ( _textQuery != null )
return _textQuery;
else if ( _defaultURLs != null && _defaultURLs.length > 0 )
return _defaultURLs[0];
else
return "";
}
}