package org.limewire.core.impl.download;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.limewire.concurrent.ManagedThread;
import org.limewire.core.api.Category;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.URN;
import org.limewire.core.api.download.DownloadException;
import org.limewire.core.api.download.DownloadItem;
import org.limewire.core.api.download.DownloadPiecesInfo;
import org.limewire.core.api.download.DownloadPropertyKey;
import org.limewire.core.api.download.DownloadState;
import org.limewire.core.api.endpoint.RemoteHost;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.api.malware.VirusEngine;
import org.limewire.core.api.transfer.SourceInfo;
import org.limewire.core.impl.RemoteHostRFD;
import org.limewire.core.impl.friend.GnutellaPresence;
import org.limewire.core.impl.util.FilePropertyKeyPopulator;
import org.limewire.friend.api.FriendManager;
import org.limewire.friend.api.FriendPresence;
import org.limewire.friend.impl.address.FriendAddress;
import org.limewire.io.Address;
import org.limewire.listener.EventListener;
import org.limewire.listener.SwingSafePropertyChangeSupport;
import org.limewire.util.FileUtils;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.limegroup.bittorrent.BTDownloader;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.downloader.DownloadStateEvent;
import com.limegroup.gnutella.malware.VirusDefinitionDownloader;
import com.limegroup.gnutella.malware.VirusDefinitionDownloaderKeys;
import com.limegroup.gnutella.xml.LimeXMLDocument;
class CoreDownloadItem implements DownloadItem, Downloader.ScanListener {
static interface Factory {
CoreDownloadItem create(Downloader downloader, QueueTimeCalculator calculator);
}
private final PropertyChangeSupport support = new SwingSafePropertyChangeSupport(this);
private final Downloader downloader;
private volatile int hashCode = 0;
private volatile long cachedSize;
private volatile boolean cancelled = false;
private volatile boolean scanningFragment = false;
private final QueueTimeCalculator queueTimeCalculator;
private final FriendManager friendManager;
private final DownloadItemType downloadItemType;
private final CategoryManager categoryManager;
@Inject
public CoreDownloadItem(@Assisted Downloader downloader, @Assisted QueueTimeCalculator queueTimeCalculator, FriendManager friendManager, CategoryManager categoryManager) {
this.downloader = downloader;
this.queueTimeCalculator = queueTimeCalculator;
this.friendManager = friendManager;
this.categoryManager = categoryManager;
if(downloader instanceof BTDownloader)
downloadItemType = DownloadItemType.BITTORRENT;
else if(downloader instanceof VirusDefinitionDownloader)
downloadItemType = DownloadItemType.ANTIVIRUS;
else
downloadItemType = DownloadItemType.GNUTELLA;
downloader.addListener(new EventListener<DownloadStateEvent>() {
@Override
public void handleEvent(DownloadStateEvent event) {
// broadcast the status has changed
fireDataChanged();
if (event.getType() == com.limegroup.gnutella.Downloader.DownloadState.ABORTED) {
//attempt to delete ABORTED file
CoreDownloadItem.this.downloader.deleteIncompleteFiles();
}
}
});
}
@Override
public DownloadItemType getDownloadItemType() {
return downloadItemType;
}
void fireDataChanged() {
cachedSize = downloader.getAmountRead();
support.firePropertyChange("state", null, getState());
}
private Downloader getDownloader(){
return downloader;
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener){
support.removePropertyChangeListener(listener);
}
@Override
public void cancel() {
cancelled = true;
support.firePropertyChange("state", null, getState());
new ManagedThread(new Runnable() {
@Override
public void run() {
downloader.stop();
downloader.deleteIncompleteFiles();
}
}, "CoreDownloadItem.cancel").start();
// TODO there is a race condition with the delete action, the stop does
// not happen right away. should revisit how this will be handled.
}
@Override
public Category getCategory() {
if(downloadItemType == DownloadItemType.BITTORRENT) {
return Category.OTHER;
} else {
File file = downloader.getFile();
if(file != null) {
return categoryManager.getCategoryForFile(file);
} else {
// TODO: See if it's OK to always use save file.
return categoryManager.getCategoryForFile(downloader.getSaveFile());
}
}
}
@Override
public long getCurrentSize() {
DownloadState state = getState();
if (state == DownloadState.SCANNING || state.isFinished()) {
return getTotalSize();
} else {
return cachedSize;
}
}
@Override
public long getAmountVerified() {
return downloader.getAmountVerified();
}
@Override
public long getAmountLost() {
return downloader.getAmountLost();
}
@Override
public List<Address> getSources() {
return downloader.getSourcesAsAddresses();
}
@Override
public List<SourceInfo> getSourcesDetails() {
return downloader.getSourcesDetails();
}
@Override
public DownloadPiecesInfo getPiecesInfo() {
return downloader.getPieceInfo();
}
@Override
public Collection<RemoteHost> getRemoteHosts() {
List<RemoteFileDesc> remoteFiles = downloader.getRemoteFileDescs();
if(remoteFiles.size() > 0) {
List<RemoteHost> remoteHosts = new ArrayList<RemoteHost>(remoteFiles.size());
for(RemoteFileDesc rfd : remoteFiles) {
remoteHosts.add(new RemoteHostRFD(rfd, getFriendPresence(rfd)));
}
return remoteHosts;
} else {
return Collections.emptyList();
}
}
/**
* Returns a FriendPresence for this RemoteFileDesc. If this is
* from a friend returns an associated LW FriendPresnce otherwise
* returns a generic GnutellaPresence.
*/
private FriendPresence getFriendPresence(RemoteFileDesc rfd) {
FriendPresence friendPresence = null;
if(rfd.getAddress() instanceof FriendAddress) {
friendPresence = friendManager.getMostRelevantFriendPresence(((FriendAddress)rfd.getAddress()).getId());
}
if(friendPresence == null) {
friendPresence = new GnutellaPresence.GnutellaPresenceWithGuid(rfd.getAddress(), rfd.getClientGUID());
}
return friendPresence;
}
@Override
public int getDownloadSourceCount() {
return downloader.getNumHosts();
}
@Override
public int getPercentComplete() {
DownloadState state = getState();
if(state == DownloadState.FINISHING ||
state == DownloadState.SCANNING ||
state.isFinished()){
return 100;
}
if(getTotalSize() == 0)
return 0;
else
return (int) (100 * getCurrentSize() / getTotalSize());
}
@Override
public long getRemainingDownloadTime() {
double remaining = (getTotalSize() - getCurrentSize()) / 1024.0;
float speed = getDownloadSpeed();
if (speed > 0) {
return (long) (remaining / speed);
} else {
return UNKNOWN_TIME;
}
}
@Override
public float getDownloadSpeed(){
try {
return downloader.getMeasuredBandwidth();
} catch (InsufficientDataException e) {
return 0;
}
}
@Override
public DownloadState getState() {
if(cancelled){
return DownloadState.CANCELLED;
}
if(scanningFragment) {
return DownloadState.SCANNING_FRAGMENT;
}
return convertState(downloader.getState());
}
@Override
public String getTitle() {
// TODO return title, not file name
return downloader.getSaveFile().getName();
}
@Override
public long getTotalSize() {
return downloader.getContentLength();
}
@Override
public void pause() {
downloader.pause();
}
@Override
public void resume() {
downloader.resume();
}
@Override
public int getRemoteQueuePosition(){
if(downloader.getState() == com.limegroup.gnutella.Downloader.DownloadState.REMOTE_QUEUED) {
return downloader.getQueuePosition();
} else {
return -1;
}
}
private DownloadState convertState(com.limegroup.gnutella.Downloader.DownloadState state) {
switch (state) {
case RESUMING:
return DownloadState.RESUMING;
case SAVING:
case HASHING:
if (getTotalSize() > 0) {
return DownloadState.FINISHING;
} else {
return DownloadState.DONE;
}
case DOWNLOADING:
return DownloadState.DOWNLOADING;
case CONNECTING:
case INITIALIZING:
case WAITING_FOR_CONNECTIONS:
return DownloadState.CONNECTING;
case COMPLETE:
return DownloadState.DONE;
case REMOTE_QUEUED:
case BUSY://BUSY should look like locally queued but acts like remotely
return DownloadState.REMOTE_QUEUED;
case QUEUED:
return DownloadState.LOCAL_QUEUED;
case PAUSED:
return DownloadState.PAUSED;
case WAITING_FOR_GNET_RESULTS:
case ITERATIVE_GUESSING:
case QUERYING_DHT:
return DownloadState.TRYING_AGAIN;
case WAITING_FOR_USER:
case GAVE_UP:
return DownloadState.STALLED;
case ABORTED:
return DownloadState.CANCELLED;
case DISK_PROBLEM:
case CORRUPT_FILE:
case INVALID:
case UNABLE_TO_CONNECT:
return DownloadState.ERROR;
case DANGEROUS:
return DownloadState.DANGEROUS;
case SCANNING:
return DownloadState.SCANNING;
case THREAT_FOUND:
return DownloadState.THREAT_FOUND;
case SCAN_FAILED:
return DownloadState.SCAN_FAILED;
case APPLYING_ANTIVIRUS_DEFINITION_UPDATE:
return DownloadState.APPLYING_DEFINITION_UPDATE;
default:
throw new IllegalStateException("Unknown State: " + state);
}
}
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof CoreDownloadItem)) {
return false;
}
return getDownloader().equals(((CoreDownloadItem) o).getDownloader());
}
//TODO: better hashCode
@Override
public int hashCode(){
if(hashCode == 0){
hashCode = 37* getDownloader().hashCode();
}
return hashCode;
}
@Override
public ErrorState getErrorState() {
switch (downloader.getState()) {
case CORRUPT_FILE:
return ErrorState.CORRUPT_FILE;
case DISK_PROBLEM:
return ErrorState.DISK_PROBLEM;
case INVALID:
return ErrorState.INVALID;
case UNABLE_TO_CONNECT:
return ErrorState.UNABLE_TO_CONNECT;
default:
return ErrorState.NONE;
}
}
@Override
public boolean isTryAgainEnabled() {
return DownloadItemType.BITTORRENT == downloadItemType || downloader.getState() == com.limegroup.gnutella.Downloader.DownloadState.WAITING_FOR_USER;
}
@Override
public long getRemainingTimeInState() {
long remaining = downloader.getRemainingStateTime();
// Change a few state times explicitly.
switch(downloader.getState()) {
case QUEUED:
remaining = queueTimeCalculator.getRemainingQueueTime(this);
break;
case QUERYING_DHT:
remaining = UNKNOWN_TIME;
break;
}
if(remaining == Integer.MAX_VALUE) {
remaining = UNKNOWN_TIME;
}
return remaining;
}
@Override
public int getLocalQueuePriority() {
return downloader.getInactivePriority();
}
@Override
public boolean isLaunchable() {
return downloader.isLaunchable();
}
@Override
public File getDownloadingFile() {
return downloader.getFile();
}
@Override
public File getLaunchableFile() {
return downloader.getDownloadFragment(this);
}
@Override
public void scanStarted() {
scanningFragment = true;
}
@Override
public void scanStopped() {
scanningFragment = false;
}
@Override
public URN getUrn() {
com.limegroup.gnutella.URN urn = downloader.getSha1Urn();
return urn;
}
@Override
public String getFileName() {
return downloader.getSaveFile().getName();
}
@Override
public void setSaveFile(File saveFile, boolean overwrite) throws DownloadException {
File saveDir = null;
String fileName = null;
// Determine save directory and file name.
if (saveFile != null) {
if (saveFile.isDirectory()) {
saveDir = saveFile;
fileName = getFileName();
} else {
saveDir = saveFile.getParentFile();
fileName = saveFile.getName();
}
}
// Update save directory and file name.
downloader.setSaveFile(saveDir, fileName, overwrite);
}
@Override
public File getSaveFile(){
return downloader.getSaveFile();
}
@Override
public Object getProperty(FilePropertyKey property) {
switch(property) {
case NAME:
return FileUtils.getFilenameNoExtension(getFileName());
case DATE_CREATED:
File file = downloader.getFile();
long ct = -1;
if(file != null) {
ct = file.lastModified();
}
return ct == -1 ? null : ct;
case FILE_SIZE:
return getTotalSize();
case TORRENT:
if(downloadItemType == DownloadItemType.BITTORRENT) {
BTDownloader btDownloader = (BTDownloader)downloader;
return btDownloader.getTorrent();
} else {
return null;
}
default:
LimeXMLDocument doc = (LimeXMLDocument)downloader.getAttribute("LimeXMLDocument");
if(doc != null) {
Category category = categoryManager.getCategoryForFile(getSaveFile());
return FilePropertyKeyPopulator.get(category, property, doc);
} else {
return null;
}
}
}
@Override
public String getPropertyString(FilePropertyKey key) {
Object value = getProperty(key);
if (value != null) {
String stringValue = value.toString();
return stringValue;
} else {
return null;
}
}
@Override
public Date getStartDate() {
return (Date)downloader.getAttribute(DownloadItem.DOWNLOAD_START_DATE);
}
@Override
public boolean isRelocatable() {
return downloader.isRelocatable();
}
@Override
public Collection<File> getCompleteFiles() {
List<File> files = new ArrayList<File>();
if(downloader instanceof BTDownloader) {
BTDownloader btDownloader = (BTDownloader)downloader;
files.addAll(btDownloader.getCompleteFiles());
} else {
files.add(downloader.getSaveFile());
}
return files;
}
@Override
public Object getDownloadProperty(DownloadPropertyKey key) {
switch(key) {
case ANTIVIRUS_FAIL_HINT:
return downloader.getAttribute(VirusEngine.DOWNLOAD_FAILURE_HINT);
case ANTIVIRUS_UPDATE_TYPE:
return downloader.getAttribute(VirusDefinitionDownloaderKeys.TYPE);
case ANTIVIRUS_INCREMENT_COUNT:
Object count = downloader.getAttribute(VirusDefinitionDownloaderKeys.COUNT);
return count == null ? 0 : count;
case ANTIVIRUS_INCREMENT_INDEX:
Object index = downloader.getAttribute(VirusDefinitionDownloaderKeys.INDEX);
return index == null ? 0 : index;
}
return null;
}
}