/*
* #%L
* Common package for I/O and related utilities
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package loci.common;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A wrapper for buffered NIO logic that implements the IRandomAccess interface.
*
* @see IRandomAccess
* @see java.io.RandomAccessFile
*
* @author Chris Allan <callan at blackcat dot ca>
*/
public class NIOFileHandle extends AbstractNIOHandle {
// -- Constants --
/** Logger for this class. */
private static final Logger LOGGER =
LoggerFactory.getLogger(NIOFileHandle.class);
//-- Static fields --
/** Default NIO buffer size to facilitate buffered I/O. */
protected static int defaultBufferSize = 1048576;
/**
* Default NIO buffer size to facilitate buffered I/O for read/write streams.
*/
protected static int defaultRWBufferSize = 8192;
// -- Fields --
/** The random access file object backing this FileHandle. */
protected RandomAccessFile raf;
/** The file channel backed by the random access file. */
protected FileChannel channel;
/** The absolute position within the file. */
protected long position = 0;
/** The absolute position of the start of the buffer. */
protected long bufferStartPosition = 0;
/** The buffer size. */
protected int bufferSize;
/** The buffer itself. */
protected ByteBuffer buffer;
/** Whether or not the file is opened read/write. */
protected boolean isReadWrite = false;
/** The default map mode for the file. */
protected FileChannel.MapMode mapMode = FileChannel.MapMode.READ_ONLY;
/** The buffer's byte ordering. */
protected ByteOrder order;
/** Provider class for NIO byte buffers, allocated or memory mapped. */
protected NIOByteBufferProvider byteBufferProvider;
/** The original length of the file. */
private Long defaultLength;
// -- Constructors --
/**
* Creates a random access file stream to read from, and
* optionally to write to, the file specified by the File argument.
*/
public NIOFileHandle(File file, String mode, int bufferSize)
throws IOException
{
this.bufferSize = bufferSize;
validateMode(mode);
if (mode.equals("rw")) {
isReadWrite = true;
mapMode = FileChannel.MapMode.READ_WRITE;
}
raf = new RandomAccessFile(file, mode);
channel = raf.getChannel();
byteBufferProvider = new NIOByteBufferProvider(channel, mapMode);
buffer(position, 0);
// if we know the length won't change, cache the original length
if (mode.equals("r")) {
defaultLength = raf.length();
}
}
/**
* Creates a random access file stream to read from, and
* optionally to write to, the file specified by the File argument.
*/
public NIOFileHandle(File file, String mode) throws IOException {
this(file, mode,
mode.equals("rw") ? defaultRWBufferSize : defaultBufferSize);
}
/**
* Creates a random access file stream to read from, and
* optionally to write to, a file with the specified name.
*/
public NIOFileHandle(String name, String mode) throws IOException {
this(new File(name), mode);
}
// -- NIOFileHandle API methods --
/**
* Set the default buffer size for read-only files.
*
* Subsequent uses of the NIOFileHandle(String, String) and
* NIOFileHandle(File, String) constructors will use this buffer size.
*/
public static void setDefaultBufferSize(int size) {
defaultBufferSize = size;
}
/**
* Set the default buffer size for read/write files.
*
* Subsequent uses of the NIOFileHandle(String, String) and
* NIOFileHandle(File, String) constructors will use this buffer size.
*/
public static void setDefaultReadWriteBufferSize(int size) {
defaultRWBufferSize = size;
}
// -- FileHandle and Channel API methods --
/** Gets the random access file object backing this FileHandle. */
public RandomAccessFile getRandomAccessFile() {
return raf;
}
/** Gets the FileChannel from this FileHandle. */
public FileChannel getFileChannel() {
try {
channel.position(position);
}
catch (IOException e) {
LOGGER.warn("FileChannel.position failed", e);
}
return channel;
}
/** Gets the current buffer size. */
public int getBufferSize() {
return bufferSize;
}
// -- AbstractNIOHandle API methods --
/* @see AbstractNIOHandle.setLength(long) */
@Override
public void setLength(long length) throws IOException {
raf.seek(length - 1);
raf.write((byte) 0);
buffer = null;
}
// -- IRandomAccess API methods --
/* @see IRandomAccess.close() */
@Override
public void close() throws IOException {
raf.close();
}
/* @see IRandomAccess.getFilePointer() */
@Override
public long getFilePointer() {
return position;
}
/* @see IRandomAccess.length() */
@Override
public long length() throws IOException {
if (defaultLength != null) {
return defaultLength;
}
return raf.length();
}
/* @see IRandomAccess.getOrder() */
@Override
public ByteOrder getOrder() {
return buffer == null ? order : buffer.order();
}
/* @see IRandomAccess.setOrder(ByteOrder) */
@Override
public void setOrder(ByteOrder order) {
this.order = order;
if (buffer != null) {
buffer.order(order);
}
}
/* @see IRandomAccess.read(byte[]) */
@Override
public int read(byte[] b) throws IOException {
return read(ByteBuffer.wrap(b));
}
/* @see IRandomAccess.read(byte[], int, int) */
@Override
public int read(byte[] b, int off, int len) throws IOException {
return read(ByteBuffer.wrap(b), off, len);
}
/* @see IRandomAccess.read(ByteBuffer) */
@Override
public int read(ByteBuffer buf) throws IOException {
return read(buf, 0, buf.capacity());
}
/* @see IRandomAccess.read(ByteBuffer, int, int) */
@Override
public int read(ByteBuffer buf, int off, int len) throws IOException {
buf.position(off);
int realLength = (int) Math.min(len, length() - position);
if (realLength < 0) {
return -1;
}
buf.limit(off + realLength);
buffer(position, realLength);
position += realLength;
while (buf.hasRemaining()) {
try {
buf.put(buffer.get());
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
return realLength;
}
/* @see IRandomAccess.seek(long) */
@Override
public void seek(long pos) throws IOException {
if (mapMode == FileChannel.MapMode.READ_WRITE && pos > length()) {
setLength(pos);
}
buffer(pos, 0);
}
/* @see java.io.DataInput.readBoolean() */
@Override
public boolean readBoolean() throws IOException {
return readByte() == 1;
}
/* @see java.io.DataInput.readByte() */
@Override
public byte readByte() throws IOException {
buffer(position, 1);
position += 1;
try {
return buffer.get();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readChar() */
@Override
public char readChar() throws IOException {
buffer(position, 2);
position += 2;
try {
return buffer.getChar();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readDouble() */
@Override
public double readDouble() throws IOException {
buffer(position, 8);
position += 8;
try {
return buffer.getDouble();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readFloat() */
@Override
public float readFloat() throws IOException {
buffer(position, 4);
position += 4;
try {
return buffer.getFloat();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readFully(byte[]) */
@Override
public void readFully(byte[] b) throws IOException {
read(b);
}
/* @see java.io.DataInput.readFully(byte[], int, int) */
@Override
public void readFully(byte[] b, int off, int len) throws IOException {
read(b, off, len);
}
/* @see java.io.DataInput.readInt() */
@Override
public int readInt() throws IOException {
buffer(position, 4);
position += 4;
try {
return buffer.getInt();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readLine() */
@Override
public String readLine() throws IOException {
raf.seek(position);
String line = raf.readLine();
buffer(raf.getFilePointer(), 0);
return line;
}
/* @see java.io.DataInput.readLong() */
@Override
public long readLong() throws IOException {
buffer(position, 8);
position += 8;
try {
return buffer.getLong();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readShort() */
@Override
public short readShort() throws IOException {
buffer(position, 2);
position += 2;
try {
return buffer.getShort();
} catch (BufferUnderflowException e) {
EOFException eof = new EOFException(EOF_ERROR_MSG);
eof.initCause(e);
throw eof;
}
}
/* @see java.io.DataInput.readUnsignedByte() */
@Override
public int readUnsignedByte() throws IOException {
return readByte() & 0xFF;
}
/* @see java.io.DataInput.readUnsignedShort() */
@Override
public int readUnsignedShort() throws IOException {
return readShort() & 0xFFFF;
}
/* @see java.io.DataInput.readUTF() */
@Override
public String readUTF() throws IOException {
raf.seek(position);
String utf8 = raf.readUTF();
buffer(raf.getFilePointer(), 0);
return utf8;
}
/* @see java.io.DataInput.skipBytes(int) */
@Override
public int skipBytes(int n) throws IOException {
if (n < 1) {
return 0;
}
long oldPosition = position;
long newPosition = oldPosition + Math.min(n, length());
buffer(newPosition, 0);
return (int) (position - oldPosition);
}
// -- DataOutput API methods --
/* @see java.io.DataOutput.write(byte[]) */
@Override
public void write(byte[] b) throws IOException {
write(ByteBuffer.wrap(b));
}
/* @see java.io.DataOutput.write(byte[], int, int) */
@Override
public void write(byte[] b, int off, int len) throws IOException {
write(ByteBuffer.wrap(b), off, len);
}
/* @see IRandomAccess.write(ByteBuffer) */
@Override
public void write(ByteBuffer buf) throws IOException {
write(buf, 0, buf.capacity());
}
/* @see IRandomAccess.write(ByteBuffer, int, int) */
@Override
public void write(ByteBuffer buf, int off, int len) throws IOException {
writeSetup(len);
buf.limit(off + len);
buf.position(off);
position += channel.write(buf, position);
buffer = null;
}
/* @see java.io.DataOutput.write(int b) */
@Override
public void write(int b) throws IOException {
writeByte(b);
}
/* @see java.io.DataOutput.writeBoolean(boolean) */
@Override
public void writeBoolean(boolean v) throws IOException {
writeByte(v ? 1 : 0);
}
/* @see java.io.DataOutput.writeByte(int) */
@Override
public void writeByte(int v) throws IOException {
writeSetup(1);
buffer.put((byte) v);
doWrite(1);
}
/* @see java.io.DataOutput.writeBytes(String) */
@Override
public void writeBytes(String s) throws IOException {
write(s.getBytes(Constants.ENCODING));
}
/* @see java.io.DataOutput.writeChar(int) */
@Override
public void writeChar(int v) throws IOException {
writeSetup(2);
buffer.putChar((char) v);
doWrite(2);
}
/* @see java.io.DataOutput.writeChars(String) */
@Override
public void writeChars(String s) throws IOException {
write(s.getBytes("UTF-16BE"));
}
/* @see java.io.DataOutput.writeDouble(double) */
@Override
public void writeDouble(double v) throws IOException {
writeSetup(8);
buffer.putDouble(v);
doWrite(8);
}
/* @see java.io.DataOutput.writeFloat(float) */
@Override
public void writeFloat(float v) throws IOException {
writeSetup(4);
buffer.putFloat(v);
doWrite(4);
}
/* @see java.io.DataOutput.writeInt(int) */
@Override
public void writeInt(int v) throws IOException {
writeSetup(4);
buffer.putInt(v);
doWrite(4);
}
/* @see java.io.DataOutput.writeLong(long) */
@Override
public void writeLong(long v) throws IOException {
writeSetup(8);
buffer.putLong(v);
doWrite(8);
}
/* @see java.io.DataOutput.writeShort(int) */
@Override
public void writeShort(int v) throws IOException {
writeSetup(2);
buffer.putShort((short) v);
doWrite(2);
}
/* @see java.io.DataOutput.writeUTF(String) */
@Override
public void writeUTF(String str) throws IOException {
// NB: number of bytes written is greater than the length of the string
int strlen = str.getBytes(Constants.ENCODING).length + 2;
writeSetup(strlen);
raf.seek(position);
raf.writeUTF(str);
position += strlen;
buffer = null;
}
/**
* Aligns the NIO buffer, maps it if it is not currently and sets all
* relevant positions and offsets.
* @param offset The location within the file to read from.
* @param size The requested read length.
* @throws IOException If there is an issue mapping, aligning or allocating
* the buffer.
*/
private void buffer(long offset, int size) throws IOException {
position = offset;
long newPosition = offset + size;
if (newPosition < bufferStartPosition ||
newPosition > bufferStartPosition + bufferSize || buffer == null)
{
bufferStartPosition = offset;
if (length() > 0 && length() - 1 < bufferStartPosition) {
bufferStartPosition = length() - 1;
}
long newSize = Math.min(length() - bufferStartPosition, bufferSize);
if (newSize < size && newSize == bufferSize) newSize = size;
if (newSize + bufferStartPosition > length()) {
newSize = length() - bufferStartPosition;
}
offset = bufferStartPosition;
ByteOrder byteOrder = buffer == null ? order : getOrder();
buffer = byteBufferProvider.allocate(bufferStartPosition, (int) newSize);
if (byteOrder != null) setOrder(byteOrder);
}
buffer.position((int) (offset - bufferStartPosition));
if (buffer.position() + size > buffer.limit() &&
mapMode == FileChannel.MapMode.READ_WRITE)
{
buffer.limit(buffer.position() + size);
}
}
private void writeSetup(int length) throws IOException {
validateLength(length);
buffer(position, length);
}
private void doWrite(int length) throws IOException {
buffer.position(buffer.position() - length);
channel.write(buffer, position);
position += length;
}
}