From a26188d393b11040b8ee8476338a73bfadabffb6 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Sat, 27 Apr 2019 18:14:39 +0200 Subject: [PATCH] New: BufferedOutputStream --- src/be/nikiroo/utils/BufferedInputStream.java | 46 ++-- .../nikiroo/utils/BufferedOutputStream.java | 242 ++++++++++++++++++ src/be/nikiroo/utils/NextableInputStream.java | 16 +- .../test_code/BufferedInputStreamTest.java | 6 +- .../test_code/BufferedOutputStreamTest.java | 80 ++++++ src/be/nikiroo/utils/test_code/Test.java | 1 + 6 files changed, 357 insertions(+), 34 deletions(-) create mode 100644 src/be/nikiroo/utils/BufferedOutputStream.java create mode 100644 src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java diff --git a/src/be/nikiroo/utils/BufferedInputStream.java b/src/be/nikiroo/utils/BufferedInputStream.java index 9db12f1..aa455a2 100644 --- a/src/be/nikiroo/utils/BufferedInputStream.java +++ b/src/be/nikiroo/utils/BufferedInputStream.java @@ -15,9 +15,9 @@ import java.util.Arrays; */ public class BufferedInputStream extends InputStream { /** The current position in the buffer. */ - protected int pos; + protected int start; /** The index of the last usable position of the buffer. */ - protected int len; + protected int stop; /** The buffer itself. */ protected byte[] buffer; /** An End-Of-File (or buffer, here) marker. */ @@ -47,8 +47,8 @@ public class BufferedInputStream extends InputStream { this.buffer = new byte[4096]; this.originalBuffer = this.buffer; - this.pos = 0; - this.len = 0; + this.start = 0; + this.stop = 0; } /** @@ -90,8 +90,8 @@ public class BufferedInputStream extends InputStream { this.buffer = in; this.originalBuffer = this.buffer; - this.pos = offset; - this.len = length; + this.start = offset; + this.stop = length; } /** @@ -167,7 +167,7 @@ public class BufferedInputStream extends InputStream { if (available() >= search.length) { // Easy path - return startsWith(search, buffer, pos, len); + return startsWith(search, buffer, start, stop); } else if (!eof) { // Harder path if (buffer2 == null && buffer.length == originalBuffer.length) { @@ -205,7 +205,7 @@ public class BufferedInputStream extends InputStream { * @return TRUE if it is */ public boolean eof() { - return closed || (len < 0 && !hasMoreData()); + return closed || (stop < 0 && !hasMoreData()); } @Override @@ -217,7 +217,7 @@ public class BufferedInputStream extends InputStream { return -1; } - return buffer[pos++]; + return buffer[start++]; } @Override @@ -241,10 +241,10 @@ public class BufferedInputStream extends InputStream { while (hasMoreData() && done < blen) { preRead(); if (hasMoreData()) { - int now = Math.min(blen, len) - pos; + int now = Math.min(blen, stop) - start; if (now > 0) { - System.arraycopy(buffer, pos, b, boff + done, now); - pos += now; + System.arraycopy(buffer, start, b, boff + done, now); + start += now; done += now; } } @@ -264,7 +264,7 @@ public class BufferedInputStream extends InputStream { preRead(); long inBuffer = Math.min(n, available()); - pos += inBuffer; + start += inBuffer; n -= inBuffer; skipped += inBuffer; } @@ -278,7 +278,7 @@ public class BufferedInputStream extends InputStream { return 0; } - return Math.max(0, len - pos); + return Math.max(0, stop - start); } /** @@ -348,12 +348,12 @@ public class BufferedInputStream extends InputStream { */ protected boolean preRead() throws IOException { boolean hasRead = false; - if (!eof && in != null && pos >= len) { - pos = 0; + if (!eof && in != null && start >= stop) { + start = 0; if (buffer2 != null) { buffer = buffer2; - pos = pos2; - len = len2; + start = pos2; + stop = len2; buffer2 = null; pos2 = 0; @@ -361,16 +361,16 @@ public class BufferedInputStream extends InputStream { } else { buffer = originalBuffer; - len = read(in, buffer); - if (len > 0) { - bytesRead += len; + stop = read(in, buffer); + if (stop > 0) { + bytesRead += stop; } } hasRead = true; } - if (pos >= len) { + if (start >= stop) { eof = true; } @@ -400,7 +400,7 @@ public class BufferedInputStream extends InputStream { * @return TRUE if it is the case, FALSE if not */ protected boolean hasMoreData() { - return !closed && !(eof && pos >= len); + return !closed && !(eof && start >= stop); } /** diff --git a/src/be/nikiroo/utils/BufferedOutputStream.java b/src/be/nikiroo/utils/BufferedOutputStream.java new file mode 100644 index 0000000..4be1d0d --- /dev/null +++ b/src/be/nikiroo/utils/BufferedOutputStream.java @@ -0,0 +1,242 @@ +package be.nikiroo.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A simple {@link OutputStream} that is buffered with a bytes array. + *

+ * It is mostly intended to be used as a base class to create new + * {@link OutputStream}s with special operation modes, and to give some default + * methods. + * + * @author niki + */ +public class BufferedOutputStream extends OutputStream { + /** The current position in the buffer. */ + protected int start; + /** The index of the last usable position of the buffer. */ + protected int stop; + /** The buffer itself. */ + protected byte[] buffer; + /** An End-Of-File (or buffer, here) marker. */ + protected boolean eof; + + private boolean closed; + private OutputStream out; + private int openCounter; + + private long bytesWritten; + + /** + * Create a new {@link BufferedInputStream} that wraps the given + * {@link InputStream}. + * + * @param out + * the {@link OutputStream} to wrap + */ + public BufferedOutputStream(OutputStream out) { + this.out = out; + + this.buffer = new byte[4096]; + this.start = 0; + this.stop = 0; + } + + @Override + public void write(int b) throws IOException { + checkClose(); + + if (available() <= 0) { + flush(false); + } + + buffer[start++] = (byte) b; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] source, int sourceOffset, int sourceLength) + throws IOException { + + checkClose(); + + if (source == null) { + throw new NullPointerException(); + } else if ((sourceOffset < 0) || (sourceOffset > source.length) + || (sourceLength < 0) + || ((sourceOffset + sourceLength) > source.length) + || ((sourceOffset + sourceLength) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (sourceLength == 0) { + return; + } + + if (sourceLength >= buffer.length) { + /* + * If the request length exceeds the size of the output buffer, + * flush the output buffer and then write the data directly. In this + * way buffered streams will cascade harmlessly. + */ + flush(false); + out.write(source, sourceOffset, sourceLength); + return; + } + + int done = 0; + while (done < sourceLength) { + if (available() <= 0) { + flush(false); + } + + int now = Math.min(sourceLength, available()); + System.arraycopy(source, sourceOffset + done, buffer, stop, now); + stop += now; + done += now; + } + } + + /** + * The available space in the buffer. + * + * @return the space in bytes + */ + private int available() { + if (closed) { + return 0; + } + + return Math.max(0, buffer.length - stop - 1); + } + + /** + * The number of bytes written to the under-laying {@link OutputStream}. + * + * @return the number of bytes + */ + public long getBytesWritten() { + return bytesWritten; + } + + /** + * Return this very same {@link BufferedInputStream}, but keep a counter of + * how many streams were open this way. When calling + * {@link BufferedInputStream#close()}, decrease this counter if it is not + * already zero instead of actually closing the stream. + *

+ * You are now responsible for it — you must close it. + *

+ * This method allows you to use a wrapping stream around this one and still + * close the wrapping stream. + * + * @return the same stream, but you are now responsible for closing it + * + * @throws IOException + * in case of I/O error or if the stream is closed + */ + public synchronized OutputStream open() throws IOException { + checkClose(); + openCounter++; + return this; + } + + /** + * Check that the stream was not closed, and throw an {@link IOException} if + * it was. + * + * @throws IOException + * if it was closed + */ + protected void checkClose() throws IOException { + if (closed) { + throw new IOException( + "This BufferedInputStream was closed, you cannot use it anymore."); + } + } + + @Override + public void flush() throws IOException { + flush(true); + } + + /** + * Flush the {@link BufferedOutputStream}, and optionally the under-laying + * stream, too. + * + * @param includingSubStream + * also flush the under-laying stream + * @throws IOException + * in case of I/O error + */ + private void flush(boolean includingSubStream) throws IOException { + out.write(buffer, start, stop - start); + bytesWritten += (stop - start); + start = 0; + stop = 0; + if (includingSubStream) { + out.flush(); + } + } + + /** + * Closes this stream and releases any system resources associated with the + * stream. + *

+ * Including the under-laying {@link InputStream}. + *

+ * Note: if you called the {@link BufferedInputStream#open()} method + * prior to this one, it will just decrease the internal count of how many + * open streams it held and do nothing else. The stream will actually be + * closed when you have called {@link BufferedInputStream#close()} once more + * than {@link BufferedInputStream#open()}. + * + * @exception IOException + * in case of I/O error + */ + @Override + public synchronized void close() throws IOException { + close(true); + } + + /** + * Closes this stream and releases any system resources associated with the + * stream. + *

+ * Including the under-laying {@link InputStream} if + * incudingSubStream is true. + *

+ * You can call this method multiple times, it will not cause an + * {@link IOException} for subsequent calls. + *

+ * Note: if you called the {@link BufferedInputStream#open()} method + * prior to this one, it will just decrease the internal count of how many + * open streams it held and do nothing else. The stream will actually be + * closed when you have called {@link BufferedInputStream#close()} once more + * than {@link BufferedInputStream#open()}. + * + * @param includingSubStream + * also close the under-laying stream + * + * @exception IOException + * in case of I/O error + */ + public synchronized void close(boolean includingSubStream) + throws IOException { + if (!closed) { + if (openCounter > 0) { + openCounter--; + } else { + closed = true; + flush(true); + if (includingSubStream && out != null) { + out.close(); + } + } + } + } +} diff --git a/src/be/nikiroo/utils/NextableInputStream.java b/src/be/nikiroo/utils/NextableInputStream.java index 7e2766a..f221cbb 100644 --- a/src/be/nikiroo/utils/NextableInputStream.java +++ b/src/be/nikiroo/utils/NextableInputStream.java @@ -142,7 +142,7 @@ public class NextableInputStream extends BufferedInputStream { return bufferChanged; } - if (pos >= len) { + if (start >= stop) { eof = true; } @@ -161,9 +161,9 @@ public class NextableInputStream extends BufferedInputStream { /** * Check that the buffer didn't overshot to the next item, and fix - * {@link NextableInputStream#len} if needed. + * {@link NextableInputStream#stop} if needed. *

- * If {@link NextableInputStream#len} is fixed, + * If {@link NextableInputStream#stop} is fixed, * {@link NextableInputStream#eof} and {@link NextableInputStream#stopped} * are set to TRUE. * @@ -172,14 +172,14 @@ public class NextableInputStream extends BufferedInputStream { * the {@link NextableInputStreamStep} */ private void checkBuffer(boolean newBuffer) { - if (step != null && len > 0) { + if (step != null && stop > 0) { if (newBuffer) { step.clearBuffer(); } - int stopAt = step.stop(buffer, pos, len); + int stopAt = step.stop(buffer, start, stop); if (stopAt >= 0) { - len = stopAt; + stop = stopAt; eof = true; stopped = true; } @@ -214,8 +214,8 @@ public class NextableInputStream extends BufferedInputStream { } if (step != null && !hasMoreData() && stopped) { - len = step.getResumeLen(); - pos += step.getResumeSkip(); + stop = step.getResumeLen(); + start += step.getResumeSkip(); eof = false; if (all) { diff --git a/src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java b/src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java index e2fc80b..c8eb21f 100644 --- a/src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java +++ b/src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java @@ -8,7 +8,7 @@ import be.nikiroo.utils.IOUtils; import be.nikiroo.utils.test.TestCase; import be.nikiroo.utils.test.TestLauncher; -public class BufferedInputStreamTest extends TestLauncher { +class BufferedInputStreamTest extends TestLauncher { public BufferedInputStreamTest(String[] args) { super("BufferedInputStream test", args); @@ -39,8 +39,8 @@ public class BufferedInputStreamTest extends TestLauncher { + " resulting array has not the correct number of items", expected.length, actual.length); for (int i = 0; i < actual.length; i++) { - test.assertEquals("Item " + i + " (0-based) is not the same", - expected[i], actual[i]); + test.assertEquals(prefix + ": item " + i + + " (0-based) is not the same", expected[i], actual[i]); } } } diff --git a/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java b/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java new file mode 100644 index 0000000..2ed712e --- /dev/null +++ b/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java @@ -0,0 +1,80 @@ +package be.nikiroo.utils.test_code; + +import java.io.ByteArrayOutputStream; + +import be.nikiroo.utils.BufferedOutputStream; +import be.nikiroo.utils.test.TestCase; +import be.nikiroo.utils.test.TestLauncher; + +class BufferedOutputStreamTest extends TestLauncher { + public BufferedOutputStreamTest(String[] args) { + super("BufferedOutputStream test", args); + + addTest(new TestCase("Single write") { + @Override + public void test() throws Exception { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + BufferedOutputStream out = new BufferedOutputStream(bout); + + byte[] data = new byte[] { 42, 12, 0, 127 }; + + out.write(data); + out.close(); + + checkArrays(this, "FIRST", bout, data); + } + }); + + addTest(new TestCase("Multiple writes") { + @Override + public void test() throws Exception { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + BufferedOutputStream out = new BufferedOutputStream(bout); + + byte[] data1 = new byte[] { 42, 12, 0, 127 }; + byte[] data2 = new byte[] { 15, 55 }; + byte[] data3 = new byte[] {}; + + byte[] dataAll = new byte[] { 42, 12, 0, 127, 15, 55 }; + + out.write(data1); + out.write(data2); + out.write(data3); + out.close(); + + checkArrays(this, "FIRST", bout, dataAll); + } + }); + } + + static void checkArrays(TestCase test, String prefix, + ByteArrayOutputStream bout, byte[] expected) throws Exception { + byte[] actual = bout.toByteArray(); + + if (false) { + System.out.print("\nExpected data: [ "); + for (int i = 0; i < actual.length; i++) { + if (i > 0) + System.out.print(", "); + System.out.print(expected[i]); + } + System.out.println(" ]"); + + System.out.print("Actual data : [ "); + for (int i = 0; i < actual.length; i++) { + if (i > 0) + System.out.print(", "); + System.out.print(actual[i]); + } + System.out.println(" ]"); + } + + test.assertEquals("The " + prefix + + " resulting array has not the correct number of items", + expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + test.assertEquals(prefix + ": item " + i + + " (0-based) is not the same", expected[i], actual[i]); + } + } +} diff --git a/src/be/nikiroo/utils/test_code/Test.java b/src/be/nikiroo/utils/test_code/Test.java index 24d6bba..e00da11 100644 --- a/src/be/nikiroo/utils/test_code/Test.java +++ b/src/be/nikiroo/utils/test_code/Test.java @@ -36,6 +36,7 @@ public class Test extends TestLauncher { addSeries(new BufferedInputStreamTest(args)); addSeries(new NextableInputStreamTest(args)); addSeries(new ReplaceInputStreamTest(args)); + addSeries(new BufferedOutputStreamTest(args)); // TODO: test cache and downloader Cache cache = null; -- 2.27.0