// This software is released into the Public Domain. See copying.txt for details.
package org.openstreetmap.osmosis.core.store;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.LinkedList;
import java.util.List;
/**
* Wraps a random access file adding buffered input stream capabilities. This
* allows a file to be randomly accessed while providing performance
* improvements over the non-buffered random access file implementation.
*
* @author Brett Henderson
*/
public class BufferedRandomAccessFileInputStream extends InputStream {
private static final int DEFAULT_BUFFER_COUNT = 4;
private static final int DEFAULT_INITIAL_BUFFER_SIZE = 16;
private static final int DEFAULT_MAXIMUM_BUFFER_SIZE = 4096;
private static final float DEFAULT_BUFFER_INCREASE_FACTOR = 2;
private RandomAccessFile randomFile;
private int bufferCount;
private List<BufferedReader> readerList;
/**
* Creates a new instance.
*
* @param file
* The file to be read.
* @throws FileNotFoundException
* if the file cannot be opened.
*/
public BufferedRandomAccessFileInputStream(File file) throws FileNotFoundException {
this(
file,
DEFAULT_BUFFER_COUNT,
DEFAULT_INITIAL_BUFFER_SIZE,
DEFAULT_MAXIMUM_BUFFER_SIZE,
DEFAULT_BUFFER_INCREASE_FACTOR);
}
/**
* Creates a new instance.
*
* @param file
* The file to be read.
* @param bufferCount
* The number of buffers to use. Use of multiple buffers allows
* some random seeks to occur without losing existing buffered
* data that might be returned to.
* @param initialBufferSize
* After a seek, this is the number of bytes that will be
* initially read.
* @param maxBufferSize
* This is the maximum number of bytes that will ever be read
* from the underlying file at a time.
* @param bufferIncreaseFactor
* During sequential reads, if the buffer is exhausted the next
* read will be longer than the previous read according to this
* factor. A value of 1 means the buffer never gets larger. The
* buffer will never get larger than maxBufferSize.
* @throws FileNotFoundException
* if the file cannot be opened.
*/
public BufferedRandomAccessFileInputStream(
File file, int bufferCount, int initialBufferSize,
int maxBufferSize, float bufferIncreaseFactor) throws FileNotFoundException {
this.bufferCount = bufferCount;
randomFile = new RandomAccessFile(file, "r");
readerList = new LinkedList<BufferedReader>();
for (int i = 0; i < bufferCount; i++) {
readerList.add(new BufferedReader(randomFile, initialBufferSize, maxBufferSize, bufferIncreaseFactor));
}
}
/**
* {@inheritDoc}
*/
@Override
public int read() throws IOException {
return readerList.get(0).read();
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
/**
* {@inheritDoc}
*/
@Override
public int read(byte[] b, int off, int len) throws IOException {
return readerList.get(0).read(b, off, len);
}
/**
* Seeks to the specified position in the file.
*
* @param pos
* The position within the file to seek to.
* @throws IOException
* if an error occurs during seeking.
*/
public void seek(long pos) throws IOException {
BufferedReader reader;
reader = null;
for (int i = 0; i < bufferCount; i++) {
BufferedReader tmpReader;
tmpReader = readerList.get(i);
if (tmpReader.containsPosition(pos)) {
reader = tmpReader;
if (i > 0) {
readerList.remove(i);
readerList.add(0, reader);
}
break;
}
}
if (reader == null) {
reader = readerList.remove(bufferCount - 1);
readerList.add(0, reader);
}
reader.seek(pos);
}
/**
* Returns the length of the data file.
*
* @return The file length in bytes.
* @throws IOException
* if an error occurs during the length operation.
*/
public long length() throws IOException {
return randomFile.length();
}
/**
* Returns the current read position in the data file.
*
* @return The current file offset in bytes.
* @throws IOException
* if an error occurs during the position operation.
*/
public long position() throws IOException {
return readerList.get(0).position();
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws IOException {
randomFile.close();
}
private static class BufferedReader {
private RandomAccessFile randomFile;
private int initialBufferSize;
private int maxBufferSize;
private float bufferIncreaseFactor;
private byte[] buffer;
private long bufferFilePosition;
private int currentBufferSize;
private int currentBufferByteCount;
private int currentBufferOffset;
/**
* Creates a new instance.
*
* @param randomFile
* The file to be read.
* @param initialBufferSize
* After a seek, this is the number of bytes that will be
* initially read.
* @param maxBufferSize
* This is the maximum number of bytes that will ever be read
* from the underlying file at a time.
* @param bufferIncreaseFactor
* During sequential reads, if the buffer is exhausted the next
* read will be longer than the previous read according to this
* factor. A value of 1 means the buffer never gets larger. The
* buffer will never get larger than maxBufferSize.
* @throws FileNotFoundException
* if the file cannot be opened.
*/
public BufferedReader(
RandomAccessFile randomFile, int initialBufferSize,
int maxBufferSize, float bufferIncreaseFactor) {
this.randomFile = randomFile;
this.initialBufferSize = initialBufferSize;
this.maxBufferSize = maxBufferSize;
this.bufferIncreaseFactor = bufferIncreaseFactor;
buffer = new byte[maxBufferSize];
bufferFilePosition = 0;
currentBufferSize = 0;
currentBufferByteCount = 0;
currentBufferOffset = 0;
}
/**
* Returns the current read position in the data file.
*
* @return The current file offset in bytes.
* @throws IOException
* if an error occurs during the position operation.
*/
public long position() throws IOException {
return bufferFilePosition + currentBufferOffset;
}
/**
* Indicates if the specified position is contained within the current
* buffer. Note that the requested position may not be currently loaded
* in the buffer, it may be within maxBufferSize bytes of the buffer
* start position.
*
* @param position
* The requested file position.
* @return True if the position is within maxBufferSize bytes of the
* buffer start.
*/
public boolean containsPosition(long position) {
return (position >= bufferFilePosition) && (position < (bufferFilePosition + maxBufferSize));
}
/**
* Seeks to the specified position in the file.
*
* @param pos
* The position within the file to seek to.
* @throws IOException
* if an error occurs during seeking.
*/
public void seek(long pos) throws IOException {
long bufferBeginFileOffset;
long bufferEndFileOffset;
bufferBeginFileOffset = bufferFilePosition;
bufferEndFileOffset = bufferFilePosition + currentBufferByteCount;
// If the requested position is within the current buffer just move to
// that position.
if ((pos >= bufferBeginFileOffset) && pos <= (bufferEndFileOffset)) {
// The request position is within the current buffer so just move to
// that position.
currentBufferOffset = (int) (pos - bufferBeginFileOffset);
} else if ((pos < bufferBeginFileOffset) && (bufferBeginFileOffset - pos) <= maxBufferSize) {
// The request position is within a max buffer size of the beginning
// of the buffer so just move there without resetting the current
// buffer size.
randomFile.seek(pos);
bufferFilePosition = pos;
// Mark the current buffer as empty so that the next read will read
// from disk.
currentBufferByteCount = 0;
} else if ((pos > bufferEndFileOffset) && (pos - bufferEndFileOffset) <= maxBufferSize) {
// The request position is within a max buffer size of the end
// of the buffer so just move there without resetting the current
// buffer size.
randomFile.seek(pos);
bufferFilePosition = pos;
// Mark the current buffer as empty so that the next read will read
// from disk.
currentBufferByteCount = 0;
} else {
// The request position is not close to the current position so move
// there and reset the buffer completely.
randomFile.seek(pos);
bufferFilePosition = pos;
currentBufferSize = 0;
currentBufferByteCount = 0;
currentBufferOffset = 0;
}
}
/**
* Ensures data is available in the buffer.
*
* @return True if data is available. False indicates that the end of stream
* has been reached.
*/
private boolean populateBuffer() throws IOException {
if (currentBufferOffset >= currentBufferByteCount) {
// If another buffered reader has moved the file position, we need
// to move it back.
if (randomFile.getFilePointer() != (bufferFilePosition + currentBufferByteCount)) {
randomFile.seek(bufferFilePosition + currentBufferByteCount);
}
// Update the current buffer file position to be at the
// beginning of the next read location.
bufferFilePosition += currentBufferByteCount;
currentBufferOffset = 0;
if (currentBufferSize == 0) {
currentBufferSize = initialBufferSize;
} else if (currentBufferSize < maxBufferSize) {
currentBufferSize = (int) (currentBufferSize * bufferIncreaseFactor);
if (currentBufferSize > maxBufferSize) {
currentBufferSize = maxBufferSize;
}
}
currentBufferByteCount = randomFile.read(buffer, 0, currentBufferSize);
if (currentBufferByteCount < 0) {
return false;
}
}
return true;
}
/**
* Reads the next byte from the underlying stream.
*
* @return The next byte.
* @throws IOException
* if an error occurs.
*/
public int read() throws IOException {
if (populateBuffer()) {
return buffer[currentBufferOffset++] & 0xff;
} else {
return -1;
}
}
/**
* Reads an array of bytes from the underlying stream.
*
* @param b
* The buffer to fill.
* @param off
* The offset to fill the buffer from.
* @param len
* The number of bytes to read.
* @return The number of bytes read.
* @throws IOException
* if an error occurs.
*/
public int read(byte[] b, int off, int len) throws IOException {
if (populateBuffer()) {
int readLength;
// Determine how many bytes to read from the current buffer.
readLength = currentBufferByteCount - currentBufferOffset;
if (readLength > len) {
readLength = len;
}
// Copy the bytes into the output buffer and update the current buffer position.
System.arraycopy(buffer, currentBufferOffset, b, off, readLength);
currentBufferOffset += readLength;
return readLength;
} else {
return -1;
}
}
}
}