From: Niki Roo Date: Wed, 24 Apr 2019 19:34:56 +0000 (+0200) Subject: Simple NextableInputStream X-Git-Url: http://git.nikiroo.be/?p=nikiroo-utils.git;a=commitdiff_plain;h=63b46ca9f1703134ef2979b72d474e9c9b8f5737 Simple NextableInputStream --- diff --git a/src/be/nikiroo/utils/NextableInputStream.java b/src/be/nikiroo/utils/NextableInputStream.java index 6f3afc2..0def936 100644 --- a/src/be/nikiroo/utils/NextableInputStream.java +++ b/src/be/nikiroo/utils/NextableInputStream.java @@ -2,12 +2,19 @@ package be.nikiroo.utils; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; +/** + * 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). + *

+ * The separation in sub-streams is done via {@link NextableInputStreamStep}. + * + * @author niki + */ public class NextableInputStream extends InputStream { - private List steps = new ArrayList(); - private NextableInputStreamStep step = null; + private NextableInputStreamStep step; + private boolean stopped; private InputStream in; private boolean eof; @@ -15,24 +22,51 @@ public class NextableInputStream extends InputStream { private int len = 0; private byte[] buffer = new byte[4096]; - public NextableInputStream(InputStream in) { + /** + * 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) { this.in = in; + this.step = step; } - public void addStep(NextableInputStreamStep step) { - steps.add(step); - } - - public boolean next() { - if (!hasMoreData() && step != null) { + /** + * Unblock the processing of the next sub-stream. + *

+ * It can only be called when the "current" stream is spent (i.e., you must + * first process the stream until it is spent). + *

+ * We consider that when the under-laying {@link InputStream} is also spent, + * we cannot have a next sub-stream (it will thus return FALSE). + *

+ * {@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 + */ + public boolean next() throws IOException { + if (!hasMoreData() && stopped) { len = step.getResumeLen(); - pos += step.getSkip(); + pos += step.getResumeSkip(); eof = false; - step = null; - - checkNexts(false); - return true; + if (!preRead()) { + checkBuffer(false); + } + + // consider that if EOF, there is no next + return hasMoreData(); } return false; @@ -83,36 +117,63 @@ public class NextableInputStream extends InputStream { return Math.max(0, len - pos); } - private void preRead() throws IOException { - if (!eof && in != null && pos >= len && step == null) { + /** + * 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 + */ + private boolean preRead() throws IOException { + boolean hasRead = false; + if (!eof && in != null && pos >= len && !stopped) { pos = 0; len = in.read(buffer); - checkNexts(true); + checkBuffer(true); + hasRead = true; } if (pos >= len) { eof = true; } + + return hasRead; } + /** + * We have more data available in the buffer or we can fetch more. + * + * @return TRUE if it is the case, FALSE if not + */ private boolean hasMoreData() { return !(eof && pos >= len); } - private void checkNexts(boolean newBuffer) { - if (!eof) { - for (NextableInputStreamStep step : steps) { - if (newBuffer) { - step.clearBuffer(); - } + /** + * Check that the buffer didn't overshot to the next item, and fix + * {@link NextableInputStream#len} if needed. + *

+ * If {@link NextableInputStream#len} 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) { + if (newBuffer) { + step.clearBuffer(); + } - int stopAt = step.stop(buffer, pos, len); - if (stopAt >= 0) { - this.step = step; - len = stopAt; - eof = true; - break; - } + int stopAt = step.stop(buffer, pos, len); + if (stopAt >= 0) { + len = stopAt; + eof = true; + stopped = true; } } } diff --git a/src/be/nikiroo/utils/NextableInputStreamStep.java b/src/be/nikiroo/utils/NextableInputStreamStep.java index ab22562..a2ee039 100755 --- a/src/be/nikiroo/utils/NextableInputStreamStep.java +++ b/src/be/nikiroo/utils/NextableInputStreamStep.java @@ -1,26 +1,58 @@ package be.nikiroo.utils; +import java.io.InputStream; + +/** + * Divide an {@link InputStream} into sub-streams. + * + * @author niki + */ public class NextableInputStreamStep { private int stopAt; - private boolean disabled; - private int pos; private int resumeLen; private int last = -1; private int skip; + /** + * Create a new divider that will separate the sub-streams each time it sees + * this byte. + *

+ * Note that the byte will be bypassed by the {@link InputStream} as far as + * the consumers will be aware. + * + * @param byt + * the byte at which to separate two sub-streams + */ public NextableInputStreamStep(int byt) { stopAt = byt; } - // do NOT stop twice on the same item + /** + * Check if we need to stop the {@link InputStream} reading at some point in + * the current buffer. + *

+ * If we do, return the index at which to stop; if not, return -1. + *

+ * This method will not return the same index a second time (unless + * we cleared the buffer). + * + * @param buffer + * the buffer to check + * @param pos + * the current position of what was read in the buffer + * @param len + * the maximum index to use in the buffer (anything above that is + * not to be used) + * + * @return the index at which to stop, or -1 + */ public int stop(byte[] buffer, int pos, int len) { for (int i = pos; i < len; i++) { if (buffer[i] == stopAt) { if (i > this.last) { // we skip the sep this.skip = 1; - - this.pos = pos; + this.resumeLen = len; this.last = i; return i; @@ -31,22 +63,34 @@ public class NextableInputStreamStep { return -1; } + /** + * Get the maximum index to use in the buffer used in + * {@link NextableInputStreamStep#stop(byte[], int, int)} at resume time. + * + * @return the index + */ public int getResumeLen() { return resumeLen; } - - public int getSkip() { + + /** + * Get the number of bytes to skip at resume time. + * + * @return the number of bytes to skip + */ + public int getResumeSkip() { return skip; } + /** + * Clear the information we may have kept about the current buffer + *

+ * You should call this method each time you change the content of the + * buffer used in {@link NextableInputStreamStep#stop(byte[], int, int)}. + */ public void clearBuffer() { this.last = -1; - this.pos = 0; this.skip = 0; this.resumeLen = 0; } - - public boolean isEnabled() { - return !disabled; - } } diff --git a/src/be/nikiroo/utils/test_code/NextableInputStreamTest.java b/src/be/nikiroo/utils/test_code/NextableInputStreamTest.java index 51ff128..b5f6780 100644 --- a/src/be/nikiroo/utils/test_code/NextableInputStreamTest.java +++ b/src/be/nikiroo/utils/test_code/NextableInputStreamTest.java @@ -1,7 +1,6 @@ package be.nikiroo.utils.test_code; import java.io.ByteArrayInputStream; -import java.io.InputStream; import be.nikiroo.utils.IOUtils; import be.nikiroo.utils.NextableInputStream; @@ -17,8 +16,8 @@ public class NextableInputStreamTest extends TestLauncher { @Override public void test() throws Exception { byte[] expected = new byte[] { 42, 12, 0, 127 }; - InputStream bin = new ByteArrayInputStream(expected); - NextableInputStream in = new NextableInputStream(bin); + NextableInputStream in = new NextableInputStream( + new ByteArrayInputStream(expected), null); byte[] actual = IOUtils.toByteArray(in); assertEquals( @@ -35,61 +34,62 @@ public class NextableInputStreamTest extends TestLauncher { @Override public void test() throws Exception { byte[] expected = new byte[] { 42, 12, 0, 127 }; - InputStream bin = new ByteArrayInputStream(expected); - NextableInputStream in = new NextableInputStream(bin); - in.addStep(new NextableInputStreamStep(12)); - byte[] actual = IOUtils.toByteArray(in); + NextableInputStream in = new NextableInputStream( + new ByteArrayInputStream(expected), + new NextableInputStreamStep(12)); - assertEquals( - "The resulting array has not the correct number of items", - 1, actual.length); - for (int i = 0; i < actual.length; i++) { - assertEquals("Item " + i + " (0-based) is not the same", - expected[i], actual[i]); - } + checkNext(this, false, "FIRST", in, new byte[] { 42 }); } }); addTest(new TestCase("Stop at 12, resume, stop again, resume") { @Override public void test() throws Exception { - byte[] expected = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 }; + byte[] data = new byte[] { 42, 12, 0, 127, 12, 51, 11, 12 }; NextableInputStream in = new NextableInputStream( - new ByteArrayInputStream(expected)); - in.addStep(new NextableInputStreamStep(12)); + new ByteArrayInputStream(data), + new NextableInputStreamStep(12)); - byte[] actual1 = IOUtils.toByteArray(in); - byte[] expected1 = new byte[] { 42 }; - assertEquals( - "The FIRST resulting array has not the correct number of items", - expected1.length, actual1.length); - for (int i = 0; i < actual1.length; i++) { - assertEquals("Item " + i + " (0-based) is not the same", - expected1[i], actual1[i]); - } + checkNext(this, false, "FIRST", in, new byte[] { 42 }); + checkNext(this, true, "SECOND", in, new byte[] { 0, 127 }); + checkNext(this, true, "THIRD", in, new byte[] { 51, 11 }); + } + }); - assertEquals("Cannot get SECOND entry", true, in.next()); - byte[] actual2 = IOUtils.toByteArray(in); - byte[] expected2 = new byte[] { 0, 127 }; - assertEquals( - "The SECOND resulting array has not the correct number of items", - expected2.length, actual2.length); - for (int i = 0; i < actual2.length; i++) { - assertEquals("Item " + i + " (0-based) is not the same", - expected2[i], actual2[i]); - } + addTest(new TestCase("Encapsulation") { + @Override + public void test() throws Exception { + byte[] data = new byte[] { 42, 12, 0, 4, 127, 12, 5 }; + NextableInputStream in4 = new NextableInputStream( + new ByteArrayInputStream(data), + new NextableInputStreamStep(4)); + NextableInputStream subIn12 = new NextableInputStream(in4, + new NextableInputStreamStep(12)); - assertEquals("Cannot get next THIRD entry", true, in.next()); - byte[] actual3 = IOUtils.toByteArray(in); - byte[] expected3 = new byte[] { 51, 11 }; - assertEquals( - "The THIRD resulting array has not the correct number of items", - expected3.length, actual3.length); - for (int i = 0; i < actual3.length; i++) { - assertEquals("Item " + i + " (0-based) is not the same", - expected3[i], actual3[i]); - } + checkNext(this, false, "SUB FIRST", subIn12, new byte[] { 42 }); + checkNext(this, true, "SUB SECOND", subIn12, new byte[] { 0 }); + + assertEquals("The subIn still has some data", false, + subIn12.next()); + + checkNext(this, true, "MAIN LAST", in4, new byte[] { 127, 12, 5 }); } }); } + + static void checkNext(TestCase test, boolean callNext, String prefix, + NextableInputStream in, byte[] expected) throws Exception { + if (callNext) { + test.assertEquals("Cannot get " + prefix + " entry", true, + in.next()); + } + 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]); + } + } }