New: BufferedOutputStream
authorNiki Roo <niki@nikiroo.be>
Sat, 27 Apr 2019 16:14:39 +0000 (18:14 +0200)
committerNiki Roo <niki@nikiroo.be>
Sat, 27 Apr 2019 16:14:39 +0000 (18:14 +0200)
src/be/nikiroo/utils/BufferedInputStream.java
src/be/nikiroo/utils/BufferedOutputStream.java [new file with mode: 0644]
src/be/nikiroo/utils/NextableInputStream.java
src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java
src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test_code/Test.java

index 9db12f1926cc372544998ae6128a20016370c0e7..aa455a2426d0b96d2534f27c0740c545510c858f 100644 (file)
@@ -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 (file)
index 0000000..4be1d0d
--- /dev/null
@@ -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.
+ * <p>
+ * 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.
+        * <p>
+        * You are now responsible for it &mdash; you <b>must</b> close it.
+        * <p>
+        * 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.
+        * <p>
+        * Including the under-laying {@link InputStream}.
+        * <p>
+        * <b>Note:</b> 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.
+        * <p>
+        * Including the under-laying {@link InputStream} if
+        * <tt>incudingSubStream</tt> is true.
+        * <p>
+        * You can call this method multiple times, it will not cause an
+        * {@link IOException} for subsequent calls.
+        * <p>
+        * <b>Note:</b> 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();
+                               }
+                       }
+               }
+       }
+}
index 7e2766ad950d6f10efa0dbd5c16214ebcb581605..f221cbbda9aa2ce9ad0eff7f215c725cbc886ab0 100644 (file)
@@ -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.
         * <p>
-        * 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) {
index e2fc80bf38275b666fdf8f892eece519e1d80537..c8eb21fdf4b0d446d7a57d8edbee7f01484d64e1 100644 (file)
@@ -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 (file)
index 0000000..2ed712e
--- /dev/null
@@ -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]);
+               }
+       }
+}
index 24d6bba5fd908efe3b18750a8027f213d5651fe5..e00da1173129031bda4acefb261379a325c8bf57 100644 (file)
@@ -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;