From 59c392c8c2d70ad7537677d531eaa6f8d108ebea Mon Sep 17 00:00:00 2001 From: Joe Lauer Date: Wed, 6 Nov 2024 14:22:25 -0500 Subject: [PATCH] New crux-util ByteBufferInputStream, ByteInput, Unclosable input/output stream utility classes --- .../crux/util/ByteBufferInputStream.java | 70 +++++++++++ .../java/com/fizzed/crux/util/ByteInput.java | 119 ++++++++++++++++++ .../crux/util/UnclosableInputStream.java | 48 +++++++ .../crux/util/UnclosableOutputStream.java | 50 ++++++++ .../crux/util/ByteBufferInputStreamTest.java | 43 +++++++ .../com/fizzed/crux/util/ByteInputTest.java | 104 +++++++++++++++ 6 files changed, 434 insertions(+) create mode 100644 crux-util/src/main/java/com/fizzed/crux/util/ByteBufferInputStream.java create mode 100644 crux-util/src/main/java/com/fizzed/crux/util/ByteInput.java create mode 100644 crux-util/src/main/java/com/fizzed/crux/util/UnclosableInputStream.java create mode 100644 crux-util/src/main/java/com/fizzed/crux/util/UnclosableOutputStream.java create mode 100644 crux-util/src/test/java/com/fizzed/crux/util/ByteBufferInputStreamTest.java create mode 100644 crux-util/src/test/java/com/fizzed/crux/util/ByteInputTest.java diff --git a/crux-util/src/main/java/com/fizzed/crux/util/ByteBufferInputStream.java b/crux-util/src/main/java/com/fizzed/crux/util/ByteBufferInputStream.java new file mode 100644 index 0000000..4f73de5 --- /dev/null +++ b/crux-util/src/main/java/com/fizzed/crux/util/ByteBufferInputStream.java @@ -0,0 +1,70 @@ +package com.fizzed.crux.util; + +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class ByteBufferInputStream extends InputStream { + + private final ByteBuffer buf; + + public ByteBufferInputStream(ByteBuffer buffer) { + if (buffer == null) throw new NullPointerException(); + buf = buffer; + } + + @Override + public synchronized int read() { + if (buf.hasRemaining()) return buf.get() & 0xff; + return -1; + } + + @Override + public synchronized int read(byte b[]) { + return read(0, b.length, b); + } + + @Override + public synchronized int read(byte b[], int off, int len) { + if ((off | len | off + len | b.length - (off + len)) < 0) { + throw new IndexOutOfBoundsException(); + } + return read(off, len, b); + } + + private int read(int off, int len, byte[] b) { + if (len == 0) return 0; + + int rem = buf.remaining(); + if (rem <= 0) return -1; + + if (rem > len) rem = len; + buf.get(b, off, rem); + return rem; + } + + @Override + public synchronized long skip(long n) { + if (n <= 0) return 0; + + int rem = buf.remaining(); + if (n > rem) n = rem; + + buf.position((int)(buf.position() + n)); + return n; + } + + @Override + public synchronized int available() { return buf.remaining(); } + + @Override + public synchronized void mark(int readAheadLimit) { buf.mark(); } + + @Override + public synchronized void reset() { buf.reset(); } + + @Override + public boolean markSupported() { return true; } + + @Override + public void close() {} +} \ No newline at end of file diff --git a/crux-util/src/main/java/com/fizzed/crux/util/ByteInput.java b/crux-util/src/main/java/com/fizzed/crux/util/ByteInput.java new file mode 100644 index 0000000..74a0ce7 --- /dev/null +++ b/crux-util/src/main/java/com/fizzed/crux/util/ByteInput.java @@ -0,0 +1,119 @@ +package com.fizzed.crux.util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +/** + * Generic way of providing "byte inputs" as files, byte arrays, etc. to methods. + */ +public class ByteInput { + + protected final Path file; + protected final byte[] bytes; + protected final ByteBuffer buffer; + protected final InputStream in; + protected final boolean closeable; + + static public ByteInput byteInput(Path file) { + Objects.requireNonNull(file, "file cannot be null"); + return new ByteInput(file, null, null, null, true); + } + + static public ByteInput byteInput(File file) { + Objects.requireNonNull(file, "file cannot be null"); + return new ByteInput(file.toPath(), null, null, null, true); + } + + static public ByteInput byteInput(byte[] bytes) { + Objects.requireNonNull(bytes, "bytes cannot be null"); + return new ByteInput(null, bytes, null, null, true); + } + + static public ByteInput byteInput(ByteBuffer buffer) { + Objects.requireNonNull(buffer, "buffer cannot be null"); + return new ByteInput(null, null, buffer, null, true); + } + + static public ByteInput byteInput(InputStream in) { + Objects.requireNonNull(in, "input cannot be null"); + return new ByteInput(null, null, null, in, true); + } + + static public ByteInput byteInput(InputStream in, boolean closeable) { + Objects.requireNonNull(in, "input cannot be null"); + return new ByteInput(null, null, null, in, closeable); + } + + protected Long size; + + protected ByteInput(Path file, byte[] bytes, ByteBuffer buffer, InputStream in, boolean closeable) { + this.file = file; + this.bytes = bytes; + this.buffer = buffer; + this.in = in; + this.closeable = closeable; + } + + /** + * If the size is definitively known ahead of time, this is the number of bytes. Will work on files, byte arrays, + * and byte buffers, but others will return -1 since the size is not known definitively. + * @return The number of bytes this input represents or -1 if that size is not definitively known + * @throws IOException + */ + public long available() throws IOException { + if (this.file != null || this.bytes != null || this.buffer != null) { + return this.size(); + } else if (this.in != null) { + return this.in.available(); + } + return -1L; + } + + /** + * If the size is definitively known ahead of time, this is the number of bytes. Will work on files, byte arrays, + * and byte buffers, but others will return -1 since the size is not known definitively. + * @return The number of bytes this input represents or -1 if that size is not definitively known + * @throws IOException + */ + public long size() throws IOException { + if (size == null) { + if (this.file != null) { + this.size = Files.size(this.file); + } else if (this.bytes != null) { + this.size = (long)this.bytes.length; + } else if (this.buffer != null) { + this.size = (long)this.buffer.remaining(); + } else { + // otherwise we just don't know what size this is + this.size = -1l; + } + } + return this.size; + } + + public InputStream open() throws IOException { + if (this.file != null) { + return Files.newInputStream(this.file); + } else if (this.bytes != null) { + return new ByteArrayInputStream(this.bytes); + } else if (this.buffer != null) { + return new ByteBufferInputStream(this.buffer); + } else if (this.in != null) { + if (this.closeable) { + return this.in; + } else { + // wrap input stream in an un-closeable version + return new UnclosableInputStream(this.in); + } + } else { + throw new IllegalStateException("All available inputs were null (file, bytes, input)"); + } + } + +} \ No newline at end of file diff --git a/crux-util/src/main/java/com/fizzed/crux/util/UnclosableInputStream.java b/crux-util/src/main/java/com/fizzed/crux/util/UnclosableInputStream.java new file mode 100644 index 0000000..6600b05 --- /dev/null +++ b/crux-util/src/main/java/com/fizzed/crux/util/UnclosableInputStream.java @@ -0,0 +1,48 @@ +package com.fizzed.crux.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class UnclosableInputStream extends FilterInputStream { + + protected boolean closed; + + public UnclosableInputStream(InputStream in) { + super(in); + } + + public boolean isClosed() { + return this.closed; + } + + @Override + public int read() throws IOException { + if (this.closed) return -1; + return super.read(); + } + + @Override + public int read(byte[] b) throws IOException { + if (this.closed) return -1; + return super.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (this.closed) return -1; + return super.read(b, off, len); + } + + @Override + public int available() throws IOException { + if (this.closed) throw new IOException("Stream closed"); + return super.available(); + } + + @Override + public void close() throws IOException { + this.closed = true; + } + +} \ No newline at end of file diff --git a/crux-util/src/main/java/com/fizzed/crux/util/UnclosableOutputStream.java b/crux-util/src/main/java/com/fizzed/crux/util/UnclosableOutputStream.java new file mode 100644 index 0000000..44a4447 --- /dev/null +++ b/crux-util/src/main/java/com/fizzed/crux/util/UnclosableOutputStream.java @@ -0,0 +1,50 @@ +package com.fizzed.crux.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class UnclosableOutputStream extends FilterOutputStream { + + private boolean closed; + + public UnclosableOutputStream(OutputStream output) { + super(output); + this.closed = false; + } + + public boolean isClosed() { + return this.closed; + } + + @Override + public void write(int b) throws IOException { + if (closed) throw new IOException("Stream closed"); + super.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + if (closed) throw new IOException("Stream closed"); + super.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (closed) throw new IOException("Stream closed"); + super.write(b, off, len); + } + + @Override + public void flush() throws IOException { + if (closed) throw new IOException("Stream closed"); + super.flush(); + } + + @Override + public void close() throws IOException { + this.closed = true; + } + +} \ No newline at end of file diff --git a/crux-util/src/test/java/com/fizzed/crux/util/ByteBufferInputStreamTest.java b/crux-util/src/test/java/com/fizzed/crux/util/ByteBufferInputStreamTest.java new file mode 100644 index 0000000..9d6a00c --- /dev/null +++ b/crux-util/src/test/java/com/fizzed/crux/util/ByteBufferInputStreamTest.java @@ -0,0 +1,43 @@ +package com.fizzed.crux.util; + +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +public class ByteBufferInputStreamTest { + + @Test + public void read() { + final ByteBuffer buf = ByteBuffer.wrap("abcdef".getBytes()); + + final ByteBufferInputStream in = new ByteBufferInputStream(buf); + int read; + + assertThat(in.available(), is(6)); + assertThat(in.read(), is((int)'a')); + + final byte[] bytes = new byte[2]; + read = in.read(bytes); + assertThat(read, is(2)); + assertThat(bytes[0], is((byte)'b')); + assertThat(bytes[1], is((byte)'c')); + + read = in.read(bytes, 1, 1); + assertThat(read, is(1)); + assertThat(bytes[1], is((byte)'d')); + + assertThat(in.available(), is(2)); + read = in.read(bytes); + assertThat(read, is(2)); + assertThat(bytes[0], is((byte)'e')); + assertThat(bytes[1], is((byte)'f')); + + // this should be EOF now + read = in.read(bytes); + assertThat(read, is(-1)); + } + +} \ No newline at end of file diff --git a/crux-util/src/test/java/com/fizzed/crux/util/ByteInputTest.java b/crux-util/src/test/java/com/fizzed/crux/util/ByteInputTest.java new file mode 100644 index 0000000..782ed42 --- /dev/null +++ b/crux-util/src/test/java/com/fizzed/crux/util/ByteInputTest.java @@ -0,0 +1,104 @@ +package com.fizzed.crux.util; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.fizzed.crux.util.ByteInput.byteInput; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +public class ByteInputTest { + + @Test + public void file() throws Exception { + Path file = Resources.file("/fixtures/hello.txt"); + + ByteInput bi = byteInput(file); + + assertThat(bi.size(), is(11L)); + assertThat(bi.available(), is(11L)); + + try (InputStream in = bi.open()) { + String s = InOuts.stringUTF8(in); + assertThat(s, is("I am a test")); + } + } + + @Test + public void byteArray() throws Exception { + byte[] bytes = "I am a test".getBytes(); + + ByteInput bi = byteInput(bytes); + + assertThat(bi.size(), is(11L)); + assertThat(bi.available(), is(11L)); + + try (InputStream in = bi.open()) { + String s = InOuts.stringUTF8(in); + assertThat(s, is("I am a test")); + } + } + + @Test + public void byteBuffer() throws Exception { + byte[] bytes = "I am a test".getBytes(); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + ByteInput bi = byteInput(buffer); + + assertThat(bi.size(), is(11L)); + assertThat(bi.available(), is(11L)); + + try (InputStream in = bi.open()) { + String s = InOuts.stringUTF8(in); + assertThat(s, is("I am a test")); + } + } + + @Test + public void inputStream() throws Exception { + byte[] bytes = "I am a test".getBytes(); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + + ByteInput bi = byteInput(bais); + + assertThat(bi.size(), is(-1L)); + assertThat(bi.available(), is(11L)); + + try (InputStream in = bi.open()) { + String s = InOuts.stringUTF8(in); + assertThat(s, is("I am a test")); + } + } + + @Test + public void inputStreamUncloseable() throws Exception { + byte[] bytes = "I am a test".getBytes(); + final AtomicBoolean closed = new AtomicBoolean(false); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes) { + @Override + public void close() throws IOException { + closed.set(true); + } + }; + + ByteInput bi1 = byteInput(bais); + + try (InputStream in = bi1.open()) { } + assertThat(closed.get(), is(true)); + + + closed.set(false); + ByteInput bi2 = byteInput(bais, false); + + try (InputStream in = bi2.open()) { } + assertThat(closed.get(), is(false)); + } + +} \ No newline at end of file