/*
* Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation.
*
* PowerFolder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.transfer;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.disk.Folder;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.message.AbortDownload;
import de.dal33t.powerfolder.message.FileChunk;
import de.dal33t.powerfolder.message.RequestDownload;
import de.dal33t.powerfolder.message.RequestDownloadExt;
import de.dal33t.powerfolder.message.RequestFilePartsRecord;
import de.dal33t.powerfolder.message.RequestPart;
import de.dal33t.powerfolder.message.RequestPartExt;
import de.dal33t.powerfolder.message.StopUpload;
import de.dal33t.powerfolder.message.StopUploadExt;
import de.dal33t.powerfolder.util.Range;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.Util;
import de.dal33t.powerfolder.util.Validate;
import de.dal33t.powerfolder.util.delta.FilePartsRecord;
/**
* Download class, containing file and member.<BR>
* Serializable for remembering completed Downloads in DownLoadTableModel.
* <P>
* Attention: All synchronized method are only allowed to be called by
* DownloadManager
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.30 $
*/
public class Download extends Transfer {
private static final long serialVersionUID = 100L;
private Date lastTouch;
private final boolean automatic;
private boolean queued;
private boolean markedBroken;
private Queue<RequestPart> pendingRequests = new ConcurrentLinkedQueue<RequestPart>();
private transient DownloadManager dlManager;
/** for serialisation */
public Download() {
automatic = false;
}
/**
* Constuctor for download, package protected, can only be created by
* transfer manager
* <p>
* Downloads start in pending state. Move to requested by calling
* <code>request(Member)</code>
*/
Download(TransferManager tm, FileInfo file, boolean automatic) {
super(tm, file, null);
// from can be null
this.lastTouch = new Date();
this.automatic = automatic;
this.queued = false;
this.markedBroken = false;
}
/**
* Re-initialized the Transfer with the TransferManager. Use this only if
* you are know what you are doing .
*
* @param aTransferManager
* the transfermanager
*/
void init(TransferManager aTransferManager) {
super.init(aTransferManager);
queued = false;
markedBroken = false;
}
/**
* @return if this download was automatically requested
*/
public boolean isRequestedAutomatic() {
return automatic;
}
public synchronized void setDownloadManager(DownloadManager handler) {
Reject.ifNull(handler, "Handler is null!");
if (!handler.getFileInfo().isVersionDateAndSizeIdentical(getFile())) {
throw new IllegalArgumentException("Fileinfos mismatch. expected "
+ getFile().toDetailString() + ", got "
+ handler.getFileInfo().toDetailString());
}
if (this.dlManager != null) {
throw new IllegalStateException("DownloadManager already set!");
}
this.dlManager = handler;
}
public DownloadManager getDownloadManager() {
return dlManager;
}
/**
* Called when the partner supports part-transfers and is ready to upload
*
* @param fileInfo
* the fileInfo the remote side uses.
*/
public void uploadStarted(FileInfo fileInfo) {
checkFileInfo(fileInfo);
lastTouch.setTime(System.currentTimeMillis());
// Maybe remove this check?
if (isStarted()) {
if (isWarning()) {
logWarning("Aborting. Received multiple upload start messages: "
+ this);
}
abort();
return;
}
if (isFiner()) {
logFiner("Uploader supports partial transfers for "
+ fileInfo.toDetailString());
}
setStarted();
dlManager.readyForRequests(Download.this);
}
/**
* Requests a FPR from the remote side.
*/
void requestFilePartsRecord() {
assert Util.useDeltaSync(getController(), this) : "Requesting FilePartsRecord from a client that doesn't support that!";
requestCheckState();
getPartner().sendMessagesAsynchron(
new RequestFilePartsRecord(getFile()));
}
/**
* Invoked when a record for this download was received.
*
* @param fileInfo
* the fileInfo the remote side uses.
* @param record
* the record received.
*/
public void receivedFilePartsRecord(FileInfo fileInfo,
final FilePartsRecord record)
{
Reject.ifNull(record, "Record is null");
checkFileInfo(fileInfo);
lastTouch.setTime(System.currentTimeMillis());
if (isFine()) {
logFine("Received parts record for " + fileInfo.toDetailString()
+ ": " + record);
}
dlManager.filePartsRecordReceived(Download.this, record);
}
/**
* Requests a single part from the remote peer.
*
* @param range
* @return
* @throws BrokenDownloadException
*/
boolean requestPart(Range range) throws BrokenDownloadException {
Validate.notNull(range);
requestCheckState();
RequestPart rp;
if (pendingRequests.size() >= getTransferManager()
.getMaxRequestsQueued())
{
if (isFiner()) {
logFiner("X Skipping request. Already got too many pending requests: " + range);
}
return false;
}
if (isFiner()) {
logFiner("X requestPart: " + range);
}
try {
if (getPartner().getProtocolVersion() >= 100) {
rp = new RequestPartExt(getFile(), range, Math.max(0,
state.getProgress()));
} else {
rp = new RequestPart(getFile(), range, Math.max(0,
state.getProgress()));
}
} catch (IllegalArgumentException e) {
// I need to do this because FileInfos are NOT immutable...
if (isWarning()) {
logWarning("Concurrent file change while requesting:" + e);
}
throw new BrokenDownloadException(
"Concurrent file change while requesting: " + e);
}
pendingRequests.add(rp);
getPartner().sendMessagesAsynchron(rp);
return true;
}
public Collection<RequestPart> getPendingRequests() {
return Collections.unmodifiableCollection(pendingRequests);
}
/**
* Adds a chunk to the download
*
* @param chunk
* @return true if the chunk was successfully appended to the download file.
*/
public boolean addChunk(final FileChunk chunk) {
Reject.ifNull(chunk, "Chunk is null");
checkFileInfo(chunk.file);
getTransferManager().chunkAdded(Download.this, chunk);
if (isBroken()) {
setBroken(TransferProblem.BROKEN_DOWNLOAD, "isBroken()");
return false;
}
// donwload begins to start
if (!isStarted()) {
if (Util.useSwarming(getController(), getPartner())) {
// Old passive downloads=Started with the first file chunk
setStarted();
} else {
if (isWarning()) {
logWarning("Ignoring file chunk for non-started " + this);
}
return false;
}
}
lastTouch.setTime(System.currentTimeMillis());
// Remove pending requests for the received chunk since
// the manager below might want to request new parts.
Range range = Range.getRangeByLength(chunk.offset, chunk.data.length);
// Maybe the sender merged requests from us, so check all
// requests
for (Iterator<RequestPart> ip = pendingRequests.iterator(); ip
.hasNext();)
{
RequestPart p = ip.next();
if (p.getRange().contains(range)) {
ip.remove();
}
}
getCounter().chunkTransferred(chunk);
dlManager.chunkReceived(Download.this, chunk);
return true;
}
/**
* Requests this download from the partner.
*
* @param startOffset
*/
void request(long startOffset) {
Reject.ifTrue(startOffset < 0 || startOffset >= getFile().getSize(),
"Invalid startOffset: " + startOffset);
requestCheckState();
if (isFiner()) {
logFiner("request(" + startOffset + "): "
+ getFile().toDetailString());
}
if (getPartner().getProtocolVersion() >= 103) {
getPartner().sendMessagesAsynchron(
new RequestDownloadExt(getFile(), startOffset));
} else {
getPartner().sendMessagesAsynchron(
new RequestDownload(getFile(), startOffset));
}
}
/**
* Requests to abort this dl
*/
void abort() {
abort(true);
}
/**
* @param informRemote
* if a <code>AbortDownload</code> message should be sent to the
* partner
*/
void abort(boolean informRemote) {
shutdown();
if (getPartner() != null && getPartner().isCompletelyConnected()
&& informRemote)
{
getPartner().sendMessageAsynchron(new AbortDownload(getFile()));
}
if (getDownloadManager() == null) {
logSevere(this + " has no DownloadManager! (abort before start?)");
// For Pending downloads without download manager
getController().getTransferManager().downloadAborted(Download.this);
return;
}
getController().getTransferManager().downloadAborted(Download.this);
}
/**
* This download is queued at the remote side
*
* @param fInfo
* the fileinfo
*/
public void setQueued(FileInfo fInfo) {
Reject.ifNull(fInfo, "fInfo is null!");
checkFileInfo(fInfo);
if (isFiner()) {
logFiner("DL queued by remote side: " + this);
}
queued = true;
getTransferManager().downloadQueued(Download.this, getPartner());
}
@Override
void setCompleted() {
super.setCompleted();
if (getPartner().getProtocolVersion() >= 101) {
getPartner().sendMessagesAsynchron(new StopUploadExt(getFile()));
} else {
getPartner().sendMessagesAsynchron(new StopUpload(getFile()));
}
getTransferManager().setCompleted(Download.this);
}
/**
* @return if this is a pending download
*/
public boolean isPending() {
return !isCompleted() && getPartner() == null;
}
/**
* Sets the download to a broken state.
*
* @param problem
* @param message
*/
void setBroken(final TransferProblem problem, final String message) {
synchronized (this) {
// Prevent setBroken from being called more than once on a
// single download
if (markedBroken) {
if (isFiner()) {
logFiner("Not breaking already marked broken download");
}
return;
}
markedBroken = true;
}
Member p = getPartner();
if (p != null && p.isCompletelyConnected()) {
p.sendMessageAsynchron(new AbortDownload(getFile()));
}
shutdown();
getTransferManager().downloadBroken(Download.this, problem, message);
}
private long lastBrokenCheck;
private boolean brokenCache;
/**
* @return if this download is broken. timed out or has no connection
* anymore or (on blacklist in folder and isRequestedAutomatic)
*/
public boolean isBroken() {
if (markedBroken) {
return true;
}
if (super.isBroken()) {
return true;
}
if (System.currentTimeMillis() - lastBrokenCheck < 1000) {
// Check every second only
return brokenCache;
}
lastBrokenCheck = System.currentTimeMillis();
// timeout is, when dl is not enqued at remote side,
// and has timeout. Don't check timeout during filehasing of UPLOADER
// and DOWNLOADER (#1829)
if (stateCanTimeout()) {
boolean timedOut = System.currentTimeMillis()
- Constants.DOWNLOAD_REQUEST_TIMEOUT_LIMIT > lastTouch
.getTime() && !queued;
if (timedOut) {
if (isFine()) {
logFine("Break cause: Timeout. "
+ getFile().toDetailString());
}
brokenCache = true;
return true;
}
}
// Check queueing at remote side
boolean isQueuedAtPartner = stillQueuedAtPartner();
if (!isQueuedAtPartner) {
if (isFine()) {
logFine("Break cause: not queued.");
}
brokenCache = true;
return true;
}
// Not done here, may cause too much CPU.
// #2532: Done in TransferManager.checkActiveTranfersForExcludes();
// Folder folder = getFile().getFolder(
// getController().getFolderRepository());
// boolean onBlacklist =
// folder.getDiskItemFilter().isExcluded(getFile());
// if (onBlacklist) {
// if (isFine()) {
// logFine("Break cause: Excluded from sync.");
// }
// return true;
// }
/*
* Wrong place to check, since we could actually want to load an old
* version!
*/
// True, but re-added check because of #1326
// Check if newer file is available. boolean
boolean newerFileAvailable = getFile().isNewerAvailable(
getController().getFolderRepository());
if (newerFileAvailable) {
if (isFine()) {
logFine("Break cause: Newer version available. "
+ getFile().toDetailString());
}
brokenCache = true;
return true;
}
brokenCache = false;
return false;
}
private boolean stateCanTimeout() {
TransferState state = getTransferState();
return state != TransferState.FILERECORD_REQUEST
&& state != TransferState.VERIFYING
&& state != TransferState.MATCHING;
}
/**
* @return if this download is queued
*/
public boolean isQueued() {
return queued && !isBroken();
}
/*
* General
*/
public int hashCode() {
int hash = 37;
if (getFile() != null) {
hash += getFile().hashCode();
}
return hash;
}
public boolean equals(Object o) {
if (o instanceof Download) {
Download other = (Download) o;
return Util.equals(this.getFile(), other.getFile());
}
return false;
}
// General ****************************************************************
@Override
public String toString() {
String msg = "Download: " + getFile().toDetailString();
if (getPartner() != null) {
msg += " from '" + getPartner().getNick() + "'";
if (getPartner().isOnLAN()) {
msg += " (local-net)";
}
} else {
msg += " (pending)";
}
return msg;
}
// Checks ****************************************************************
private void requestCheckState() {
if (dlManager == null) {
throw new IllegalStateException("DownloadManager not set!");
}
}
private void checkFileInfo(FileInfo fileInfo) {
Reject.ifFalse(fileInfo.isVersionDateAndSizeIdentical(getFile()),
"FileInfo mismatch!");
}
}