From c8ce09c4dc57142f94afcdc290dd79c4bddf7820 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Sat, 27 Apr 2019 18:55:28 +0200 Subject: [PATCH] new: ReplaceOutputStream --- .../utils/streams/BufferedInputStream.java | 46 +---- .../utils/streams/BufferedOutputStream.java | 25 ++- .../utils/streams/ReplaceInputStream.java | 28 +-- .../utils/streams/ReplaceOutputStream.java | 72 ++++++++ src/be/nikiroo/utils/streams/StreamUtils.java | 70 ++++++++ .../test_code/BufferedOutputStreamTest.java | 2 +- .../test_code/ReplaceOutputStreamTest.java | 168 ++++++++++++++++++ .../utils/test_code/StringUtilsTest.java | 40 ++--- src/be/nikiroo/utils/test_code/Test.java | 1 + 9 files changed, 358 insertions(+), 94 deletions(-) create mode 100644 src/be/nikiroo/utils/streams/ReplaceOutputStream.java create mode 100644 src/be/nikiroo/utils/streams/StreamUtils.java create mode 100644 src/be/nikiroo/utils/test_code/ReplaceOutputStreamTest.java diff --git a/src/be/nikiroo/utils/streams/BufferedInputStream.java b/src/be/nikiroo/utils/streams/BufferedInputStream.java index 7c20f63..be4b24d 100644 --- a/src/be/nikiroo/utils/streams/BufferedInputStream.java +++ b/src/be/nikiroo/utils/streams/BufferedInputStream.java @@ -167,7 +167,7 @@ public class BufferedInputStream extends InputStream { if (available() >= search.length) { // Easy path - return startsWith(search, buffer, start, stop); + return StreamUtils.startsWith(search, buffer, start, stop); } else if (!eof) { // Harder path if (buffer2 == null && buffer.length == originalBuffer.length) { @@ -183,7 +183,7 @@ public class BufferedInputStream extends InputStream { len2 += pos2; } - return startsWith(search, buffer2, pos2, len2); + return StreamUtils.startsWith(search, buffer2, pos2, len2); } return false; @@ -416,46 +416,4 @@ public class BufferedInputStream extends InputStream { "This BufferedInputStream was closed, you cannot use it anymore."); } } - - /** - * Check if the buffer starts with the given search term (given as an array, - * a start position and a end position). - *

- * Note: the parameter len is the index of the last - * position, not the length. - *

- * Note: the search term size must be smaller or equal the internal - * buffer size. - * - * @param search - * the term to search for - * @param buffer - * the buffer to look into - * @param offset - * the offset at which to start the search - * @param len - * the maximum index of the data to check (this is not a - * length, but an index) - * - * @return TRUE if the search content is present at the given location and - * does not exceed the len index - */ - static protected boolean startsWith(byte[] search, byte[] buffer, - int offset, int len) { - - // Check if there even is enough space for it - if (search.length > (len - offset)) { - return false; - } - - 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 index 6f600eb..8b74ae1 100644 --- a/src/be/nikiroo/utils/streams/BufferedOutputStream.java +++ b/src/be/nikiroo/utils/streams/BufferedOutputStream.java @@ -22,13 +22,22 @@ public class BufferedOutputStream extends OutputStream { 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 > the internal buffer + * size). + *

+ * By default, this is true. + */ + protected boolean bypassFlush = true; private boolean closed; - private OutputStream out; private int openCounter; - private long bytesWritten; - /** * Create a new {@link BufferedInputStream} that wraps the given * {@link InputStream}. @@ -165,19 +174,23 @@ public class BufferedOutputStream extends OutputStream { } /** - * Flush the {@link BufferedOutputStream}, and optionally the under-laying - * stream, too. + * Flush the {@link BufferedOutputStream}, write the current buffered data + * to (and optionally also flush) the under-laying stream. + *

+ * 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 */ - private void flush(boolean includingSubStream) throws IOException { + protected void flush(boolean includingSubStream) throws IOException { out.write(buffer, start, stop - start); bytesWritten += (stop - start); start = 0; stop = 0; + if (includingSubStream) { out.flush(); } diff --git a/src/be/nikiroo/utils/streams/ReplaceInputStream.java b/src/be/nikiroo/utils/streams/ReplaceInputStream.java index 5eac0b4..f5138ee 100644 --- a/src/be/nikiroo/utils/streams/ReplaceInputStream.java +++ b/src/be/nikiroo/utils/streams/ReplaceInputStream.java @@ -2,11 +2,10 @@ package be.nikiroo.utils.streams; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; /** - * This {@link InputStream} will replace some of its content by replacing it - * with something else. + * This {@link InputStream} will change some of its content by replacing it with + * something else. * * @author niki */ @@ -30,7 +29,7 @@ public class ReplaceInputStream extends BufferedInputStream { * the {@link String} to replace with */ public ReplaceInputStream(InputStream in, String from, String to) { - this(in, bytes(from), bytes(to)); + this(in, StreamUtils.bytes(from), StreamUtils.bytes(to)); } /** @@ -69,7 +68,8 @@ public class ReplaceInputStream extends BufferedInputStream { // Note: very simple, not efficient implementation, sorry. int count = 0; while (spos < slen && count < buffer.length - to.length) { - if (from.length > 0 && startsWith(from, source, spos, slen)) { + if (from.length > 0 + && StreamUtils.startsWith(from, source, spos, slen)) { System.arraycopy(to, 0, buffer, spos, to.length); count += to.length; spos += from.length; @@ -80,22 +80,4 @@ public class ReplaceInputStream extends BufferedInputStream { return count; } - - /** - * 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 private 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/streams/ReplaceOutputStream.java b/src/be/nikiroo/utils/streams/ReplaceOutputStream.java new file mode 100644 index 0000000..e889b76 --- /dev/null +++ b/src/be/nikiroo/utils/streams/ReplaceOutputStream.java @@ -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 from with + * to. + * + * @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 from with + * to. + * + * @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 index 0000000..6b8251a --- /dev/null +++ b/src/be/nikiroo/utils/streams/StreamUtils.java @@ -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). + *

+ * Note: the parameter stop is the index of the last + * position, not the length. + *

+ * Note: the search term size must 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 not a + * length, but an index) + * + * @return TRUE if the search content is present at the given location and + * does not exceed the len 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/BufferedOutputStreamTest.java b/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java index 8b5d5e9..cf6eb2a 100644 --- a/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java +++ b/src/be/nikiroo/utils/test_code/BufferedOutputStreamTest.java @@ -53,7 +53,7 @@ class BufferedOutputStreamTest extends TestLauncher { if (false) { System.out.print("\nExpected data: [ "); - for (int i = 0; i < actual.length; i++) { + for (int i = 0; i < expected.length; i++) { if (i > 0) System.out.print(", "); System.out.print(expected[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 index 0000000..1db3397 --- /dev/null +++ b/src/be/nikiroo/utils/test_code/ReplaceOutputStreamTest.java @@ -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]); + } + } +} diff --git a/src/be/nikiroo/utils/test_code/StringUtilsTest.java b/src/be/nikiroo/utils/test_code/StringUtilsTest.java index f9c5272..de28359 100644 --- a/src/be/nikiroo/utils/test_code/StringUtilsTest.java +++ b/src/be/nikiroo/utils/test_code/StringUtilsTest.java @@ -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)); } }); } diff --git a/src/be/nikiroo/utils/test_code/Test.java b/src/be/nikiroo/utils/test_code/Test.java index e00da11..f994480 100644 --- a/src/be/nikiroo/utils/test_code/Test.java +++ b/src/be/nikiroo/utils/test_code/Test.java @@ -37,6 +37,7 @@ public class Test extends TestLauncher { addSeries(new NextableInputStreamTest(args)); addSeries(new ReplaceInputStreamTest(args)); addSeries(new BufferedOutputStreamTest(args)); + addSeries(new ReplaceOutputStreamTest(args)); // TODO: test cache and downloader Cache cache = null; -- 2.27.0