Add OutputStreamWriter emulation in GWT

Change-Id: Idc42124cfc6421b9e58432a36b1bf4246b91c979
diff --git a/user/super/com/google/gwt/emul/java/io/IOUtils.java b/user/super/com/google/gwt/emul/java/io/IOUtils.java
index b8d3049..118cba2 100644
--- a/user/super/com/google/gwt/emul/java/io/IOUtils.java
+++ b/user/super/com/google/gwt/emul/java/io/IOUtils.java
@@ -59,6 +59,23 @@
   }
 
   /**
+   * Validates the offset and the byte count for the given string.
+   *
+   * @param str String to be checked.
+   * @param offset Starting offset in the string.
+   * @param count Total number of characters to be accessed.
+   * @throws NullPointerException if the given reference to the string is null.
+   * @throws IndexOutOfBoundsException if {@code offset} is negative, {@code count} is
+   *     negative or their sum exceeds the string length.
+   */
+  public static void checkOffsetAndCount(String str, int offset, int count) {
+    // Ensure we throw a NullPointerException instead of a JavascriptException in case the
+    // given string is null.
+    checkNotNull(str);
+    checkOffsetAndCount(str.length(), offset, count);
+  }
+
+  /**
    * Validates the offset and the byte count for the given array length.
    *
    * @param length Length of the array to be checked.
diff --git a/user/super/com/google/gwt/emul/java/io/OutputStreamWriter.java b/user/super/com/google/gwt/emul/java/io/OutputStreamWriter.java
new file mode 100644
index 0000000..e03612d
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/io/OutputStreamWriter.java
@@ -0,0 +1,62 @@
+/*
+ * 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.checkNotNull;
+
+import java.nio.charset.Charset;
+import javaemul.internal.EmulatedCharset;
+
+/**
+ * See <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/OutputStreamWriter.html">the
+ * official Java API doc</a> for details.
+ */
+public class OutputStreamWriter extends Writer {
+
+  private final OutputStream out;
+
+  private final Charset charset;
+
+  public OutputStreamWriter(OutputStream out, String charsetName) {
+    this(out, Charset.forName(charsetName));
+  }
+
+  public OutputStreamWriter(OutputStream out, Charset charset) {
+    this.out = checkNotNull(out);
+    this.charset = checkNotNull(charset);
+  }
+
+  @Override
+  public void close() throws IOException {
+    out.close();
+  }
+
+  @Override
+  public void flush() throws IOException {
+    out.flush();
+  }
+
+  public String getEncoding() {
+    return charset.name();
+  }
+
+  @Override
+  public void write(char[] buffer, int offset, int count) throws IOException {
+    IOUtils.checkOffsetAndCount(buffer, offset, count);
+    byte[] byteBuffer = ((EmulatedCharset) charset).getBytes(buffer, offset, count);
+    out.write(byteBuffer, 0, byteBuffer.length);
+  }
+}
diff --git a/user/super/com/google/gwt/emul/java/io/Writer.java b/user/super/com/google/gwt/emul/java/io/Writer.java
new file mode 100644
index 0000000..4bba793
--- /dev/null
+++ b/user/super/com/google/gwt/emul/java/io/Writer.java
@@ -0,0 +1,84 @@
+/*
+ * 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.checkNotNull;
+import static javaemul.internal.InternalPreconditions.checkPositionIndexes;
+
+import java.util.Objects;
+
+/**
+ * See <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/Writer.html">the official Java API
+ * doc</a> for details.
+ */
+public abstract class Writer implements Appendable, Closeable, Flushable {
+
+  protected Writer() {}
+
+  protected Writer(Object lock) {}
+
+  public abstract void close() throws IOException;
+
+  public abstract void flush() throws IOException;
+
+  public void write(char[] buf) throws IOException {
+    // Ensure we throw a NullPointerException instead of a JavascriptException in case the
+    // given buffer is null.
+    checkNotNull(buf);
+    write(buf, 0, buf.length);
+  }
+
+  public abstract void write(char[] buf, int offset, int count) throws IOException;
+
+  public void write(int oneChar) throws IOException {
+    char[] oneCharArray = new char[1];
+    oneCharArray[0] = (char) oneChar;
+    write(oneCharArray);
+  }
+
+  public void write(String str) throws IOException {
+    // Ensure we throw a NullPointerException instead of a JavascriptException in case the
+    // given string is null.
+    checkNotNull(str);
+    write(str, 0, str.length());
+  }
+
+  public void write(String str, int offset, int count) throws IOException {
+    IOUtils.checkOffsetAndCount(str, offset, count);
+    char[] buf = new char[count];
+    str.getChars(offset, offset + count, buf, 0);
+    write(buf, 0, buf.length);
+  }
+
+  public Writer append(char c) throws IOException {
+    write(c);
+    return this;
+  }
+
+  public Writer append(CharSequence csq) throws IOException {
+    write(Objects.toString(csq));
+    return this;
+  }
+
+  public Writer append(CharSequence csq, int start, int end) throws IOException {
+    if (csq == null) {
+      csq = "null";
+    }
+    checkPositionIndexes(start, end, csq.length());
+    write(csq.subSequence(start, end).toString());
+    return this;
+  }
+}
diff --git a/user/super/com/google/gwt/emul/javaemul/internal/EmulatedCharset.java b/user/super/com/google/gwt/emul/javaemul/internal/EmulatedCharset.java
index ddde0fa..4cdf924 100644
--- a/user/super/com/google/gwt/emul/javaemul/internal/EmulatedCharset.java
+++ b/user/super/com/google/gwt/emul/javaemul/internal/EmulatedCharset.java
@@ -34,6 +34,16 @@
     }
 
     @Override
+    public byte[] getBytes(char[] buffer, int offset, int count) {
+      int n = offset + count;
+      byte[] bytes = new byte[count];
+      for (int i = offset; i < n; ++i) {
+        bytes[i] = (byte) (buffer[i] & 255);
+      }
+      return bytes;
+    }
+
+    @Override
     public byte[] getBytes(String str) {
       int n = str.length();
       byte[] bytes = new byte[n];
@@ -118,26 +128,23 @@
     }
 
     @Override
+    public byte[] getBytes(char[] buffer, int offset, int count) {
+      int n = offset + count;
+      byte[] bytes = new byte[0];
+      int out = 0;
+      for (int i = offset; i < n; ) {
+        int ch = Character.codePointAt(buffer, i, n);
+        i += Character.charCount(ch);
+        out += encodeUtf8(bytes, out, ch);
+      }
+      return bytes;
+    }
+
+    @Override
     public byte[] getBytes(String str) {
       // TODO(jat): consider using unescape(encodeURIComponent(bytes)) instead
       int n = str.length();
-      int byteCount = 0;
-      for (int i = 0; i < n;) {
-        int ch = str.codePointAt(i);
-        i += Character.charCount(ch);
-        if (ch < (1 << 7)) {
-          byteCount++;
-        } else if (ch < (1 << 11)) {
-          byteCount += 2;
-        } else if (ch < (1 << 16)) {
-          byteCount += 3;
-        } else if (ch < (1 << 21)) {
-          byteCount += 4;
-        } else if (ch < (1 << 26)) {
-          byteCount += 5;
-        }
-      }
-      byte[] bytes = new byte[byteCount];
+      byte[] bytes = new byte[0];
       int out = 0;
       for (int i = 0; i < n;) {
         int ch = str.codePointAt(i);
@@ -197,5 +204,7 @@
 
   public abstract byte[] getBytes(String string);
 
+  public abstract byte[] getBytes(char[] buffer, int offset, int count);
+
   public abstract char[] decodeString(byte[] bytes, int ofs, int len);
 }
diff --git a/user/test/com/google/gwt/emultest/EmulSuite.java b/user/test/com/google/gwt/emultest/EmulSuite.java
index 575fd5b..0d8bf9f 100644
--- a/user/test/com/google/gwt/emultest/EmulSuite.java
+++ b/user/test/com/google/gwt/emultest/EmulSuite.java
@@ -22,6 +22,8 @@
 import com.google.gwt.emultest.java.io.FilterOutputStreamTest;
 import com.google.gwt.emultest.java.io.InputStreamTest;
 import com.google.gwt.emultest.java.io.OutputStreamTest;
+import com.google.gwt.emultest.java.io.OutputStreamWriterTest;
+import com.google.gwt.emultest.java.io.WriterTest;
 import com.google.gwt.emultest.java.lang.BooleanTest;
 import com.google.gwt.emultest.java.lang.ByteTest;
 import com.google.gwt.emultest.java.lang.CharacterTest;
@@ -74,6 +76,8 @@
   FilterOutputStreamTest.class,
   InputStreamTest.class,
   OutputStreamTest.class,
+  OutputStreamWriterTest.class,
+  WriterTest.class,
 
   // -- java.lang
   BooleanTest.class,
diff --git a/user/test/com/google/gwt/emultest/java/io/OutputStreamWriterTest.java b/user/test/com/google/gwt/emultest/java/io/OutputStreamWriterTest.java
new file mode 100644
index 0000000..49ae2b2
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java/io/OutputStreamWriterTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gwt.junit.client.GWTTestCase;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * Unit test for the {@link java.io.OutputStreamWriter} emulated class.
+ */
+public class OutputStreamWriterTest extends GWTTestCase {
+
+  private final Charset encodingUTF8Charset = UTF_8;
+
+  /** String containing unicode characters. */
+  private static final String UNICODE_STRING = "ËÛëŶǾȜϞ";
+
+  /** Array of characters that contains ASCII characters. */
+  private static final char[] ASCII_CHAR_ARRAY = {'a', 'b', 'c', '"', '&', '<', '>'};
+
+  /** {@link java.io.OutputStreamWriter} object being tested. */
+  private OutputStreamWriter writer;
+
+  /** Underlying output stream used by the {@link OutputStreamWriter} object. */
+  private ByteArrayOutputStream baos;
+
+  /**
+   * 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();
+    baos = new ByteArrayOutputStream();
+    writer = new OutputStreamWriter(baos, encodingUTF8Charset);
+  }
+
+  public void testNullCharset() throws UnsupportedEncodingException {
+    Charset nullCharset = null;
+    try {
+      new OutputStreamWriter(baos, nullCharset);
+      fail("should have thrown NullPointerException");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testNullOutputStream() throws UnsupportedEncodingException {
+    try {
+      new OutputStreamWriter(/* out = */ null, encodingUTF8Charset);
+      fail("should have thrown NullPointerException");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testWriteUnicodeChar() throws IOException {
+    writer.write(UNICODE_STRING, 0, UNICODE_STRING.length());
+    assertTrue(Arrays.equals(UNICODE_STRING.getBytes(encodingUTF8Charset), baos.toByteArray()));
+  }
+
+  public void testWriteASCIIChar() throws IOException {
+    writer.write(ASCII_CHAR_ARRAY, 0, ASCII_CHAR_ARRAY.length);
+    assertTrue(
+        Arrays.equals(
+            new String(ASCII_CHAR_ARRAY).getBytes(encodingUTF8Charset), baos.toByteArray()));
+  }
+
+  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) {
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/emultest/java/io/WriterTest.java b/user/test/com/google/gwt/emultest/java/io/WriterTest.java
new file mode 100644
index 0000000..7df1001
--- /dev/null
+++ b/user/test/com/google/gwt/emultest/java/io/WriterTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit test for the {@link java.io.Writer} emulated class.
+ */
+public class WriterTest extends GWTTestCase {
+
+  private TestWriter writer;
+
+  /**
+   * Instatiable version of {@link java.io.Writer} for testing purposes.
+   */
+  private static class TestWriter extends Writer {
+
+    private List<Character> outputChars = new ArrayList<>(1024);
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public void flush() {
+    }
+
+    @Override
+    public void write(char[] cbuf, int off, int len) {
+      for (int i = off; i < (off + len); i++) {
+        outputChars.add(cbuf[i]);
+      }
+    }
+
+   /**
+    * Converts {@code outputChars} to primitive character array.
+    *
+    * @return primitive char array
+    */
+    public char[] toCharArray() {
+      if (outputChars.isEmpty()) {
+        return null;
+      }
+      char[] charArray = new char[outputChars.size()];
+      for (int i = 0; i < outputChars.size(); i++) {
+        charArray[i] = outputChars.get(i);
+      }
+      return charArray;
+    }
+  }
+
+  /**
+   * 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();
+    writer = new TestWriter();
+  }
+
+  public void testAppendChar() throws IOException {
+    Writer w = writer.append('a');
+    assertEquals(writer, w);
+
+    assertTrue(Arrays.equals(new char[] { 'a' }, writer.toCharArray()));
+
+    w = writer.append('b');
+    assertEquals(writer, w);
+    assertTrue(Arrays.equals(new char[] { 'a', 'b' }, writer.toCharArray()));
+
+    w = writer.append('c');
+    assertEquals(writer, w);
+    assertTrue(Arrays.equals(new char[] { 'a', 'b', 'c' }, writer.toCharArray()));
+  }
+
+  public void testAppendNullCharSequence() throws IOException {
+    final Writer w = writer.append(null);
+    assertEquals(writer, w);
+    assertTrue(Arrays.equals("null".toCharArray(), writer.toCharArray()));
+  }
+
+  public void testAppendEmptyCharSequence() throws IOException {
+    final CharSequence csq = "";
+    final Writer w = writer.append(csq);
+    assertEquals(writer, w);
+    assertNull(writer.toCharArray());
+  }
+
+  public void testAppendNonEmptyCharSequence() throws IOException {
+    final CharSequence csq = "hola";
+    final Writer w = writer.append(csq);
+    assertEquals(writer, w);
+    assertTrue(Arrays.equals("hola".toCharArray(), writer.toCharArray()));
+  }
+
+  public void testAppendSubCharSequenceUsingNegativeStartValue() throws IOException {
+    final CharSequence csq = "hola";
+    try {
+      writer.append(csq, -1, 2);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testAppendSubCharSequenceUsingNegativeEndValue() throws IOException {
+    final CharSequence csq = "hola";
+    try {
+      writer.append(csq, 0, -1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testAppendSubCharSequenceStartIsGreaterThanEnd() throws IOException {
+    final CharSequence csq = "hola";
+    try {
+      writer.append(csq, 2, 1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testAppendNullSubCharSequence() throws IOException {
+    final Writer w = writer.append(null, 1, "null".length() - 1);
+    assertEquals(writer, w);
+    assertTrue(Arrays.equals("ul".toCharArray(), writer.toCharArray()));
+  }
+
+  public void testAppendEmptySubCharSequence() throws IOException {
+    final CharSequence csq = "";
+    final Writer w = writer.append(csq, 0, 0);
+    assertEquals(writer, w);
+    assertNull(writer.toCharArray());
+  }
+
+  public void testAppendNonEmptySubCharSequence() throws IOException {
+    final CharSequence csq = "hola";
+    final Writer w = writer.append(csq, 1, "hola".length() - 1);
+    assertEquals(writer, w);
+    assertTrue(Arrays.equals("ol".toCharArray(), writer.toCharArray()));
+  }
+
+  public void testWriteChar() throws IOException {
+    writer.write('a');
+    assertTrue(Arrays.equals(new char[] { 'a' }, writer.toCharArray()));
+
+    writer.write('b');
+    assertTrue(Arrays.equals(new char[] { 'a', 'b' }, writer.toCharArray()));
+
+    writer.write('c');
+    assertTrue(Arrays.equals(new char[] { 'a', 'b', 'c' }, writer.toCharArray()));
+  }
+
+  public void testWriteEmptyCharArray() throws IOException {
+    final char[] charArray = new char[] { };
+    writer.write(charArray);
+    assertNull(writer.toCharArray());
+  }
+
+  public void testWriteNonEmptyCharArray() throws IOException {
+    final char[] charArray = "hola".toCharArray();
+    writer.write(charArray);
+    assertTrue(Arrays.equals(charArray, writer.toCharArray()));
+  }
+
+  public void testWriteEmptyString() throws IOException {
+    final String str = "";
+    writer.write(str);
+    assertNull(writer.toCharArray());
+  }
+
+  public void testWriteNullString() throws IOException {
+    try {
+      final String str = null;
+      writer.write(str);
+      fail("should have thrown NullPointerException");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testWriteNonEmptyString() throws IOException {
+    final String str = "hola";
+    writer.write(str);
+    assertTrue(Arrays.equals(str.toCharArray(), writer.toCharArray()));
+  }
+
+  public void testWriteSubStringUsingNegativeStartValue() throws IOException {
+    final String str = "hola";
+    try {
+      writer.append(str, -1, 2);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException expected) {
+    }
+  }
+
+  public void testWriteSubStringUsingNegativeEndValue() throws IOException {
+    final String str = "hola";
+    try {
+      writer.append(str, 0, -1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testWriteSubStringStartIsGreaterThanEnd() throws IOException {
+    final String str = "hola";
+    try {
+      writer.append(str, 2, 1);
+      fail("should have thrown IndexOutOfBoundsException");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  public void testWriteEmptySubstring() throws IOException {
+    final String str = "";
+    writer.write(str, 0, 0);
+    assertNull(writer.toCharArray());
+  }
+
+  public void testWriteNonEmptySubstring() throws IOException {
+    final String str = "hola";
+    writer.write(str, 1, 2);
+    assertTrue(Arrays.equals("ol".toCharArray(), writer.toCharArray()));
+  }
+}