package com.limegroup.gnutella.downloader;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpException;
import org.limewire.core.api.download.DownloadException;
import org.limewire.core.settings.LWSSettings;
import org.limewire.core.settings.SharingSettings;
import org.limewire.inject.EagerSingleton;
import org.limewire.lifecycle.Service;
import org.limewire.util.Visitor;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.limegroup.gnutella.DownloadServices;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.Downloader.DownloadState;
import com.limegroup.gnutella.lws.server.LWSManager;
import com.limegroup.gnutella.lws.server.LWSManagerCommandResponseHandlerWithCallback;
import com.limegroup.gnutella.lws.server.LWSUtil;
import com.limegroup.gnutella.util.LimeWireUtils;
import com.limegroup.gnutella.util.Tagged;
@EagerSingleton
public final class LWSIntegrationServicesImpl implements LWSIntegrationServices, Service {
private static final Log LOG = LogFactory.getLog(LWSIntegrationServicesImpl.class);
/**
* The period in milliseconds for calling {@link #getDownloadProgress()} to
* remove old references to completed downloaders.
*/
private final static int CALL_GET_DOWNLOAD_PROGRESS_PERIOD_MILLIS = 5 * 60 * 1000;
/** Maintain the last time we called {@link #getDownloadProgress()}, initialized to <code>-1</code>. */
private long lastTimeWeCalledGetDownloadProgress = -1;
private final LWSManager lwsManager;
private final DownloadServices downloadServices;
private final LWSIntegrationServicesDelegate lwsIntegrationServicesDelegate;
private final RemoteFileDescFactory remoteFileDescFactory;
private final ScheduledExecutorService scheduler;
/**
* Maintain a map from downloader IDs to progress bar IDs, because the client sometimes
* cannot keep this state. Clear them whenever the downloader finishes.
*/
private final Map<String,String> downloaderIDs2progressBarIDs = new HashMap<String,String>();
/**
* We maintain a collection of ever-active downloaders, so that when
* we iterate over all the current downloaders, we know that if one
* appears in the ever-active collection, but not the current we
* need to inspect further.
* <p>
* More specifically, that downloader could have completed normally
* or it could have been completed by being cancelled. If the
* downloader completed:
* <pre>
* - Normally: Remove it from the ever-active
* collection and pass back a progress of 1.0
*
* - By being cancelled: Pass back the String 'X' denoting it
* was cancelled.
* </pre>
* The reason we need to do this is because, when the Store is on
* the download page and connected to the Client, it will poll the
* Client for the progress of all active and waiting downloads. In
* the case that a downloader is either (1) finished normally or (2)
* cancelled, and the Client polls after this occurs, we don't know
* which occurred. Basically this allows the Store to sync with the
* Client, so we know when a download actually completes.
* <p>
* Furthermore, if the download is at 99% and then completes, and
* the Store polls after this occurs the progress bars on the Store
* web site will remain at 99%, because we never passed back
* notification that the download completed.
*/
private final Map<String, Downloader> everActiveDownloaderIDs2Downloaders = new HashMap<String, Downloader>();
private String downloadPrefix;
/**
* Maps DownloadState values to their names. These names are used on the
* wire. The Store's JavaScript displays these in the Store UI and also
* depends on specific names. See recProgress in lws_downloads.js.
*/
private final EnumMap<DownloadState, String> downloadStateName =
new EnumMap<DownloadState, String>(DownloadState.class) { {
put(DownloadState.INITIALIZING, "Initializing");
put(DownloadState.QUEUED, "Queued"); // JS depends on this string
put(DownloadState.CONNECTING, "Connecting");
put(DownloadState.DOWNLOADING, "Downloading");
put(DownloadState.BUSY, "Busy");
put(DownloadState.COMPLETE, "Complete");
put(DownloadState.ABORTED, "Aborted"); // JS depends on this string
put(DownloadState.GAVE_UP, "Gave up");
put(DownloadState.DISK_PROBLEM, "Disk problem");
put(DownloadState.WAITING_FOR_GNET_RESULTS, "Waiting for gnet results");
put(DownloadState.CORRUPT_FILE, "Corrupt file");
put(DownloadState.REMOTE_QUEUED, "Remote queued");
put(DownloadState.HASHING, "Hashing");
put(DownloadState.SAVING, "Saving");
put(DownloadState.WAITING_FOR_USER, "Waiting for user");
put(DownloadState.WAITING_FOR_CONNECTIONS, "Waiting for connections");
put(DownloadState.ITERATIVE_GUESSING, "Iterative guessing");
put(DownloadState.QUERYING_DHT, "Querying DHT");
put(DownloadState.IDENTIFY_CORRUPTION, "Identify corruption");
put(DownloadState.RECOVERY_FAILED, "Recovery failed");
put(DownloadState.PAUSED, "Paused");
put(DownloadState.INVALID, "Invalid");
put(DownloadState.RESUMING, "Resuming");
put(DownloadState.FETCHING, "Fetching");
put(DownloadState.DANGEROUS, "Dangerous file");
assert isComplete();
}
private boolean isComplete() {
for (DownloadState state: DownloadState.values()) {
if (!this.containsKey(state)) {
return false;
}
}
return true;
}
};
@Inject
public LWSIntegrationServicesImpl(LWSManager lwsManager,
DownloadServices downloadServices,
LWSIntegrationServicesDelegate lwsIntegrationServicesDelegate,
RemoteFileDescFactory remoteFileDescFactory,
@Named("backgroundExecutor") ScheduledExecutorService scheduler) {
this(lwsManager,downloadServices,lwsIntegrationServicesDelegate,remoteFileDescFactory,scheduler,LWSSettings.LWS_DOWNLOAD_PREFIX.get());
}
/** For testing. */
LWSIntegrationServicesImpl(LWSManager lwsManager,
DownloadServices downloadServices,
LWSIntegrationServicesDelegate lwsIntegrationServicesDelegate,
RemoteFileDescFactory remoteFileDescFactory,
ScheduledExecutorService scheduler,
String downloadPrefix) {
this.lwsManager = lwsManager;
this.downloadServices = downloadServices;
this.lwsIntegrationServicesDelegate = lwsIntegrationServicesDelegate;
this.remoteFileDescFactory = remoteFileDescFactory;
this.scheduler = scheduler;
this.downloadPrefix = downloadPrefix;
}
@Inject
void register(org.limewire.lifecycle.ServiceRegistry registry) {
registry.register(this);
}
public String getServiceName() {
return org.limewire.i18n.I18nMarker.marktr("LimeWire Store Integration");
}
public void start() {
// Call getDownloadProgress() if it has not run for
// CALL_GET_DOWNLOAD_PROGRESS_PERIOD_MILLIS milliseconds
// The check is needed in addition to the scheduling period because
// the Store JS also calls getDownloadProgress().
//
// The point of doing this is to eventually remove references to
// completed downloaders so that we don't hold on to them for the
// entire lifetime of the client.
this.scheduler.scheduleWithFixedDelay(
new Runnable() {
public void run() {
long now = System.currentTimeMillis();
if ( (lastTimeWeCalledGetDownloadProgress == -1) ||
((now - lastTimeWeCalledGetDownloadProgress) <
CALL_GET_DOWNLOAD_PROGRESS_PERIOD_MILLIS) ) {
getDownloadProgress();
}
}
},
CALL_GET_DOWNLOAD_PROGRESS_PERIOD_MILLIS,
CALL_GET_DOWNLOAD_PROGRESS_PERIOD_MILLIS,
TimeUnit.MILLISECONDS);
}
public void stop() {
}
public void setDownloadPrefix(String downloadPrefix) {
this.downloadPrefix = downloadPrefix;
}
/**
* Returns a new {@link RemoveFileDesc} for the file name, relative path,
* and file length given.
*
* @param fileName simple file name to which we save
* @param urlString relative path of the URL we use to perform the download
* @param length length of the file or <code>-1</code> to look up the
* length remotely
* @return a new {@link RemoveFileDesc} for the file name, relative path,
* and file length given.
*/
public RemoteFileDesc createRemoteFileDescriptor(String fileName, String urlString, long length)
throws IOException, URISyntaxException, HttpException, InterruptedException {
// We don't want to pass in a full URL and download it, so have
// the remote setting LWSSettings.LWS_DOWNLOAD_PREFIX specifying
// the entire prefix of where we're getting the file from and
// construct the full URL from that
// This will need NO url encoding, and will contain ?'s and &'s
// which we want to keep. So for testing, we can only pass in
// URLs that don't contain spaces
URL url = new URL("http://" + downloadPrefix + urlString);
if (fileName == null) {
fileName = fileNameFromURL(urlString);
}
// this make the size looked up
RemoteFileDesc rfd = remoteFileDescFactory.createUrlRemoteFileDesc(url, fileName, null, length);
rfd.setHTTP11(false);
return rfd;
}
/**
* Returns a new Store {@link Downloader} for the given arguments.
*
* @param rfd file descriptor used for the download. This should be created
* from {@link #createRemoteFileDescriptor(String, String, long)}.
* @param saveDir directory to which we save the downloaded file
* @return a new Store {@link Downloader} for the given arguments.
* @throws DownloadException
*/
public Downloader createDownloader(RemoteFileDesc rfd, File saveDir)
throws DownloadException {
//
// We'll associate the identity hash code of the downloader
// with this file so that the web page can keep track
// of this downloader w.r.t this file
//
//
// Make sure we aren't already downloading this
//
String fileName = rfd.getFileName();
final AtomicReference<Downloader> downloader = new AtomicReference<Downloader>();
synchronized (lwsIntegrationServicesDelegate) {
final File saveFile = new File(saveDir, fileName);
lwsIntegrationServicesDelegate.visitDownloads(new Visitor<CoreDownloader>() {
public boolean visit(CoreDownloader d) {
if (d.conflictsSaveFile(saveFile)) {
downloader.set(d);
return false; // don't continue
}
return true; // continue
}
});
}
if (downloader.get() == null) {
downloader.set(downloadServices.downloadFromStore(rfd, true, saveDir, fileName));
}
if (LOG.isDebugEnabled()) {
LOG.debug("Have downloader " + downloader.toString());
}
return downloader.get();
}
public void initialize() {
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can keep track of downloads that were made on the
// DownloadMediator
//
// INPUT
// OUTPUT
// ID of download - a string of form
// <ID> ' ' <percentage-downloaded> ':' <download-status> [ '|' <ID> ' '
// <percentage-downloaded> ':' <download-status> ]
// This ID is the identity hash code
// ====================================================================================================================================
lwsManager.registerHandler("GetDownloadProgress", new LWSManagerCommandResponseHandlerWithCallback("GetDownloadProgress") {
@Override
protected String handleRest(Map<String, String> args) {
return getDownloadProgress();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// url - to download
// file - name of file (optional)
// id - id of progress bar to update on the way back
// length - length of the track (optional)
// OUTPUT
// URN - of downloader for keeping track of progress
// -or-
// timeout - if we timeout
// ====================================================================================================================================
lwsManager.registerHandler("Download", new LWSManagerCommandResponseHandlerWithCallback("Download") {
@Override
protected String handleRest(Map<String, String> args) {
//
// The relative URL
//
Tagged<String> urlString = LWSUtil.getArg(args, "url", "downloading");
if (!urlString.isValid()) return urlString.getValue();
//
// The file name. If this isn't given (mainly for testing),
// we'll let it
// go, but note that it was missing
//
Tagged<String> fileString = LWSUtil.getArg(args, "file", "downloading");
if (!fileString.isValid()) LOG.info("no file name given to downloader...");
//
// The id of the tag we want to associate with the URN we return
//
Tagged<String> idOfTheProgressBarString = LWSUtil.getArg(args, "id", "downloading");
if (!idOfTheProgressBarString.isValid()) return idOfTheProgressBarString.getValue();
//
// The length of the URL (optional)
//
Tagged<String> lengthString = LWSUtil.getArg(args, "length", "downloading");
long length = -1;
if (lengthString.isValid()) {
try {
length = Long.parseLong(lengthString.getValue());
} catch (NumberFormatException e) { /* ignore */ }
}
try {
File saveDir = SharingSettings.getSaveLWSDirectory();
RemoteFileDesc rfd = createRemoteFileDescriptor(fileString.getValue(), urlString.getValue(), length);
Downloader theDownloader = createDownloader(rfd, saveDir);
long idOfTheDownloader = System.identityHashCode(theDownloader);
downloaderIDs2progressBarIDs.put(String.valueOf(idOfTheDownloader), idOfTheProgressBarString.getValue());
return idOfTheDownloader + " " + idOfTheProgressBarString.getValue();
} catch (IOException e) {
// invalid url or other causes, fail silently
} catch (HttpException e) {
// invalid url or other causes, fail silently
} catch (InterruptedException e) {
// invalid url or other causes, fail silently
} catch (URISyntaxException e) {
// invalid url or other causes, fail silently
}
return "invalid.download";
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// id - to pause
// OUTPUT
// OK | ID of downloader paused
// ====================================================================================================================================
lwsManager.registerHandler("PauseDownload", new LWSManagerCommandResponseForDownloading("PauseDownload", lwsIntegrationServicesDelegate) {
@Override
protected void takeAction(Downloader d) {
d.pause();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// id - to stop
// OUTPUT
// OK | ID of downloader stopped
// ====================================================================================================================================
lwsManager.registerHandler("StopDownload", new LWSManagerCommandResponseForDownloading("StopDownload", lwsIntegrationServicesDelegate) {
@Override
protected void takeAction(Downloader d) {
d.stop();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// id - to resume
// OUTPUT
// OK | ID of downloader resumed
// ====================================================================================================================================
lwsManager.registerHandler("ResumeDownload", new LWSManagerCommandResponseForDownloading("ResumeDownload", lwsIntegrationServicesDelegate) {
@Override
protected void takeAction(Downloader d) {
d.resume();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// OUTPUT
// OK | IDs of downloader paused
// ====================================================================================================================================
lwsManager.registerHandler("PauseAllDownloads", new LWSManagerCommandResponseForDownloadingAll("PauseAllDownloads", lwsIntegrationServicesDelegate) {
@Override
protected void takeAction(Downloader d) {
d.pause();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// OUTPUT
// OK | IDs of downloader stopped
// ====================================================================================================================================
lwsManager.registerHandler("StopAllDownloads", new LWSManagerCommandResponseForDownloadingAll("StopAllDownloads", lwsIntegrationServicesDelegate) {
@Override
protected void takeAction(Downloader d) {
d.stop();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that
// we can download songs from The Store
// INPUT
// OUTPUT
// OK | IDs of downloader resumed
// ====================================================================================================================================
lwsManager.registerHandler("ResumeAllDownloads", new LWSManagerCommandResponseForDownloadingAll("ResumeAllDownloads", lwsIntegrationServicesDelegate) {
@Override
protected void takeAction(Downloader d) {
d.resume();
}
});
// ====================================================================================================================================
// Add a handler for the LimeWire Store Server so that we can find the
// info of the client running
// INPUT
// --
// OUTPUT
// ( <name> '=' <value> '\t' )*
// ====================================================================================================================================
lwsManager.registerHandler("GetInfo", new LWSManagerCommandResponseHandlerWithCallback("GetInfo") {
private void add(StringBuilder b, String name, Object value) {
b.append(name);
b.append('=');
b.append(value);
b.append('\t');
}
@Override
protected String handleRest(Map<String, String> args) {
StringBuilder res = new StringBuilder();
add(res, "version" ,LimeWireUtils.getLimeWireVersion());
add(res, "major.version.number" ,LimeWireUtils.getMajorVersionNumber());
add(res, "minor.version.number" ,LimeWireUtils.getMinorVersionNumber());
add(res, "vendor" ,LimeWireUtils.getVendor());
add(res, "service.version.number" ,LimeWireUtils.getServiceVersionNumber());
add(res, "is.alpha.release" ,LimeWireUtils.isAlphaRelease());
add(res, "is.beta.release" ,LimeWireUtils.isBetaRelease());
add(res, "is.pro" ,LimeWireUtils.isPro());
add(res, "is.testing.version" ,LimeWireUtils.isTestingVersion());
return res.toString();
}
});
}
/**
* A class to find a downloader, given an identity hashcode and take an
* action. Returns the ID of the downloader upon which was taken action.
*/
private abstract class LWSManagerCommandResponseForDownloading extends LWSManagerCommandResponseHandlerWithCallback {
private final LWSIntegrationServicesDelegate del;
LWSManagerCommandResponseForDownloading(String name, LWSIntegrationServicesDelegate del) {
super(name);
this.del = del;
}
protected abstract void takeAction(Downloader d);
@Override
protected final String handleRest(Map<String, String> args) {
//
// The id of the downloader we want to pause
//
Tagged<String> idOfTheDownloader = LWSUtil.getArg(args, "id", "downloading");
if (!idOfTheDownloader.isValid()) return idOfTheDownloader.getValue();
final String id = idOfTheDownloader.getValue();
//
// Find the downloader, by System.identityHashCode()
//
final AtomicReference<String> res = new AtomicReference<String>("OK");
del.visitDownloads(new Visitor<CoreDownloader>() {
public boolean visit(CoreDownloader d) {
String hash = String.valueOf(System.identityHashCode(d));
if (hash.equals(id)) {
takeAction(d);
res.set(hash);
return false; // we're done
}
return true; // continue
}
});
// The response doesn't matter
return res.get();
}
}
/**
* A class to find all the downloaders, given an identity hashcode and take an
* action. Returns the list of ids took action upon.
*/
private abstract class LWSManagerCommandResponseForDownloadingAll extends LWSManagerCommandResponseHandlerWithCallback {
private final LWSIntegrationServicesDelegate del;
LWSManagerCommandResponseForDownloadingAll(String name, LWSIntegrationServicesDelegate del) {
super(name);
this.del = del;
}
protected abstract void takeAction(Downloader d);
@Override
protected final String handleRest(Map<String, String> args) {
//
// Find the downloaders and compare by System.identityHashCode()
//
final StringBuffer res = new StringBuffer();
//
// Use another list to avoid concurrent modification errors
//
final List<Downloader> downloadersToAffect = new ArrayList<Downloader>();
del.visitDownloads(new Visitor<CoreDownloader>() {
public boolean visit(CoreDownloader d) {
String hash = String.valueOf(System.identityHashCode(d));
if (downloaderIDs2progressBarIDs.containsKey(hash)) {
downloadersToAffect.add(d);
if (res.length() > 0) {
res.append(" ");
}
res.append(hash);
}
return true; // continue
}
});
for (int i=0; i<downloadersToAffect.size(); i++) {
takeAction(downloadersToAffect.get(i));
}
return res.toString();
}
}
private String fileNameFromURL(String urlString) {
int ilast = urlString.lastIndexOf("/");
if (ilast == -1) {
ilast = urlString.lastIndexOf("\\");
}
return urlString.substring(ilast+1);
}
private synchronized String getDownloadProgress() {
//
// Record the last time this was called, so that we know when to not call this from the periodic checker
//
lastTimeWeCalledGetDownloadProgress = System.currentTimeMillis();
//
// Return a string mapping urns to download percentages
//
final StringBuilder res = new StringBuilder();
//
// Remember the downloaders that are actually active, so we see
// if there are any that once existed
// but are completed and take the correct action. See the
// comment for 'everActiveDownloaderIDs'
// for why we have to do this
//
final Set<String> activeDownloaderIDS = new HashSet<String>();
lwsIntegrationServicesDelegate.visitDownloads(new Visitor<CoreDownloader>() {
public boolean visit(CoreDownloader d) {
if (d == null) return true;
if (!(d instanceof StoreDownloader)) return true;
String id = String.valueOf(System.identityHashCode(d));
if (!everActiveDownloaderIDs2Downloaders.containsKey(id)) {
everActiveDownloaderIDs2Downloaders.put(id, d);
}
activeDownloaderIDS.add(id);
recordProgress(d, res, id);
return true;
}
});
//
// Now check whether the list of ever-active downloaders
// contains a downloader not in the current list
//
final Collection<String> idsToRemove = new ArrayList<String>();
for (String downloaderID : everActiveDownloaderIDs2Downloaders.keySet()) {
Downloader d = everActiveDownloaderIDs2Downloaders.get(downloaderID);
if (!activeDownloaderIDS.contains(downloaderID)) {
// record it now, but don't record it next time
recordProgress(d, res, downloaderID);
idsToRemove.add(downloaderID);
}
}
for (String idToRemove: idsToRemove) {
everActiveDownloaderIDs2Downloaders.remove(idToRemove);
downloaderIDs2progressBarIDs.remove(idToRemove);
}
return res.toString();
}
private void recordProgress(Downloader d, StringBuilder res, String id) {
String ratio = String.valueOf((float)d.getAmountRead() / (float)d.getContentLength());
String progressBarID = downloaderIDs2progressBarIDs.get(id);
String stateName = downloadStateName.get(d.getState());
res.append(id).append(" ").append(progressBarID).append(" ").
append(ratio).append(":").append(stateName).append("|");
}
}