/******************************************************************************
* 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.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* A <code>DataFile</code> is a representation of a file that will be
* downloaded by a torrent.
*/
public class DataFile {
/**
* One of the files being downloaded by the torrent.
*/
private RandomAccessFile file;
/**
* An array of integers that indicates the pieces of a torrent file that
* this file represents.
*/
private int[] pieces;
/**
* The number of bytes that occupy a given piece
*/
private int[] pieceLengths;
/**
* The size of the file.
*/
private long length;
/**
* Constructs a <code>DataFile</code> to handle the reading and writing of
* pieces and blocks.
*
* @param aFile
* the file to wrap around
* @param length
* the length that the file should be, as specified by the
* metainfo stored within a <i>.torrent</i> file
* @throws IOException
* If an I/O error occurs while creating the wrapper around the
* file and specifying its length
*/
public DataFile(File aFile, long length) throws IOException {
file = new RandomAccessFile(aFile, "rw");
if (aFile.length() > length) {
aFile.delete();
}
if (aFile.length() != length) {
file.seek(length - 1);
file.write(0);
}
this.length = length;
}
/**
* Sets the piece numbers that this file represents and the first length of
* the piece and the length of subsequent pieces excluding the last.
*
* @param pieces
* an array of integers that specifies the pieces that this file
* is a part of
* @param initialLength
* the length of the first piece
* @param length
* the length of pieces after the second, excluding the final
* piece
* @throws IllegalArgumentException
* If <code>initialLength</code> is greater than
* <code>length</code> when there are more than two pieces
*/
public void setPieces(int[] pieces, int initialLength, int length)
throws IllegalArgumentException {
if (pieces.length > 2 && initialLength > length) {
throw new IllegalArgumentException("The first piece's length "
+ "cannot be larger than a regular piece's length");
}
this.pieces = pieces;
int numPieces = pieces.length;
pieceLengths = new int[numPieces];
pieceLengths[0] = 0;
// if there is only one piece, the length has been set and there is no
// need to set anymore additional values
if (numPieces == 1) {
return;
}
pieceLengths[1] = initialLength;
for (int i = 2; i < numPieces; i++) {
pieceLengths[i] = pieceLengths[i - 1] + length;
}
}
/**
* Writes data retrieved from a peer onto this file.
*
* @param piece
* the piece's number
* @param offset
* the offset for the given piece
* @param block
* the data that has been retrieved from the peer of which the
* contents may be written onto this file
* @param data
* an integer array with information on how to write the amount
* of information stored within <code>block</code>, the first
* value represents the amount written thus far, the second value
* represents the amount of available data to write, and the
* third value is the index within the piece itself
* @return the information to use for the next file that needs writing to,
* if applicable, if the returned value is <code>null</code>, no
* other files needs data written to
* @throws IllegalArgumentException
* If <code>piece</code> is not a part of this file, or if the
* provided offset to seek to to write to this file exceeds this
* file's length
* @throws IOException
* If an I/O error occurs while attempting to write to the file
*/
int[] write(int piece, int offset, byte[] block, int[] data)
throws IllegalArgumentException, IOException {
if (offset == -1) {
return data;
}
int index = indexOf(piece);
if (index == -1) {
throw new IllegalArgumentException();
}
int seek = pieceLengths[index] + offset;
if (seek >= this.length) {
throw new IllegalArgumentException("The seeking position cannot "
+ "be greater than this file's length");
}
synchronized (file) {
// move to the specified position and write to the file
file.seek(seek);
// check to see if there's more data to available to write than how
// large this file actually holds
if (seek + data[1] > this.length) {
// since there is an excessive amount of data, just take the
// difference
int write = (int) (this.length - seek);
file.write(block, data[0], write);
data[0] += write;
data[1] -= write;
data[2] = write;
return data;
} else {
file.write(block, data[0], data[1]);
return null;
}
}
}
/**
* Retrieves the data that a particular piece represents within this file. A
* piece can potentially be split between multiple files, so the array
* returned here may or may not be equal to the piece's length.
*
* @param piece
* the number of the interested piece
* @return the block of data that is represented by the interested piece, or
* <code>null</code> if this file does not contain the specified
* piece
* @throws IOException
* If an I/O error occurs while attempting to read the data from
* this file
*/
public byte[] getData(int piece) throws IOException {
// check to see whether this file contains this piece
int index = indexOf(piece);
if (index == -1) {
return null;
}
int dataLength = -1;
// if this is the last piece, retrieve the length by decrementing the
// length of this file and the starting length of the last piece
if (pieceLengths.length - 1 == index) {
dataLength = (int) (length - pieceLengths[index]);
} else {
// get the length by decrementing the length of the piece after it
// with the current piece
dataLength = pieceLengths[index + 1] - pieceLengths[index];
}
// create a new byte array to store the data so that it can be returned
byte[] data = new byte[dataLength];
synchronized (file) {
file.seek(pieceLengths[index]);
file.read(data, 0, dataLength);
}
return data;
}
/**
* Retrieves the length of this file.
*
* @return this file's length
*/
public long length() {
return length;
}
/**
* Checks to see if the piece is part of this file.
*
* @param piece
* the piece to check
* @return <code>true</code> if this file contains this piece,
* <code>false</code> otherwise
*/
public boolean containsPiece(int piece) {
if (pieces.length == 1) {
return pieces[0] == piece;
}
return pieces[0] <= piece && pieces[pieces.length - 1] >= piece;
}
/**
* Gets the position of the specified piece within {@link #pieces}.
*
* @param piece
* the piece's number
* @return the index of the provided piece's number in <code>pieces</code>,
* or <code>-1</code> if it could not be found
*/
private int indexOf(int piece) {
if (!containsPiece(piece)) {
return -1;
}
for (int i = 0; i < pieces.length; i++) {
if (pieces[i] == piece) {
return i;
}
}
return -1;
}
/**
* Retrieves the <code>FileChannel</code> associated with the file being
* wrapped.
*
* @return this file's <code>FileChannel</code>
*/
public FileChannel getChannel() {
return file.getChannel();
}
/**
* Gets an integer array that stores the numbers of all the pieces that are
* a part of this file.
*
* @return an array of integers with all of the piece's numbers that this
* file contains
*/
public int[] getPieces() {
return pieces;
}
}