org.scijava.io.handle.DataHandle Maven / Gradle / Ivy
Show all versions of scijava-common Show documentation
/*
* #%L
* SciJava Common shared library for SciJava software.
* %%
* Copyright (C) 2009 - 2017 Board of Regents of the University of
* Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck
* Institute of Molecular Cell Biology and Genetics, University of
* Konstanz, and KNIME GmbH.
* %%
* 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 org.scijava.io.handle;
import java.io.Closeable;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
import org.scijava.io.location.Location;
import org.scijava.plugin.WrapperPlugin;
/**
* A data handle is a plugin which provides both streaming and random
* access to bytes at a {@link Location} (e.g., files or arrays).
*
* @author Curtis Rueden
* @see DataHandleInputStream
* @see DataHandleOutputStream
*/
public interface DataHandle extends WrapperPlugin,
DataInput, DataOutput, Closeable
{
public enum ByteOrder {
LITTLE_ENDIAN, BIG_ENDIAN
}
/** Default block size to use when searching through the stream. */
int DEFAULT_BLOCK_SIZE = 256 * 1024; // 256 KB
/** Default bound on bytes to search when searching through the stream. */
int MAX_SEARCH_SIZE = 512 * 1024 * 1024; // 512 MB
/** Gets whether reading from this handle is supported. */
boolean isReadable();
/** Gets whether writing to this handle is supported. */
boolean isWritable();
/**
* Tests whether this handle's location actually exists at the source.
*
* @return True if the location exists; false if not.
* @throws IOException If something goes wrong with the existence check.
*/
boolean exists() throws IOException;
/**
* Gets the last modified timestamp of the location.
*
* @return The last modified timestamp, or null if the handle does not support
* this feature or if the location does not exist.
* @throws IOException If something goes wrong with the last modified check.
*/
default Date lastModified() throws IOException {
return null;
}
/**
* Gets a "fast" checksum which succinctly represents the contents of the data
* stream. The term "fast" here refers to the idea that the checksum be
* retrievable quickly, without actually performing a thorough computation
* across the entire data stream. Typically, such a thing is feasible because
* the checksum was calculated a priori; e.g., artifacts deployed to remote
* Maven repositories are always deployed with corresponding checksum files.
*
* No guarantee is made about the exact nature of the checksum (e.g., SHA-1 or
* MD5), only that the value is deterministic for this particular location
* with its current contents. In other words: if a checksum differs from a
* previous inquiry, you can be sure the contents have changed; conversely, if
* the checksum is still the same, the contents are highly likely to be
* unchanged.
*
*
* @return The checksum, or null if the handle does not support this feature.
* @throws IOException If something goes wrong when accessing the checksum.
*/
default String checksum() throws IOException {
return null;
}
/** Returns the current offset in the stream. */
long offset() throws IOException;
/**
* Sets the stream offset, measured from the beginning of the stream, at which
* the next read or write occurs.
*/
void seek(long pos) throws IOException;
/**
* Returns the length of the data in bytes.
*
* @return The length, or -1 if the length is unknown.
*/
long length() throws IOException;
/**
* Sets the new length of the handle.
*
* @param length New length.
* @throws IOException If there is an error changing the handle's length.
*/
void setLength(long length) throws IOException;
/**
* Gets the number of bytes which can be read from, or written to, the
* data handle, bounded by the specified number of bytes.
*
* In the case of reading, attempting to read the returned number of bytes is
* guaranteed not to throw {@link EOFException}. However, be aware that the
* following methods might still process fewer bytes than indicated
* by this method:
*
*
* - {@link #read(byte[])}
* - {@link #read(byte[], int, int)}
* - {@link #skip(long)}
* - {@link #skipBytes(int)}
*
*
* In the case of writing, attempting to write the returned number of bytes is
* guaranteed not to expand the length of the handle; i.e., the write will
* only overwrite bytes already within the handle's bounds.
*
*
* @param count Desired number of bytes to read/write.
* @return The actual number of bytes which could be read/written,
* which might be less than the requested value.
* @throws IOException If something goes wrong with the check.
*/
default long available(final long count) throws IOException {
final long remain = length() - offset();
return remain < count ? remain : count;
}
/**
* Ensures that the handle has sufficient bytes available to read.
*
* @param count Number of bytes to read.
* @see #available(long)
* @throws EOFException If there are insufficient bytes available.
* @throws IOException If the handle is write-only, or something goes wrong
* with the check.
*/
default void ensureReadable(final long count) throws IOException {
if (!isReadable()) throw new IOException("This handle is write-only.");
if (available(count) < count) throw new EOFException();
}
/**
* Ensures that the handle has the correct length to be written to, and
* extends it as required.
*
* @param count Number of bytes to write.
* @return {@code true} if the handle's length was sufficient, or
* {@code false} if the handle's length required an extension.
* @throws IOException If the handle is read-only, or something goes wrong
* with the check, or there is an error changing the handle's
* length.
*/
default boolean ensureWritable(final long count) throws IOException {
if (!isWritable()) throw new IOException("This handle is read-only.");
final long minLength = offset() + count;
if (length() < minLength) {
setLength(minLength);
return false;
}
return true;
}
/** Returns the byte order of the stream. */
ByteOrder getOrder();
/**
* Sets the byte order of the stream.
*
* @param order Order to set.
*/
void setOrder(ByteOrder order);
/**
* Returns true iff the stream's order is {@link ByteOrder#BIG_ENDIAN}.
*
* @see #getOrder()
*/
default boolean isBigEndian() {
return getOrder() == ByteOrder.BIG_ENDIAN;
}
/**
* Returns true iff the stream's order is {@link ByteOrder#LITTLE_ENDIAN}.
*
* @see #getOrder()
*/
default boolean isLittleEndian() {
return getOrder() == ByteOrder.LITTLE_ENDIAN;
}
/**
* Sets the endianness of the stream.
*
* @param little If true, sets the order to {@link ByteOrder#LITTLE_ENDIAN};
* otherwise, sets the order to {@link ByteOrder#BIG_ENDIAN}.
* @see #setOrder(ByteOrder)
*/
default void setLittleEndian(final boolean little) {
setOrder(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
}
/** Gets the native encoding of the stream. */
String getEncoding();
/** Sets the native encoding of the stream. */
void setEncoding(String encoding);
/** Reads a string of arbitrary length, terminated by a null char. */
default String readCString() throws IOException {
final String line = findString("\0");
return line.length() == 0 ? null : line;
}
/** Reads a string of up to length n. */
default String readString(final int n) throws IOException {
final int r = (int) available(n);
final byte[] b = new byte[r];
readFully(b);
return new String(b, getEncoding());
}
/**
* Reads a string ending with one of the characters in the given string.
*
* @see #findString(String...)
*/
default String readString(final String lastChars) throws IOException {
if (lastChars.length() == 1) return findString(lastChars);
final String[] terminators = new String[lastChars.length()];
for (int i = 0; i < terminators.length; i++) {
terminators[i] = lastChars.substring(i, i + 1);
}
return findString(terminators);
}
/**
* Reads a string ending with one of the given terminating substrings.
*
* @param terminators The strings for which to search.
* @return The string from the initial position through the end of the
* terminating sequence, or through the end of the stream if no
* terminating sequence is found.
*/
default String findString(final String... terminators) throws IOException {
return findString(true, DEFAULT_BLOCK_SIZE, terminators);
}
/**
* Reads or skips a string ending with one of the given terminating
* substrings.
*
* @param saveString Whether to collect the string from the current offset to
* the terminating bytes, and return it. If false, returns null.
* @param terminators The strings for which to search.
* @throws IOException If saveString flag is set and the maximum search length
* (512 MB) is exceeded.
* @return The string from the initial position through the end of the
* terminating sequence, or through the end of the stream if no
* terminating sequence is found, or null if saveString flag is unset.
*/
default String findString(final boolean saveString,
final String... terminators) throws IOException
{
return findString(saveString, DEFAULT_BLOCK_SIZE, terminators);
}
/**
* Reads a string ending with one of the given terminating substrings, using
* the specified block size for buffering.
*
* @param blockSize The block size to use when reading bytes in chunks.
* @param terminators The strings for which to search.
* @return The string from the initial position through the end of the
* terminating sequence, or through the end of the stream if no
* terminating sequence is found.
*/
default String findString(final int blockSize, final String... terminators)
throws IOException
{
return findString(true, blockSize, terminators);
}
/**
* Reads or skips a string ending with one of the given terminating
* substrings, using the specified block size for buffering.
*
* @param saveString Whether to collect the string from the current offset to
* the terminating bytes, and return it. If false, returns null.
* @param blockSize The block size to use when reading bytes in chunks.
* @param terminators The strings for which to search.
* @throws IOException If saveString flag is set and the maximum search length
* (512 MB) is exceeded.
* @return The string from the initial position through the end of the
* terminating sequence, or through the end of the stream if no
* terminating sequence is found, or null if saveString flag is unset.
*/
default String findString(final boolean saveString, final int blockSize,
final String... terminators) throws IOException
{
final StringBuilder out = new StringBuilder();
final long startPos = offset();
long bytesDropped = 0;
final long inputLen = length();
long maxLen = inputLen - startPos;
final boolean tooLong = saveString && maxLen > MAX_SEARCH_SIZE;
if (tooLong) maxLen = MAX_SEARCH_SIZE;
boolean match = false;
int maxTermLen = 0;
for (final String term : terminators) {
final int len = term.length();
if (len > maxTermLen) maxTermLen = len;
}
@SuppressWarnings("resource")
final InputStreamReader in = new InputStreamReader(
new DataHandleInputStream<>(this), getEncoding());
final char[] buf = new char[blockSize];
long loc = 0;
while (loc < maxLen && offset() < length() - 1) {
// if we're not saving the string, drop any old, unnecessary output
if (!saveString) {
final int outLen = out.length();
if (outLen >= maxTermLen) {
final int dropIndex = outLen - maxTermLen + 1;
final String last = out.substring(dropIndex, outLen);
out.setLength(0);
out.append(last);
bytesDropped += dropIndex;
}
}
// read block from stream
final int r = in.read(buf, 0, blockSize);
if (r <= 0) throw new IOException("Cannot read from stream: " + r);
// append block to output
out.append(buf, 0, r);
// check output, returning smallest possible string
int min = Integer.MAX_VALUE, tagLen = 0;
for (final String t : terminators) {
final int len = t.length();
final int start = (int) (loc - bytesDropped - len);
final int value = out.indexOf(t, start < 0 ? 0 : start);
if (value >= 0 && value < min) {
match = true;
min = value;
tagLen = len;
}
}
if (match) {
// reset stream to proper location
seek(startPos + bytesDropped + min + tagLen);
// trim output string
if (saveString) {
out.setLength(min + tagLen);
return out.toString();
}
return null;
}
loc += r;
}
// no match
if (tooLong) throw new IOException("Maximum search length reached.");
return saveString ? out.toString() : null;
}
/**
* Writes the provided string, followed by a newline character.
*
* @param string The string to write.
* @throws IOException If an I/O error occurs.
*/
default void writeLine(final String string) throws IOException {
writeBytes(string);
writeBytes("\n");
}
// -- InputStream look-alikes --
/**
* Reads the next byte of data from the stream.
*
* @return the next byte of data, or -1 if the end of the stream is reached.
* @throws IOException - if an I/O error occurs.
*/
default int read() throws IOException {
return offset() < length() ? readByte() & 0xff : -1;
}
/**
* Reads up to b.length bytes of data from the stream into an array of bytes.
*
* @return the total number of bytes read into the buffer.
*/
default int read(final byte[] b) throws IOException {
return read(b, 0, b.length);
}
/**
* Reads up to {@code len} bytes of data from the stream into an array of
* bytes.
*
* @return the total number of bytes read into the buffer.
*/
int read(byte[] b, int off, int len) throws IOException;
/**
* Skips over and discards {@code n} bytes of data from the stream. The
* {@code skip} method may, for a variety of reasons, end up skipping over
* some smaller number of bytes, possibly {@code 0}. This may result from any
* of a number of conditions; reaching end of file before {@code n} bytes have
* been skipped is only one possibility. The actual number of bytes skipped is
* returned. If {@code n} is negative, no bytes are skipped.
*
* @param n - the number of bytes to be skipped.
* @return the actual number of bytes skipped.
* @throws IOException - if an I/O error occurs.
*/
default long skip(final long n) throws IOException {
final long skip = available(n);
if (skip <= 0) return 0;
seek(offset() + skip);
return skip;
}
// -- DataInput methods --
@Override
default void readFully(final byte[] b) throws IOException {
readFully(b, 0, b.length);
}
@Override
default void readFully(final byte[] b, final int off, final int len)
throws IOException
{
// NB: Adapted from java.io.DataInputStream.readFully(byte[], int, int).
if (len < 0) throw new IndexOutOfBoundsException();
int n = 0;
while (n < len) {
final int count = read(b, off + n, len - n);
if (count < 0) throw new EOFException();
n += count;
}
}
@Override
default int skipBytes(final int n) throws IOException {
// NB: Cast here is safe since the value of n bounds the result to an int.
final int skip = (int) available(n);
if (skip < 0) return 0;
seek(offset() + skip);
return skip;
}
@Override
default boolean readBoolean() throws IOException {
return readByte() != 0;
}
@Override
default int readUnsignedByte() throws IOException {
return readByte() & 0xff;
}
@Override
default short readShort() throws IOException {
final int ch0;
final int ch1;
if (isBigEndian()) {
ch0 = read();
ch1 = read();
}
else {
ch1 = read();
ch0 = read();
}
if ((ch0 | ch1) < 0) throw new EOFException();
return (short) ((ch0 << 8) + (ch1 << 0));
}
@Override
default int readUnsignedShort() throws IOException {
return readShort() & 0xffff;
}
@Override
default char readChar() throws IOException {
return (char) readShort();
}
@Override
default int readInt() throws IOException {
final int ch0;
final int ch1;
final int ch2;
final int ch3;
if (isBigEndian()) {
ch0 = read();
ch1 = read();
ch2 = read();
ch3 = read();
}
else {
ch3 = read();
ch2 = read();
ch1 = read();
ch0 = read();
}
if ((ch0 | ch1 | ch2 | ch3) < 0) throw new EOFException();
return ((ch0 << 24) + (ch1 << 16) + (ch2 << 8) + (ch3 << 0));
}
@Override
default long readLong() throws IOException {
final int ch0;
final int ch1;
final int ch2;
final int ch3;
final int ch4;
final int ch5;
final int ch6;
final int ch7;
if (isBigEndian()) {
ch0 = read();
ch1 = read();
ch2 = read();
ch3 = read();
ch4 = read();
ch5 = read();
ch6 = read();
ch7 = read();
}
else {
ch7 = read();
ch6 = read();
ch5 = read();
ch4 = read();
ch3 = read();
ch2 = read();
ch1 = read();
ch0 = read();
}
if ((ch0 | ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7) < 0) {
throw new EOFException();
}
// TODO: Double check this inconsistent code.
return ((long) ch0 << 56) + //
((long) (ch1 & 255) << 48) + //
((long) (ch2 & 255) << 40) + //
((long) (ch3 & 255) << 32) + //
((long) (ch4 & 255) << 24) + //
((ch5 & 255) << 16) + //
((ch6 & 255) << 8) + //
((ch7 & 255) << 0);
}
@Override
default float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
@Override
default double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
@Override
default String readLine() throws IOException {
// NB: Adapted from java.io.RandomAccessFile.readLine().
final StringBuffer input = new StringBuffer();
int c = -1;
boolean eol = false;
while (!eol) {
switch (c = read()) {
case -1:
case '\n':
eol = true;
break;
case '\r':
eol = true;
final long cur = offset();
if (read() != '\n') seek(cur);
break;
default:
input.append((char) c);
break;
}
}
if (c == -1 && input.length() == 0) {
return null;
}
return input.toString();
}
@Override
default String readUTF() throws IOException {
return DataInputStream.readUTF(this);
}
// -- DataOutput methods --
@Override
default void write(final byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
default void writeBoolean(final boolean v) throws IOException {
write(v ? 1 : 0);
}
@Override
default void writeByte(final int v) throws IOException {
write(v);
}
@Override
default void writeShort(final int v) throws IOException {
if (isBigEndian()) {
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
}
else {
write((v >>> 0) & 0xFF);
write((v >>> 8) & 0xFF);
}
}
@Override
default void writeChar(final int v) throws IOException {
if (isBigEndian()) {
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
}
else {
write((v >>> 0) & 0xFF);
write((v >>> 8) & 0xFF);
}
}
@Override
default void writeInt(final int v) throws IOException {
if (isBigEndian()) {
write((v >>> 24) & 0xFF);
write((v >>> 16) & 0xFF);
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
}
else {
write((v >>> 0) & 0xFF);
write((v >>> 8) & 0xFF);
write((v >>> 16) & 0xFF);
write((v >>> 24) & 0xFF);
}
}
@Override
default void writeLong(final long v) throws IOException {
if (isBigEndian()) {
write((byte) (v >>> 56));
write((byte) (v >>> 48));
write((byte) (v >>> 40));
write((byte) (v >>> 32));
write((byte) (v >>> 24));
write((byte) (v >>> 16));
write((byte) (v >>> 8));
write((byte) (v >>> 0));
}
else {
write((byte) (v >>> 0));
write((byte) (v >>> 8));
write((byte) (v >>> 16));
write((byte) (v >>> 24));
write((byte) (v >>> 32));
write((byte) (v >>> 40));
write((byte) (v >>> 48));
write((byte) (v >>> 56));
}
}
@Override
default void writeFloat(final float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
@Override
default void writeDouble(final double v) throws IOException {
writeLong(Double.doubleToLongBits(v));
}
@Override
default void writeBytes(final String s) throws IOException {
write(s.getBytes("UTF-8"));
}
@Override
default void writeChars(final String s) throws IOException {
final int len = s.length();
for (int i = 0; i < len; i++) {
final int v = s.charAt(i);
write((v >>> 8) & 0xFF);
write((v >>> 0) & 0xFF);
}
}
@Override
default void writeUTF(final String str) throws IOException {
DataHandles.writeUTF(str, this);
}
}