com.turn.ttorrent.client.Client.java Source code

Java tutorial

Introduction

Here is the source code for com.turn.ttorrent.client.Client.java

Source

/**
 * Copyright (C) 2011-2012 Turn, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.turn.ttorrent.client;

import com.turn.ttorrent.client.announce.Announce;
import com.turn.ttorrent.client.announce.AnnounceException;
import com.turn.ttorrent.client.announce.AnnounceResponseListener;
import com.turn.ttorrent.client.peer.PeerActivityListener;
import com.turn.ttorrent.client.peer.SharingPeer;
import com.turn.ttorrent.common.Peer;
import com.turn.ttorrent.common.Torrent;
import com.turn.ttorrent.common.protocol.PeerMessage;
import com.turn.ttorrent.common.protocol.TrackerMessage;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.BitSet;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A pure-java BitTorrent client.
 *
 * <p>
 * A BitTorrent client in its bare essence shares a given torrent. If the
 * torrent is not complete locally, it will continue to download it. If or
 * after the torrent is complete, the client may eventually continue to seed it
 * for other clients.
 * </p>
 *
 * <p>
 * This BitTorrent client implementation is made to be simple to embed and
 * simple to use. First, initialize a ShareTorrent object from a torrent
 * meta-info source (either a file or a byte array, see
 * com.turn.ttorrent.SharedTorrent for how to create a SharedTorrent object).
 * Then, instantiate your Client object with this SharedTorrent and call one of
 * {@link #download} to simply download the torrent, or {@link #share} to
 * download and continue seeding for the given amount of time after the
 * download completes.
 * </p>
 *
 * @author mpetazzoni
 */
public class Client extends Observable implements Runnable, AnnounceResponseListener, IncomingConnectionListener,
        PeerActivityListener, GenerationListener {

    private static final Logger logger = LoggerFactory.getLogger(Client.class);

    /** Peers unchoking frequency, in seconds. Current BitTorrent specification
     * recommends 10 seconds to avoid choking fibrilation. */
    private static final int UNCHOKING_FREQUENCY = 3;

    /** Optimistic unchokes are done every 2 loop iterations, i.e. every
     * 2*UNCHOKING_FREQUENCY seconds. */
    private static final int OPTIMISTIC_UNCHOKE_ITERATIONS = 3;

    private static final int RATE_COMPUTATION_ITERATIONS = 2;
    private static final int MAX_DOWNLOADERS_UNCHOKE = 4;

    public enum ClientState {
        WAITING, VALIDATING, SHARING, SEEDING, ERROR, DONE;
    };

    private static final String BITTORRENT_ID_PREFIX = "-TO0042-";

    private SharedTorrent torrent;
    private ClientState state;
    private Peer self;
    private Thread thread;
    private boolean stop;
    private long seed;

    private ConnectionHandler service;
    private Generator generator;
    private DHTManager dhtManager;
    private Announce announce;
    private ConcurrentMap<String, SharingPeer> peers;
    private ConcurrentMap<String, SharingPeer> connected;

    private Random random;

    /**
     * @author mpetazzoni
     * @author Arnaud Durand
     * 
     * Initialize the BitTorrent client.
     *
     * @param address The address to bind to.
     * @param torrent The torrent to download and share.
     */
    public Client(InetAddress address, SharedTorrent torrent) throws UnknownHostException, IOException {
        this.torrent = torrent;
        this.state = ClientState.WAITING;

        String id = Client.BITTORRENT_ID_PREFIX + UUID.randomUUID().toString().split("-")[4];

        // Initialize the incoming connection handler and register ourselves to
        // it.
        this.service = new ConnectionHandler(this.torrent, id, address);
        this.service.register(this);

        // Initialize the generator thread, and register ourselves to it.
        this.generator = new Generator(this.torrent);
        this.generator.register(this);

        // Initialize the DHT manager thread, and register ourselves to it.
        this.dhtManager = new DHTManager(this.torrent, (short) this.service.getSocketAddress().getPort());
        this.dhtManager.register(this);

        this.self = new Peer(this.service.getSocketAddress().getAddress().getHostAddress(),
                (short) this.service.getSocketAddress().getPort(),
                ByteBuffer.wrap(id.getBytes(Torrent.BYTE_ENCODING)));

        // Initialize the announce request thread, and register ourselves to it
        // as well.
        this.announce = new Announce(this.torrent, this.self);
        this.announce.register(this);

        logger.info("BitTorrent client [{}] for {} started and " + "listening at {}:{}...", new Object[] {
                this.self.getShortHexPeerId(), this.torrent.getName(), this.self.getIp(), this.self.getPort() });

        this.peers = new ConcurrentHashMap<String, SharingPeer>();
        this.connected = new ConcurrentHashMap<String, SharingPeer>();
        this.random = new Random(System.currentTimeMillis());
    }

    /**
     * Set the maximum download rate (in kB/second) for this
     * torrent. A setting of <= 0.0 disables rate limiting.
     *
     * @param rate The maximum download rate
     */
    public void setMaxDownloadRate(double rate) {
        this.torrent.setMaxDownloadRate(rate);
    }

    /**
     * Set the maximum upload rate (in kB/second) for this
     * torrent. A setting of <= 0.0 disables rate limiting.
     *
     * @param rate The maximum upload rate
     */
    public void setMaxUploadRate(double rate) {
        this.torrent.setMaxUploadRate(rate);
    }

    /**
     * Get this client's peer specification.
     */
    public Peer getPeerSpec() {
        return this.self;
    }

    /**
     * Return the torrent this client is exchanging on.
     */
    public SharedTorrent getTorrent() {
        return this.torrent;
    }

    /**
     * Returns the set of known peers.
     */
    public Set<SharingPeer> getPeers() {
        return new HashSet<SharingPeer>(this.peers.values());
    }

    /**
     * Change this client's state and notify its observers.
     *
     * <p>
     * If the state has changed, this client's observers will be notified.
     * </p>
     *
     * @param state The new client state.
     */
    private synchronized void setState(ClientState state) {
        if (this.state != state) {
            this.setChanged();
        }
        this.state = state;
        this.notifyObservers(this.state);
    }

    /**
     * Return the current state of this BitTorrent client.
     */
    public ClientState getState() {
        return this.state;
    }

    /**
     * Download the torrent without seeding after completion.
     */
    public void download() {
        this.share(0);
    }

    /**
     * Download and share this client's torrent until interrupted.
     */
    public void share() {
        this.share(-1);
    }

    /**
     * Download and share this client's torrent.
     *
     * @param seed Seed time in seconds after the download is complete. Pass
     * <code>0</code> to immediately stop after downloading.
     */
    public synchronized void share(int seed) {
        this.seed = seed;
        this.stop = false;

        if (this.thread == null || !this.thread.isAlive()) {
            this.thread = new Thread(this);
            this.thread.setName("bt-client(" + this.self.getShortHexPeerId() + ")");
            this.thread.start();
        }
    }

    /**
     * Immediately but gracefully stop this client.
     */
    public void stop() {
        this.stop(true);
    }

    /**
     * Immediately but gracefully stop this client.
     *
     * @param wait Whether to wait for the client execution thread to complete
     * or not. This allows for the client's state to be settled down in one of
     * the <tt>DONE</tt> or <tt>ERROR</tt> states when this method returns.
     */
    public void stop(boolean wait) {
        this.stop = true;

        if (this.thread != null && this.thread.isAlive()) {
            this.thread.interrupt();
            if (wait) {
                this.waitForCompletion();
            }
        }

        this.thread = null;
    }

    /**
     * Wait for downloading (and seeding, if requested) to complete.
     */
    public void waitForCompletion() {
        if (this.thread != null && this.thread.isAlive()) {
            try {
                this.thread.join();
            } catch (InterruptedException ie) {
                logger.error(ie.getMessage(), ie);
            }
        }
    }

    /**
     * Tells whether we are a seed for the torrent we're sharing.
     */
    public boolean isSeed() {
        return this.torrent.isComplete();
    }

    /**
     * Main client loop.
     *
     * <p>
     * The main client download loop is very simple: it starts the announce
     * request thread, the incoming connection handler service, and loops
     * unchoking peers every UNCHOKING_FREQUENCY seconds until told to stop.
     * Every OPTIMISTIC_UNCHOKE_ITERATIONS, an optimistic unchoke will be
     * attempted to try out other peers.
     * </p>
     *
     * <p>
     * Once done, it stops the announce and connection services, and returns.
     * </p>
     */
    @Override
    public void run() {
        // First, analyze the torrent's local data.
        try {
            this.setState(ClientState.VALIDATING);
            this.torrent.init();
        } catch (IOException ioe) {
            logger.warn("Error while initializing torrent data: {}!", ioe.getMessage(), ioe);
        } catch (InterruptedException ie) {
            logger.warn("Client was interrupted during initialization. " + "Aborting right away.");
        } finally {
            if (!this.torrent.isInitialized()) {
                try {
                    this.service.close();
                } catch (IOException ioe) {
                    logger.warn("Error while releasing bound channel: {}!", ioe.getMessage(), ioe);
                }

                this.setState(ClientState.ERROR);
                this.torrent.close();
                return;
            }
        }

        // Initial completion test
        if (this.torrent.isComplete()) {
            this.seed();
        } else {
            this.setState(ClientState.SHARING);
        }

        // Detect early stop
        if (this.stop) {
            logger.info("Download is complete and no seeding was requested.");
            this.finish();
            return;
        }

        this.announce.start();
        this.service.start();
        this.generator.start();
        this.dhtManager.start();

        int optimisticIterations = 0;
        int rateComputationIterations = 0;

        while (!this.stop) {
            optimisticIterations = (optimisticIterations == 0 ? Client.OPTIMISTIC_UNCHOKE_ITERATIONS
                    : optimisticIterations - 1);

            rateComputationIterations = (rateComputationIterations == 0 ? Client.RATE_COMPUTATION_ITERATIONS
                    : rateComputationIterations - 1);

            try {
                this.unchokePeers(optimisticIterations == 0);
                this.info();
                if (rateComputationIterations == 0) {
                    this.resetPeerRates();
                }
            } catch (Exception e) {
                logger.error("An exception occurred during the BitTorrent " + "client main loop execution!", e);
            }

            try {
                Thread.sleep(Client.UNCHOKING_FREQUENCY * 1000);
            } catch (InterruptedException ie) {
                logger.trace("BitTorrent main loop interrupted.");
            }
        }

        logger.debug("Stopping BitTorrent client connection service " + "and announce threads...");

        this.service.stop();
        try {
            this.service.close();
        } catch (IOException ioe) {
            logger.warn("Error while releasing bound channel: {}!", ioe.getMessage(), ioe);
        }

        this.dhtManager.stop();
        this.announce.stop();

        // Close all peer connections
        logger.debug("Closing all remaining peer connections...");
        for (SharingPeer peer : this.connected.values()) {
            peer.unbind(true);
        }

        this.finish();
    }

    /**
     * Close torrent and set final client state before signing off.
     */
    private void finish() {
        //MMM delete
        //this.torrent.close();

        // Determine final state
        if (this.torrent.isFinished()) {
            this.setState(ClientState.DONE);
        } else {
            this.setState(ClientState.ERROR);
        }

        logger.info("BitTorrent client signing off.");
    }

    /**
     * Display information about the BitTorrent client state.
     *
     * <p>
     * This emits an information line in the log about this client's state. It
     * includes the number of choked peers, number of connected peers, number
     * of known peers, information about the torrent availability and
     * completion and current transmission rates.
     * </p>
     */
    public synchronized void info() {
        float dl = 0;
        float ul = 0;
        for (SharingPeer peer : this.connected.values()) {
            dl += peer.getDLRate().get();
            ul += peer.getULRate().get();
        }

        logger.info("{} {}/{} pieces ({}%) [{}/{}] with {}/{} peers at {}/{} kB/s.",
                new Object[] { this.getState().name(), this.torrent.getCompletedPieces().cardinality(),
                        this.torrent.getPieceCount(), String.format("%.2f", this.torrent.getCompletion()),
                        this.torrent.getAvailablePieces().cardinality(),
                        this.torrent.getRequestedPieces().cardinality(), this.connected.size(), this.peers.size(),
                        String.format("%.2f", dl / 1024.0), String.format("%.2f", ul / 1024.0), });
        for (SharingPeer peer : this.connected.values()) {
            Piece piece = peer.getRequestedPiece();
            logger.debug("  | {} {}", peer, piece != null ? "(downloading " + piece + ")" : "");
        }
    }

    /**
     * Reset peers download and upload rates.
     *
     * <p>
     * This method is called every RATE_COMPUTATION_ITERATIONS to reset the
     * download and upload rates of all peers. This contributes to making the
     * download and upload rate computations rolling averages every
     * UNCHOKING_FREQUENCY * RATE_COMPUTATION_ITERATIONS seconds (usually 20
     * seconds).
     * </p>
     */
    private synchronized void resetPeerRates() {
        for (SharingPeer peer : this.connected.values()) {
            peer.getDLRate().reset();
            peer.getULRate().reset();
        }
    }

    /**
     * Retrieve a SharingPeer object from the given peer specification.
     *
     * <p>
     * This function tries to retrieve an existing peer object based on the
     * provided peer specification or otherwise instantiates a new one and adds
     * it to our peer repository.
     * </p>
     *
     * @param search The {@link Peer} specification.
     */
    private SharingPeer getOrCreatePeer(Peer search) {
        SharingPeer peer;

        synchronized (this.peers) {
            logger.trace("Searching for {}...", search);
            if (search.hasPeerId()) {
                peer = this.peers.get(search.getHexPeerId());
                if (peer != null) {
                    logger.trace("Found peer (by peer ID): {}.", peer);
                    this.peers.put(peer.getHostIdentifier(), peer);
                    this.peers.put(search.getHostIdentifier(), peer);
                    return peer;
                }
            }

            peer = this.peers.get(search.getHostIdentifier());
            if (peer != null) {
                if (search.hasPeerId()) {
                    logger.trace("Recording peer ID {} for {}.", search.getHexPeerId(), peer);
                    peer.setPeerId(search.getPeerId());
                    this.peers.put(search.getHexPeerId(), peer);
                }

                logger.debug("Found peer (by host ID): {}.", peer);
                return peer;
            }

            peer = new SharingPeer(search.getIp(), search.getPort(), search.getPeerId(), this.torrent);
            logger.trace("Created new peer: {}.", peer);

            this.peers.put(peer.getHostIdentifier(), peer);
            if (peer.hasPeerId()) {
                this.peers.put(peer.getHexPeerId(), peer);
            }

            return peer;
        }
    }

    /**
     * Retrieve a peer comparator.
     *
     * <p>
     * Returns a peer comparator based on either the download rate or the
     * upload rate of each peer depending on our state. While sharing, we rely
     * on the download rate we get from each peer. When our download is
     * complete and we're only seeding, we use the upload rate instead.
     * </p>
     *
     * @return A SharingPeer comparator that can be used to sort peers based on
     * the download or upload rate we get from them.
     */
    private Comparator<SharingPeer> getPeerRateComparator() {
        if (ClientState.SHARING.equals(this.state)) {
            return new SharingPeer.DLRateComparator();
        } else if (ClientState.SEEDING.equals(this.state)) {
            return new SharingPeer.ULRateComparator();
        } else {
            throw new IllegalStateException(
                    "Client is neither sharing nor " + "seeding, we shouldn't be comparing peers at this point.");
        }
    }

    /**
     * Unchoke connected peers.
     *
     * <p>
     * This is one of the "clever" places of the BitTorrent client. Every
     * OPTIMISTIC_UNCHOKING_FREQUENCY seconds, we decide which peers should be
     * unchocked and authorized to grab pieces from us.
     * </p>
     *
     * <p>
     * Reciprocation (tit-for-tat) and upload capping is implemented here by
     * carefully choosing which peers we unchoke, and which peers we choke.
     * </p>
     *
     * <p>
     * The four peers with the best download rate and are interested in us get
     * unchoked. This maximizes our download rate as we'll be able to get data
     * from there four "best" peers quickly, while allowing these peers to
     * download from us and thus reciprocate their generosity.
     * </p>
     *
     * <p>
     * Peers that have a better download rate than these four downloaders but
     * are not interested get unchoked too, we want to be able to download from
     * them to get more data more quickly. If one becomes interested, it takes
     * a downloader's place as one of the four top downloaders (i.e. we choke
     * the downloader with the worst upload rate).
     * </p>
     *
     * @param optimistic Whether to perform an optimistic unchoke as well.
     */
    private synchronized void unchokePeers(boolean optimistic) {
        // Build a set of all connected peers, we don't care about peers we're
        // not connected to.
        TreeSet<SharingPeer> bound = new TreeSet<SharingPeer>(this.getPeerRateComparator());
        bound.addAll(this.connected.values());

        if (bound.size() == 0) {
            logger.trace("No connected peers, skipping unchoking.");
            return;
        } else {
            logger.trace("Running unchokePeers() on {} connected peers.", bound.size());
        }

        int downloaders = 0;
        Set<SharingPeer> choked = new HashSet<SharingPeer>();

        // We're interested in the top downloaders first, so use a descending
        // set.
        for (SharingPeer peer : bound.descendingSet()) {
            if (downloaders < Client.MAX_DOWNLOADERS_UNCHOKE) {
                // Unchoke up to MAX_DOWNLOADERS_UNCHOKE interested peers
                if (peer.isChoking()) {
                    if (peer.isInterested()) {
                        downloaders++;
                    }

                    peer.unchoke();
                }
            } else {
                // Choke everybody else
                choked.add(peer);
            }
        }

        // Actually choke all chosen peers (if any), except the eventual
        // optimistic unchoke.
        if (choked.size() > 0) {
            SharingPeer randomPeer = choked.toArray(new SharingPeer[0])[this.random.nextInt(choked.size())];

            for (SharingPeer peer : choked) {
                if (optimistic && peer == randomPeer) {
                    logger.debug("Optimistic unchoke of {}.", peer);
                    continue;
                }

                peer.choke();
            }
        }
    }

    /** AnnounceResponseListener handler(s). **********************************/

    /**
     * Handle an announce response event.
     *
     * @param interval The announce interval requested by the tracker.
     * @param complete The number of seeders on this torrent.
     * @param incomplete The number of leechers on this torrent.
     */
    @Override
    public void handleAnnounceResponse(int interval, int complete, int incomplete) {
        this.announce.setInterval(interval);
    }

    /**
     * Handle the discovery of new peers.
     *
     * @param peers The list of peers discovered (from the announce response or
     * any other means like DHT/PEX, etc.).
     */
    @Override
    public void handleDiscoveredPeers(List<Peer> peers) {
        if (peers == null || peers.isEmpty()) {
            // No peers returned by the tracker. Apparently we're alone on
            // this one for now.
            return;
        }

        logger.info("Got {} peer(s) in tracker response.", peers.size());

        if (!this.service.isAlive()) {
            logger.warn("Connection handler service is not available.");
            return;
        }

        for (Peer peer : peers) {
            // Attempt to connect to the peer if and only if:
            //   - We're not already connected or connecting to it;
            //   - We're not a seeder (we leave the responsibility
            //      of connecting to peers that need to download
            //     something).
            SharingPeer match = this.getOrCreatePeer(peer);
            if (this.isSeed()) {
                continue;
            }

            synchronized (match) {
                if (!match.isConnected()) {
                    this.service.connect(match);
                }
            }
        }
    }

    /** IncomingConnectionListener handler(s). ********************************/

    /**
     * Handle a new peer connection.
     *
     * <p>
     * This handler is called once the connection has been successfully
     * established and the handshake exchange made. This generally simply means
     * binding the peer to the socket, which will put in place the communication
     * thread and logic with this peer.
     * </p>
     *
     * @param channel The connected socket channel to the remote peer. Note
     * that if the peer somehow rejected our handshake reply, this socket might
     * very soon get closed, but this is handled down the road.
     * @param peerId The byte-encoded peerId extracted from the peer's
     * handshake, after validation.
     * @see com.turn.ttorrent.client.peer.SharingPeer
     */
    @Override
    public void handleNewPeerConnection(SocketChannel channel, byte[] peerId) {
        Peer search = new Peer(channel.socket().getInetAddress().getHostAddress(), channel.socket().getPort(),
                (peerId != null ? ByteBuffer.wrap(peerId) : (ByteBuffer) null));

        logger.info("Handling new peer connection with {}...", search);
        SharingPeer peer = this.getOrCreatePeer(search);

        try {
            synchronized (peer) {
                if (peer.isConnected()) {
                    logger.info("Already connected with {}, closing link.", peer);
                    channel.close();
                    return;
                }

                peer.register(this);
                peer.bind(channel);
            }

            this.connected.put(peer.getHexPeerId(), peer);
            peer.register(this.torrent);
            logger.debug("New peer connection with {} [{}/{}].",
                    new Object[] { peer, this.connected.size(), this.peers.size() });
        } catch (Exception e) {
            this.connected.remove(peer.getHexPeerId());
            logger.warn("Could not handle new peer connection " + "with {}: {}", peer, e.getMessage());
        }
    }

    /**
     * Handle a failed peer connection.
     *
     * <p>
     * If an outbound connection failed (could not connect, invalid handshake,
     * etc.), remove the peer from our known peers.
     * </p>
     *
     * @param peer The peer we were trying to connect with.
     * @param cause The exception encountered when connecting with the peer.
     */
    @Override
    public void handleFailedConnection(SharingPeer peer, Throwable cause) {
        logger.warn("Could not connect to {}: {}.", peer, cause.getMessage());
        this.peers.remove(peer.getHostIdentifier());
        if (peer.hasPeerId()) {
            this.peers.remove(peer.getHexPeerId());
        }
    }

    /** PeerActivityListener handler(s). **************************************/

    @Override
    public void handlePeerChoked(SharingPeer peer) {
        /* Do nothing */ }

    @Override
    public void handlePeerReady(SharingPeer peer) {
        /* Do nothing */ }

    @Override
    public void handlePieceAvailability(SharingPeer peer, Piece piece) {
        /* Do nothing */ }

    @Override
    public void handleBitfieldAvailability(SharingPeer peer, BitSet availablePieces) {
        /* Do nothing */ }

    @Override
    public void handlePieceSent(SharingPeer peer, Piece piece) {
        /* Do nothing */ }

    /**
     * Piece download completion handler.
     *
     * <p>
     * When a piece is completed, and valid, we announce to all connected peers
     * that we now have this piece.
     * </p>
     *
     * <p>
     * We use this handler to identify when all of the pieces have been
     * downloaded. When that's the case, we can start the seeding period, if
     * any.
     * </p>
     *
     * @param peer The peer we got the piece from.
     * @param piece The piece in question.
     */
    @Override
    public void handlePieceCompleted(SharingPeer peer, Piece piece) throws IOException {
        synchronized (this.torrent) {
            if (piece.isValid()) {
                // Make sure the piece is marked as completed in the torrent
                // Note: this is required because the order the
                // PeerActivityListeners are called is not defined, and we
                // might be called before the torrent's piece completion
                // handler is.
                this.torrent.markCompleted(piece);
                logger.debug("Completed download of {} from {}. " + "We now have {}/{} pieces",
                        new Object[] { piece, peer, this.torrent.getCompletedPieces().cardinality(),
                                this.torrent.getPieceCount() });

                // Send a HAVE message to all connected peers
                PeerMessage have = PeerMessage.HaveMessage.craft(piece.getIndex());
                for (SharingPeer remote : this.connected.values()) {
                    remote.send(have);
                }

                // Force notify after each piece is completed to propagate download
                // completion information (or new seeding state)
                this.setChanged();
                this.notifyObservers(this.state);
            } else {
                logger.warn("Downloaded piece#{} from {} was not valid ;-(", piece.getIndex(), peer);
            }

            if (this.torrent.isComplete()) {
                logger.info("Last piece validated and completed, finishing download...");

                // Cancel all remaining outstanding requests
                for (SharingPeer remote : this.connected.values()) {
                    if (remote.isDownloading()) {
                        int requests = remote.cancelPendingRequests().size();
                        logger.info("Cancelled {} remaining pending requests on {}.", requests, remote);
                    }
                }

                this.torrent.finish();

                if (this.torrent.getCompleteCommand() != null) {
                    mergeFiles();
                }
                try {

                    this.announce.getCurrentTrackerClient()
                            .announce(TrackerMessage.AnnounceRequestMessage.RequestEvent.COMPLETED, true);
                } catch (AnnounceException ae) {
                    logger.warn("Error announcing completion event to " + "tracker: {}", ae.getMessage());
                }
            }
            logger.info("Download is complete and finalized.");
            this.seed();
        }
    }

    private void mergeFiles() {
        for (Torrent.TorrentFile file : this.torrent.files) {
            String filesPath = "";
            for (int i = 0; i < (file.size / this.torrent.pieceLength); i++) {
                filesPath += new File(this.torrent.parentPath, file.getPath() + "." + i).getAbsolutePath() + " ";
                //FilenameUtils.concat(this.torrent.parentPath, file.getPath()+"."+i+" ");
                System.out.println(filesPath);
            }
            try {
                System.out.println("Complete command " + this.torrent.getCompleteCommand());
                ProcessBuilder pb = new ProcessBuilder("/bin/bash", "-c",
                        this.torrent.getCompleteCommand()/*"cat $PART_LIST > $FILE"*/);
                Map<String, String> env = pb.environment();
                env.put("PART_LIST", filesPath);
                env.put("FILE", new File(this.torrent.parentPath, file.getPath()).getAbsolutePath());
                //FilenameUtils.concat(this.torrent.parentPath, file.getPath()));
                Process pr = pb.start();

                BufferedReader bre = new BufferedReader(new InputStreamReader(pr.getErrorStream()));
                String line;
                while ((line = bre.readLine()) != null) {
                    System.err.println(line);
                }
                bre.close();

                int exitVal;
                if ((exitVal = pr.waitFor()) != 0)
                    ;
                else
                    logger.info("Files merged...");
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    @Override
    public void handlePeerDisconnected(SharingPeer peer) {
        if (this.connected.remove(peer.hasPeerId() ? peer.getHexPeerId() : peer.getHostIdentifier()) != null) {
            logger.debug("Peer {} disconnected, [{}/{}].",
                    new Object[] { peer, this.connected.size(), this.peers.size() });
        }

        peer.reset();
    }

    @Override
    public void handleIOException(SharingPeer peer, IOException ioe) {
        logger.warn("I/O error while exchanging data with {}, " + "closing connection with it!", peer,
                ioe.getMessage());
        peer.unbind(true);
    }

    /** Post download seeding. ************************************************/

    /**
     * Start the seeding period, if any.
     *
     * <p>
     * This method is called when all the pieces of our torrent have been
     * retrieved. This may happen immediately after the client starts if the
     * torrent was already fully download or we are the initial seeder client.
     * </p>
     *
     * <p>
     * When the download is complete, the client switches to seeding mode for
     * as long as requested in the <code>share()</code> call, if seeding was
     * requested. If not, the {@link ClientShutdown} will execute
     * immediately to stop the client's main loop.
     * </p>
     *
     * @see ClientShutdown
     */
    private synchronized void seed() {
        // Silently ignore if we're already seeding.
        if (ClientState.SEEDING.equals(this.getState())) {
            return;
        }

        logger.info("Download of {} pieces completed.", this.torrent.getPieceCount());

        this.setState(ClientState.SEEDING);
        if (this.seed < 0) {
            logger.info("Seeding indefinetely...");
            return;
        }

        // In case seeding for 0 seconds we still need to schedule the task in
        // order to call stop() from different thread to avoid deadlock
        logger.info("Seeding for {} seconds...", this.seed);
        Timer timer = new Timer();
        timer.schedule(new ClientShutdown(this, timer), this.seed * 1000);
    }

    /**
     * Timer task to stop seeding.
     *
     * <p>
     * This TimerTask will be called by a timer set after the download is
     * complete to stop seeding from this client after a certain amount of
     * requested seed time (might be 0 for immediate termination).
     * </p>
     *
     * <p>
     * This task simply contains a reference to this client instance and calls
     * its <code>stop()</code> method to interrupt the client's main loop.
     * </p>
     *
     * @author mpetazzoni
     */
    public static class ClientShutdown extends TimerTask {

        private final Client client;
        private final Timer timer;

        public ClientShutdown(Client client, Timer timer) {
            this.client = client;
            this.timer = timer;
        }

        @Override
        public void run() {
            this.client.stop();
            if (this.timer != null) {
                this.timer.cancel();
            }
        }
    }

    @Override
    public void handlePieceGenerationCompleted(Piece piece) throws IOException {
        handlePieceCompleted(null, piece);
    }

}