From 30da3953041764efb181940fb3955e22e37541ee Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 16 Nov 2017 13:43:31 +0300 Subject: [PATCH] Add BufferedWriter --- .../classlib/java/io/TBufferedWriter.java | 193 +++++++++++ .../classlib/java/io/BufferedWriterTest.java | 323 ++++++++++++++++++ 2 files changed, 516 insertions(+) create mode 100644 classlib/src/main/java/org/teavm/classlib/java/io/TBufferedWriter.java create mode 100644 tests/src/test/java/org/teavm/classlib/java/io/BufferedWriterTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/java/io/TBufferedWriter.java b/classlib/src/main/java/org/teavm/classlib/java/io/TBufferedWriter.java new file mode 100644 index 000000000..d3d49c657 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/io/TBufferedWriter.java @@ -0,0 +1,193 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * 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 org.teavm.classlib.java.io; + +import java.io.IOException; +import java.io.Writer; +import java.util.Objects; + +public class TBufferedWriter extends Writer { + private Writer out; + private char[] buf; + private int pos; + private final String lineSeparator = "\n"; + + public TBufferedWriter(Writer out) { + super(out); + this.out = out; + buf = new char[8192]; + } + + public TBufferedWriter(Writer out, int size) { + super(out); + if (size <= 0) { + throw new IllegalArgumentException(); + } + this.out = out; + this.buf = new char[size]; + } + + @Override + public void close() throws IOException { + if (isClosed()) { + return; + } + + Throwable thrown = null; + try { + flushInternal(); + } catch (Throwable e) { + thrown = e; + } + buf = null; + + try { + out.close(); + } catch (Throwable e) { + if (thrown == null) { + thrown = e; + } + } + out = null; + + if (thrown != null) { + if (thrown instanceof IOException) { + throw (IOException) thrown; + } else if (thrown instanceof RuntimeException) { + throw (RuntimeException) thrown; + } else { + throw new IOException(thrown); + } + } + } + + @Override + public void flush() throws IOException { + if (isClosed()) { + throw new IOException(); + } + flushInternal(); + out.flush(); + } + + private void flushInternal() throws IOException { + if (pos > 0) { + out.write(buf, 0, pos); + } + pos = 0; + } + + private boolean isClosed() { + return out == null; + } + + public void newLine() throws IOException { + write(lineSeparator, 0, lineSeparator.length()); + } + + @Override + public void write(char[] cbuf, int offset, int count) throws IOException { + if (isClosed()) { + throw new IOException(); + } + if (offset < 0 || offset > Objects.requireNonNull(cbuf).length - count || count < 0) { + throw new IndexOutOfBoundsException(); + } + if (pos == 0 && count >= this.buf.length) { + out.write(cbuf, offset, count); + return; + } + int available = this.buf.length - pos; + if (count < available) { + available = count; + } + if (available > 0) { + System.arraycopy(cbuf, offset, this.buf, pos, available); + pos += available; + } + if (pos == this.buf.length) { + out.write(this.buf, 0, this.buf.length); + pos = 0; + if (count > available) { + offset += available; + available = count - available; + if (available >= this.buf.length) { + out.write(cbuf, offset, available); + return; + } + + System.arraycopy(cbuf, offset, this.buf, pos, available); + pos += available; + } + } + } + + @Override + public void write(int oneChar) throws IOException { + if (isClosed()) { + throw new IOException(); + } + if (pos >= buf.length) { + out.write(buf, 0, buf.length); + pos = 0; + } + buf[pos++] = (char) oneChar; + } + + @Override + public void write(String str, int offset, int count) throws IOException { + if (isClosed()) { + throw new IOException(); + } + if (count <= 0) { + return; + } + if (offset > Objects.requireNonNull(str).length() - count || offset < 0) { + throw new StringIndexOutOfBoundsException(); + } + if (pos == 0 && count >= buf.length) { + char[] chars = new char[count]; + str.getChars(offset, offset + count, chars, 0); + out.write(chars, 0, count); + return; + } + int available = buf.length - pos; + if (count < available) { + available = count; + } + if (available > 0) { + str.getChars(offset, offset + available, buf, pos); + pos += available; + } + if (pos == buf.length) { + out.write(this.buf, 0, this.buf.length); + pos = 0; + if (count > available) { + offset += available; + available = count - available; + if (available >= buf.length) { + char[] chars = new char[count]; + str.getChars(offset, offset + available, chars, 0); + out.write(chars, 0, available); + return; + } + str.getChars(offset, offset + available, buf, pos); + pos += available; + } + } + } +} diff --git a/tests/src/test/java/org/teavm/classlib/java/io/BufferedWriterTest.java b/tests/src/test/java/org/teavm/classlib/java/io/BufferedWriterTest.java new file mode 100644 index 000000000..90ed95d45 --- /dev/null +++ b/tests/src/test/java/org/teavm/classlib/java/io/BufferedWriterTest.java @@ -0,0 +1,323 @@ +/* + * Copyright 2017 Alexey Andreev. + * + * 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 org.teavm.classlib.java.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.Writer; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class BufferedWriterTest { + private BufferedWriter bw; + private StringWriter sw; + + private String testString = "" + + "Test_All_Tests\n" + "Test_java_io_BufferedInputStream\n" + "Test_java_io_BufferedOutputStream\n" + + "Test_java_io_ByteArrayInputStream\n" + "Test_java_io_ByteArrayOutputStream\n" + + "Test_java_io_DataInputStream\n" + "Test_java_io_File\n" + "Test_java_io_FileDescriptor\n" + + "Test_java_io_FileInputStream\n" + "Test_java_io_FileNotFoundException\n" + + "Test_java_io_FileOutputStream\n" + "Test_java_io_FilterInputStream\n" + + "Test_java_io_FilterOutputStream\n" + "Test_java_io_InputStream\n" + "Test_java_io_IOException\n" + + "Test_java_io_OutputStream\n" + "Test_java_io_PrintStream\n" + "Test_java_io_RandomAccessFile\n" + + "Test_java_io_SyncFailedException\n" + "Test_java_lang_AbstractMethodError\n" + + "Test_java_lang_ArithmeticException\n" + "Test_java_lang_ArrayIndexOutOfBoundsException\n" + + "Test_java_lang_ArrayStoreException\n" + "Test_java_lang_Boolean\n" + "Test_java_lang_Byte\n" + + "Test_java_lang_Character\n" + "Test_java_lang_Class\n" + "Test_java_lang_ClassCastException\n" + + "Test_java_lang_ClassCircularityError\n" + "Test_java_lang_ClassFormatError\n" + + "Test_java_lang_ClassLoader\n" + "Test_java_lang_ClassNotFoundException\n" + + "Test_java_lang_CloneNotSupportedException\n" + "Test_java_lang_Double\n" + + "Test_java_lang_Error\n" + "Test_java_lang_Exception\n" + + "Test_java_lang_ExceptionInInitializerError\n" + "Test_java_lang_Float\n" + + "Test_java_lang_IllegalAccessError\n" + "Test_java_lang_IllegalAccessException\n" + + "Test_java_lang_IllegalArgumentException\n" + "Test_java_lang_IllegalMonitorStateException\n" + + "Test_java_lang_IllegalThreadStateException\n" + "Test_java_lang_IncompatibleClassChangeError\n" + + "Test_java_lang_IndexOutOfBoundsException\n" + "Test_java_lang_InstantiationError\n" + + "Test_java_lang_InstantiationException\n" + "Test_java_lang_Integer\n" + + "Test_java_lang_InternalError\n" + "Test_java_lang_InterruptedException\n" + + "Test_java_lang_LinkageError\n" + "Test_java_lang_Long\n" + "Test_java_lang_Math\n" + + "Test_java_lang_NegativeArraySizeException\n" + "Test_java_lang_NoClassDefFoundError\n" + + "Test_java_lang_NoSuchFieldError\n" + "Test_java_lang_NoSuchMethodError\n" + + "Test_java_lang_NullPointerException\n" + "Test_java_lang_Number\n" + + "Test_java_lang_NumberFormatException\n" + "Test_java_lang_Object\n" + + "Test_java_lang_OutOfMemoryError\n" + "Test_java_lang_RuntimeException\n" + + "Test_java_lang_SecurityManager\n" + "Test_java_lang_Short\n" + + "Test_java_lang_StackOverflowError\n" + "Test_java_lang_String\n" + + "Test_java_lang_StringBuffer\n" + "Test_java_lang_StringIndexOutOfBoundsException\n" + + "Test_java_lang_System\n" + "Test_java_lang_Thread\n" + "Test_java_lang_ThreadDeath\n" + + "Test_java_lang_ThreadGroup\n" + "Test_java_lang_Throwable\n" + "Test_java_lang_UnknownError\n" + + "Test_java_lang_UnsatisfiedLinkError\n" + "Test_java_lang_VerifyError\n" + + "Test_java_lang_VirtualMachineError\n" + "Test_java_lang_vm_Image\n" + + "Test_java_lang_vm_MemorySegment\n" + "Test_java_lang_vm_ROMStoreException\n" + + "Test_java_lang_vm_VM\n" + "Test_java_lang_Void\n" + "Test_java_net_BindException\n" + + "Test_java_net_ConnectException\n" + "Test_java_net_DatagramPacket\n" + + "Test_java_net_DatagramSocket\n" + "Test_java_net_DatagramSocketImpl\n" + + "Test_java_net_InetAddress\n" + "Test_java_net_NoRouteToHostException\n" + + "Test_java_net_PlainDatagramSocketImpl\n" + "Test_java_net_PlainSocketImpl\n" + + "Test_java_net_Socket\n" + "Test_java_net_SocketException\n" + "Test_java_net_SocketImpl\n" + + "Test_java_net_SocketInputStream\n" + "Test_java_net_SocketOutputStream\n" + + "Test_java_net_UnknownHostException\n" + "Test_java_util_ArrayEnumerator\n" + + "Test_java_util_Date\n" + "Test_java_util_EventObject\n" + "Test_java_util_HashEnumerator\n" + + "Test_java_util_Hashtable\n" + "Test_java_util_Properties\n" + "Test_java_util_ResourceBundle\n" + + "Test_java_util_tm\n" + "Test_java_util_Vector\n"; + + public BufferedWriterTest() { + sw = new StringWriter(); + bw = new BufferedWriter(sw, 500); + } + + @Test + public void constructorLjava_io_Writer() { + sw = new StringWriter(); + bw = new BufferedWriter(sw); + sw.write("Hi"); + assertEquals("Constructor failed", "Hi", sw.toString()); + } + + @Test + public void constructorLjava_io_WriterI() { + assertTrue("Used in tests", true); + } + + static class MockWriter extends Writer { + StringBuffer sb = new StringBuffer(); + boolean flushCalled; + + @Override + public void write(char[] buf, int off, int len) { + for (int i = off; i < off + len; i++) { + sb.append(buf[i]); + } + } + + @Override + public void close() { + // Empty + } + + @Override + public void flush() { + flushCalled = true; + } + + String getWritten() { + return sb.toString(); + } + + boolean isFlushCalled() { + return flushCalled; + } + } + + @Test + public void close() throws IOException { + try { + bw.close(); + bw.write(testString); + fail("Writing to a closed stream should throw IOException"); + } catch (IOException e) { + // Expected + } + assertTrue("Write after close", !sw.toString().equals(testString)); + + // Regression test for HARMONY-4178 + MockWriter mw = new MockWriter(); + BufferedWriter bw = new BufferedWriter(mw); + bw.write('a'); + bw.close(); + + // flush should not be called on underlying stream + assertFalse("Flush was called in the underlying stream", mw.isFlushCalled()); + + // on the other hand the BufferedWriter itself should flush the + // buffer + assertEquals("BufferdWriter do not flush itself before close", "a", mw.getWritten()); + } + + @Test + public void close2() throws IOException { + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new ByteArrayOutputStream())); + bw.close(); + } + + @Test + public void flush() throws Exception { + bw.write("This should not cause a flush"); + assertTrue("Bytes written without flush", sw.toString().equals("")); + bw.flush(); + assertEquals("Bytes not flushed", "This should not cause a flush", sw.toString()); + } + + @Test + public void newLine() throws Exception { + String separator = System.getProperty("line.separator"); + bw.write("Hello"); + bw.newLine(); + bw.write("World"); + bw.flush(); + assertTrue("Incorrect string written: " + sw.toString(), sw.toString().equals("Hello" + separator + "World")); + } + + @Test + public void write$CII() throws Exception { + char[] testCharArray = testString.toCharArray(); + bw.write(testCharArray, 500, 1000); + bw.flush(); + assertTrue("Incorrect string written", sw.toString().equals(testString.substring(500, 1500))); + } + + @Test + public void write_$CII_Exception() throws IOException { + BufferedWriter bWriter = new BufferedWriter(sw); + char[] nullCharArray = null; + + try { + bWriter.write(nullCharArray, -1, -1); + fail("should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // expected + } + + try { + bWriter.write(nullCharArray, -1, 0); + fail("should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // expected + } + + try { + bWriter.write(nullCharArray, 0, -1); + fail("should throw NullPointerException"); + } catch (NullPointerException e) { + // expected + } + + try { + bWriter.write(nullCharArray, 0, 0); + fail("should throw NullPointerException"); + } catch (NullPointerException e) { + // expected + } + + char[] testCharArray = testString.toCharArray(); + + bWriter.write(testCharArray, 0, 0); + + bWriter.write(testCharArray, testCharArray.length, 0); + + try { + bWriter.write(testCharArray, testCharArray.length + 1, 0); + fail("should throw IndexOutOfBoundsException"); + } catch (IndexOutOfBoundsException e) { + // expected + } + + bWriter.close(); + + try { + bWriter.write(nullCharArray, -1, -1); + fail("should throw IOException"); + } catch (IOException e) { + // expected + } + } + + @Test + public void writeI() throws Exception { + bw.write('T'); + assertTrue("Char written without flush", sw.toString().equals("")); + bw.flush(); + assertEquals("Incorrect char written", "T", sw.toString()); + } + + @Test + public void writeLjava_lang_StringII() throws Exception { + bw.write(testString); + bw.flush(); + assertTrue("Incorrect string written", sw.toString().equals(testString)); + } + + @Test + public void write_LStringII_Exception() throws IOException { + BufferedWriter bWriter = new BufferedWriter(sw); + + bWriter.write((String) null, -1, -1); + bWriter.write((String) null, -1, 0); + bWriter.write((String) null, 0, -1); + bWriter.write((String) null, 0, 0); + + try { + bWriter.write((String) null, -1, 1); + fail("should throw NullPointerException"); + } catch (NullPointerException e) { + // expected + } + + bWriter.write(testString, 0, 0); + bWriter.write(testString, testString.length(), 0); + bWriter.write(testString, testString.length() + 1, 0); + + try { + bWriter.write(testString, testString.length() + 1, 1); + fail("should throw StringIndexOutOfBoundsException"); + } catch (StringIndexOutOfBoundsException e) { + // expected + } + + bWriter.close(); + + try { + bWriter.write((String) null, -1, -1); + fail("should throw IOException"); + } catch (IOException e) { + // expected + } + + try { + bWriter.write((String) null, -1, 1); + fail("should throw IOException"); + } catch (IOException e) { + // expected + } + + try { + bWriter.write(testString, -1, -1); + fail("should throw IOException"); + } catch (IOException e) { + // expected + } + } + + @After + public void tearDown() { + try { + bw.close(); + } catch (Exception e) { + // Do nothing + } + } +}