Skip to content

Fix #5014: Implement java.io.BufferedWriter #5015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions javalib/src/main/scala/java/io/BufferedWriter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Ported from Scala-native

/*
* Scala.js (https://www.scala-js.org/)
*
* Copyright EPFL.
*
* Licensed under Apache License 2.0
* (https://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package java.io

class BufferedWriter(out: Writer, sz: Int) extends Writer {

if (sz <= 0) throw new IllegalArgumentException("Buffer size <= 0")

def this(out: Writer) = this(out, 4096)

private[this] val buffer: Array[Char] = new Array[Char](sz)
private[this] var pos: Int = 0
private[this] var closed: Boolean = false

def close(): Unit = if (!closed) {
flush()
out.close()
closed = true
}

def flush(): Unit = {
ensureOpen()
if (pos > 0) {
out.write(buffer, 0, pos)
out.flush()
pos = 0
}
}

def newLine(): Unit =
write(System.lineSeparator())

override def write(c: Int): Unit = {
buffer.update(pos, c.toChar)
pos += 1
if (pos == sz)
flush()
}

override def write(s: String, off: Int, len: Int): Unit =
write(s.toCharArray, off, len)

def write(cbuf: Array[Char], off: Int, len: Int): Unit = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the JDK doc:

If the requested length is at least as large as the buffer, however, then this method will flush the buffer and write the characters directly to the underlying stream.

IIUC this behavior isn't implemented.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reimplemented the method

ensureOpen()

val cbufLength = cbuf.length
if (off < 0 || off + len > cbufLength)
throw new IndexOutOfBoundsException(s"Range [${off}, ${off + len}) out of bounds for length $cbufLength")

if (len > 0 && off + len > 0) {
val available = sz - pos
if (available > len) {
System.arraycopy(cbuf, off, buffer, pos, len)
pos += len
} else {
flush()
out.write(cbuf, off, len)
out.flush()
}
}
}

private def ensureOpen(): Unit =
if (closed) throw new IOException("Stream closed")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Ported from Scala-native

/*
* Scala.js (https://www.scala-js.org/)
*
* Copyright EPFL.
*
* Licensed under Apache License 2.0
* (https://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package org.scalajs.testsuite.javalib.io

import java.io._

import org.junit.Ignore
import org.junit.Test
import org.junit.Assert._

import org.scalajs.testsuite.utils.AssertThrows.assertThrows

class BufferedWriterTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll probably have to add more tests, notably (probably not a complete list, sorry):

  • operations on closed writers throw
  • index out of bounds use cases
  • corner cases pointed out in the implementation review (notably flushing behavior)
  • writing to a buffered writer multiple times (with different buffer boundary conditions).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added more test cases.


@Test def creatingBufferedWriterWithBufferSizeZeroThrowsException(): Unit = {
val writer = new OutputStreamWriter(new ByteArrayOutputStream)
assertThrows(
classOf[IllegalArgumentException],
new BufferedWriter(writer, -1)
)
}

@Test def canWriteSmallChunksToBufferedWriter(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter)
val string = "Hello, world!"
writer.write(string)
assertTrue(stringWriter.toString == "")
writer.flush()
assertTrue(stringWriter.toString == string)
}

@Test def canWriteChunkLargerThanBufferSizeToBufferedWriter(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter, 1)
val string = "Hello, world!"
writer.write(string)
assertTrue(stringWriter.toString == string)
writer.flush()
assertTrue(stringWriter.toString == string)
}

@Test def canWriteInMultiplePartsToBufferedWriter(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter, 5)
writer.write("Hello,")
assertTrue(stringWriter.toString == "Hello,")
writer.write(" ")
assertTrue(stringWriter.toString == "Hello,")
writer.flush()
assertTrue(stringWriter.toString == "Hello, ")
writer.write("w")
assertTrue(stringWriter.toString == "Hello, ")
writer.write("orld!")
assertTrue(stringWriter.toString == "Hello, world!")
}

@Test def writeNegativeLengthWritesNothing(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter)
val string = "Hello, world!"
writer.write(string, 0, -1)
writer.flush()
assertTrue(stringWriter.toString == "")
}

@Ignore @Test def writeNegativeLengthThrowsIndexOutOfBound(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter)
val string = "Hello, world!"
assertThrows(classOf[IndexOutOfBoundsException], writer.write(string, 0, -1))
}

@Ignore @Test def writeNegativeOffsetPlusLengthWritesNothing(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter)
val string = "Hello, world!"
writer.write(string, -2, 1)
writer.flush()
assertTrue(stringWriter.toString == "")
}

@Test def writeNegativeOffsetPlusLengthThrowsIndexOutOfBound(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter)
val string = "Hello, world!"
assertThrows(classOf[IndexOutOfBoundsException], writer.write(string, -2, 1))
}

@Test def closedWritersThrow(): Unit = {
val stream = new ByteArrayOutputStream
val writer = new BufferedWriter(new OutputStreamWriter(stream))
writer.close()
assertThrows(classOf[IOException], writer.write("Hello, world!"))
}

@Test def closingTwiceIsHarmless(): Unit = {
val stream = new ByteArrayOutputStream
val writer = new BufferedWriter(new OutputStreamWriter(stream))
writer.close()
writer.close()
}

@Test def canWriteNewLineSeparator(): Unit = {
val stringWriter = new StringWriter()
val writer = new BufferedWriter(stringWriter)
writer.write("Hello")
writer.newLine()
writer.write("world!")
writer.flush()
val expectedResult = "Hello" + System.lineSeparator() + "world!"
assertTrue(stringWriter.toString == expectedResult)
}
}