From 57a4218a118df30cc449e162740b91fa43e310ea Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sun, 5 Nov 2023 17:37:14 -0500 Subject: [PATCH] Added FastReader and FastWriter. --- README.md | 7 +- changelog.md | 4 + pom.xml | 2 +- .../com/cedarsoftware/util/FastReader.java | 145 ++++++++++++++++++ .../com/cedarsoftware/util/FastWriter.java | 102 ++++++++++++ .../java/com/cedarsoftware/util/TestUtil.java | 1 + .../java/com/cedarsoftware/util/TestIO.java | 134 ++++++++++++++++ .../com/cedarsoftware/util/TestUtilTest.java | 19 +++ src/test/resources/junit-platform.properties | 1 + src/test/resources/prettyPrint.json | 25 +++ 10 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/cedarsoftware/util/FastReader.java create mode 100644 src/main/java/com/cedarsoftware/util/FastWriter.java create mode 100644 src/test/java/com/cedarsoftware/util/TestIO.java create mode 100644 src/test/resources/junit-platform.properties create mode 100644 src/test/resources/prettyPrint.json diff --git a/README.md b/README.md index 9c572e4d..337fbbb8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The classes in the`.jar`file are version 52 (`JDK 1.8`). To include in your project: ##### Gradle ``` -implementation 'com.cedarsoftware:java-util:2.2.0' +implementation 'com.cedarsoftware:java-util:2.3.0' ``` ##### Maven @@ -23,7 +23,7 @@ implementation 'com.cedarsoftware:java-util:2.2.0' com.cedarsoftware java-util - 2.2.0 + 2.3.0 ``` --- @@ -63,6 +63,9 @@ Included in java-util: * **Converter** - Convert from one instance to another. For example, `convert("45.3", BigDecimal.class)` will convert the `String` to a `BigDecimal`. Works for all primitives, primitive wrappers, `Date`, `java.sql.Date`, `String`, `BigDecimal`, `BigInteger`, `AtomicBoolean`, `AtomicLong`, etc. The method is very generous on what it allows to be converted. For example, a `Calendar` instance can be input for a `Date` or `Long`. Examine source to see all possibilities. * **DateUtilities** - Robust date String parser that handles date/time, date, time, time/date, string name months or numeric months, skips comma, etc. English month names only (plus common month name abbreviations), time with/without seconds or milliseconds, `y/m/d` and `m/d/y` ordering as well. * **DeepEquals** - Compare two object graphs and return 'true' if they are equivalent, 'false' otherwise. This will handle cycles in the graph, and will call an `equals()` method on an object if it has one, otherwise it will do a field-by-field equivalency check for non-transient fields. Has options to turn on/off using `.equals()` methods that may exist on classes. +* **IO** + * **FastReader** - Works like `BufferedReader` and `PushbackReader` without the synchronization. Tracks `line` and `col` by watching for `0x0a,` which can be useful when reading text/json/xml files. You can `.pushback()` a character read, which is very useful in parsers. + * **FastWriter** - Works like `BufferedWriter` without the synchronization. * **EncryptionUtilities** - Makes it easy to compute MD5, SHA-1, SHA-256, SHA-512 checksums for `Strings`, `byte[]`, as well as making it easy to AES-128 encrypt `Strings` and `byte[]`'s. * **Executor** - One line call to execute operating system commands. `Executor executor = new Executor(); executor.exec('ls -l');` Call `executor.getOut()` to fetch the output, `executor.getError()` retrieve error output. If a -1 is returned, there was an error. * **FastByteArrayOutputStream** - Unlike the JDK `ByteArrayOutputStream`, `FastByteArrayOutputStream` is 1) not `synchronized`, and 2) allows access to it's internal `byte[]` eliminating the duplication of the `byte[]` when `toByteArray()` is called. diff --git a/changelog.md b/changelog.md index 7628b92b..ef6288ba 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,8 @@ ### Revision History +* 2.3.0 + * Added `FastReader` and `FastWriter.` + * `FastReader` can be used instead of the JDK `PushbackReader(BufferedReader)).` It is much faster with no synchronization and combines both. It also tracks line `[getLine()]`and column `[getCol()]` position monitoring for `0x0a` which it can be queried for. It also can be queried for the last snippet read: `getLastSnippet().` Great for showing parsing error messages that accurately point out where a syntax error occurred. Make sure you use a new instance per each thread. + * `FastWriter` can be used instead of the JDK `BufferedWriter` as it has no synchronization. Make sure you use a new Instance per each thread. * 2.2.0 * Built with JDK 1.8 and runs with JDK 1.8 through JDK 21. * The 2.2.x will continue to maintain JDK 1.8. The 3.0 branch [not yet created] will be JDK11+ diff --git a/pom.xml b/pom.xml index d54b0888..2b0a27ed 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.cedarsoftware java-util jar - 2.3.0-SNAPSHOT + 2.3.0 Java Utilities https://github.com/jdereg/java-util diff --git a/src/main/java/com/cedarsoftware/util/FastReader.java b/src/main/java/com/cedarsoftware/util/FastReader.java new file mode 100644 index 00000000..e0787e6c --- /dev/null +++ b/src/main/java/com/cedarsoftware/util/FastReader.java @@ -0,0 +1,145 @@ +package com.cedarsoftware.util; + +import java.io.IOException; +import java.io.Reader; + +public class FastReader extends Reader { + private Reader in; + private char[] buf; + private int bufferSize; + private int pushbackBufferSize; + private int position; // Current position in the buffer + private int limit; // Number of characters currently in the buffer + private char[] pushbackBuffer; + private int pushbackPosition; // Current position in the pushback buffer + private int line = 1; + private int col = 0; + + public FastReader(Reader in, int bufferSize, int pushbackBufferSize) { + super(in); + if (bufferSize <= 0 || pushbackBufferSize < 0) { + throw new IllegalArgumentException("Buffer sizes must be positive"); + } + this.in = in; + this.bufferSize = bufferSize; + this.pushbackBufferSize = pushbackBufferSize; + this.buf = new char[bufferSize]; + this.pushbackBuffer = new char[pushbackBufferSize]; + this.position = 0; + this.limit = 0; + this.pushbackPosition = pushbackBufferSize; // Start from the end of pushbackBuffer + } + + private void fill() throws IOException { + if (position >= limit) { + limit = in.read(buf, 0, bufferSize); + if (limit > 0) { + position = 0; + } + } + } + + public void pushback(char ch) throws IOException { + if (pushbackPosition == 0) { + throw new IOException("Pushback buffer overflow"); + } + pushbackBuffer[--pushbackPosition] = ch; + if (ch == 0x0a) { + line--; + } + else { + col--; + } + } + + protected void movePosition(char ch) + { + if (ch == 0x0a) { + line++; + col = 0; + } + else { + col++; + } + } + + public int read() throws IOException { + if (in == null) { + throw new IOException("FastReader stream is closed."); + } + char ch; + if (pushbackPosition < pushbackBufferSize) { + ch = pushbackBuffer[pushbackPosition++]; + movePosition(ch); + return ch; + } + + fill(); + if (limit == -1) { + return -1; + } + + ch = buf[position++]; + movePosition(ch); + return ch; + } + + public int read(char[] cbuf, int off, int len) throws IOException { + if (in == null) { + throw new IOException("FastReader stream is closed."); + } + int bytesRead = 0; + + while (len > 0) { + int available = pushbackBufferSize - pushbackPosition; + if (available > 0) { + int toRead = Math.min(available, len); + System.arraycopy(pushbackBuffer, pushbackPosition, cbuf, off, toRead); + pushbackPosition += toRead; + off += toRead; + len -= toRead; + bytesRead += toRead; + } else { + fill(); + if (limit == -1) { + return bytesRead > 0 ? bytesRead : -1; + } + int toRead = Math.min(limit - position, len); + System.arraycopy(buf, position, cbuf, off, toRead); + position += toRead; + off += toRead; + len -= toRead; + bytesRead += toRead; + } + } + + return bytesRead; + } + + public void close() throws IOException { + if (in != null) { + in.close(); + in = null; + } + } + + public int getLine() + { + return line; + } + + public int getCol() + { + return col; + } + + public String getLastSnippet() + { + StringBuilder s = new StringBuilder(); + for (int i=0; i < position; i++) + { + s.append(buf[i]); + } + return s.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/cedarsoftware/util/FastWriter.java b/src/main/java/com/cedarsoftware/util/FastWriter.java new file mode 100644 index 00000000..7a8049e2 --- /dev/null +++ b/src/main/java/com/cedarsoftware/util/FastWriter.java @@ -0,0 +1,102 @@ +package com.cedarsoftware.util; + +import java.io.IOException; +import java.io.Writer; + +public class FastWriter extends Writer { + private static final int DEFAULT_BUFFER_SIZE = 8192; + + private Writer out; + private char[] cb; + private int nextChar; + + public FastWriter(Writer out) { + this(out, DEFAULT_BUFFER_SIZE); + } + + public FastWriter(Writer out, int bufferSize) { + super(out); + if (bufferSize <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + this.out = out; + this.cb = new char[bufferSize]; + this.nextChar = 0; + } + + private void flushBuffer() throws IOException { + if (nextChar == 0) { + return; + } + out.write(cb, 0, nextChar); + nextChar = 0; + } + + public void write(int c) throws IOException { + if (out == null) { + throw new IOException("FastWriter stream is closed."); + } + if (nextChar >= cb.length) { + flushBuffer(); + } + cb[nextChar++] = (char) c; + } + + public void write(char[] cbuf, int off, int len) throws IOException { + if (out == null) { + throw new IOException("FastWriter stream is closed."); + } + if ((off < 0) || (off > cbuf.length) || (len < 0) || + ((off + len) > cbuf.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + if (len >= cb.length) { + // If the request length exceeds the size of the output buffer, + // flush the buffer and then write the data directly. + flushBuffer(); + out.write(cbuf, off, len); + return; + } + if (len > cb.length - nextChar) { + flushBuffer(); + } + System.arraycopy(cbuf, off, cb, nextChar, len); + nextChar += len; + } + + public void write(String str, int off, int len) throws IOException { + if (out == null) { + throw new IOException("FastWriter stream is closed."); + } + int b = off, t = off + len; + while (b < t) { + int d = Math.min(cb.length - nextChar, t - b); + str.getChars(b, b + d, cb, nextChar); + b += d; + nextChar += d; + if (nextChar >= cb.length) { + flushBuffer(); + } + } + } + + public void flush() throws IOException { + flushBuffer(); + out.flush(); + } + + public void close() throws IOException { + if (out == null) { + return; + } + try { + flushBuffer(); + } finally { + out.close(); + out = null; + cb = null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/cedarsoftware/util/TestUtil.java b/src/main/java/com/cedarsoftware/util/TestUtil.java index bc9b2bb7..e1fce639 100644 --- a/src/main/java/com/cedarsoftware/util/TestUtil.java +++ b/src/main/java/com/cedarsoftware/util/TestUtil.java @@ -75,4 +75,5 @@ public static boolean checkContainsIgnoreCase(String source, String... contains) } return true; } + } diff --git a/src/test/java/com/cedarsoftware/util/TestIO.java b/src/test/java/com/cedarsoftware/util/TestIO.java new file mode 100644 index 00000000..3c6bd8fa --- /dev/null +++ b/src/test/java/com/cedarsoftware/util/TestIO.java @@ -0,0 +1,134 @@ +package com.cedarsoftware.util; + +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; + +public class TestIO +{ + @Test + public void testFastReader() throws Exception + { + String content = TestUtilTest.fetchResource("prettyPrint.json"); + ByteArrayInputStream bin = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + FastReader reader = new FastReader(new InputStreamReader(bin, StandardCharsets.UTF_8), 1024,10); + assert reader.read() == '{'; + int c; + boolean done = false; + while ((c = reader.read()) != -1 && !done) + { + if (c == '{') + { + assert reader.getLine() == 4; + assert reader.getCol() == 11; + reader.pushback('n'); + reader.pushback('h'); + reader.pushback('o'); + reader.pushback('j'); + StringBuilder sb = new StringBuilder(); + sb.append((char)reader.read()); + sb.append((char)reader.read()); + sb.append((char)reader.read()); + sb.append((char)reader.read()); + assert sb.toString().equals("john"); + + Set chars = new HashSet<>(); + chars.add('}'); + readUntil(reader, chars); + c = reader.read(); + assert c == ','; + assert reader.getLastSnippet().length() > 25; + char[] buf = new char[12]; + reader.read(buf); + String s = new String(buf); + assert s.contains("true"); + done = true; + } + } + reader.close(); + } + + @Test + public void testFastWriter() throws Exception + { + String content = TestUtilTest.fetchResource("prettyPrint.json"); + ByteArrayInputStream bin = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + FastReader reader = new FastReader(new InputStreamReader(bin, StandardCharsets.UTF_8), 1024,10); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + FastWriter out = new FastWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8)); + + int c; + boolean done = false; + while ((c = reader.read()) != -1 && !done) + { + out.write(c); + } + reader.close(); + out.flush(); + out.close(); + + assert content.equals(new String(baos.toByteArray(), StandardCharsets.UTF_8)); + } + + @Test + public void testFastWriterCharBuffer() throws Exception + { + String content = TestUtilTest.fetchResource("prettyPrint.json"); + ByteArrayInputStream bin = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + FastReader reader = new FastReader(new InputStreamReader(bin, StandardCharsets.UTF_8), 1024,10); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + FastWriter out = new FastWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8)); + + char buffer[] = new char[100]; + reader.read(buffer); + out.write(buffer, 0, 100); + reader.close(); + out.flush(); + out.close(); + + for (int i=0; i < 100; i++) + { + assert content.charAt(i) == buffer[i]; + } + } + + @Test + public void testFastWriterString() throws Exception + { + String content = TestUtilTest.fetchResource("prettyPrint.json"); + ByteArrayInputStream bin = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + FastReader reader = new FastReader(new InputStreamReader(bin, StandardCharsets.UTF_8), 1024,10); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + FastWriter out = new FastWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8)); + + char buffer[] = new char[100]; + reader.read(buffer); + String s = new String(buffer); + out.write(s, 0, 100); + reader.close(); + out.flush(); + out.close(); + + for (int i=0; i < 100; i++) + { + assert content.charAt(i) == s.charAt(i); + } + } + + private int readUntil(FastReader input, Set chars) throws IOException + { + FastReader in = input; + int c; + do + { + c = in.read(); + } while (!chars.contains((char)c) && c != -1); + return c; + } +} diff --git a/src/test/java/com/cedarsoftware/util/TestUtilTest.java b/src/test/java/com/cedarsoftware/util/TestUtilTest.java index 1375ee6c..2143f95b 100644 --- a/src/test/java/com/cedarsoftware/util/TestUtilTest.java +++ b/src/test/java/com/cedarsoftware/util/TestUtilTest.java @@ -2,6 +2,11 @@ import org.junit.jupiter.api.Test; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + /** * @author John DeRegnaucourt (jdereg@gmail.com) *
@@ -51,4 +56,18 @@ public void testContains() assert !TestUtil.checkContainsIgnoreCase("This is the source string to test.", "Source", "string", "Text"); assert !TestUtil.checkContainsIgnoreCase("This is the source string to test.", "Test", "Source", "string"); } + + public static String fetchResource(String name) + { + try + { + URL url = TestUtil.class.getResource("/" + name); + Path resPath = Paths.get(url.toURI()); + return new String(Files.readAllBytes(resPath)); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } } diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties new file mode 100644 index 00000000..10a0dd33 --- /dev/null +++ b/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testclass.order.default = org.junit.jupiter.api.ClassOrderer$ClassName \ No newline at end of file diff --git a/src/test/resources/prettyPrint.json b/src/test/resources/prettyPrint.json new file mode 100644 index 00000000..f6acbde0 --- /dev/null +++ b/src/test/resources/prettyPrint.json @@ -0,0 +1,25 @@ +{ + "@type":"com.cedarsoftware.util.io.PrettyPrintTest$Nice", + "name":"Louie", + "items":{ + "@type":"java.util.ArrayList", + "@items":[ + "One", + 1, + { + "@type":"int", + "value":1 + }, + true + ] + }, + "dictionary":{ + "@type":"java.util.LinkedHashMap", + "grade":"A", + "price":100.0, + "bigdec":{ + "@type":"java.math.BigDecimal", + "value":"3.141592653589793238462643383" + } + } +} \ No newline at end of file