/******************************************************************************
* Copyright (c) 2006 Remy Suen. 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, and also the MIT license, which
* also accompanies this distribution. This dual licensing scheme allows a
* developer to choose either license for use when developing applications with
* this code.
*
* Contributors:
* Remy Suen <remy.suen@gmail.com> - initial API and implementation
******************************************************************************/
package org.eclipse.bittorrent.internal.torrent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Vector;
/**
* A <code>Piece</code> is a section of data specified by the torrent
* metainfo. Each piece has a corresponding SHA-1 hash which is used to verify
* the integrity of the data that has been received from peers.
*/
public class Piece {
/**
* The amount of data to request when asking peers for data. The value is
* set at 16384, which is equal to 2^14.
*/
private static final int BLOCK_REQUEST_SIZE = 16384;
/**
* An <code>ArrayList</code> that contains {@link DataFile}(s) that this
* piece corresponds to.
*/
private final ArrayList files;
/**
* An <code>ArrayList</code> that contains <code>Integer</code>(s)
* which corresponsd to the length of data of each {@link DataFile} within
* {@link #files} that this piece represents.
*/
private final ArrayList fileLengths;
/**
* This piece's number.
*/
private final int number;
/**
* This piece's {@link PieceState} which stores information pertaining to
* the amount of data that has been written for each block.
*/
private PieceState state;
/**
* An array that corresponds to the number of bytes that has been written
* for a specific block.
*/
private int[] writtenBlocks;
/**
* An array that indicates whether a specific block has been requested from
* a peer or not.
*/
private boolean[] requested;
/**
* An array that indicates whether a specific block has been completed or
* not.
*/
private boolean[] completed;
/**
* The length of this piece.
*/
private int length = -1;
/**
* The number of {@link Block}s that this piece contains.
*/
private int blocks;
/**
* Indicates whether this piece is the final piece specified by the
* torrent's metainfo.
*/
private boolean isLastPiece;
/**
* Creates a new <code>Piece</code> with the provided {@link PieceState}
* to store information in and the specified number to represent.
*
* @param state
* the <code>PieceStaet</code> to use to store information
* @param number
* the number of this piece
* @throws IllegalArgumentException
* If <code>number < 0 </code> returns <code>true</code>
*/
public Piece(PieceState state, int number) throws IllegalArgumentException {
if (number < 0) {
throw new IllegalArgumentException("A piece number cannot be "
+ "negative");
}
this.state = state;
this.number = number;
files = new ArrayList();
fileLengths = new ArrayList();
}
/**
* Sets the length of this piece. The length of a piece is the amount of
* bytes that it represents.
*
* @param length
* the length of this piece
* @throws IllegalArgumentException
* If the specified length is negative
*/
public void setLength(int length) throws IllegalArgumentException {
if (length < 0) {
throw new IllegalArgumentException("A piece's length cannot be "
+ "negative");
}
this.length = length;
isLastPiece = length % BLOCK_REQUEST_SIZE != 0;
blocks = (length / BLOCK_REQUEST_SIZE) + (isLastPiece ? 1 : 0);
requested = new boolean[blocks];
completed = new boolean[blocks];
writtenBlocks = new int[blocks];
}
/**
* Retrieves this piece's length.
*
* @return the length of this piece
*/
public int getLength() {
return length;
}
/**
* Removes all information regarding the data that has been written for this
* piece. This changes the state of this piece such that it is as if no data
* has been written and no blocks are currently being requested.
*/
public void reset() {
Arrays.fill(requested, false);
Arrays.fill(completed, false);
Arrays.fill(writtenBlocks, 0);
state.reset();
}
/**
* Retrieves the index offset position within a file at position
* <code>pos</code> within {@link #files} requires writing to.
*
* @param pos
* the position of the {@link DataFile} within <code>files</code>
* @param index
* the index within this piece that data is available for writing
* to
* @return the additional offset index that this piece corresponds to within
* the <code>DataFile</code> or <code>-1</code> if the offset
* does not pertain to that file
*/
private int getFileOffset(int pos, int index) {
if (files.size() == 1) {
return index;
}
int count = 0;
int offset = 0;
do {
offset += ((Integer) fileLengths.get(count++)).intValue();
} while (count <= pos);
if (index > offset) {
return -1;
} else {
pos = offset - ((Integer) fileLengths.get(count - 1)).intValue();
if (pos <= index && index < offset) {
return index - pos;
} else {
return -1;
}
}
}
/**
* Writes the bytes received from peers onto the corresponding files on the
* local file system.
*
* @param pieceIndex
* the index within this piece that the block of data received
* starts at
* @param block
* an array with a subsection that holds data received from the
* peer
* @param offset
* the starting offset index within <code>block</code> that the
* data for the files begins at
* @param length
* the length of bytes that has been received from the peer
* @return <code>true</code> if the writing has been performed,
* <code>false</code> otherwise
* @throws IOException
* If an I/O error occurs while attempting to write the data to
* the files
*/
public boolean write(int pieceIndex, byte[] block, int offset, int length)
throws IOException {
int blockIndex = pieceIndex / BLOCK_REQUEST_SIZE;
if (completed[blockIndex]) {
return false;
}
state.addDownloadedBlock(pieceIndex, length);
int[] ret = null;
// check to see if we're writing to this piece's last block
if (blockIndex == blocks - 1) {
int limit = isLastPiece ? this.length % BLOCK_REQUEST_SIZE
: BLOCK_REQUEST_SIZE;
ret = new int[] { offset, length, 0 };
for (int i = 0; i < files.size(); i++) {
ret = ((DataFile) files.get(i)).write(number, getFileOffset(i,
pieceIndex), block, ret);
if (ret == null) {
break;
}
pieceIndex += ret[2];
}
writtenBlocks[blockIndex] += length;
if (writtenBlocks[blockIndex] == limit) {
completed[blockIndex] = true;
}
} else {
ret = new int[] { offset, length, 0 };
for (int i = 0; i < files.size(); i++) {
ret = ((DataFile) files.get(i)).write(number, getFileOffset(i,
pieceIndex), block, ret);
if (ret == null) {
break;
}
pieceIndex += ret[2];
}
writtenBlocks[blockIndex] += length;
// if the entire block has been written, note this fact
if (writtenBlocks[blockIndex] == BLOCK_REQUEST_SIZE) {
completed[blockIndex] = true;
}
}
return true;
}
/**
* Retrieves the amount of bytes that has been written thus far for this
* piece.
*
* @return the amount of data written so far
*/
public int getWritten() {
int total = 0;
for (int i = 0; i < writtenBlocks.length; i++) {
total += writtenBlocks[i];
}
return total;
}
/**
* Adds a {@link DataFile} as being a part of this piece with the specified
* length as the length of data within the file that this piece holds.
*
* @param file
* the <code>DataFile</code> to be added as being a part of
* this piece
* @param length
* a length of bytes within <code>file</code> that is being
* contained by this piece
* @throws IllegalArgumentException
* If <code>(file == null || length < 0)</code> returns
* <code>true</code>
*/
public void addFile(DataFile file, int length)
throws IllegalArgumentException {
if (file == null) {
throw new IllegalArgumentException("The file cannot be null");
} else if (length < 0) {
throw new IllegalArgumentException("The length cannot be a "
+ "negative number");
}
files.add(file);
fileLengths.add(new Integer(length));
}
/**
* Returns whether the amount of data that this piece has written is equal
* to its length. Note that this method does not perform a hash check to see
* whether the data is corrupt or not.
*
* @return <code>true</code> if this piece has written data equal to its
* length, <code>false</code> otherwise
*/
public boolean isComplete() {
synchronized (completed) {
for (int i = 0; i < blocks; i++) {
if (!completed[i]) {
return false;
}
}
}
return true;
}
/**
* Sets this piece's state as being completed. All blocks are set as being
* completed and all the corresponding data as being written.
*/
public void setAsCompleted() {
Arrays.fill(requested, true);
Arrays.fill(completed, true);
if (isLastPiece) {
for (int i = 0; i < blocks - 1; i++) {
writtenBlocks[i] = BLOCK_REQUEST_SIZE;
}
writtenBlocks[blocks - 1] = length % BLOCK_REQUEST_SIZE;
} else {
for (int i = 0; i < blocks; i++) {
writtenBlocks[i] = BLOCK_REQUEST_SIZE;
}
}
state.setAsComplete(length);
}
/**
* Sets the state of this piece to a new state. This may set blocks within
* this piece as having been requested and completed.
*
* @param state
* the new state to set to
*/
public void setState(PieceState state) {
this.state = state;
Vector blocks = state.getBlocks();
for (int i = 0; i < blocks.size(); i++) {
Block block = (Block) blocks.get(i);
int index = block.getIndex() / BLOCK_REQUEST_SIZE;
if (isLastPiece && block.getIndex() % BLOCK_REQUEST_SIZE != 0) {
index++;
}
int length = block.getBlockLength();
int remainder = length % BLOCK_REQUEST_SIZE;
if (remainder == 0) {
for (int j = 0; j < length; j += BLOCK_REQUEST_SIZE) {
writtenBlocks[index] = BLOCK_REQUEST_SIZE;
requested[index] = true;
completed[index] = true;
index++;
}
} else {
for (int j = 0; j < length - BLOCK_REQUEST_SIZE; j += BLOCK_REQUEST_SIZE) {
writtenBlocks[index] = BLOCK_REQUEST_SIZE;
requested[index] = true;
completed[index] = true;
index++;
}
writtenBlocks[index] = remainder;
}
}
}
/**
* Retrieves this piece's number as specified by the torrent metadata file.
*
* @return this piece's number
*/
public int getNumber() {
return number;
}
/**
* Returns an array of size three with information about the next block of
* data that should be requested from a peer to complete this piece.
*
* @return an array of size three with the first index containing this
* piece's identification number, the second index with the starting
* index of this piece in which the data received should begin from,
* and the third index with the length of data that should be sent,
* if this array is <code>null</code>, this block has been
* completed and does not need to have anything requested
* @throws IllegalStateException
* If the length of this piece has not been set yet with
* {@link #setLength(int)}
*/
public synchronized int[] getRequestInformation()
throws IllegalStateException {
if (length == -1) {
throw new IllegalStateException("The length has not been set "
+ "yet for this piece");
} else if (isComplete()) {
return null;
}
boolean allRequested = true;
for (int i = 0; i < blocks; i++) {
if (!completed[i] && !requested[i]) {
allRequested = false;
break;
}
}
int random = -1;
if (!allRequested) {
random = (int) (Math.random() * blocks);
while (requested[random]) {
if (isComplete()) {
return null;
}
random = (int) (Math.random() * blocks);
}
requested[random] = true;
} else {
random = (int) (Math.random() * blocks);
while (completed[random]) {
if (isComplete()) {
return null;
}
random = (int) (Math.random() * blocks);
}
}
return new int[] {
number,
writtenBlocks[random] + random * BLOCK_REQUEST_SIZE,
(random == blocks - 1) ? (isLastPiece ? length
% BLOCK_REQUEST_SIZE : BLOCK_REQUEST_SIZE)
: BLOCK_REQUEST_SIZE };
}
}