/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io;
import java.io.IOException;
/**
* <code>BlockRandomInputStream</code> is a specialized-yet-still-abstract <code>RandomAccessInputStream</code> that
* is geared towards resources that are read block by block, either because of a particular constrain or for performance
* reasons. This class typically comes in handy for network resources such as HTTP which have to request a block range
* for reading the resource.
*
* <p>Seeking inside the file is implemented transparently by reading a block starting at the seek offset.
* If {@link #seek(long)} is called with an offset that is within the current block, no read occurs.
* The block size should be carefully chosen as it affects seek performance and thus overall performance greatly:
* the larger the block size, the more data is fetched when seeking outside the current block and consequently the
* longer it takes to reposition the stream. On the other hand, a larger block size will yield better performance when
* reading the resource sequentially, as it lessens the overhead of requesting a particular block.</p>
*
* @author Maxence Bernard
*/
public abstract class BlockRandomInputStream extends RandomAccessInputStream {
/** Block size, i.e. length of the {@link #block} array */
protected final int blockSize;
/** Contains the current file block. Data may end before the array does. */
private final byte block[];
/** Current offset within the block array to the next byte to return */
private int blockOff;
/** Length of the current block */
private int blockLen;
/** Global offset within the file */
private long offset;
/**
* Creates a new <code>BlockRandomInputStream</code> using the specified block size.
*
* <p>The block size should be carefully chosen as it affects seek performance and thus overall performance greatly:
* the larger the block size, the more data is fetched when seeking outside the current block and consequently the
* longer it takes to reposition the stream. On the other hand, a larger block size will yield better performance
* when reading the resource sequentially, as it lessens the overhead of requesting a particular block.</p>
*
* @param blockSize controls the amount of data requested when reading a block
*/
protected BlockRandomInputStream(int blockSize) {
this.blockSize = blockSize;
block = new byte[blockSize];
}
/**
* Returns <code>true</code> if the end of file has been reached.
*
* @return true if the end of file has been reached.
* @throws IOException if an I/O error occurred
*/
private boolean eofReached() throws IOException {
return offset>=getLength();
}
/**
* Checks if the current buffered block has been read completely (i.e. no more data is available) and if it has,
* calls {@link #readBlock(long, byte[], int)} to fetch the next block.
*
* @throws IOException if an I/O error occurred
*/
private void checkBuffer() throws IOException {
if(blockOff >= blockLen) // True initially
readBlock();
}
/**
* Calls {@link #readBlock(long, byte[], int)} to read a block of up to <code>blockSize</code>, less if the
* the end of file is near.
*
* @throws IOException if an I/O error occurred
*/
private void readBlock() throws IOException {
int len = Math.min((int)(getLength()-offset), blockSize);
// update len with the number of bytes actually read
len = readBlock(offset, block, len);
// Note: these fields won't be updated if an I/O error occurs
this.blockOff = 0;
this.blockLen = len;
}
////////////////////////////////////////////
// RandomAccessInputStream implementation //
////////////////////////////////////////////
@Override
public int read() throws IOException {
if(eofReached())
return -1;
checkBuffer();
int ret = block[blockOff];
blockOff++;
offset ++;
return ret;
}
@Override
public int read(byte b[], int off, int len) throws IOException {
if(len==0)
return 0;
if(eofReached())
return -1;
checkBuffer();
int nbBytes = Math.min(len, blockLen - blockOff);
System.arraycopy(block, blockOff, b, off, nbBytes);
blockOff += nbBytes;
offset += nbBytes;
return nbBytes;
}
public long getOffset() throws IOException {
return offset;
}
public void seek(long newOffset) throws IOException {
// If the new offset is within the current buffer's range, simply reposition the offsets
if(newOffset>=offset && newOffset<offset+ blockLen) {
blockOff += (int)(newOffset-offset);
offset = newOffset;
}
// If not, retrieve a block of data starting at the new offset and fill the buffer with it
else {
offset = newOffset;
readBlock();
}
}
///////////////////////
// Abstract methods //
///////////////////////
/**
* Reads a block, that spawns from <code>fileOffset</code> to <code>fileOffset+blockLen</code>, an returns
* the number of bytes that could be read, normally <code>blockLen</code> but can be less.
*
* <p>Note that <code>blockLen</code> may be smaller than {@link #blockSize} if the end of file is near, to prevent
* <code>EOF</code> from being reached. In other words, <code>fileOffset+blockLen</code> should theorically not
* exceed the file's length, but this could happen in the unlikely event that the file just shrinked after
* {@link #getLength()} was last called. So this method's implementation should handle the case where
* <code>EOF</code> is reached prematurely and return the number of bytes that were actually read.</p>
*
* @param fileOffset global file offset that marks the beginning of the block
* @param block the array to fill with data, starting at 0
* @param blockLen number of bytes to read
* @return the number of bytes that were actually read, normally blockLen unless
* @throws IOException if an I/O error occurred
*/
protected abstract int readBlock(long fileOffset, byte block[], int blockLen) throws IOException;
}