Add BufferedWriter emulation to GWT

Change-Id: Ia7f7e66e404432d3ee32b4d06df704b09a7f2b4c
diff --git a/user/super/com/google/gwt/emul/java/io/BufferedWriter.java b/user/super/com/google/gwt/emul/java/io/BufferedWriter.java
new file mode 100644
index 0000000..922a482
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/io/BufferedWriter.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package java.io;
+
+import static javaemul.internal.InternalPreconditions.checkArgument;
+import static javaemul.internal.InternalPreconditions.checkNotNull;
+import static javaemul.internal.InternalPreconditions.checkState;
+
+/**
+ * See <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/BufferedWriter.html">the official
+ * Java API doc</a> for details.
+ */
+public class BufferedWriter extends Writer {
+  private static int defaultCharBufferSize = 8192;
+
+  private Writer out;
+  private char[] buf;
+  private int pos;
+  private int size;
+
+  public BufferedWriter(Writer out) {
+    this(out, defaultCharBufferSize);
+  }
+
+  public BufferedWriter(Writer out, int size) {
+    super(out);
+    checkArgument(size > 0, "Buffer size <= 0");
+    this.out = out;
+    this.buf = new char[size];
+    this.size = size;
+    this.pos = 0;
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (out == null) {
+      return;
+    }
+    try (Writer w = out) {
+      flushBuffer();
+    } finally {
+      out = null;
+      buf = null;
+    }
+  }
+
+  @Override
+  public void flush() throws IOException {
+    flushBuffer();
+    out.flush();
+  }
+
+  private void ensureOpen() throws IOException {
+    checkState(out != null, "stream closed");
+  }
+
+  private void flushBuffer() throws IOException {
+    ensureOpen();
+    if (pos > 0) {
+      out.write(buf, 0, pos);
+    }
+    pos = 0;
+  }
+
+  public void newLine() throws IOException {
+    write("\n");
+  }
+
+  @Override
+  public void write(char[] buffer, int offset, int count) throws IOException {
+    ensureOpen();
+    IOUtils.checkOffsetAndCount(buffer, offset, count);
+    if (count >= size) {
+      /* If the request length exceeds the size of the output buffer,
+      flush the buffer and then write the data directly.  In this
+      way buffered streams will cascade harmlessly. */
+      flushBuffer();
+      out.write(buffer, offset, count);
+      return;
+    }
+
+    int b = offset, t = offset + count;
+    while (b < t) {
+      int d = Math.min(size - pos, t - b);
+      System.arraycopy(buffer, b, buf, pos, d);
+      b += d;
+      pos += d;
+      if (pos >= size) {
+        flushBuffer();
+      }
+    }
+  }
+
+  @Override
+  public void write(int oneChar) throws IOException {
+    ensureOpen();
+    if (pos >= size) {
+      out.write(buf, 0, buf.length);
+      pos = 0;
+    }
+    buf[pos++] = (char) oneChar;
+  }
+
+  @Override
+  public void write(String str, int offset, int count) throws IOException {
+    ensureOpen();
+    // Ensure we throw a NullPointerException instead of a JavascriptException in case the
+    // given string is null.
+    checkNotNull(str);
+    int b = offset, t = offset + count;
+    while (b < t) {
+      int d = Math.min(size - pos, t - b);
+      str.getChars(b, b + d, buf, pos);
+      b += d;
+      pos += d;
+      if (pos >= size) {
+        flushBuffer();
+      }
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/EmulSuite.java b/user/test/com/google/gwt/emultest/EmulSuite.java
index 0d8bf9f..d28f9ec 100644
--- a/user/test/com/google/gwt/emultest/EmulSuite.java
+++ b/user/test/com/google/gwt/emultest/EmulSuite.java
@@ -16,6 +16,7 @@
 package com.google.gwt.emultest;
 
 import com.google.gwt.emultest.java.internal.CoercionsTest;
+import com.google.gwt.emultest.java.io.BufferedWriterTest;
 import com.google.gwt.emultest.java.io.ByteArrayInputStreamTest;
 import com.google.gwt.emultest.java.io.ByteArrayOutputStreamTest;
 import com.google.gwt.emultest.java.io.FilterInputStreamTest;
@@ -70,6 +71,7 @@
   CoercionsTest.class,
 
   // -- java.io
+  BufferedWriterTest.class,
   ByteArrayInputStreamTest.class,
   ByteArrayOutputStreamTest.class,
   FilterInputStreamTest.class,
diff --git a/user/test/com/google/gwt/emultest/java/io/BufferedWriterTest.java b/user/test/com/google/gwt/emultest/java/io/BufferedWriterTest.java
new file mode 100644
index 0000000..38b9a28
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java/io/BufferedWriterTest.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.emultest.java.io;
+
+import com.google.gwt.junit.client.GWTTestCase;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/** Unit test for the {@link java.io.BufferedWriter} emulated class. */
+public class BufferedWriterTest extends GWTTestCase {
+
+  /** Default buffer size for the {@link java.io.BufferedWriter} object being tested. */
+  private static final int DEFAULT_BUFFER_SIZE = 2;
+
+  /** Array of characters that exceeds the default buffer size. */
+  private static final char[] BIG_CHAR_ARRAY = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
+
+  /** {@link java.io.BufferedWriter} object being tested. */
+  private BufferedWriter writer;
+
+  /** Underliying writer used by the {@link BufferedWriter} object. */
+  private SinkWriter sink;
+
+  /**
+   * {@link java.io.Writer} we pass to the {@link java.io.BufferedWriter} object being tested. It
+   * allows checking that the characters written by the buffered writter is what we expect.
+   */
+  private static class SinkWriter extends Writer {
+
+    private boolean closed;
+
+    private boolean flushed;
+
+    private ArrayList<Character> chars;
+
+    SinkWriter() {
+      closed = false;
+      flushed = false;
+      chars = new ArrayList<>(1024);
+    }
+
+    @Override
+    public void close() {
+      closed = true;
+    }
+
+    @Override
+    public void flush() {
+      flushed = true;
+    }
+
+    public ArrayList<Character> getChars() {
+      return chars;
+    }
+
+    public boolean isClosed() {
+      return closed;
+    }
+
+    public boolean isFlushed() {
+      return flushed;
+    }
+
+    /**
+     * Converts {@code outputChars} to primitive character array.
+     *
+     * @return primitive char array
+     */
+    public char[] toCharArray() {
+      char[] charArray = new char[chars.size()];
+      for (int i = 0; i < chars.size(); i++) {
+        charArray[i] = chars.get(i);
+      }
+      return charArray;
+    }
+
+    @Override
+    public void write(char[] cbuf, int off, int len) {
+      for (int i = off; i < (off + len); i++) {
+        chars.add(cbuf[i]);
+      }
+    }
+  }
+
+  /** Sets module name so that javascript compiler can operate. */
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.emultest.EmulSuite";
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    super.gwtSetUp();
+    sink = new SinkWriter();
+    writer = new BufferedWriter(sink, DEFAULT_BUFFER_SIZE);
+  }
+
+  public void testWriteChar() throws IOException {
+    writer.write('a');
+    assertTrue(sink.getChars().isEmpty());
+
+    writer.write('b');
+    assertTrue(sink.getChars().isEmpty());
+
+    writer.write('c');
+    assertTrue(Arrays.equals(new char[] {'a', 'b'}, sink.toCharArray()));
+  }
+
+  public void testWriteArrayUsingNullArray() throws IOException {
+    final char[] b = null;
+    try {
+      writer.write(b, 0, 2);
+      fail("should have thrown NullPointerException");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testWriteArrayUsingNegativeOffsetValue() throws IOException {
+    final char[] b = {'a', 'b'};
+    try {
+      writer.write(b, -1, 1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testWriteArrayUsingNegativeLengthValue() throws IOException {
+    final char[] b = {'a', 'b'};
+    try {
+      writer.write(b, 0, -1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testWriteArrayUsingInvalidRangeValue() throws IOException {
+    final char[] b = {'a', 'b'};
+    try {
+      writer.write(b, 1, 2);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testWriteArraySmallerThanBufferSize() throws IOException {
+    final char[] b = {'a'};
+    writer.write(b, 0, 1);
+    assertTrue(sink.getChars().isEmpty());
+  }
+
+  public void testWriteArrayEqualThanBufferSize() throws IOException {
+    final int len = DEFAULT_BUFFER_SIZE;
+    writer.write(BIG_CHAR_ARRAY, 1, len);
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 1, len + 1), sink.toCharArray()));
+  }
+
+  public void testWriteArrayLargerThanBufferSize() throws IOException {
+    final int len = DEFAULT_BUFFER_SIZE + 1;
+    writer.write(BIG_CHAR_ARRAY, 1, len);
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 1, len + 1), sink.toCharArray()));
+  }
+
+  public void testWriteArrayMultipleTimes() throws IOException {
+    writer.write(BIG_CHAR_ARRAY, 0, 1);
+    // writer: [ 'a' ], sink: [ ]
+    assertTrue(sink.getChars().isEmpty());
+
+    writer.write(BIG_CHAR_ARRAY, 1, 2);
+    // writer: [ ], sink: [ 'a', 'b', 'c' ]
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 0, 3), sink.toCharArray()));
+
+    writer.write(BIG_CHAR_ARRAY, 3, 3);
+    // writer: [ ], sink: [ 'a', 'b', 'c', 'd', 'e', 'f' ]
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 0, 6), sink.toCharArray()));
+
+    writer.write(BIG_CHAR_ARRAY, 6, 1);
+    // writer: [ 'g' ], sink: [ 'a', 'b', 'c', 'd', 'e', 'f' ]
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 0, 6), sink.toCharArray()));
+  }
+
+  public void testWriteStringUsingNullString() throws IOException {
+    final String s = null;
+    try {
+      writer.write(s, 0, 2);
+      fail("should have thrown NullPointerException");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testWriteStringUsingNegativeOffsetValue() throws IOException {
+    final String s = "ab";
+    try {
+      writer.write(s, -1, 1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testWriteStringUsingNegativeLengthValue() throws IOException {
+    final String s = "ab";
+    writer.write(s, 0, -1);
+    assertTrue(sink.getChars().isEmpty());
+  }
+
+  public void testWriteStringUsingInvalidRangeValue() throws IOException {
+    final String s = "ab";
+    try {
+      writer.write(s, 1, 2);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testWriteStringSmallerThanBufferSize() throws IOException {
+    final String s = "a";
+    writer.write(s, 0, 1);
+    assertTrue(sink.getChars().isEmpty());
+  }
+
+  public void testWriteStringEqualThanBufferSize() throws IOException {
+    final String s = new String(BIG_CHAR_ARRAY);
+    writer.write(s, 0, 1);
+    assertTrue(sink.getChars().isEmpty());
+  }
+
+  public void testWriteStringLargerThanBufferSize() throws IOException {
+    final String s = new String(BIG_CHAR_ARRAY);
+    final int len = DEFAULT_BUFFER_SIZE + 1;
+    writer.write(s, 1, len);
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 1, len), sink.toCharArray()));
+  }
+
+  public void testWriteStringMultipleTimes() throws IOException {
+    final String s = new String(BIG_CHAR_ARRAY);
+    writer.write(s, 0, 1);
+    // writer: [ 'a' ], sink: [ ]
+    assertTrue(sink.getChars().isEmpty());
+
+    writer.write(s, 1, 2);
+    // writer: [ 'c' ], sink: [ 'a', 'b' ]
+    assertTrue(
+        Arrays.equals(
+            Arrays.copyOfRange(BIG_CHAR_ARRAY, 0, DEFAULT_BUFFER_SIZE), sink.toCharArray()));
+
+    writer.write(s, 3, 3);
+    // writer: [ ], sink: [ 'a', 'b', 'c', 'd', 'e', 'f' ]
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 0, 6), sink.toCharArray()));
+
+    writer.write(s, 6, 1);
+    // writer: [ 'g' ], sink: [ 'a', 'b', 'c', 'd', 'e', 'f' ]
+    assertTrue(Arrays.equals(Arrays.copyOfRange(BIG_CHAR_ARRAY, 0, 6), sink.toCharArray()));
+  }
+
+  public void testFlushWithEmptyBuffer() throws IOException {
+    writer.flush();
+    assertTrue(sink.getChars().isEmpty());
+    assertTrue(sink.isFlushed());
+  }
+
+  public void testFlushWithNonEmptyBuffer() throws IOException {
+    final char[] b = new char[] {'a'};
+    writer.write(b, 0, 1);
+    assertTrue(sink.getChars().isEmpty());
+
+    writer.flush();
+    assertTrue(Arrays.equals(b, sink.toCharArray()));
+  }
+
+  public void testCloseWithEmptyBuffer() throws IOException {
+    writer.close();
+    assertTrue(sink.getChars().isEmpty());
+    assertTrue(sink.isClosed());
+  }
+
+  public void testCloseWithNonEmptyBuffer() throws IOException {
+    final char[] b = new char[] {'a'};
+    writer.write(b, 0, 1);
+    assertTrue(sink.getChars().isEmpty());
+
+    writer.close();
+    assertTrue(Arrays.equals(b, sink.toCharArray()));
+  }
+}