/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.pieShare.pieShareApp.service.shareService;
import org.pieShare.pieShareApp.model.pieFile.FileMeta;
import com.turn.ttorrent.common.Torrent;
import com.turn.ttorrent.tracker.TrackedTorrent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import org.pieShare.pieShareApp.model.PieShareAppBeanNames;
import org.pieShare.pieShareApp.model.pieFile.PieFile;
import org.pieShare.pieShareApp.service.networkService.INetworkService;
import org.pieShare.pieShareApp.task.localTasks.TorrentTask;
import org.pieShare.pieTools.pieUtilities.service.base64Service.api.IBase64Service;
import org.pieShare.pieTools.pieUtilities.service.beanService.IBeanService;
import org.pieShare.pieTools.pieUtilities.service.pieExecutorService.api.IExecutorService;
import org.pieShare.pieTools.pieUtilities.service.pieLogger.PieLogger;
/**
*
* @author Svetoslav
*/
public class BitTorrentService implements IBitTorrentService {
private INetworkService networkService;
private IBeanService beanService;
private IExecutorService executorService;
private IBase64Service base64Service;
private Semaphore readPorts;
private Semaphore writePorts;
//for the time being it is neccessary to work with the FileMeta and not the PieFile due to the fact
//that there can be multiple unrelated trackers present
private ConcurrentHashMap<FileMeta, Integer> sharedFiles;
private ConcurrentHashMap<File, byte[]> cachedMetaInformation;
public BitTorrentService() {
this.sharedFiles = new ConcurrentHashMap<>();
this.cachedMetaInformation = new ConcurrentHashMap<>();
}
public void setNetworkService(INetworkService networkService) {
this.networkService = networkService;
}
public void setBeanService(IBeanService beanService) {
this.beanService = beanService;
}
public void setExecutorService(IExecutorService executorService) {
this.executorService = executorService;
}
public void setBase64Service(IBase64Service base64Service) {
this.base64Service = base64Service;
}
/**
* If this FileMeta allready exists the value will be changed by the given
* value. If not it will be created with the given value. It returns true if
* the FileMeta was new and false otherwise.
*
* @param file
* @param value
* @return
*/
private boolean manipulateShareState(FileMeta file, Integer value) {
synchronized (this.sharedFiles) {
boolean isNew = true;
PieLogger.trace(this.getClass(), "Manipulating share state for {} with HashCode {}.", file.getFile().getFileName(), file.hashCode());
if (this.sharedFiles.containsKey(file)) {
PieLogger.trace(this.getClass(), "Share state for {} exists.", file.getFile().getFileName());
value = this.sharedFiles.get(file) + value;
if (value <= 0) {
this.sharedFiles.remove(file);
return false;
}
isNew = false;
}
if (value >= 0) {
this.sharedFiles.put(file, value);
}
return isNew;
}
}
@Override
public void initTorrentService() {
//this section inits the semaphores
int availablePorts = this.networkService.getNumberOfAvailablePorts(6881, 6889);
if (availablePorts == 0) {
//todo: handle this
PieLogger.error(this.getClass(), "NO PORTS AVAILABLE ON THIS MACHINE!!!");
}
this.writePorts = new Semaphore(availablePorts);
this.readPorts = new Semaphore((availablePorts / 2) - 1);
}
@Override
public byte[] createMetaInformation(File localFile) throws CouldNotCreateMetaDataException {
try {
//todo: in future we need to synchronize on the file context so requests for different files can work in parallel
synchronized (this.cachedMetaInformation) {
if (this.cachedMetaInformation.containsKey(localFile)) {
return this.cachedMetaInformation.get(localFile);
}
int port = -1;
synchronized (this) {
port = this.networkService.reserveAvailablePortStartingFrom(6969);
}
URI trackerUri = new URI("http://" + networkService.getLocalHost().getHostAddress() + ":" + String.valueOf(port) + "/announce");
//todo: error handling when torrent null
//todo: replace name by nodeName
Torrent torrent = Torrent.create(localFile, trackerUri, "replaceThisByNodeName");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
torrent.save(baos);
byte[] meta = this.base64Service.encode(baos.toByteArray());
this.cachedMetaInformation.put(localFile, meta);
return meta;
}
} catch (InterruptedException | IOException | URISyntaxException ex) {
throw new CouldNotCreateMetaDataException(ex);
}
}
@Override
public void shareFile(FileMeta meta) {
//this is important in case we are already sharing that file!
if (!this.manipulateShareState(meta, 1)) {
PieLogger.trace(this.getClass(), "Allready sharing file {}!", meta.getFile().getFileName());
return;
}
try {
this.writePorts.acquire();
this.handleSharedTorrent(meta, true);
} catch (InterruptedException ex) {
PieLogger.error(this.getClass(), "Acquire write failed!", ex);
} catch (IOException ex) {
PieLogger.error(this.getClass(), "Init torrent failed!", ex);
this.torrentClientDone(true, meta);
}
}
@Override
public void clientDone(FileMeta meta) {
this.manipulateShareState(meta, -1);
}
@Override
public void handleFile(FileMeta meta) {
//this is important in case we are already sharing that file!
if (!this.manipulateShareState(meta, 1)) {
PieLogger.trace(this.getClass(), "Allready handling file {}!", meta.getFile().getFileName());
return;
}
try {
this.readPorts.acquire();
this.writePorts.acquire();
this.handleSharedTorrent(meta, false);
} catch (InterruptedException ex) {
PieLogger.error(this.getClass(), "Acquire read failed!", ex);
} catch (IOException ex) {
PieLogger.error(this.getClass(), "Init torrent failed!", ex);
this.torrentClientDone(false, meta);
}
}
private void handleSharedTorrent(FileMeta meta, boolean seeder) throws IOException {
Torrent torrent = new Torrent(base64Service.decode(meta.getData()), seeder);
TorrentTask task = this.beanService.getBean(PieShareAppBeanNames.getTorrentTask());
task.setFile(meta);
task.setTorrent(torrent);
this.executorService.execute(task);
}
@Override
public void torrentClientDone(boolean seeder, FileMeta file) {
this.writePorts.release();
if (!seeder) {
this.readPorts.release();
}
synchronized (this) {
this.sharedFiles.remove(file);
}
}
@Override
public boolean isShareActive(FileMeta file) {
return (this.sharedFiles.getOrDefault(file, 0) > 0);
}
public boolean activeTorrents() {
return !this.sharedFiles.isEmpty();
}
}