Package org.eclipse.ecf.protocol.bittorrent.internal.net

Source Code of org.eclipse.ecf.protocol.bittorrent.internal.net.TorrentManager$SpeedMonitoringThread

/*******************************************************************************
* Copyright (c) 2006, 2008 Remy Suen, Composent Inc., and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*    Remy Suen <remy.suen@gmail.com> - initial API and implementation
******************************************************************************/
package org.eclipse.ecf.protocol.bittorrent.internal.net;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Vector;

import org.eclipse.ecf.protocol.bittorrent.IHashCheckListener;
import org.eclipse.ecf.protocol.bittorrent.IPieceProgressListener;
import org.eclipse.ecf.protocol.bittorrent.ITorrentErrorListener;
import org.eclipse.ecf.protocol.bittorrent.ITorrentProgressListener;
import org.eclipse.ecf.protocol.bittorrent.ITorrentStateListener;
import org.eclipse.ecf.protocol.bittorrent.Torrent;
import org.eclipse.ecf.protocol.bittorrent.TorrentConfiguration;
import org.eclipse.ecf.protocol.bittorrent.TorrentFile;
import org.eclipse.ecf.protocol.bittorrent.TorrentServer;
import org.eclipse.ecf.protocol.bittorrent.internal.encode.BEncodedDictionary;
import org.eclipse.ecf.protocol.bittorrent.internal.encode.Decode;
import org.eclipse.ecf.protocol.bittorrent.internal.encode.Encode;
import org.eclipse.ecf.protocol.bittorrent.internal.torrent.DataFile;
import org.eclipse.ecf.protocol.bittorrent.internal.torrent.Piece;
import org.eclipse.ecf.protocol.bittorrent.internal.torrent.PieceState;

/**
* The <code>TorrentManager</code> class is used to handle all of the
* internals of what's being publicly exposed by {@link Torrent}.
*/
public class TorrentManager {

  private static final String DOWN_SPEED_KEY = "down.speed"; //$NON-NLS-1$

  private static final String UP_SPEED_KEY = "up.speed"; //$NON-NLS-1$

  private static MessageDigest shaDigest;

  private static File statePath;

  private final ConnectionPool connectionPool;

  /**
   * An array of files that will be read and written to to exchange pieces
   * with peers.
   */
  private final DataFile[] files;

  private final Vector stateListeners;

  private final Vector progressListeners;

  private final Vector errorListeners;

  private final Vector pieceListeners;

  private final Vector hashCheckListeners;

  private final TorrentFile torrent;

  private final Properties properties;

  private final File propertiesFile;

  private final File targetFile;

  /**
   * The file in which the amount of data that has been downloaded thus far
   * for this torrent is stored. This allows the torrent to resume downloading
   * or seeding very quickly.
   */
  private final File torrentState;

  private final Vector pieces;

  /**
   * A collection of {@link Piece}s that has had a portion of its bytes
   * completed.
   */
  private final Vector incompletePieces;

  /**
   * An integer array that stores the number of peers that possesses a
   * specific piece. This is used to determine which piece is a rare piece.
   */
  private final int[] pieceAvailability;

  private final byte[] bitfield;

  /**
   * A boolean array that indicates whether a specific piece has been
   * downloaded successfully or not.
   */
  private final boolean[] hasPiece;

  private final boolean[] priorityPieces;

  private final boolean[] interestedPieces;

  private final boolean[] uninterestedPieces;

  private final String infoHash;

  /**
   * The URL of the tracker that this host should connect to.
   */
  private final String tracker;

  /**
   * A unique identification string is used to identify this client when
   * talking with the tracker.
   */
  private final String peerID = "E088----" + createPeerID(); //$NON-NLS-1$

  private final String hexHash;

  /**
   * An additional identification that is required by some trackers in the
   * event that the client's IP changes.
   */
  private final char key = createKey();

  /**
   * The total number of bytes that is encompassed by the torrent file. This
   * only takes files into accounts and ignores the file size of folders.
   */
  private final long total;

  /**
   * The length of a piece.
   */
  private final int pieceLength;

  private TrackerThread trackerThread;

  private SpeedMonitoringThread speedMonitoringThread;

  private HashCheckThread hashCheckThread;

  private PieceState[] states;

  /**
   * A unique string that may be returned by the connected tracker for
   * identification purposes.
   */
  private String trackerID;

  /**
   * The amount of bytes that has been downloaded from other peers thus far.
   */
  private long downloaded = 0;

  /**
   * The amount of bytes that has been uploaded to other peers thus far.
   */
  private long uploaded = 0;

  /**
   * The speed in which the data is being downloaded from peers in bytes as
   * calculated from a twenty second rolling average.
   */
  private long downSpeed = 0;

  /**
   * The speed in which the data is being uploaded to peers in bytes as
   * calculated from a twenty second rolling average.
   */
  private long upSpeed = 0;

  private long maxDownSpeed = -1;

  private long maxUpSpeed = -1;

  private long requestDownSpeed = -1;

  private long requestUpSpeed = -1;

  /**
   * The amount of data that has been discarded thus far because of hash check
   * failures.
   */
  private long discarded = 0;

  /**
   * The amount of data that still needs to be downloaded for completion.
   */
  private long remaining;

  /**
   * The number of peers to request from the tracker.
   */
  private int request = 50;

  /**
   * The total number of seeds on the torrent as returned by the tracker. If
   * the value is -1, the tracker has either not returned this value or the
   * tracker has not yet been queried.
   */
  private int seeders = -1;

  /**
   * The total number of peers on the torrent as returned by the tracker. If
   * the value is -1, the tracker has either not returned this value or the
   * tracker has not yet been queried.
   */
  private int peers = -1;

  /**
   * The amount of time to wait before querying the tracker again for peers.
   * This is in milliseconds.
   */
  private int timeout = 1800000;

  /**
   * The number of completed pieces thus far. This is updated within
   * {@link #write(int, int, byte[], int, int)} and is used to inform attached
   * {@link ITorrentProgressListener}s via the
   * {@link #firePieceCompletedEvent(int)} method.
   */
  private int completedPieces = 0;

  private int state = ITorrentStateListener.STOPPED;

  /**
   * Used to indicate whether the torrent is currently running.
   */
  private boolean running = false;

  /**
   * Indicates that all the pieces have passed the SHA-1 hash check
   * successfully.
   */
  private boolean isCompleted = false;

  /**
   * Indicates whether the user is deciding to not download some files.
   */
  private boolean isSelective = false;

  private boolean isPrioritizing = false;

  private boolean isWaitingToStart = false;

  private boolean isHashChecking = false;

  static {
    try {
      shaDigest = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    }
  }

  public static void setStatePath(File path) {
    statePath = path;
  }

  private static String createPeerID() {
    char[] numbers = new char[12];
    for (int i = 0; i < 12; i++) {
      numbers[i] = (char) (48 + ConnectionPool.RANDOM.nextInt(10));
    }
    return new String(numbers);
  }

  private static char createKey() {
    char key = (char) (ConnectionPool.RANDOM.nextInt(75) + 48);
    while (!Character.isDigit(key) && !Character.isLetter(key)) {
      key = (char) (ConnectionPool.RANDOM.nextInt(75) + 48);
    }
    return key;
  }

  /**
   * Creates a new <code>Host</code> to begin with exchanging pieces with
   * other pieces for the given torrent.
   *
   * @param torrent
   *            the torrent to use
   * @param properties
   *            the <code>Properties</code> instance to use to store
   *            information, if <code>null</code>, a new instance will be
   *            created
   * @throws IOException
   *             If an I/O error occurs whilst creating or hooking up with the
   *             files associated with the torrent
   */
  public TorrentManager(TorrentFile torrent, Properties properties)
      throws IOException {
    this.torrent = torrent;
    targetFile = torrent.getTargetFile();
    connectionPool = new ConnectionPool(this);
    tracker = torrent.getTracker();
    pieceLength = torrent.getPieceLength();
    infoHash = torrent.getInfoHash();
    hexHash = torrent.getHexHash();
    torrentState = new File(statePath, hexHash);
    total = torrent.getTotalLength();
    torrent.save(new File(statePath, hexHash + ".torrent")); //$NON-NLS-1$

    int numPieces = torrent.getNumPieces();
    pieces = new Vector(numPieces);
    trackerThread = new TrackerThread();
    states = PieceState.createStates(numPieces);
    for (int i = 0; i < numPieces; i++) {
      pieces.add(new Piece(states[i], i));
    }
    bitfield = new byte[numPieces % 8 != 0 ? (numPieces / 8) + 1
        : (numPieces / 8)];
    hasPiece = new boolean[numPieces];
    pieceAvailability = new int[numPieces];
    priorityPieces = new boolean[numPieces];
    interestedPieces = new boolean[numPieces];
    uninterestedPieces = new boolean[numPieces];
    incompletePieces = new Vector();
    stateListeners = new Vector();
    errorListeners = new Vector();
    pieceListeners = new Vector();
    progressListeners = new Vector();
    hashCheckListeners = new Vector();
    propertiesFile = new File(statePath, hexHash + ".properties"); //$NON-NLS-1$
    if (!propertiesFile.exists()) {
      properties = new Properties();
      properties.setProperty("target", targetFile.getAbsolutePath()); //$NON-NLS-1$
    } else if (properties == null) {
      properties = new Properties();
      properties.load(new FileInputStream(propertiesFile));
      restore(properties);
    } else {
      restore(properties);
    }
    this.properties = properties;
    store();

    String[] filenames = torrent.getFilenames();
    if (filenames.length != 1 && !targetFile.exists()
        && !targetFile.mkdirs()) {
      throw new IOException("The folders needed by this torrent could not be created"); //$NON-NLS-1$
    }
    files = new DataFile[filenames.length];

    fileInitialization(filenames, targetFile);
    for (int i = 0; i < numPieces; i++) {
      Piece piece = (Piece) pieces.get(i);
      piece.setLength(pieceLength);
    }
    ((Piece) pieces.get(numPieces - 1))
        .setLength((int) (total % pieceLength));

    checkFile();
    setPieces();
  }

  private void checkFile() {
    if (torrentState.exists()) {
      int count = 0;
      try {
        BufferedReader reader = new BufferedReader(new FileReader(
            torrentState));
        String input = reader.readLine();
        if (input != null
            && Long.parseLong(input) == targetFile.lastModified()) {
          input = reader.readLine();
          PieceState[] states = PieceState.createStates(torrent
              .getNumPieces());
          while (input != null) {
            states[count++].parse(input);
            input = reader.readLine();
          }
          if (count != states.length) {
            startHashCheck();
          } else {
            setPieces(states);
          }
        } else {
          startHashCheck();
        }
      } catch (IOException e) {
        startHashCheck();
      }
    } else {
      startHashCheck();
    }
  }

  public boolean performHashCheck() {
    switch (state) {
    case ITorrentStateListener.STOPPED:
      startHashCheck();
      return true;
    case ITorrentStateListener.HASH_CHECKING:
      return true;
    default:
      return false;
    }
  }

  private void startHashCheck() {
    if (hashCheckThread == null || !hashCheckThread.isAlive()) {
      hashCheckThread = new HashCheckThread();
      hashCheckThread.start();
      isHashChecking = true;
      fireStateChangedEvent(ITorrentStateListener.HASH_CHECKING);
    }
  }

  private void fileInitialization(String[] filenames, File targetFile)
      throws IOException {
    if (files.length == 1) {
      long length = torrent.getLengths()[0];
      files[0] = new DataFile(targetFile, length);
      int[] pieces = new int[torrent.getPieces().length];
      for (int i = 0; i < pieces.length; i++) {
        pieces[i] = i;
      }
      files[0].setPieces(pieces, pieceLength, pieceLength);
      remaining = length;
    } else {
      int count = 0;
      int piece = 0;
      int currentLength = pieceLength;
      for (int i = 0; i < filenames.length; i++) {
        File file = new File(targetFile + File.separator + filenames[i]);
        if (!file.getParentFile().exists()
            && !file.getParentFile().mkdirs()) {
          throw new IOException("The folders needed by this torrent could not be created"); //$NON-NLS-1$
        }

        long fileLength = torrent.getLengths()[i];
        files[i] = new DataFile(file, fileLength);

        if (currentLength > fileLength) {
          files[i].setPieces(new int[] { count }, (int) fileLength,
              (int) fileLength);
          currentLength -= fileLength;
          continue;
        } else if (currentLength == fileLength) {
          files[i].setPieces(new int[] { count }, (int) fileLength,
              (int) fileLength);
          currentLength = pieceLength;
          count++;
          piece++;
          continue;
        } else if (currentLength != pieceLength
            && currentLength < fileLength) {
          fileLength -= currentLength;
          count++;
        }

        while (fileLength >= pieceLength) {
          count++;
          fileLength -= pieceLength;
        }
        count++;

        int[] pieces = new int[count - piece];
        for (int j = piece; j < count; j++) {
          pieces[j - piece] = j;
        }

        if (fileLength == 0) {
          files[i].setPieces(pieces, pieceLength, pieceLength);
        } else {
          files[i].setPieces(pieces, currentLength, pieceLength);
          currentLength = (int) (pieceLength - fileLength);
        }
        count--;
        piece = count;
      }
      remaining = total;
    }
  }

  private void setPieces() {
    int count = 0;
    int pieceLen = pieceLength;
    for (int i = 0; i < files.length; i++) {
      long length = files[i].length();
      if (pieceLen == 0) {
        pieceLen = pieceLength;
      }
      while (pieceLen < length) {
        Piece piece = (Piece) pieces.get(count);
        piece.addFile(files[i], pieceLen);
        count++;
        length -= pieceLen;
        if (pieceLen < pieceLength) {
          pieceLen = pieceLength;
        }
      }
      pieceLen -= length;
      ((Piece) pieces.get(count)).addFile(files[i], (int) length);
    }
  }

  private void restore(Properties properties) {
    String value = properties.getProperty(DOWN_SPEED_KEY);
    requestDownSpeed = value != null ? Long.parseLong(value) : -1;
    value = properties.getProperty(UP_SPEED_KEY);
    requestUpSpeed = value != null ? Long.parseLong(value) : -1;
  }

  private void store() throws IOException {
    properties.setProperty(DOWN_SPEED_KEY, Long.toString(requestDownSpeed));
    properties.setProperty(UP_SPEED_KEY, Long.toString(requestUpSpeed));
    properties.store(new FileOutputStream(propertiesFile), null);
  }

  private void updateBitfield() {
    int count = 0;
    int size = hasPiece.length;
    // iterate over all the pieces by multiples of 8
    char[] bits = new char[8];
    for (int i = 0; i < size; i += 8) {
      Arrays.fill(bits, '0');
      // iterate over the 8 (or less) pieces within this segment
      for (int j = i; j < (i + 8) && j < size; j++) {
        // if we have this piece, flag it so
        if (hasPiece[j]) {
          bits[j - i] = '1';
        }
      }
      // encode the binary string into the bitfield
      bitfield[count] = Encode.encodeForBitfield(bits);
      count++;
    }
  }

  private void updateBitfield(int piece) {
    int offset = piece / 8;
    char[] bits = new char[8];
    Arrays.fill(bits, '0');
    for (int i = offset; i < (offset + 8) && i < hasPiece.length; i++) {
      if (hasPiece[i]) {
        bits[i - offset] = '1';
      }
    }
    bitfield[offset] = Encode.encodeForBitfield(bits);
  }

  /**
   * Contacts the tracker to begin exchanging pieces with any peers that are
   * found.
   *
   * @throws IOException
   *             If an error occurs while querying the tracker or connecting
   *             to one of the provided peers
   */
  public void start() throws IOException {
    if (hashCheckThread != null && hashCheckThread.isAlive()
        && isHashChecking) {
      isWaitingToStart = true;
      return;
    } else if (!torrentState.exists()) {
      startHashCheck();
      isWaitingToStart = true;
      return;
    } else if (running) {
      return;
    }
    isWaitingToStart = false;
    running = true;
    speedMonitoringThread = new SpeedMonitoringThread();
    trackerThread = new TrackerThread();
    speedMonitoringThread.start();
    trackerThread.start();
    fireStateChangedEvent(ITorrentStateListener.STARTED);
    queryTracker("started"); //$NON-NLS-1$
    fireStateChangedEvent(ITorrentStateListener.EXCHANGING);
  }

  /**
   * Stops downloading or seeding the torrent.
   *
   * @throws IOException
   *             If an IOException occurred while informing the tracker that
   *             the client is stopping
   */
  public void stop() throws IOException {
    isWaitingToStart = false;
    if (hashCheckThread != null && hashCheckThread.isAlive()) {
      hashCheckThread.interrupt();
      isHashChecking = false;
      hashCheckThread = null;
      fireStateChangedEvent(ITorrentStateListener.STOPPED);
      return;
    } else if (!running) {
      return;
    }
    state = ITorrentStateListener.STOPPED;
    trackerThread.interrupt();
    speedMonitoringThread.interrupt();
    trackerThread = null;
    speedMonitoringThread = null;
    running = false;
    connectionPool.close();
    queryTracker("stopped"); //$NON-NLS-1$
    fireStateChangedEvent(ITorrentStateListener.STOPPED);
    store();
  }

  /**
   * Removes all previously saved status and configuration information
   * regarding the opened torrent. This will call {@link #stop()} prior to the
   * deletion of the files.
   */
  public void remove() {
    try {
      stop();
    } catch (IOException e) {
      // ignored
    }

    File[] files = statePath.listFiles();
    for (int i = 0; i < files.length; i++) {
      if (files[i].getName().startsWith(hexHash)) {
        files[i].delete();
      }
    }

    remaining = total;
    seeders = -1;
    peers = -1;
    Arrays.fill(priorityPieces, false);
    Arrays.fill(interestedPieces, false);
    Arrays.fill(uninterestedPieces, false);
    for (int i = 0; i < pieces.size(); i++) {
      ((Piece) pieces.get(i)).reset();
    }
  }

  public boolean delete() {
    remove();
    return targetFile.delete();
  }

  private void queryTracker(String event) throws IOException {
    String link = tracker
        + "?info_hash=" //$NON-NLS-1$
        + URLEncoder.encode(infoHash, "ISO-8859-1").replaceAll("\\+", //$NON-NLS-1$ //$NON-NLS-2$
            "%20") //$NON-NLS-1$
        + "&peer_id=" //$NON-NLS-1$
        + URLEncoder.encode(peerID, "ISO-8859-1").replaceAll("\\+", //$NON-NLS-1$ //$NON-NLS-2$
            "%20") + "&port=" + TorrentServer.getPort() //$NON-NLS-1$ //$NON-NLS-2$
        + "&uploaded=" + uploaded + "&downloaded=" + downloaded //$NON-NLS-1$ //$NON-NLS-2$
        + "&left=" + remaining //$NON-NLS-1$
        + (event == null ? "" : "&event=" + event) + "&numwant=" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        + request + "&compact=1" + "&key=" + key //$NON-NLS-1$ //$NON-NLS-2$
        + (trackerID != null ? "&trackerid=" + trackerID : ""); //$NON-NLS-1$ //$NON-NLS-2$
    TorrentConfiguration.debug("Querying the tracker at " + link); //$NON-NLS-1$
    URL url = new URL(link);
    BEncodedDictionary dictionary = Decode.bDecode(url.openStream());
    if (event != null && event.equals("stopped")) { //$NON-NLS-1$
      return;
    }

    String failure = (String) dictionary.get("failure reason"); //$NON-NLS-1$
    if (failure != null) {
      fireTrackerErrorEvent(failure);
      TorrentConfiguration.debug("The client could not connect to the tracker, the reason provided was - " + failure); //$NON-NLS-1$
      return;
    }
    timeout = ((Long) dictionary.get("interval")).intValue() * 1000; //$NON-NLS-1$
    if (trackerID == null) {
      trackerID = (String) dictionary.get("tracker id"); //$NON-NLS-1$
    }

    Long number = (Long) dictionary.get("completed"); //$NON-NLS-1$
    seeders = number != null ? number.intValue() : -1;
    number = (Long) dictionary.get("incompleted"); //$NON-NLS-1$
    peers = number != null ? number.intValue() : -1;

    Object peersList = dictionary.get("peers"); //$NON-NLS-1$
    if (peersList instanceof List) {
      TorrentConfiguration.debug("No peers were returned"); //$NON-NLS-1$
      return;
    }

    String string = (String) peersList;
    byte[] bytes = string.getBytes("ISO-8859-1"); //$NON-NLS-1$
    for (int i = 0; i < string.length(); i += 6) {
      String ip = (bytes[i] & 0xff) + "." + (bytes[i + 1] & 0xff) + "." //$NON-NLS-1$ //$NON-NLS-2$
          + (bytes[i + 2] & 0xff) + "." + (bytes[i + 3] & 0xff); //$NON-NLS-1$
      int port = Integer.parseInt(Integer
          .toHexString(bytes[i + 4] & 0xff)
          + Integer.toHexString(bytes[i + 5] & 0xff), 16);
      connectionPool.connectTo(ip, port);
    }
  }

  public void connectTo(SocketChannel channel)
      throws UnsupportedEncodingException {
    connectionPool.connectTo(channel);
  }

  /**
   * Retrieves the block of data that corresponds to the specified piece.
   *
   * @param piece
   *            the interested piece's number
   * @return an array of bytes corresponding to the data that is encompassed
   *         by the specified piece, or <code>null</code> if no data is
   *         available
   * @throws IllegalArgumentException
   *             If <code>piece</code> is negative or if it is not one of
   *             the piece numbers
   * @throws IOException
   *             If an I/O/ error occurs while reading the data from the local
   *             files
   */
  private byte[] getPiece(int piece) throws IOException {
    if (piece < 0) {
      throw new IllegalArgumentException("The piece number cannot be negative"); //$NON-NLS-1$
    } else if (piece >= hasPiece.length) {
      throw new IllegalArgumentException("The piece number " + piece //$NON-NLS-1$
          + " does not exist"); //$NON-NLS-1$
    }

    byte[][] data = new byte[files.length][0];
    Arrays.fill(data, null);
    for (int i = 0; i < files.length; i++) {
      if (files[i].containsPiece(piece)) {
        data[i] = files[i].getData(piece);
      }
    }

    boolean empty = true;
    for (int i = 0; i < data.length; i++) {
      if (data[i] != null && data[i].length != 0) {
        empty = false;
        break;
      }
    }

    if (empty) {
      return null;
    }

    byte[] bytes = new byte[((Piece) pieces.get(piece)).getLength()];
    int offset = 0;
    for (int i = 0; i < data.length; i++) {
      if (data[i] != null) {
        System.arraycopy(data[i], 0, bytes, offset, data[i].length);
        offset += data[i].length;
      }
    }
    return bytes;
  }

  byte[] getPieceData(int number, int offset, int length)
      throws IllegalArgumentException, IOException {
    byte[] piece = getPiece(number);
    if (piece == null) {
      return null;
    }
    if (offset + length > piece.length) {
      throw new IllegalArgumentException("The block of data that is being requested goes beyond the range of the requested piece"); //$NON-NLS-1$
    }
    byte[] block = new byte[length];
    System.arraycopy(piece, offset, block, 0, length);
    return block;
  }

  /**
   * Performs a hash check on the specified piece number to see whether the
   * data is corrupt or not.
   *
   * @param piece
   *            the number of the piece to check
   * @return <code>true</code> if the SHA-1 hash of the particular piece's
   *         data matches the torrent file's hash, <code>false</code>
   *         otherwise
   * @throws IllegalArgumentException
   *             If the specified piece has not been completed
   * @throws IOException
   *             If an I/O error occurs while retrieving the piece's data from
   *             a particular file
   */
  private boolean hashCheck(int piece) throws IllegalArgumentException,
      IOException {
    byte[] data = getPiece(piece);
    return data == null ? false : torrent.getPieces()[piece]
        .equals(new String(shaDigest.digest(data), "ISO-8859-1")); //$NON-NLS-1$
  }

  private void saveState() throws IOException {
    BufferedWriter writer = new BufferedWriter(new FileWriter(torrentState,
        false));
    writer.write(Long.toString(targetFile.lastModified()));
    writer.newLine();
    for (int i = 0; i < states.length; i++) {
      writer.write(states[i].toString());
      writer.newLine();
    }
    writer.flush();
  }

  /**
   * Writes the data that has been received from a peer onto the local files.
   *
   * @param number
   *            the piece's number that this data corresponds to
   * @param index
   *            the position within the piece that the retrieved data starts
   *            from
   * @param data
   *            the data sent from the peer
   * @param offset
   *            the offset within <code>data</code> that the actual bytes
   *            for the files start at
   * @param length
   *            the amount of bytes of data that has been received
   * @throws IOException
   *             If an I/O error occurs while writing to a file, saving the
   *             state information, performing a hash check, or querying the
   *             tracker
   */
  synchronized void write(int number, int index, byte[] data, int offset,
      int length) throws IOException {
    Piece piece = (Piece) pieces.get(number);
    if (!hasPiece[number] && piece.write(index, data, offset, length)) {
      remaining -= length;
      downloaded += length;
      saveState();
      fireBlockDownloadedEvent(number, index, length);
      if (!incompletePieces.contains(piece)) {
        incompletePieces.add(piece);
      }

      if (piece.isComplete()) {
        incompletePieces.remove(piece);
        checkCompletedPiece(piece, number);
      }
    }
  }

  private void checkCompletedPiece(Piece piece, int number)
      throws IOException {
    if (hashCheck(number)) {
      TorrentConfiguration
          .debug("Piece " + number + " passed hash check"); //$NON-NLS-1$ //$NON-NLS-2$
      hasPiece[number] = true;
      updateBitfield(number);
      firePieceCompletedEvent(++completedPieces);
      connectionPool.queueHaveMessage(number);
      for (int i = 0; i < hasPiece.length; i++) {
        if (!hasPiece[i]) {
          return;
        }
      }

      isCompleted = true;
      fireStateChangedEvent(ITorrentStateListener.FINISHED);
      connectionPool.disconnectSeeds();
      // let the tracker know that the download has completed
      queryTracker("completed"); //$NON-NLS-1$
    } else {
      TorrentConfiguration.debug("Piece " + number //$NON-NLS-1$
          + " has failed the hash check"); //$NON-NLS-1$
      piece.reset();
      int pieceLength = piece.getLength();
      discarded += pieceLength;
      remaining += (remaining == total) ? 0 : pieceLength;
      firePieceDiscardEvent(number, pieceLength);
    }
  }

  synchronized Piece request(boolean[] peerPieces) {
    if (isCompleted) {
      return null;
    }

    Piece request = null;
    if (!isSelective) {
      request = request(hasPiece, peerPieces);
    } else if (isPrioritizing) {
      request = request(priorityPieces, peerPieces);
      if (request == null) {
        request = request(interestedPieces, peerPieces);
      }
    } else {
      request = request(interestedPieces, peerPieces);
    }
    return request;
  }

  private Piece request(boolean[] compare, boolean[] peerPieces) {
    boolean isInterested = false;
    if (compare == hasPiece) {
      for (int i = 0; i < peerPieces.length; i++) {
        if (!hasPiece[i] && peerPieces[i]) {
          isInterested = true;
          break;
        }
      }
    } else {
      for (int i = 0; i < peerPieces.length; i++) {
        if (compare[i] && peerPieces[i]) {
          isInterested = true;
          break;
        }
      }
    }

    if (!isInterested) {
      return null;
    }

    boolean hasIncompletePiece = false;
    for (int i = 0; i < incompletePieces.size(); i++) {
      Piece piece = (Piece) incompletePieces.get(i);
      if (peerPieces[piece.getNumber()]) {
        hasIncompletePiece = true;
        break;
      }
    }

    if (hasIncompletePiece) {
      if (incompletePieces.size() == 0) {
        return hasPiece == compare ? getRarePiece(peerPieces)
            : getRarePiece(compare, peerPieces);
      }
      Piece piece = (Piece) incompletePieces.get(ConnectionPool.RANDOM
          .nextInt(incompletePieces.size()));
      while (!peerPieces[piece.getNumber()]) {
        if (isCompleted) {
          return null;
        } else if (incompletePieces.size() == 0) {
          return hasPiece == compare ? getRarePiece(peerPieces)
              : getRarePiece(compare, peerPieces);
        }
        piece = (Piece) incompletePieces.get(ConnectionPool.RANDOM
            .nextInt(incompletePieces.size()));
      }
      return piece;
    } else {
      return hasPiece == compare ? getRarePiece(peerPieces)
          : getRarePiece(compare, peerPieces);
    }
  }

  private Piece getRarePiece(boolean[] peerPieces) {
    int min = pieceAvailability[0];
    for (int i = 1; i < peerPieces.length; i++) {
      if (!hasPiece[i] && peerPieces[i] && min > pieceAvailability[i]) {
        min = pieceAvailability[i];
      }
    }

    int size = pieces.size();
    int pieceNumber = ConnectionPool.RANDOM.nextInt(size);
    while (hasPiece[pieceNumber] || pieceAvailability[pieceNumber] != min
        || !peerPieces[pieceNumber]) {
      if (isCompleted) {
        return null;
      }
      pieceNumber = ConnectionPool.RANDOM.nextInt(size);
    }
    return (Piece) pieces.get(pieceNumber);
  }

  private Piece getRarePiece(boolean[] compare, boolean[] peerPieces) {
    int min = pieceAvailability[0];
    for (int i = 1; i < peerPieces.length; i++) {
      if (!hasPiece[i] && compare[i] && peerPieces[i]
          && min > pieceAvailability[i]) {
        min = pieceAvailability[i];
      }
    }

    int size = pieces.size();
    int pieceNumber = ConnectionPool.RANDOM.nextInt(size);
    while (hasPiece[pieceNumber] || pieceAvailability[pieceNumber] != min
        || !compare[pieceNumber] || !peerPieces[pieceNumber]) {
      if (isCompleted) {
        return null;
      }
      pieceNumber = ConnectionPool.RANDOM.nextInt(size);
    }
    return (Piece) pieces.get(pieceNumber);
  }

  String getPeerID() {
    return peerID;
  }

  byte[] getBitfield() {
    return bitfield;
  }

  /**
   * Used to indicate that the specified piece now has another user that has
   * it. This information is for identifying which pieces are rare amongst the
   * list of connected peers.
   *
   * @param piece
   *            the number of the piece
   */
  void updatePieceAvailability(int piece) {
    pieceAvailability[piece]++;
  }

  void addPieceAvailability(boolean[] peerPieces) {
    if (peerPieces.length != pieceAvailability.length) {
      throw new IllegalArgumentException("The length of the array is not " + pieceAvailability.length); //$NON-NLS-1$
    }
    for (int i = 0; i < pieceAvailability.length; i++) {
      if (peerPieces[i]) {
        pieceAvailability[i]++;
      }
    }
  }

  void removePieceAvailability(boolean[] peerPieces) {
    if (peerPieces.length != pieceAvailability.length) {
      throw new IllegalArgumentException("The length of the array is not " + pieceAvailability.length); //$NON-NLS-1$
    }
    for (int i = 0; i < pieceAvailability.length; i++) {
      if (peerPieces[i]) {
        pieceAvailability[i]--;
      }
    }
  }

  void addToUploaded(long length) {
    uploaded += length;
  }

  /**
   * Sets the maximum number of connections that the host should attempt to
   * connect to. The default value is set to 50 although 30 peers should
   * already be plenty. This value should not be heightened unless there is a
   * good reason to do so as it will likely cause network congestions.
   *
   * @param maxConnections
   *            the maximum number of connections that should be used
   */
  public void setMaxConnections(int maxConnections) {
    connectionPool.setMaxConnections(maxConnections);
  }

  /**
   * Setup file download priority levels and whether a file should even be
   * downloaded at all.
   *
   * @param downloadChoices
   *            an integer array which stores a value greater than zero if the
   *            file should have a high priority, a value equal to zero if it
   *            should have a regular priority, or less than zero if it should
   *            not be downloaded at all, the values should correspond to the
   *            files returned from {@link TorrentFile}'s
   *            {@link TorrentFile#getFilenames()} method
   */
  public synchronized void setFilesToDownload(int[] downloadChoices) {
    if (files.length != downloadChoices.length) {
      throw new IllegalArgumentException("The provided array should be of length " + files.length); //$NON-NLS-1$
    }

    for (int i = 0; i < downloadChoices.length; i++) {
      int[] pieces = files[i].getPieces();
      for (int j = 0; j < pieces.length; j++) {
        if (downloadChoices[i] > 0) {
          priorityPieces[j] = true;
        } else if (downloadChoices[i] == 0) {
          interestedPieces[j] = true;
        } else {
          uninterestedPieces[j] = uninterestedPieces[j] || false;
        }
      }
    }

    if (isSelective) {
      return;
    }

    for (int i = 0; i < downloadChoices.length; i++) {
      if (downloadChoices[i] > 0) {
        isPrioritizing = true;
        break;
      }
    }

    for (int i = 0; i < downloadChoices.length; i++) {
      for (int j = i + 1; j < downloadChoices.length; j++) {
        if (downloadChoices[i] != downloadChoices[j]) {
          isSelective = true;
          return;
        }
      }
    }
    isSelective = false;
  }

  public void setMaxDownloadSpeed(long maximum) {
    if (maximum < 1) {
      maxDownSpeed = -1;
      requestDownSpeed = -1;
    } else {
      maxDownSpeed = maximum;
      requestDownSpeed = maximum;
    }
  }

  public void setMaxUploadSpeed(long maximum) {
    if (maximum < 1) {
      maxUpSpeed = -1;
      requestDownSpeed = -1;
    } else {
      maxUpSpeed = maximum;
      requestUpSpeed = maximum;
    }
  }

  long getMaxDownloadSpeed() {
    return maxDownSpeed;
  }

  long getMaxUploadSpeed() {
    return maxUpSpeed;
  }

  long getDownloadRequestSpeed() {
    return requestDownSpeed;
  }

  long getUploadRequestSpeed() {
    return requestUpSpeed;
  }

  void updateDownloadRequestSpeed(int amount) {
    if (requestDownSpeed == -1) {
      return;
    }
    requestDownSpeed -= requestDownSpeed > amount ? amount
        : requestDownSpeed;
  }

  void updateUploadRequestSpeed(int amount) {
    if (requestUpSpeed == -1) {
      return;
    }
    requestUpSpeed -= requestUpSpeed > amount ? amount : requestUpSpeed;
  }

  /**
   * Retrieves the amount that has been downloaded thus far since the original
   * call to {@link #start()}.
   *
   * @return the amount of bytes that has been downloaded from peers
   */
  public long getDownloaded() {
    return downloaded;
  }

  /**
   * Returns the number of bytes that has been uploaded to peers thus far
   * since calling {@link #start()}.
   *
   * @return the amount of bytes that has been uploaded to peers
   */
  public long getUploaded() {
    return uploaded;
  }

  /**
   * Retreives the number of bytes that are required to complete the download.
   *
   * @return the number of bytes left to complete the download
   */
  public long getRemaining() {
    return remaining;
  }

  /**
   * Gets the downloading speed as calculated over a twenty second rolling
   * average.
   *
   * @return the speed at which bytes are being downloaded from peers
   */
  public long getDownSpeed() {
    return downSpeed;
  }

  /**
   * Retrieves the uploading speed per calculations over a twenty second
   * rolling average.
   *
   * @return the speed at which bytes are being uploaded to peers
   */
  public long getUpSpeed() {
    return upSpeed;
  }

  public long getTimeRemaining() {
    return isCompleted ? 0 : downSpeed == 0 ? -1 : Math.round(remaining
        / downSpeed);
  }

  /**
   * Retrieves the amount of data that has been discarded thus far. This is
   * caused by pieces that has failed the integrity hash check.
   *
   * @return the amount of bytes that has been discarded
   */
  public long getDiscarded() {
    return discarded;
  }

  /**
   * Retrieves the number of peers that connections have been created for thus
   * far.
   *
   * @return the number of connected peers
   */
  public int getConnectedPeers() {
    return connectionPool.getConnected();
  }

  /**
   * Returns the number of seeds that are currently assisting with the
   * distribution.
   *
   * @return the number of connected seeds, if the value is <code>-1</code>,
   *         the tracker does not support the distribution of this information
   * @see #getPeers()
   */
  public int getSeeds() {
    return seeders;
  }

  /**
   * Returns the total number of peers that are downloading the torrent that
   * this <code>Host</code> is associated with.
   *
   * @return the total number of connected peers on the torrent, if the value
   *         is <code>-1</code>, the tracker does not support the
   *         distribution of this information
   * @see #getSeeds()
   */
  public int getPeers() {
    return peers;
  }

  /**
   * Retrieves the torrent that was used to create this <code>Host</code>.
   *
   * @return the <code>Torrent</code> associated with this
   */
  public TorrentFile getTorrentFile() {
    return torrent;
  }

  /**
   * Retrieves the current state in which the host is currently in. This could
   * be any one of the states provided by the {@link ITorrentStateListener}
   * interface.
   *
   * @return the state that the host is currently in
   * @see ITorrentStateListener#STARTED
   * @see ITorrentStateListener#EXCHANGING
   * @see ITorrentStateListener#STOPPED
   * @see ITorrentStateListener#FINISHED
   */
  public int getState() {
    return state;
  }

  /**
   * Adds the specified listener to the collection of listeners within this
   * host if it is not already contained. The listener will be notified of the
   * changes of the current state of the torrent's activity. The event's state
   * will correspond to the value returned from {@link #getState()}.
   *
   * @param listener
   *            the listener to notify
   * @throws IllegalArgumentException
   *             If <code>listener</code> is <code>null</code>
   */
  public void addTorrentStateListener(ITorrentStateListener listener)
      throws IllegalArgumentException {
    synchronized (stateListeners) {
      if (!stateListeners.contains(listener)) {
        stateListeners.add(listener);
      }
    }
  }

  /**
   * Adds the specified listener to the collection of listeners within this
   * host if it is not already contained. The listener will be notified when
   * another piece has been completed by verifying it against a hash sum.
   *
   * @param listener
   *            the listener to notify
   * @throws IllegalArgumentException
   *             If <code>listener</code> is <code>null</code>
   */
  public void addTorrentProgressListener(ITorrentProgressListener listener)
      throws IllegalArgumentException {
    synchronized (progressListeners) {
      if (!progressListeners.contains(listener)) {
        progressListeners.add(listener);
      }
    }
  }

  /**
   * Adds the specified listener to the collection of listeners within this
   * host if it is not already contained. The listener will be notified when a
   * piece has downloaded some amount of additional bytes.
   *
   * @param listener
   *            the listener to notify
   * @throws IllegalArgumentException
   *             If <code>listener</code> is <code>null</code>
   */
  public void addPieceProgressListener(IPieceProgressListener listener)
      throws IllegalArgumentException {
    synchronized (pieceListeners) {
      if (!pieceListeners.contains(listener)) {
        pieceListeners.add(listener);
      }
    }
  }

  /**
   * Adds the specified listener to the collection of listeners within this
   * host if it is not already contained. The listener will be notified when
   * errors such as tracker failures or hash check failures occurs.
   *
   * @param listener
   *            the listener to notify
   * @throws IllegalArgumentException
   *             If <code>listener</code> is <code>null</code>
   */
  public void addTorrentErrorListener(ITorrentErrorListener listener)
      throws IllegalArgumentException {
    synchronized (errorListeners) {
      if (!errorListeners.contains(listener)) {
        errorListeners.add(listener);
      }
    }
  }

  public void addHashCheckListener(IHashCheckListener listener) {
    synchronized (hashCheckListeners) {
      if (!hashCheckListeners.contains(listener)) {
        hashCheckListeners.add(listener);
      }
    }
  }

  public boolean removeTorrentStateListener(ITorrentStateListener listener) {
    synchronized (stateListeners) {
      return stateListeners.remove(listener);
    }
  }

  public boolean removeTorrentProgressListener(
      ITorrentProgressListener listener) {
    synchronized (progressListeners) {
      return progressListeners.remove(listener);
    }
  }

  public boolean removePieceProgressListener(IPieceProgressListener listener) {
    synchronized (pieceListeners) {
      return pieceListeners.remove(listener);
    }
  }

  public boolean removeTorrentErrorListener(ITorrentErrorListener listener) {
    synchronized (errorListeners) {
      return errorListeners.remove(listener);
    }
  }

  public boolean removeHashCheckListener(IHashCheckListener listener) {
    synchronized (hashCheckListeners) {
      return hashCheckListeners.remove(listener);
    }
  }

  private synchronized void fireStateChangedEvent(int state) {
    this.state = state;
    for (int i = 0; i < stateListeners.size(); i++) {
      ((ITorrentStateListener) stateListeners.get(i)).stateChanged(state);
    }
  }

  private void fireBlockDownloadedEvent(int piece, int index, int blockLength) {
    synchronized (pieceListeners) {
      for (int i = 0; i < pieceListeners.size(); i++) {
        ((IPieceProgressListener) pieceListeners.get(i))
            .blockDownloaded(piece, index, blockLength);
      }
    }
  }

  private void firePieceCompletedEvent(int completed) {
    synchronized (progressListeners) {
      for (int i = 0; i < progressListeners.size(); i++) {
        ((ITorrentProgressListener) progressListeners.get(i))
            .pieceCompleted(completed);
      }
    }
  }

  private void fireTrackerErrorEvent(String message) {
    synchronized (errorListeners) {
      for (int i = 0; i < errorListeners.size(); i++) {
        ((ITorrentErrorListener) errorListeners.get(i))
            .trackerError(message);
      }
    }
  }

  private void firePieceDiscardEvent(int piece, int pieceLength) {
    synchronized (errorListeners) {
      for (int i = 0; i < errorListeners.size(); i++) {
        ((ITorrentErrorListener) errorListeners.get(i)).pieceDiscarded(
            piece, pieceLength);
      }
    }
  }

  private void fireHashCheckedEvent(int piece) {
    synchronized (hashCheckListeners) {
      for (int i = 0; i < hashCheckListeners.size(); i++) {
        ((IHashCheckListener) hashCheckListeners.get(i))
            .hashChecked(piece);
      }
    }
  }

  /**
   * Loads the stored information regarding each piece's status information
   * and updates the torrent as such.
   *
   * @param states
   *            a specification of how far and how much each piece has
   *            downloaded
   * @throws IllegalArgumentException
   *             if the provided array's length is not equal to the number of
   *             pieces
   */
  private void setPieces(PieceState[] states) {
    if (this.states.length != states.length) {
      throw new IllegalArgumentException("The array's size should be " //$NON-NLS-1$
          + this.states.length);
    }
    this.states = states;
    for (int i = 0; i < states.length; i++) {
      Piece piece = (Piece) pieces.get(i);
      if (piece.isComplete()) {
        completedPieces--;
        hasPiece[i] = false;
      }
      remaining += piece.getWritten();
      piece.setState(states[i]);
      int written = piece.getWritten();
      remaining -= written;
      if (written == piece.getLength()) {
        completedPieces++;
        hasPiece[i] = true;
        incompletePieces.remove(piece);
      } else if (written != 0 && !incompletePieces.contains(piece)) {
        incompletePieces.add(piece);
      }
    }
    updateBitfield();

    for (int i = 0; i < hasPiece.length; i++) {
      if (!hasPiece[i]) {
        return;
      }
    }
    isCompleted = true;
  }

  private class HashCheckThread extends Thread {

    private HashCheckThread() {
      super("Hash Check Thread - " + torrent.getName()); //$NON-NLS-1$
    }

    private void cleanup() {
      isHashChecking = false;
      fireStateChangedEvent(ITorrentStateListener.STOPPED);
    }

    public void run() {
      try {
        int read = 0;
        int count = 0;
        ByteBuffer buffer = ByteBuffer.allocate(pieceLength);
        for (int i = 0; i < files.length; i++) {
          FileChannel channel = files[i].getChannel();
          while ((read += channel.read(buffer)) == pieceLength) {
            if (isInterrupted()) {
              cleanup();
              return;
            }
            Piece piece = (Piece) pieces.get(count);
            if (piece.isComplete()) {
              completedPieces--;
              hasPiece[i] = false;
            }
            remaining += piece.getWritten();
            buffer.rewind();
            if (torrent.getPieces()[count]
                .equals(new String(shaDigest.digest(buffer
                    .array()), "ISO-8859-1"))) { //$NON-NLS-1$
              piece.setAsCompleted();
              hasPiece[count] = true;
              completedPieces++;
              remaining -= piece.getLength();
            } else {
              piece.reset();
            }
            incompletePieces.remove(piece);
            fireHashCheckedEvent(count);
            count++;
            read = 0;
          }
        }

        if (read > 0) {
          if (isInterrupted()) {
            cleanup();
            return;
          }
          Piece piece = (Piece) pieces.get(count);
          if (piece.isComplete()) {
            completedPieces--;
            hasPiece[count] = false;
          }
          remaining += piece.getWritten();
          buffer.rewind();
          shaDigest.update(buffer.array(), 0, read);
          if (torrent.getPieces()[count].equals(new String(shaDigest
              .digest(), "ISO-8859-1"))) { //$NON-NLS-1$
            hasPiece[count] = true;
            piece.setAsCompleted();
            completedPieces++;
            remaining -= piece.getLength();
          } else {
            piece.reset();
          }
          incompletePieces.remove(piece);
          fireHashCheckedEvent(count);
        }

        updateBitfield();
        saveState();
        if (isInterrupted()) {
          cleanup();
          return;
        }
        isHashChecking = false;
        if (isWaitingToStart) {
          try {
            TorrentManager.this.start();
          } catch (IOException e) {
            throw new RuntimeException(e);
          }
        }
      } catch (ClosedByInterruptException e) {
        cleanup();
        try {
          saveState();
        } catch (IOException ex) {
          throw new RuntimeException(ex);
        }
      } catch (IOException e) {
        cleanup();
        throw new RuntimeException(e);
      }
    }
  }

  private class TrackerThread extends Thread {

    private TrackerThread() {
      super("Tracker Thread - " + torrent.getName()); //$NON-NLS-1$
    }

    public void run() {
      while (true) {
        try {
          Thread.sleep(timeout);
          queryTracker(null);
        } catch (IOException e) {
          throw new RuntimeException(e);
        } catch (InterruptedException e) {
          return;
        }
      }
    }
  }

  private class SpeedMonitoringThread extends Thread {

    private SpeedMonitoringThread() {
      super("Speed Monitoring Thread - " + torrent.getName()); //$NON-NLS-1$
    }

    public void run() {
      long totalDown;
      long totalUp;
      long lastDownloaded = downloaded;
      long lastUploaded = uploaded;
      long[] downloads = new long[20];
      long[] uploads = new long[20];

      while (true) {
        for (int i = 0; i < 20; i++) {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            return;
          }
          downloads[i] = downloaded - lastDownloaded;
          uploads[i] = uploaded - lastUploaded;

          totalDown = 0;
          totalUp = 0;
          for (int j = 0; j < 20; j++) {
            totalDown += downloads[j];
            totalUp += uploads[j];
          }

          downSpeed = totalDown / 20;
          upSpeed = totalUp / 20;
          lastDownloaded = downloaded;
          lastUploaded = uploaded;

          requestDownSpeed = maxDownSpeed;
          requestUpSpeed = maxUpSpeed;
        }
      }
    }
  }
}
TOP

Related Classes of org.eclipse.ecf.protocol.bittorrent.internal.net.TorrentManager$SpeedMonitoringThread

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.