package org.limewire.libtorrent;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.limewire.bittorrent.LimeWireTorrentProperties;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentAlert;
import org.limewire.bittorrent.TorrentEvent;
import org.limewire.bittorrent.TorrentEventType;
import org.limewire.bittorrent.TorrentFileEntry;
import org.limewire.bittorrent.TorrentInfo;
import org.limewire.bittorrent.TorrentParams;
import org.limewire.bittorrent.TorrentPeer;
import org.limewire.bittorrent.TorrentPiecesInfo;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.bittorrent.TorrentTracker;
import org.limewire.listener.AsynchronousEventMulticaster;
import org.limewire.listener.AsynchronousMulticasterImpl;
import org.limewire.listener.EventListener;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
/**
* Class representing the torrent being downloaded. It is updated periodically
* by the TorrentManager. It has all necessary helper methods to provide
* functionality to the BTDownloaderImpl. It delegates calls to native methods
* back to the TorrentManager.
*/
class TorrentImpl implements Torrent {
private static final Log LOG = LogFactory.getLog(TorrentImpl.class);
private final Map<String, Object> properties = Collections
.synchronizedMap(new HashMap<String, Object>(2));
private final AsynchronousEventMulticaster<TorrentEvent> listeners;
private final LibTorrentWrapper libTorrent;
private final AtomicReference<TorrentStatus> status = new AtomicReference<TorrentStatus>(null);
private final AtomicReference<TorrentInfo> torrentInfo = new AtomicReference<TorrentInfo>(null);
private final AtomicReference<File> torrentDataFile = new AtomicReference<File>(null);
private final AtomicReference<File> torrentFile = new AtomicReference<File>(null);
private final AtomicReference<File> fastResumeFile = new AtomicReference<File>(null);
private final AtomicLong startTime = new AtomicLong(-1);
private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicBoolean cancelled = new AtomicBoolean(false);
// used to decide if the torrent was just newly completed or not.
private final AtomicBoolean complete = new AtomicBoolean(false);
private final Lock lock = new ReentrantLock();
private final String sha1;
private final String nameAtCreation;
private final AtomicBoolean isPrivate = new AtomicBoolean(true);
public TorrentImpl(TorrentParams params, LibTorrentWrapper libTorrent, ScheduledExecutorService fastExecutor) {
this.libTorrent = libTorrent;
listeners = new AsynchronousMulticasterImpl<TorrentEvent>(fastExecutor);
this.sha1 = params.getSha1();
this.nameAtCreation = params.getName();
Boolean isPrivate = params.getPrivate();
this.torrentFile.set(params.getTorrentFile());
this.fastResumeFile.set(params.getFastResumeFile());
this.torrentDataFile.set(params.getTorrentDataFile());
if (isPrivate != null) {
this.isPrivate.set(isPrivate);
}
if(sha1 == null) {
throw new NullPointerException("Sha1 torrent parameter cannot be null.");
}
}
@Override
public void addListener(EventListener<TorrentEvent> listener) {
listeners.addListener(listener);
}
@Override
public boolean removeListener(EventListener<TorrentEvent> listener) {
return listeners.removeListener(listener);
}
@Override
public String getName() {
TorrentInfo torrentInfo = getTorrentInfo();
if (torrentInfo != null && torrentInfo.getName() != null) {
return torrentInfo.getName();
}
return nameAtCreation;
}
@Override
public void start() {
if (!started.getAndSet(true)) {
lock.lock();
try {
startTime.set(System.currentTimeMillis());
resume();
listeners.broadcast(new TorrentEvent(this, TorrentEventType.STARTED));
} finally {
lock.unlock();
}
}
}
@Override
public File getTorrentFile() {
return torrentFile.get();
}
@Override
public File getFastResumeFile() {
return fastResumeFile.get();
}
@Override
public void moveTorrent(File directory) {
lock.lock();
try {
assert isFinished();
libTorrent.move_torrent(sha1, directory.getAbsolutePath());
torrentDataFile.set(new File(directory, torrentDataFile.get().getName()));
} finally {
lock.unlock();
}
}
@Override
public void pause() {
lock.lock();
try {
libTorrent.pause_torrent(sha1);
updateStatus(getStatusInner());
} finally {
lock.unlock();
}
}
@Override
public void setTorrentFile(File torrentFile) {
this.torrentFile.set(torrentFile);
}
@Override
public void setFastResumeFile(File fastResumeFile) {
this.fastResumeFile.set(fastResumeFile);
}
@Override
public void resume() {
lock.lock();
try {
if (getStatus().isError()) {
libTorrent.clear_error_and_retry(sha1);
} else {
libTorrent.resume_torrent(sha1);
}
updateStatus(getStatusInner());
} finally {
lock.unlock();
}
}
private LibTorrentStatus getStatusInner() {
LibTorrentStatus status = new LibTorrentStatus();
libTorrent.get_torrent_status(sha1, status);
libTorrent.free_torrent_status(status);
return status;
}
@Override
public float getDownloadRate() {
TorrentStatus status = this.status.get();
return status == null ? 0 : status.getDownloadPayloadRate();
}
@Override
public String getSha1() {
return sha1;
}
@Override
public boolean isPaused() {
TorrentStatus status = this.status.get();
return status == null ? false : status.isPaused();
}
@Override
public boolean isFinished() {
TorrentStatus status = this.status.get();
return status == null ? false : status.isFinished();
}
@Override
public boolean isStarted() {
return started.get();
}
@Override
public List<URI> getTrackerURIS() {
List<TorrentTracker> trackers = getTrackers();
List<URI> trackerURIS = new ArrayList<URI>(trackers.size());
for ( TorrentTracker tracker : trackers ) {
trackerURIS.add(tracker.getURI());
}
return trackerURIS;
}
@Override
public int getNumPeers() {
TorrentStatus status = this.status.get();
return status == null ? 0 : status.getNumPeers();
}
@Override
public File getTorrentDataFile() {
return torrentDataFile.get();
}
@Override
public void stop() {
lock.lock();
try {
if (!cancelled.getAndSet(true)) {
// updating the torrent info object 1 last time.
if (isValid() && hasMetaData()) {
TorrentInfo ti = libTorrent.get_torrent_info(sha1);
torrentInfo.set(ti);
}
}
listeners.broadcast(new TorrentEvent(this, TorrentEventType.STOPPED));
} finally {
lock.unlock();
}
}
@Override
public long getTotalUploaded() {
TorrentStatus status = this.status.get();
if (status == null) {
return 0;
} else {
return status.getAllTimePayloadUpload();
}
}
@Override
public int getNumUploads() {
TorrentStatus status = this.status.get();
if (status == null) {
return 0;
} else {
return status.getNumUploads();
}
}
@Override
public float getUploadRate() {
TorrentStatus status = this.status.get();
return status == null ? 0 : status.getUploadPayloadRate();
}
@Override
public float getSeedRatio() {
TorrentStatus status = this.status.get();
if (status != null) {
float seedRatio = status.getSeedRatio();
return seedRatio;
}
return 0;
}
@Override
public boolean isCancelled() {
return cancelled.get();
}
@Override
public TorrentStatus getStatus() {
return status.get();
}
@Override
public void updateStatus(TorrentStatus torrentStatus) {
lock.lock();
if(LOG.isDebugEnabled()) {
LOG.debugf("Updating torrent status: {0} \n {1}", sha1, torrentStatus);
}
try {
if (!cancelled.get()) {
TorrentImpl.this.status.set(torrentStatus);
boolean newlyfinished = !complete.get() && torrentStatus.isFinished();
complete.set(torrentStatus.isFinished());
if (newlyfinished) {
listeners.broadcast(new TorrentEvent(this, TorrentEventType.COMPLETED));
} else {
listeners.broadcast(new TorrentEvent(this, TorrentEventType.STATUS_CHANGED));
}
}
} finally {
lock.unlock();
}
}
@Override
public void handleFastResumeAlert(TorrentAlert alert) {
lock.lock();
try {
listeners.broadcast(new TorrentEvent(this, TorrentEventType.FAST_RESUME_FILE_SAVED));
} finally {
lock.unlock();
}
}
@Override
public int getNumConnections() {
TorrentStatus status = getStatus();
if (status != null) {
return status.getNumConnections();
}
return 0;
}
@Override
public boolean isPrivate() {
return isPrivate.get();
}
@Override
public List<TorrentFileEntry> getTorrentFileEntries() {
lock.lock();
try {
if (!isValid()) {
TorrentInfo torrentInfo = this.torrentInfo.get();
if (torrentInfo == null) {
return Collections.emptyList();
}
return torrentInfo.getTorrentFileEntries();
}
TorrentFileEntry[] files = libTorrent.get_files(sha1);
return Arrays.asList(files);
} finally {
lock.unlock();
}
}
@Override
public List<TorrentPeer> getTorrentPeers() {
lock.lock();
try {
TorrentPeer[] peers = libTorrent.get_peers(sha1);
return Arrays.asList(peers);
} finally {
lock.unlock();
}
}
@Override
public boolean isAutoManaged() {
TorrentStatus status = this.status.get();
return status == null ? false : status.isAutoManaged();
}
@Override
public void setAutoManaged(boolean autoManaged) {
lock.lock();
try {
libTorrent.set_auto_managed_torrent(sha1, autoManaged);
} finally {
lock.unlock();
}
}
@Override
public void setTorrenFileEntryPriority(TorrentFileEntry torrentFileEntry, int priority) {
lock.lock();
try {
libTorrent.set_file_priority(sha1, torrentFileEntry.getIndex(), priority);
} finally {
lock.unlock();
}
}
@Override
public File getTorrentDataFile(TorrentFileEntry torrentFileEntry) {
return new File(getTorrentDataFile().getParent(), torrentFileEntry.getPath());
}
@Override
public boolean hasMetaData() {
lock.lock();
try {
return torrentInfo.get() != null || libTorrent.has_metadata(sha1);
} finally {
lock.unlock();
}
}
@Override
public TorrentInfo getTorrentInfo() {
lock.lock();
try {
if (isValid() && hasMetaData() && torrentInfo.get() == null) {
TorrentInfo ti = libTorrent.get_torrent_info(sha1);
torrentInfo.set(ti);
listeners.broadcast(new TorrentEvent(this, TorrentEventType.META_DATA_RECEIVED));
}
return torrentInfo.get();
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
@Override
public <T> T getProperty(String key, T defaultValue) {
T value = (T)properties.get(key);
if(value == null) {
value = defaultValue;
}
return value;
}
@Override
public void setProperty(String key, Object value) {
properties.put(key, value);
if (key.equals(LimeWireTorrentProperties.MAX_SEED_RATIO_LIMIT)
&& value instanceof Float) {
setSeedRatioLimit((Float)value);
}
}
@Override
public boolean isValid() {
lock.lock();
try {
return libTorrent.is_valid(sha1);
} finally {
lock.unlock();
}
}
@Override
public long getStartTime() {
return startTime.get();
}
@Override
public Lock getLock() {
return lock;
}
@Override
public void forceReannounce() {
lock.lock();
try {
libTorrent.force_reannounce(sha1);
} finally {
lock.unlock();
}
}
@Override
public void scrapeTracker() {
lock.lock();
try {
libTorrent.scrape_tracker(sha1);
} finally {
lock.unlock();
}
}
@Override
public void saveFastResumeData() {
lock.lock();
try {
libTorrent.signal_fast_resume_data_request(sha1);
} finally {
lock.unlock();
}
}
@Override
public TorrentPiecesInfo getPiecesInfo() {
lock.lock();
try {
return libTorrent.get_pieces_status(sha1);
} finally {
lock.unlock();
}
}
@Override
public List<TorrentTracker> getTrackers() {
lock.lock();
try {
TorrentTracker[] peers = libTorrent.get_trackers(sha1);
return Arrays.asList(peers);
} finally {
lock.unlock();
}
}
@Override
public void addTracker(String tracker, int tier) {
lock.lock();
try {
libTorrent.add_tracker(sha1, tracker, tier);
} finally {
lock.unlock();
}
}
@Override
public void removeTracker(String tracker, int tier) {
lock.lock();
try {
libTorrent.remove_tracker(sha1, tracker, tier);
} finally {
lock.unlock();
}
}
@Override
public boolean isEditable() {
return true;
}
@Override
public void setMaxDownloadBandwidth(int value) {
lock.lock();
try {
libTorrent.set_download_limit(sha1, value);
} finally {
lock.unlock();
}
}
@Override
public int getMaxDownloadBandwidth() {
lock.lock();
try {
return libTorrent.get_download_limit(sha1);
} finally {
lock.unlock();
}
}
@Override
public int getMaxUploadBandwidth() {
lock.lock();
try {
return libTorrent.get_upload_limit(sha1);
} finally {
lock.unlock();
}
}
@Override
public void setMaxUploadBandwidth(int value) {
lock.lock();
try {
libTorrent.set_upload_limit(sha1, value);
} finally {
lock.unlock();
}
}
private void setSeedRatioLimit(float value) {
lock.lock();
try {
libTorrent.set_seed_ratio(sha1, value);
} finally {
lock.unlock();
}
}
@Override
public long getTotalPayloadSize() {
long sum = 0;
for (TorrentFileEntry file : getTorrentFileEntries()) {
sum += file.getSize();
}
return sum;
}
}