Merge branch 'master' into streamify
authorNiki Roo <niki@nikiroo.be>
Sat, 27 Apr 2019 16:57:26 +0000 (18:57 +0200)
committerNiki Roo <niki@nikiroo.be>
Sat, 27 Apr 2019 16:57:26 +0000 (18:57 +0200)
19 files changed:
changelog.md
src/be/nikiroo/utils/Cache.java
src/be/nikiroo/utils/IOUtils.java
src/be/nikiroo/utils/StringUtils.java
src/be/nikiroo/utils/streams/BufferedInputStream.java [moved from src/be/nikiroo/utils/NextableInputStream.java with 52% similarity]
src/be/nikiroo/utils/streams/BufferedOutputStream.java [new file with mode: 0644]
src/be/nikiroo/utils/streams/MarkableFileInputStream.java [moved from src/be/nikiroo/utils/MarkableFileInputStream.java with 96% similarity]
src/be/nikiroo/utils/streams/NextableInputStream.java [new file with mode: 0644]
src/be/nikiroo/utils/streams/NextableInputStreamStep.java [moved from src/be/nikiroo/utils/NextableInputStreamStep.java with 98% similarity]
src/be/nikiroo/utils/streams/ReplaceInputStream.java [new file with mode: 0644]
src/be/nikiroo/utils/streams/ReplaceOutputStream.java [new file with mode: 0644]
src/be/nikiroo/utils/streams/StreamUtils.java [new file with mode: 0644]
src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test_code/NextableInputStreamTest.java
src/be/nikiroo/utils/test_code/ReplaceInputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test_code/ReplaceOutputStreamTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test_code/StringUtilsTest.java
src/be/nikiroo/utils/test_code/Test.java

index 5bdc9460c41852c072295bf6d76f60ea9eab6ac6..4792f9b33466a48df17efc9900af50fedffb7a9b 100644 (file)
@@ -4,8 +4,10 @@
 
 - new: server: count the bytes we rec/send
 - new: CryptUtils
+- new: streams classes
 - fix: IOUtils.readSmallStream and \n at the end
 - change: serial: SSL -> CryptUtils
+- change: MarkableFileInputStream moved to nikiroo.utils.streams
 
 ## Version 4.7.2
 
index ce33592ed3a4419cee1a6553908cae8621bfefd8..cf8a780ef9ae49f99b5536a5d549e5522a01837b 100644 (file)
@@ -8,6 +8,8 @@ import java.io.InputStream;
 import java.net.URL;
 import java.util.Date;
 
+import be.nikiroo.utils.streams.MarkableFileInputStream;
+
 /**
  * A generic cache system, with special support for {@link URL}s.
  * <p>
index 9cdaba840af79727fba495b2095af03cf5918c2f..5a7e179b684d9423182c048376f2bf014c54077b 100644 (file)
@@ -13,6 +13,8 @@ import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import java.util.zip.ZipOutputStream;
 
+import be.nikiroo.utils.streams.MarkableFileInputStream;
+
 /**
  * This class offer some utilities based around Streams and Files.
  * 
index 1ee9ac4b569001fda8036815fe8a43277e8e18ef..954d34113d09650c4b33d5ccd1dbb70f51ed32a1 100644 (file)
@@ -844,15 +844,15 @@ public class StringUtils {
                if (value >= 1000000000l) {
                        mult = 1000000000l;
                        userValue = value / 1000000000l;
-                       suffix = "G";
+                       suffix = " G";
                } else if (value >= 1000000l) {
                        mult = 1000000l;
                        userValue = value / 1000000l;
-                       suffix = "M";
+                       suffix = " M";
                } else if (value >= 1000l) {
                        mult = 1000l;
                        userValue = value / 1000l;
-                       suffix = "k";
+                       suffix = " k";
                }
 
                String deci = "";
similarity index 52%
rename from src/be/nikiroo/utils/NextableInputStream.java
rename to src/be/nikiroo/utils/streams/BufferedInputStream.java
index b5374a1329b88ce4944412830f4edb552c818b17..be4b24d2ec9b477b3556df34fd607923c8c30bf5 100644 (file)
@@ -1,30 +1,31 @@
-package be.nikiroo.utils;
+package be.nikiroo.utils.streams;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
 
 /**
- * This {@link InputStream} can be separated into sub-streams (you can process
- * it as a normal {@link InputStream} but, when it is spent, you can call
- * {@link NextableInputStream#next()} on it to unlock new data).
+ * A simple {@link InputStream} that is buffered with a bytes array.
  * <p>
- * The separation in sub-streams is done via {@link NextableInputStreamStep}.
+ * It is mostly intended to be used as a base class to create new
+ * {@link InputStream}s with special operation modes, and to give some default
+ * methods.
  * 
  * @author niki
  */
-public class NextableInputStream extends InputStream {
-       private NextableInputStreamStep step;
-       private boolean started;
-       private boolean stopped;
-       private boolean closed;
+public class BufferedInputStream extends InputStream {
+       /** 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 InputStream in;
        private int openCounter;
-       private boolean eof;
-       private int pos;
-       private int len;
-       private byte[] buffer;
 
        // special use, prefetched next buffer
        private byte[] buffer2;
@@ -35,48 +36,38 @@ public class NextableInputStream extends InputStream {
        private long bytesRead;
 
        /**
-        * Create a new {@link NextableInputStream} that wraps the given
+        * Create a new {@link BufferedInputStream} that wraps the given
         * {@link InputStream}.
         * 
         * @param in
         *            the {@link InputStream} to wrap
-        * @param step
-        *            how to separate it into sub-streams (can be NULL, but in that
-        *            case it will behave as a normal {@link InputStream})
         */
-       public NextableInputStream(InputStream in, NextableInputStreamStep step) {
+       public BufferedInputStream(InputStream in) {
                this.in = in;
-               this.step = step;
 
                this.buffer = new byte[4096];
                this.originalBuffer = this.buffer;
-               this.pos = 0;
-               this.len = 0;
+               this.start = 0;
+               this.stop = 0;
        }
 
        /**
-        * Create a new {@link NextableInputStream} that wraps the given bytes array
+        * Create a new {@link BufferedInputStream} that wraps the given bytes array
         * as a data source.
         * 
         * @param in
         *            the array to wrap, cannot be NULL
-        * @param step
-        *            how to separate it into sub-streams (can be NULL, but in that
-        *            case it will behave as a normal {@link InputStream})
         */
-       public NextableInputStream(byte[] in, NextableInputStreamStep step) {
-               this(in, step, 0, in.length);
+       public BufferedInputStream(byte[] in) {
+               this(in, 0, in.length);
        }
 
        /**
-        * Create a new {@link NextableInputStream} that wraps the given bytes array
+        * Create a new {@link BufferedInputStream} that wraps the given bytes array
         * as a data source.
         * 
         * @param in
         *            the array to wrap, cannot be NULL
-        * @param step
-        *            how to separate it into sub-streams (can be NULL, but in that
-        *            case it will behave as a normal {@link InputStream})
         * @param offset
         *            the offset to start the reading at
         * @param length
@@ -88,8 +79,7 @@ public class NextableInputStream extends InputStream {
         * @throws IndexOutOfBoundsException
         *             if the offset and length do not correspond to the given array
         */
-       public NextableInputStream(byte[] in, NextableInputStreamStep step,
-                       int offset, int length) {
+       public BufferedInputStream(byte[] in, int offset, int length) {
                if (in == null) {
                        throw new NullPointerException();
                } else if (offset < 0 || length < 0 || length > in.length - offset) {
@@ -97,20 +87,17 @@ public class NextableInputStream extends InputStream {
                }
 
                this.in = null;
-               this.step = step;
 
                this.buffer = in;
                this.originalBuffer = this.buffer;
-               this.pos = offset;
-               this.len = length;
-
-               checkBuffer(true);
+               this.start = offset;
+               this.stop = length;
        }
 
        /**
-        * Return this very same {@link NextableInputStream}, but keep a counter of
+        * Return this very same {@link BufferedInputStream}, but keep a counter of
         * how many streams were open this way. When calling
-        * {@link NextableInputStream#close()}, decrease this counter if it is not
+        * {@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.
@@ -130,49 +117,41 @@ public class NextableInputStream extends InputStream {
        }
 
        /**
-        * Unblock the processing of the next sub-stream.
-        * <p>
-        * It can only be called when the "current" stream is spent (i.e., you must
-        * first process the stream until it is spent).
+        * Check if the current content (what will be read next) starts with the
+        * given search term.
         * <p>
-        * We consider that when the under-laying {@link InputStream} is also spent,
-        * we cannot have a next sub-stream (it will thus return FALSE).
-        * <p>
-        * {@link IOException}s can happen when we have no data available in the
-        * buffer; in that case, we fetch more data to know if we can have a next
-        * sub-stream or not.
+        * Note: the search term size <b>must</b> be smaller or equal the internal
+        * buffer size.
+        * 
+        * @param search
+        *            the term to search for
         * 
-        * @return TRUE if we unblocked the next sub-stream, FALSE if not
+        * @return TRUE if the content that will be read starts with it
         * 
         * @throws IOException
-        *             in case of I/O error or if the stream is closed
+        *             in case of I/O error or if the size of the search term is
+        *             greater than the internal buffer
         */
-       public boolean next() throws IOException {
-               return next(false);
+       public boolean startsWiths(String search) throws IOException {
+               return startsWith(search.getBytes("UTF-8"));
        }
 
        /**
-        * Unblock the next sub-stream as would have done
-        * {@link NextableInputStream#next()}, but disable the sub-stream systems.
+        * Check if the current content (what will be read next) starts with the
+        * given search term.
         * <p>
-        * That is, the next stream, if any, will be the last one and will not be
-        * subject to the {@link NextableInputStreamStep}.
+        * Note: the search term size <b>must</b> be smaller or equal the internal
+        * buffer size.
+        * 
+        * @param search
+        *            the term to search for
         * 
-        * @return TRUE if we unblocked the next sub-stream, FALSE if not
+        * @return TRUE if the content that will be read starts with it
         * 
         * @throws IOException
-        *             in case of I/O error or if the stream is closed
+        *             in case of I/O error or if the size of the search term is
+        *             greater than the internal buffer
         */
-       public boolean nextAll() throws IOException {
-               return next(true);
-       }
-
-       // max is buffer.size !
-       public boolean startsWiths(String search) throws IOException {
-               return startsWith(search.getBytes("UTF-8"));
-       }
-
-       // max is buffer.size !
        public boolean startsWith(byte[] search) throws IOException {
                if (search.length > originalBuffer.length) {
                        throw new IOException(
@@ -188,7 +167,7 @@ public class NextableInputStream extends InputStream {
 
                if (available() >= search.length) {
                        // Easy path
-                       return startsWith(search, buffer, pos);
+                       return StreamUtils.startsWith(search, buffer, start, stop);
                } else if (!eof) {
                        // Harder path
                        if (buffer2 == null && buffer.length == originalBuffer.length) {
@@ -204,9 +183,7 @@ public class NextableInputStream extends InputStream {
                                len2 += pos2;
                        }
 
-                       if (available() + (len2 - pos2) >= search.length) {
-                               return startsWith(search, buffer2, pos2);
-                       }
+                       return StreamUtils.startsWith(search, buffer2, pos2, len2);
                }
 
                return false;
@@ -228,7 +205,7 @@ public class NextableInputStream extends InputStream {
         * @return TRUE if it is
         */
        public boolean eof() {
-               return closed || (len < 0 && !hasMoreData());
+               return closed || (stop < 0 && !hasMoreData());
        }
 
        @Override
@@ -240,7 +217,7 @@ public class NextableInputStream extends InputStream {
                        return -1;
                }
 
-               return buffer[pos++];
+               return buffer[start++];
        }
 
        @Override
@@ -264,10 +241,11 @@ public class NextableInputStream extends InputStream {
                while (hasMoreData() && done < blen) {
                        preRead();
                        if (hasMoreData()) {
-                               for (int i = pos; i < blen && i < len; i++) {
-                                       b[boff + done] = buffer[i];
-                                       pos++;
-                                       done++;
+                               int now = Math.min(blen, stop) - start;
+                               if (now > 0) {
+                                       System.arraycopy(buffer, start, b, boff + done, now);
+                                       start += now;
+                                       done += now;
                                }
                        }
                }
@@ -286,7 +264,7 @@ public class NextableInputStream extends InputStream {
                        preRead();
 
                        long inBuffer = Math.min(n, available());
-                       pos += inBuffer;
+                       start += inBuffer;
                        n -= inBuffer;
                        skipped += inBuffer;
                }
@@ -300,7 +278,7 @@ public class NextableInputStream extends InputStream {
                        return 0;
                }
 
-               return Math.max(0, len - pos);
+               return Math.max(0, stop - start);
        }
 
        /**
@@ -309,11 +287,11 @@ public class NextableInputStream extends InputStream {
         * <p>
         * Including the under-laying {@link InputStream}.
         * <p>
-        * <b>Note:</b> if you called the {@link NextableInputStream#open()} method
+        * <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 NextableInputStream#close()} once more
-        * than {@link NextableInputStream#open()}.
+        * closed when you have called {@link BufferedInputStream#close()} once more
+        * than {@link BufferedInputStream#open()}.
         * 
         * @exception IOException
         *                in case of I/O error
@@ -333,11 +311,14 @@ public class NextableInputStream extends InputStream {
         * 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 NextableInputStream#open()} method
+        * <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 NextableInputStream#close()} once more
-        * than {@link NextableInputStream#open()}.
+        * 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
@@ -365,31 +346,31 @@ public class NextableInputStream extends InputStream {
         * @throws IOException
         *             in case of I/O error
         */
-       private boolean preRead() throws IOException {
+       protected boolean preRead() throws IOException {
                boolean hasRead = false;
-               if (!eof && in != null && pos >= len && !stopped) {
-                       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;
                                len2 = 0;
                        } else {
                                buffer = originalBuffer;
-                               len = in.read(buffer);
-                               if (len > 0) {
-                                       bytesRead += len;
+
+                               stop = read(in, buffer);
+                               if (stop > 0) {
+                                       bytesRead += stop;
                                }
                        }
 
-                       checkBuffer(true);
                        hasRead = true;
                }
 
-               if (pos >= len) {
+               if (start >= stop) {
                        eof = true;
                }
 
@@ -397,86 +378,29 @@ public class NextableInputStream extends InputStream {
        }
 
        /**
-        * We have more data available in the buffer or we can fetch more.
+        * Read the under-laying stream into the local buffer.
         * 
-        * @return TRUE if it is the case, FALSE if not
-        */
-       private boolean hasMoreData() {
-               return !closed && started && !(eof && pos >= len);
-       }
-
-       /**
-        * Check that the buffer didn't overshot to the next item, and fix
-        * {@link NextableInputStream#len} if needed.
-        * <p>
-        * If {@link NextableInputStream#len} is fixed,
-        * {@link NextableInputStream#eof} and {@link NextableInputStream#stopped}
-        * are set to TRUE.
+        * @param in
+        *            the under-laying {@link InputStream}
+        * @param buffer
+        *            the buffer we use in this {@link BufferedInputStream}
         * 
-        * @param newBuffer
-        *            we changed the buffer, we need to clear some information in
-        *            the {@link NextableInputStreamStep}
+        * @return the number of bytes read
+        * 
+        * @throws IOException
+        *             in case of I/O error
         */
-       private void checkBuffer(boolean newBuffer) {
-               if (step != null && len > 0) {
-                       if (newBuffer) {
-                               step.clearBuffer();
-                       }
-
-                       int stopAt = step.stop(buffer, pos, len);
-                       if (stopAt >= 0) {
-                               len = stopAt;
-                               eof = true;
-                               stopped = true;
-                       }
-               }
+       protected int read(InputStream in, byte[] buffer) throws IOException {
+               return in.read(buffer);
        }
 
        /**
-        * The implementation of {@link NextableInputStream#next()} and
-        * {@link NextableInputStream#nextAll()}.
-        * 
-        * @param all
-        *            TRUE for {@link NextableInputStream#nextAll()}, FALSE for
-        *            {@link NextableInputStream#next()}
-        * 
-        * @return TRUE if we unblocked the next sub-stream, FALSE if not
+        * We have more data available in the buffer or we can fetch more.
         * 
-        * @throws IOException
-        *             in case of I/O error or if the stream is closed
+        * @return TRUE if it is the case, FALSE if not
         */
-       private boolean next(boolean all) throws IOException {
-               checkClose();
-
-               if (!started) {
-                       // First call before being allowed to read
-                       started = true;
-
-                       if (all) {
-                               step = null;
-                       }
-
-                       return true;
-               }
-
-               if (step != null && !hasMoreData() && stopped) {
-                       len = step.getResumeLen();
-                       pos += step.getResumeSkip();
-                       eof = false;
-
-                       if (all) {
-                               step = null;
-                       }
-
-                       if (!preRead()) {
-                               checkBuffer(false);
-                       }
-
-                       // consider that if EOF, there is no next
-                       return hasMoreData();
-               }
-
-               return false;
+       protected boolean hasMoreData() {
+               return !closed && !(eof && start >= stop);
        }
 
        /**
@@ -486,24 +410,10 @@ public class NextableInputStream extends InputStream {
         * @throws IOException
         *             if it was closed
         */
-       private void checkClose() throws IOException {
+       protected void checkClose() throws IOException {
                if (closed) {
                        throw new IOException(
-                                       "This NextableInputStream was closed, you cannot use it anymore.");
+                                       "This BufferedInputStream was closed, you cannot use it anymore.");
                }
        }
-
-       // buffer must be > search
-       static private boolean startsWith(byte[] search, byte[] buffer,
-                       int offset) {
-               boolean same = true;
-               for (int i = 0; i < search.length; i++) {
-                       if (search[i] != buffer[offset + i]) {
-                               same = false;
-                               break;
-                       }
-               }
-
-               return same;
-       }
 }
diff --git a/src/be/nikiroo/utils/streams/BufferedOutputStream.java b/src/be/nikiroo/utils/streams/BufferedOutputStream.java
new file mode 100644 (file)
index 0000000..8b74ae1
--- /dev/null
@@ -0,0 +1,255 @@
+package be.nikiroo.utils.streams;
+
+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;
+       /** The actual under-laying stream. */
+       protected OutputStream out;
+       /** The number of bytes written to the under-laying stream. */
+       protected long bytesWritten;
+       /**
+        * Can bypass the flush process for big writes (will directly write to the
+        * under-laying buffer if the array to write is &gt; the internal buffer
+        * size).
+        * <p>
+        * By default, this is true.
+        */
+       protected boolean bypassFlush = true;
+
+       private boolean closed;
+       private int openCounter;
+
+       /**
+        * 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}, write the current buffered data
+        * to (and optionally also flush) the under-laying stream.
+        * <p>
+        * If {@link BufferedOutputStream#bypassFlush} is false, all writes to the
+        * under-laying stream are done in this method.
+        * 
+        * @param includingSubStream
+        *            also flush the under-laying stream
+        * @throws IOException
+        *             in case of I/O error
+        */
+       protected 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();
+                               }
+                       }
+               }
+       }
+}
similarity index 96%
rename from src/be/nikiroo/utils/MarkableFileInputStream.java
rename to src/be/nikiroo/utils/streams/MarkableFileInputStream.java
index f4d95d53bf73d6326e2fbabcf7a0a656f40c75fa..dab4cdc45a536f993f8e6924d89ef4fdd5780e32 100644 (file)
@@ -1,4 +1,4 @@
-package be.nikiroo.utils;
+package be.nikiroo.utils.streams;
 
 import java.io.FileInputStream;
 import java.io.FilterInputStream;
diff --git a/src/be/nikiroo/utils/streams/NextableInputStream.java b/src/be/nikiroo/utils/streams/NextableInputStream.java
new file mode 100644 (file)
index 0000000..4a6e0ab
--- /dev/null
@@ -0,0 +1,235 @@
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This {@link InputStream} can be separated into sub-streams (you can process
+ * it as a normal {@link InputStream} but, when it is spent, you can call
+ * {@link NextableInputStream#next()} on it to unlock new data).
+ * <p>
+ * The separation in sub-streams is done via {@link NextableInputStreamStep}.
+ * 
+ * @author niki
+ */
+public class NextableInputStream extends BufferedInputStream {
+       private NextableInputStreamStep step;
+       private boolean started;
+       private boolean stopped;
+
+       /**
+        * Create a new {@link NextableInputStream} that wraps the given
+        * {@link InputStream}.
+        * 
+        * @param in
+        *            the {@link InputStream} to wrap
+        * @param step
+        *            how to separate it into sub-streams (can be NULL, but in that
+        *            case it will behave as a normal {@link InputStream})
+        */
+       public NextableInputStream(InputStream in, NextableInputStreamStep step) {
+               super(in);
+               this.step = step;
+       }
+
+       /**
+        * Create a new {@link NextableInputStream} that wraps the given bytes array
+        * as a data source.
+        * 
+        * @param in
+        *            the array to wrap, cannot be NULL
+        * @param step
+        *            how to separate it into sub-streams (can be NULL, but in that
+        *            case it will behave as a normal {@link InputStream})
+        */
+       public NextableInputStream(byte[] in, NextableInputStreamStep step) {
+               this(in, step, 0, in.length);
+       }
+
+       /**
+        * Create a new {@link NextableInputStream} that wraps the given bytes array
+        * as a data source.
+        * 
+        * @param in
+        *            the array to wrap, cannot be NULL
+        * @param step
+        *            how to separate it into sub-streams (can be NULL, but in that
+        *            case it will behave as a normal {@link InputStream})
+        * @param offset
+        *            the offset to start the reading at
+        * @param length
+        *            the number of bytes to take into account in the array,
+        *            starting from the offset
+        * 
+        * @throws NullPointerException
+        *             if the array is NULL
+        * @throws IndexOutOfBoundsException
+        *             if the offset and length do not correspond to the given array
+        */
+       public NextableInputStream(byte[] in, NextableInputStreamStep step,
+                       int offset, int length) {
+               super(in, offset, length);
+               this.step = step;
+               checkBuffer(true);
+       }
+
+       /**
+        * Unblock the processing of the next sub-stream.
+        * <p>
+        * It can only be called when the "current" stream is spent (i.e., you must
+        * first process the stream until it is spent).
+        * <p>
+        * We consider that when the under-laying {@link InputStream} is also spent,
+        * we cannot have a next sub-stream (it will thus return FALSE).
+        * <p>
+        * {@link IOException}s can happen when we have no data available in the
+        * buffer; in that case, we fetch more data to know if we can have a next
+        * sub-stream or not.
+        * 
+        * @return TRUE if we unblocked the next sub-stream, FALSE if not
+        * 
+        * @throws IOException
+        *             in case of I/O error or if the stream is closed
+        */
+       public boolean next() throws IOException {
+               return next(false);
+       }
+
+       /**
+        * Unblock the next sub-stream as would have done
+        * {@link NextableInputStream#next()}, but disable the sub-stream systems.
+        * <p>
+        * That is, the next stream, if any, will be the last one and will not be
+        * subject to the {@link NextableInputStreamStep}.
+        * 
+        * @return TRUE if we unblocked the next sub-stream, FALSE if not
+        * 
+        * @throws IOException
+        *             in case of I/O error or if the stream is closed
+        */
+       public boolean nextAll() throws IOException {
+               return next(true);
+       }
+
+       /**
+        * Check if this stream is totally spent (no more data to read or to
+        * process).
+        * <p>
+        * Note: an empty stream that is still not started will return FALSE, as we
+        * don't know yet if it is empty.
+        * 
+        * @return TRUE if it is
+        */
+       @Override
+       public boolean eof() {
+               return super.eof();
+       }
+       
+       /**
+        * Check if we still have some data in the buffer and, if not, fetch some.
+        * 
+        * @return TRUE if we fetched some data, FALSE if there are still some in
+        *         the buffer
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       @Override
+       protected boolean preRead() throws IOException {
+               if (!stopped) {
+                       boolean bufferChanged = super.preRead();
+                       checkBuffer(true);
+                       return bufferChanged;
+               }
+
+               if (start >= stop) {
+                       eof = true;
+               }
+
+               return false;
+       }
+
+       /**
+        * We have more data available in the buffer or we can fetch more.
+        * 
+        * @return TRUE if it is the case, FALSE if not
+        */
+       @Override
+       protected boolean hasMoreData() {
+               return started && super.hasMoreData();
+       }
+
+       /**
+        * Check that the buffer didn't overshot to the next item, and fix
+        * {@link NextableInputStream#stop} if needed.
+        * <p>
+        * If {@link NextableInputStream#stop} is fixed,
+        * {@link NextableInputStream#eof} and {@link NextableInputStream#stopped}
+        * are set to TRUE.
+        * 
+        * @param newBuffer
+        *            we changed the buffer, we need to clear some information in
+        *            the {@link NextableInputStreamStep}
+        */
+       private void checkBuffer(boolean newBuffer) {
+               if (step != null && stop > 0) {
+                       if (newBuffer) {
+                               step.clearBuffer();
+                       }
+
+                       int stopAt = step.stop(buffer, start, stop);
+                       if (stopAt >= 0) {
+                               stop = stopAt;
+                               eof = true;
+                               stopped = true;
+                       }
+               }
+       }
+
+       /**
+        * The implementation of {@link NextableInputStream#next()} and
+        * {@link NextableInputStream#nextAll()}.
+        * 
+        * @param all
+        *            TRUE for {@link NextableInputStream#nextAll()}, FALSE for
+        *            {@link NextableInputStream#next()}
+        * 
+        * @return TRUE if we unblocked the next sub-stream, FALSE if not
+        * 
+        * @throws IOException
+        *             in case of I/O error or if the stream is closed
+        */
+       private boolean next(boolean all) throws IOException {
+               checkClose();
+
+               if (!started) {
+                       // First call before being allowed to read
+                       started = true;
+
+                       if (all) {
+                               step = null;
+                       }
+
+                       return true;
+               }
+
+               if (step != null && !hasMoreData() && stopped) {
+                       stop = step.getResumeLen();
+                       start += step.getResumeSkip();
+                       eof = false;
+
+                       if (all) {
+                               step = null;
+                       }
+
+                       if (!preRead()) {
+                               checkBuffer(false);
+                       }
+
+                       // consider that if EOF, there is no next
+                       return hasMoreData();
+               }
+
+               return false;
+       }
+}
similarity index 98%
rename from src/be/nikiroo/utils/NextableInputStreamStep.java
rename to src/be/nikiroo/utils/streams/NextableInputStreamStep.java
index a2ee039717fa033bfd6c938bcafc211587903313..818abf5af730e8210d5151a12df0970758f694d3 100755 (executable)
@@ -1,4 +1,4 @@
-package be.nikiroo.utils;
+package be.nikiroo.utils.streams;
 
 import java.io.InputStream;
 
diff --git a/src/be/nikiroo/utils/streams/ReplaceInputStream.java b/src/be/nikiroo/utils/streams/ReplaceInputStream.java
new file mode 100644 (file)
index 0000000..f5138ee
--- /dev/null
@@ -0,0 +1,83 @@
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This {@link InputStream} will change some of its content by replacing it with
+ * something else.
+ * 
+ * @author niki
+ */
+public class ReplaceInputStream extends BufferedInputStream {
+       private byte[] from;
+       private byte[] to;
+
+       private byte[] source;
+       private int spos;
+       private int slen;
+
+       /**
+        * Create a {@link ReplaceInputStream} that will replace <tt>from</tt> with
+        * <tt>to</tt>.
+        * 
+        * @param in
+        *            the under-laying {@link InputStream}
+        * @param from
+        *            the {@link String} to replace
+        * @param to
+        *            the {@link String} to replace with
+        */
+       public ReplaceInputStream(InputStream in, String from, String to) {
+               this(in, StreamUtils.bytes(from), StreamUtils.bytes(to));
+       }
+
+       /**
+        * Create a {@link ReplaceInputStream} that will replace <tt>from</tt> with
+        * <tt>to</tt>.
+        * 
+        * @param in
+        *            the under-laying {@link InputStream}
+        * @param from
+        *            the value to replace
+        * @param to
+        *            the value to replace with
+        */
+       public ReplaceInputStream(InputStream in, byte[] from, byte[] to) {
+               super(in);
+               this.from = from;
+               this.to = to;
+
+               source = new byte[4096];
+               spos = 0;
+               slen = 0;
+       }
+
+       @Override
+       protected int read(InputStream in, byte[] buffer) throws IOException {
+               if (buffer.length < to.length || source.length < to.length * 2) {
+                       throw new IOException(
+                                       "An underlaying buffer is too small for this replace value");
+               }
+
+               if (spos >= slen) {
+                       spos = 0;
+                       slen = in.read(source);
+               }
+
+               // Note: very simple, not efficient implementation, sorry.
+               int count = 0;
+               while (spos < slen && count < buffer.length - to.length) {
+                       if (from.length > 0
+                                       && StreamUtils.startsWith(from, source, spos, slen)) {
+                               System.arraycopy(to, 0, buffer, spos, to.length);
+                               count += to.length;
+                               spos += from.length;
+                       } else {
+                               buffer[count++] = source[spos++];
+                       }
+               }
+
+               return count;
+       }
+}
diff --git a/src/be/nikiroo/utils/streams/ReplaceOutputStream.java b/src/be/nikiroo/utils/streams/ReplaceOutputStream.java
new file mode 100644 (file)
index 0000000..e889b76
--- /dev/null
@@ -0,0 +1,72 @@
+package be.nikiroo.utils.streams;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * This {@link OutputStream} will change some of its content by replacing it
+ * with something else.
+ * 
+ * @author niki
+ */
+public class ReplaceOutputStream extends BufferedOutputStream {
+       private byte[] from;
+       private byte[] to;
+
+       /**
+        * Create a {@link ReplaceOutputStream} that will replace <tt>from</tt> with
+        * <tt>to</tt>.
+        * 
+        * @param out
+        *            the under-laying {@link OutputStream}
+        * @param from
+        *            the {@link String} to replace
+        * @param to
+        *            the {@link String} to replace with
+        */
+       public ReplaceOutputStream(OutputStream out, String from, String to) {
+               this(out, StreamUtils.bytes(from), StreamUtils.bytes(to));
+       }
+
+       /**
+        * Create a {@link ReplaceOutputStream} that will replace <tt>from</tt> with
+        * <tt>to</tt>.
+        * 
+        * @param out
+        *            the under-laying {@link OutputStream}
+        * @param from
+        *            the value to replace
+        * @param to
+        *            the value to replace with
+        */
+       public ReplaceOutputStream(OutputStream out, byte[] from, byte[] to) {
+               super(out);
+               bypassFlush = false;
+
+               this.from = from;
+               this.to = to;
+       }
+
+       @Override
+       protected void flush(boolean includingSubStream) throws IOException {
+               // Note: very simple, not efficient implementation, sorry.
+               while (start < stop) {
+                       if (from.length > 0
+                                       && StreamUtils.startsWith(from, buffer, start, stop)) {
+                               out.write(to);
+                               bytesWritten += to.length;
+                               start += from.length;
+                       } else {
+                               out.write(buffer[start++]);
+                               bytesWritten++;
+                       }
+               }
+
+               start = 0;
+               stop = 0;
+
+               if (includingSubStream) {
+                       out.flush();
+               }
+       }
+}
diff --git a/src/be/nikiroo/utils/streams/StreamUtils.java b/src/be/nikiroo/utils/streams/StreamUtils.java
new file mode 100644 (file)
index 0000000..6b8251a
--- /dev/null
@@ -0,0 +1,70 @@
+package be.nikiroo.utils.streams;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Some non-public utilities used in the stream classes.
+ * 
+ * @author niki
+ */
+class StreamUtils {
+       /**
+        * Check if the buffer starts with the given search term (given as an array,
+        * a start position and an end position).
+        * <p>
+        * Note: the parameter <tt>stop</tt> is the <b>index</b> of the last
+        * position, <b>not</b> the length.
+        * <p>
+        * Note: the search term size <b>must</b> be smaller or equal the internal
+        * buffer size.
+        * 
+        * @param search
+        *            the term to search for
+        * @param buffer
+        *            the buffer to look into
+        * @param start
+        *            the offset at which to start the search
+        * @param stop
+        *            the maximum index of the data to check (this is <b>not</b> a
+        *            length, but an index)
+        * 
+        * @return TRUE if the search content is present at the given location and
+        *         does not exceed the <tt>len</tt> index
+        */
+       static public boolean startsWith(byte[] search, byte[] buffer, int start,
+                       int stop) {
+
+               // Check if there even is enough space for it
+               if (search.length > (stop - start)) {
+                       return false;
+               }
+
+               boolean same = true;
+               for (int i = 0; i < search.length; i++) {
+                       if (search[i] != buffer[start + i]) {
+                               same = false;
+                               break;
+                       }
+               }
+
+               return same;
+       }
+
+       /**
+        * Return the bytes array representation of the given {@link String} in
+        * UTF-8.
+        * 
+        * @param str
+        *            the string to transform into bytes
+        * @return the content in bytes
+        */
+       static public byte[] bytes(String str) {
+               try {
+                       return str.getBytes("UTF-8");
+               } catch (UnsupportedEncodingException e) {
+                       // All conforming JVM must support UTF-8
+                       e.printStackTrace();
+                       return null;
+               }
+       }
+}
diff --git a/src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java b/src/be/nikiroo/utils/test_code/BufferedInputStreamTest.java
new file mode 100644 (file)
index 0000000..3ba7be8
--- /dev/null
@@ -0,0 +1,46 @@
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.BufferedInputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class BufferedInputStreamTest extends TestLauncher {
+       public BufferedInputStreamTest(String[] args) {
+               super("BufferedInputStream test", args);
+
+               addTest(new TestCase("Simple InputStream reading") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               BufferedInputStream in = new BufferedInputStream(
+                                               new ByteArrayInputStream(expected));
+                               checkArrays(this, "FIRST", in, expected);
+                       }
+               });
+
+               addTest(new TestCase("Simple byte array reading") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] expected = new byte[] { 42, 12, 0, 127 };
+                               BufferedInputStream in = new BufferedInputStream(expected);
+                               checkArrays(this, "FIRST", in, expected);
+                       }
+               });
+       }
+
+       static void checkArrays(TestCase test, String prefix, InputStream in,
+                       byte[] expected) throws Exception {
+               byte[] actual = IOUtils.toByteArray(in);
+               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/BufferedOutputStreamTest.java b/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java
new file mode 100644 (file)
index 0000000..cf6eb2a
--- /dev/null
@@ -0,0 +1,80 @@
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayOutputStream;
+
+import be.nikiroo.utils.streams.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 < expected.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 4664cbf7e161b41e1b85179debbbe95fc2991b42..70123b99ee7095be9764b0605c844284665031e2 100644 (file)
@@ -4,8 +4,8 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 
 import be.nikiroo.utils.IOUtils;
-import be.nikiroo.utils.NextableInputStream;
-import be.nikiroo.utils.NextableInputStreamStep;
+import be.nikiroo.utils.streams.NextableInputStream;
+import be.nikiroo.utils.streams.NextableInputStreamStep;
 import be.nikiroo.utils.test.TestCase;
 import be.nikiroo.utils.test.TestLauncher;
 
diff --git a/src/be/nikiroo/utils/test_code/ReplaceInputStreamTest.java b/src/be/nikiroo/utils/test_code/ReplaceInputStreamTest.java
new file mode 100644 (file)
index 0000000..e6e2112
--- /dev/null
@@ -0,0 +1,106 @@
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.streams.ReplaceInputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ReplaceInputStreamTest extends TestLauncher {
+       public ReplaceInputStreamTest(String[] args) {
+               super("ReplaceInputStream test", args);
+
+               addTest(new TestCase("Empty replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), new byte[0],
+                                               new byte[0]);
+
+                               checkArrays(this, "FIRST", in, data);
+                       }
+               });
+
+               addTest(new TestCase("Simple replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), new byte[] { 0 },
+                                               new byte[] { 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 42, 12, 10, 127 });
+                       }
+               });
+
+               addTest(new TestCase("3/4 replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new byte[] { 12, 0, 127 }, new byte[] { 10, 10, 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 42, 10, 10, 10 });
+                       }
+               });
+
+               addTest(new TestCase("Lnger replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data), new byte[] { 0 },
+                                               new byte[] { 10, 10, 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 42, 12, 10, 10, 10,
+                                               127 });
+                       }
+               });
+
+               addTest(new TestCase("Shorter replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data),
+                                               new byte[] { 42, 12, 0 }, new byte[] { 10 });
+
+                               checkArrays(this, "FIRST", in, new byte[] { 10, 127 });
+                       }
+               });
+
+               addTest(new TestCase("String replace") {
+                       @Override
+                       public void test() throws Exception {
+                               byte[] data = "I like red".getBytes("UTF-8");
+                               ReplaceInputStream in = new ReplaceInputStream(
+                                               new ByteArrayInputStream(data),
+                                               "red".getBytes("UTF-8"), "blue".getBytes("UTF-8"));
+
+                               checkArrays(this, "FIRST", in, "I like blue".getBytes("UTF-8"));
+
+                               data = "I like blue".getBytes("UTF-8");
+                               in = new ReplaceInputStream(new ByteArrayInputStream(data),
+                                               "blue".getBytes("UTF-8"), "red".getBytes("UTF-8"));
+
+                               checkArrays(this, "FIRST", in, "I like red".getBytes("UTF-8"));
+                       }
+               });
+       }
+
+       static void checkArrays(TestCase test, String prefix, InputStream in,
+                       byte[] expected) throws Exception {
+               byte[] actual = IOUtils.toByteArray(in);
+               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("Item " + i + " (0-based) is not the same",
+                                       expected[i], actual[i]);
+               }
+       }
+}
diff --git a/src/be/nikiroo/utils/test_code/ReplaceOutputStreamTest.java b/src/be/nikiroo/utils/test_code/ReplaceOutputStreamTest.java
new file mode 100644 (file)
index 0000000..1db3397
--- /dev/null
@@ -0,0 +1,168 @@
+package be.nikiroo.utils.test_code;
+
+import java.io.ByteArrayOutputStream;
+
+import be.nikiroo.utils.streams.ReplaceOutputStream;
+import be.nikiroo.utils.test.TestCase;
+import be.nikiroo.utils.test.TestLauncher;
+
+class ReplaceOutputStreamTest extends TestLauncher {
+       public ReplaceOutputStreamTest(String[] args) {
+               super("ReplaceOutputStream test", args);
+
+               addTest(new TestCase("Single write, empty bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[0], new byte[0]);
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, data);
+                       }
+               });
+
+               addTest(new TestCase("Multiple writes, empty Strings replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(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);
+                       }
+               });
+
+               addTest(new TestCase("Single write, bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12 }, new byte[] { 55 });
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 0, 127 });
+                       }
+               });
+
+               addTest(new TestCase("Multiple writes, Strings replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout, "(-)",
+                                               "(.)");
+
+                               byte[] data1 = "un mot ".getBytes("UTF-8");
+                               byte[] data2 = "(-) of twee ".getBytes("UTF-8");
+                               byte[] data3 = "(-) makes the difference".getBytes("UTF-8");
+
+                               out.write(data1);
+                               out.write(data2);
+                               out.write(data3);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout,
+                                               "un mot (.) of twee (.) makes the difference"
+                                                               .getBytes("UTF-8"));
+                       }
+               });
+
+               addTest(new TestCase("Single write, longer bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12 }, new byte[] { 55, 55, 66 });
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 55, 66,
+                                               0, 127 });
+                       }
+               });
+
+               addTest(new TestCase("Single write, shorter bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12, 0 }, new byte[] { 55 });
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 55, 127 });
+                       }
+               });
+
+               addTest(new TestCase("Single write, remove bytes replaces") {
+                       @Override
+                       public void test() throws Exception {
+                               ByteArrayOutputStream bout = new ByteArrayOutputStream();
+                               ReplaceOutputStream out = new ReplaceOutputStream(bout,
+                                               new byte[] { 12 }, new byte[] {});
+
+                               byte[] data = new byte[] { 42, 12, 0, 127 };
+
+                               out.write(data);
+                               out.close();
+
+                               checkArrays(this, "FIRST", bout, new byte[] { 42, 0, 127 });
+                       }
+               });
+       }
+
+       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 < expected.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 f9c52726cf75c7fb5158d369ed2005fec5995816..de283594d467f1b73e6a998642290b8e0b63ff5c 100644 (file)
@@ -238,7 +238,7 @@ class StringUtilsTest extends TestLauncher {
                                assertEquals(21200l, StringUtils.toNumber("21200"));
                                assertEquals(0l, StringUtils.toNumber("0"));
                                assertEquals("263", StringUtils.formatNumber(263l));
-                               assertEquals("21k", StringUtils.formatNumber(21000l));
+                               assertEquals("21 k", StringUtils.formatNumber(21000l));
                                assertEquals("0", StringUtils.formatNumber(0l));
                        }
                });
@@ -246,31 +246,31 @@ class StringUtilsTest extends TestLauncher {
                addTest(new TestCase("format/toNumber not 000") {
                        @Override
                        public void test() throws Exception {
-                               assertEquals(263200l, StringUtils.toNumber("263.2k"));
-                               assertEquals(42000l, StringUtils.toNumber("42.0k"));
-                               assertEquals(12000000l, StringUtils.toNumber("12M"));
-                               assertEquals(2000000000l, StringUtils.toNumber("2G"));
-                               assertEquals("263k", StringUtils.formatNumber(263012l));
-                               assertEquals("42k", StringUtils.formatNumber(42012l));
-                               assertEquals("12M", StringUtils.formatNumber(12012121l));
-                               assertEquals("7G", StringUtils.formatNumber(7364635928l));
+                               assertEquals(263200l, StringUtils.toNumber("263.2 k"));
+                               assertEquals(42000l, StringUtils.toNumber("42.0 k"));
+                               assertEquals(12000000l, StringUtils.toNumber("12 M"));
+                               assertEquals(2000000000l, StringUtils.toNumber("2 G"));
+                               assertEquals("263 k", StringUtils.formatNumber(263012l));
+                               assertEquals("42 k", StringUtils.formatNumber(42012l));
+                               assertEquals("12 M", StringUtils.formatNumber(12012121l));
+                               assertEquals("7 G", StringUtils.formatNumber(7364635928l));
                        }
                });
 
                addTest(new TestCase("format/toNumber decimals") {
                        @Override
                        public void test() throws Exception {
-                               assertEquals(263200l, StringUtils.toNumber("263.2k"));
-                               assertEquals(1200l, StringUtils.toNumber("1.2k"));
-                               assertEquals(42700000l, StringUtils.toNumber("42.7M"));
-                               assertEquals(1220l, StringUtils.toNumber("1.22k"));
-                               assertEquals(1432l, StringUtils.toNumber("1.432k"));
-                               assertEquals(6938l, StringUtils.toNumber("6.938k"));
-                               assertEquals("1.3k", StringUtils.formatNumber(1300l, 1));
-                               assertEquals("263.2020k", StringUtils.formatNumber(263202l, 4));
-                               assertEquals("1.26k", StringUtils.formatNumber(1267l, 2));
-                               assertEquals("42.7M", StringUtils.formatNumber(42712121l, 1));
-                               assertEquals("5.09G", StringUtils.formatNumber(5094837485l, 2));
+                               assertEquals(263200l, StringUtils.toNumber("263.2 k"));
+                               assertEquals(1200l, StringUtils.toNumber("1.2 k"));
+                               assertEquals(42700000l, StringUtils.toNumber("42.7 M"));
+                               assertEquals(1220l, StringUtils.toNumber("1.22 k"));
+                               assertEquals(1432l, StringUtils.toNumber("1.432 k"));
+                               assertEquals(6938l, StringUtils.toNumber("6.938 k"));
+                               assertEquals("1.3 k", StringUtils.formatNumber(1300l, 1));
+                               assertEquals("263.2020 k", StringUtils.formatNumber(263202l, 4));
+                               assertEquals("1.26 k", StringUtils.formatNumber(1267l, 2));
+                               assertEquals("42.7 M", StringUtils.formatNumber(42712121l, 1));
+                               assertEquals("5.09 G", StringUtils.formatNumber(5094837485l, 2));
                        }
                });
        }
index 01766ac031535b33f04886ba6405dd2e6e4688ec..f99448076e9fd86106e15bd1eee560407844394d 100644 (file)
@@ -33,7 +33,11 @@ public class Test extends TestLauncher {
                addSeries(new StringUtilsTest(args));
                addSeries(new TempFilesTest(args));
                addSeries(new CryptUtilsTest(args));
+               addSeries(new BufferedInputStreamTest(args));
                addSeries(new NextableInputStreamTest(args));
+               addSeries(new ReplaceInputStreamTest(args));
+               addSeries(new BufferedOutputStreamTest(args));
+               addSeries(new ReplaceOutputStreamTest(args));
 
                // TODO: test cache and downloader
                Cache cache = null;