package com.limegroup.gnutella.downloader;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.DownloadCallback;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.SaveLocationException;
import com.limegroup.gnutella.SpeedConstants;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.browser.MagnetOptions;
import com.limegroup.gnutella.http.HttpClientManager;
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 {
private static final Log LOG = LogFactory.getLog(MagnetDownloader.class);
/** Prevent versioning problems. */
static final long serialVersionUID = 9092913030585214105L;
private static final transient String MAGNET = "MAGNET";
/**
* 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 magnet contains all the information for the download, must be
* {@link MagnetOptions#isDownloadable() downloadable}.
* @param overwrite whether file at download location should be overwritten
* @param saveDir can be null, then the default save directory is used
* @param fileName the final file name, can be <code>null</code>
*
* @throws SaveLocationException if there was an error setting the downloads
* final file location
*/
public MagnetDownloader(IncompleteFileManager ifm,
MagnetOptions magnet,
boolean overwrite,
File saveDir,
String fileName) throws SaveLocationException {
//Initialize superclass with no locations. We'll add the default
//location when the download control thread calls tryAllDownloads.
super(new RemoteFileDesc[0], ifm, null, saveDir,
checkMagnetAndExtractFileName(magnet, fileName), overwrite);
propertiesMap.put(MAGNET, magnet);
}
public void initialize(DownloadManager manager, FileManager fileManager,
DownloadCallback callback) {
Assert.that(getMagnet() != null);
downloadSHA1 = getMagnet().getSHA1Urn();
super.initialize(manager, fileManager, callback);
}
private MagnetOptions getMagnet() {
return (MagnetOptions)propertiesMap.get(MAGNET);
}
/**
* overrides ManagedDownloader to ensure that we issue requests to the known
* locations until we find out enough information to start the download
*/
protected int initializeDownload() {
if (!hasRFD()) {
MagnetOptions magnet = getMagnet();
String[] defaultURLs = magnet.getDefaultURLs();
if (defaultURLs.length == 0 )
return Downloader.GAVE_UP;
RemoteFileDesc firstDesc = null;
for (int i = 0; i < defaultURLs.length && firstDesc == null; i++) {
try {
firstDesc = createRemoteFileDesc(defaultURLs[i],
getSaveFile().getName(), magnet.getSHA1Urn());
initPropertiesMap(firstDesc);
addDownloadForced(firstDesc, true);
} catch (IOException badRFD) {}
}
// if all locations included in the magnet URI fail we can't do much
if (firstDesc == null)
return GAVE_UP;
}
return super.initializeDownload();
}
/**
* Overrides ManagedDownloader to ensure that the default location is tried.
*
protected int 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.
try {
RemoteFileDesc defaultRFD =
createRemoteFileDesc(_defaultURLs[i], _filename, _urn);
//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.
super.addDownloadForced(defaultRFD,true);
}catch(IOException badRFD) {
if(LOG.isWarnEnabled())
LOG.warn("Ignoring magnet url: " + _defaultURLs[i]);
}
}
//Start the downloads for real.
return 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) throws IOException{
if (defaultURL==null) {
LOG.debug("createRemoteFileDesc called with null URL");
return null;
}
URL url = null;
// 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);
URI uri = new URI(url);
return new URLRemoteFileDesc(
url.getHost(),
port,
0l, //index--doesn't matter since we won't push
filename != null ? filename : MagnetOptions.extractFileName(uri),
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
null, //no metadata
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
}
/** 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().toCharArray());
} catch(URIException e) {
//invalid URI, don't allow this URL.
throw new IOException("invalid url: " + url);
}
HttpClient client = HttpClientManager.getNewClient();
HttpMethod head = new HeadMethod(url.toExternalForm());
head.addRequestHeader("User-Agent",
CommonUtils.getHttpServer());
try {
client.executeMethod(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 (head.getStatusCode() != HttpStatus.SC_OK)
throw new IOException("Got " + head.getStatusCode() +
" instead of 200");
int length = head.getResponseContentLength();
if (length<0)
throw new IOException("No content length");
return length;
} finally {
if(head != null)
head.releaseConnection();
}
}
////////////////////////////// Requery Logic ///////////////////////////
/**
* Overrides ManagedDownloader to use the query words
* specified by the MAGNET URI.
*/
protected QueryRequest newRequery(int numRequeries)
throws CantResumeException {
MagnetOptions magnet = getMagnet();
String textQuery = magnet.getQueryString();
if (textQuery != null) {
String q = StringUtils.createQueryString(textQuery);
return QueryRequest.createQuery(q);
}
else {
String q = StringUtils.createQueryString(getSaveFile().getName());
return QueryRequest.createQuery(q);
}
}
/**
* Overrides ManagedDownloader to allow any files with the right
* hash even if this doesn't currently have any download
* locations.
* <p>
* We only allow for additions if the download has a sha1.
*/
protected boolean allowAddition(RemoteFileDesc other) {
// Allow if we have a hash and other matches it.
URN otherSHA1 = other.getSHA1Urn();
if (downloadSHA1 != null && otherSHA1 != null) {
return downloadSHA1.equals(otherSHA1);
}
return false;
}
/**
* Overridden for internal purposes, returns result from super method
* call.
*/
protected synchronized boolean addDownloadForced(RemoteFileDesc rfd,
boolean cache) {
if (!hasRFD())
initPropertiesMap(rfd);
return super.addDownloadForced(rfd, cache);
}
/**
* Creates a magnet downloader object when converting from the old
* downloader version.
*
* @throws IOException when the created magnet is not downloadable
*/
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException {
MagnetOptions magnet = getMagnet();
if (magnet == null) {
ObjectInputStream.GetField fields = stream.readFields();
String textQuery = (String) fields.get("_textQuery", null);
URN urn = (URN) fields.get("_urn", null);
String fileName = (String) fields.get("_filename", null);
String[] defaultURLs = (String[])fields.get("_defaultURLs", null);
magnet = MagnetOptions.createMagnet(textQuery, fileName, urn, defaultURLs);
if (!magnet.isDownloadable()) {
throw new IOException("Old undownloadable magnet");
}
propertiesMap.put(MAGNET, magnet);
}
if (propertiesMap.get(DEFAULT_FILENAME) == null)
propertiesMap.put(DEFAULT_FILENAME, magnet.getFileNameForSaving());
}
/**
* Only allow requeries when <code>downloadSHA1</code> is not null.
*/
protected boolean shouldSendRequeryImmediately(int numRequeries) {
return downloadSHA1 != null ? super.shouldSendRequeryImmediately(numRequeries)
: false;
}
/**
* Checks if the magnet is downloadable and extracts a fileName if
* <code>fileName</code> is null.
*
* @throws IllegalArgumentException if the magnet is not downloadable
*/
private static String checkMagnetAndExtractFileName(MagnetOptions magnet,
String fileName) {
if (!magnet.isDownloadable()) {
throw new IllegalArgumentException("magnet not downloadable");
}
if (fileName != null) {
return fileName;
}
return magnet.getFileNameForSaving();
}
/**
* Overridden to make sure it calls the super method only if
* the filesize is known.
*/
protected void initializeIncompleteFile() throws IOException {
if (getContentLength() != -1) {
super.initializeIncompleteFile();
}
}
}