diff options
Diffstat (limited to 'examples/scala-js/javalib/src/main/scala/java/io')
20 files changed, 1397 insertions, 0 deletions
diff --git a/examples/scala-js/javalib/src/main/scala/java/io/BufferedReader.scala b/examples/scala-js/javalib/src/main/scala/java/io/BufferedReader.scala new file mode 100644 index 0000000..0f06523 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/BufferedReader.scala @@ -0,0 +1,145 @@ +package java.io + +class BufferedReader(in: Reader, sz: Int) extends Reader { + + def this(in: Reader) = this(in, 4096) + + private[this] var buf = new Array[Char](sz) + + /** Last valid value in the buffer (exclusive) */ + private[this] var end = 0 + + /** Next position to read from buffer */ + private[this] var pos = 0 + + private[this] var closed = false + + private[this] var validMark = false + + override def close(): Unit = { + closed = true + } + + override def mark(readAheadLimit: Int): Unit = { + ensureOpen() + + val srcBuf = buf + if (buf.size < readAheadLimit) + buf = new Array[Char](readAheadLimit) + + // Move data to beginning of buffer + if (pos != 0 || (buf ne srcBuf)) + System.arraycopy(srcBuf, pos, buf, 0, end - pos) + + // Update internal state + end -= pos + pos = 0 + validMark = true + } + + override def markSupported(): Boolean = true + + override def read(): Int = { + ensureOpen() + + if (prepareRead()) { + val res = buf(pos).toInt + pos += 1 + res + } else -1 + } + + override def read(cbuf: Array[Char], off: Int, len: Int): Int = { + ensureOpen() + + if (off < 0 || len < 0 || len > cbuf.length - off) + throw new IndexOutOfBoundsException + + if (len == 0) 0 + else if (prepareRead()) { + val count = Math.min(len, end - pos) + System.arraycopy(this.buf, pos, cbuf, off, count) + pos += count + count + } else -1 + } + + def readLine(): String = { + ensureOpen() + + var res = "" + + while (prepareRead() && buf(pos) != '\n' && buf(pos) != '\r') { + res += buf(pos) + pos += 1 + } + + if (pos >= end) { + // We have reached the end of the stream (prepareRead() returned false) + if (res == "") null + else res + } else { + // Consume terminator + pos += 1 + + // Check whether we have a \r\n. This may overrun the buffer + // and then push a value back which may unnecessarily invalidate + // the mark. This mimics java behavior + if (buf(pos-1) == '\r' && prepareRead() && buf(pos) == '\n') + pos += 1 // consume '\n' + + res + } + } + + override def ready(): Boolean = { + ensureOpen() + pos < end || in.ready() + } + + override def reset(): Unit = { + ensureOpen() + + if (!validMark) throw new IOException("Mark invalid") + pos = 0 + } + + override def skip(n: Long): Long = { + if (n < 0) throw new IllegalArgumentException("n negative") + else if (pos < end) { + val count = Math.min(n, end - pos).toInt + pos += count + count.toLong + } else { + validMark = false + in.skip(n) + } + } + + /** Prepare the buffer for reading. Returns false if EOF */ + private def prepareRead(): Boolean = + pos < end || fillBuffer() + + /** Tries to fill the buffer. Returns false if EOF */ + private def fillBuffer(): Boolean = { + if (validMark && end < buf.length) { + // we may not do a full re-read, since we'll damage the mark. + val read = in.read(buf, end, buf.length - end) + if (read > 0) // protect from adding -1 + end += read + read > 0 + } else { + // Full re-read + validMark = false + end = in.read(buf) + pos = 0 + end > 0 + } + } + + private def ensureOpen(): Unit = { + if (closed) + throw new IOException("Operation on closed stream") + } + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/ByteArrayInputStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/ByteArrayInputStream.scala new file mode 100644 index 0000000..697e07b --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/ByteArrayInputStream.scala @@ -0,0 +1,58 @@ +package java.io + +class ByteArrayInputStream( + protected val buf: Array[Byte], + offset: Int, length: Int) extends InputStream { + + protected val count: Int = offset + length + protected var mark: Int = offset + protected var pos: Int = offset + + def this(buf: Array[Byte]) = this(buf, 0, buf.length) + + override def read(): Int = { + if (pos >= count) + -1 + else { + val res = buf(pos) & 0xFF // convert to unsigned int + pos += 1 + res + } + } + + override def read(b: Array[Byte], off: Int, reqLen: Int): Int = { + if (off < 0 || reqLen < 0 || reqLen > b.length - off) + throw new IndexOutOfBoundsException + + val len = Math.min(reqLen, count - pos) + + if (reqLen == 0) + 0 // 0 requested, 0 returned + else if (len == 0) + -1 // nothing to read at all + else { + System.arraycopy(buf, pos, b, off, len) + pos += len + len + } + } + + override def skip(n: Long): Long = { + val k = Math.max(0, Math.min(n, count - pos)) + pos += k.toInt + k.toLong + } + + override def available(): Int = count - pos + + override def markSupported(): Boolean = true + + override def mark(readlimit: Int): Unit = + mark = pos + + override def reset(): Unit = + pos = mark + + override def close(): Unit = () + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/ByteArrayOutputStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/ByteArrayOutputStream.scala new file mode 100644 index 0000000..916002d --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/ByteArrayOutputStream.scala @@ -0,0 +1,62 @@ +package java.io + +import scala.scalajs.js + +import scala.annotation.tailrec + +class ByteArrayOutputStream(initBufSize: Int) extends OutputStream { + + protected var buf: Array[Byte] = new Array(initBufSize) + protected var count: Int = 0 + + def this() = this(32) + + override def write(b: Int): Unit = { + if (count >= buf.length) + growBuf(1) + + buf(count) = b.toByte + count += 1 + } + + override def write(b: Array[Byte], off: Int, len: Int): Unit = { + if (off < 0 || len < 0 || len > b.length - off) + throw new IndexOutOfBoundsException() + + if (count + len > buf.length) + growBuf(len) + + System.arraycopy(b, off, buf, count, len) + count += len + } + + def writeTo(out: OutputStream): Unit = + out.write(buf, 0, count) + + def reset(): Unit = + count = 0 + + def toByteArray(): Array[Byte] = { + val res = new Array[Byte](count) + System.arraycopy(buf, 0, res, 0, count) + res + } + + def size(): Int = count + + override def toString(): String = + new String(buf, 0, count) + + def toString(charsetName: String): String = + new String(buf, 0, count, charsetName) + + override def close(): Unit = () + + private def growBuf(minIncrement: Int): Unit = { + val newSize = Math.max(count + minIncrement, buf.length * 2) + val newBuf = new Array[Byte](newSize) + System.arraycopy(buf, 0, newBuf, 0, count) + buf = newBuf + } + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/Closeable.scala b/examples/scala-js/javalib/src/main/scala/java/io/Closeable.scala new file mode 100644 index 0000000..e572390 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/Closeable.scala @@ -0,0 +1,6 @@ +package java.io + +/** Note that Closeable doesn't extend AutoCloseable for Java6 compat */ +trait Closeable { + def close(): Unit +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/DataInput.scala b/examples/scala-js/javalib/src/main/scala/java/io/DataInput.scala new file mode 100644 index 0000000..37913b4 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/DataInput.scala @@ -0,0 +1,19 @@ +package java.io + +trait DataInput { + def readBoolean(): Boolean + def readByte(): Byte + def readChar(): Char + def readDouble(): Double + def readFloat(): Float + def readFully(b: Array[Byte]): Unit + def readFully(b: Array[Byte], off: Int, len: Int): Unit + def readInt(): Int + def readLine(): String + def readLong(): Long + def readShort(): Short + def readUnsignedByte(): Int + def readUnsignedShort(): Int + def readUTF(): String + def skipBytes(n: Int): Int +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/FilterInputStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/FilterInputStream.scala new file mode 100644 index 0000000..a85b9f6 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/FilterInputStream.scala @@ -0,0 +1,24 @@ +package java.io + +class FilterInputStream protected ( + protected val in: InputStream) extends InputStream { + + override def read(): Int = + in.read() + + override def read(b: Array[Byte]): Int = + read(b, 0, b.length) // this is spec! must not do in.read(b) + + override def read(b: Array[Byte], off: Int, len: Int): Int = + in.read(b, off, len) + + override def skip(n: Long): Long = in.skip(n) + + override def available(): Int = in.available() + + override def close(): Unit = in.close() + + override def mark(readlimit: Int): Unit = in.mark(readlimit) + override def markSupported(): Boolean = in.markSupported() + override def reset(): Unit = in.reset() +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/FilterOutputStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/FilterOutputStream.scala new file mode 100644 index 0000000..299b7b6 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/FilterOutputStream.scala @@ -0,0 +1,16 @@ +package java.io + +class FilterOutputStream(protected val out: OutputStream) extends OutputStream { + def write(b: Int): Unit = + out.write(b) + + override def write(b: Array[Byte]): Unit = + write(b, 0, b.length) // this is spec! it must not call out.write(b) + + override def write(b: Array[Byte], off: Int, len: Int): Unit = + super.write(b, off, len) // calls this.write(Int) repeatedly + + override def flush(): Unit = out.flush() + + override def close(): Unit = out.close() +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/Flushable.scala b/examples/scala-js/javalib/src/main/scala/java/io/Flushable.scala new file mode 100644 index 0000000..2879ad2 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/Flushable.scala @@ -0,0 +1,5 @@ +package java.io + +trait Flushable { + def flush(): Unit +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/InputStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/InputStream.scala new file mode 100644 index 0000000..412d84b --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/InputStream.scala @@ -0,0 +1,53 @@ +package java.io + +abstract class InputStream extends Closeable { + def read(): Int + + def read(b: Array[Byte]): Int = read(b, 0, b.length) + + def read(b: Array[Byte], off: Int, len: Int): Int = { + if (off < 0 || len < 0 || len > b.length - off) + throw new IndexOutOfBoundsException + + if (len == 0) 0 + else { + var bytesWritten = 0 + var next = 0 + + while (bytesWritten < len && next != -1) { + next = + if (bytesWritten == 0) read() + else { + try read() + catch { case _: IOException => -1 } + } + if (next != -1) { + b(off + bytesWritten) = next.toByte + bytesWritten += 1 + } + } + + if (bytesWritten <= 0) -1 + else bytesWritten + } + } + + def skip(n: Long): Long = { + var skipped = 0 + while (skipped < n && read() != -1) + skipped += 1 + skipped + } + + def available(): Int = 0 + + def close(): Unit = () + + def mark(readlimit: Int): Unit = () + + def reset(): Unit = + throw new IOException("Reset not supported") + + def markSupported(): Boolean = false + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/InputStreamReader.scala b/examples/scala-js/javalib/src/main/scala/java/io/InputStreamReader.scala new file mode 100644 index 0000000..1ef957c --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/InputStreamReader.scala @@ -0,0 +1,216 @@ +package java.io + +import scala.annotation.tailrec + +import java.nio._ +import java.nio.charset._ + +class InputStreamReader(private[this] var in: InputStream, + private[this] var decoder: CharsetDecoder) extends Reader { + + private[this] var closed: Boolean = false + + /** Buffer in which to read bytes from the underlying input stream. + * + * Class invariant: contains bytes already read from `in` but not yet + * decoded. + */ + private[this] var inBuf: ByteBuffer = ByteBuffer.allocate(4096) + inBuf.limit(0) + + /** Tells whether the end of the underlying input stream has been reached. + * Class invariant: if true, then `in.read()` has returned -1. + */ + private[this] var endOfInput: Boolean = false + + /** Buffer in which to decode bytes into chars. + * Usually, it is not used, because we try to decode directly to the + * destination array. So as long as we do not really need one, we share + * an empty buffer. + * + * Class invariant: contains chars already decoded but not yet *read* by + * the user of this instance. + */ + private[this] var outBuf: CharBuffer = InputStreamReader.CommonEmptyCharBuffer + + def this(in: InputStream, charset: Charset) = + this(in, + charset.newDecoder + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE)) + + def this(in: InputStream) = + this(in, Charset.defaultCharset) + + def this(in: InputStream, charsetName: String) = + this(in, Charset.forName(charsetName)) + + def close(): Unit = { + closed = true + in = null + decoder = null + inBuf = null + outBuf = null + } + + def getEncoding(): String = + if (closed) null else decoder.charset.name + + override def read(): Int = { + ensureOpen() + + if (outBuf.hasRemaining) outBuf.get() + else super.read() + } + + def read(cbuf: Array[Char], off: Int, len: Int): Int = { + ensureOpen() + + if (off < 0 || len < 0 || len > cbuf.length - off) + throw new IndexOutOfBoundsException + + if (len == 0) 0 + else if (outBuf.hasRemaining) { + // Reuse chars decoded last time + val available = Math.min(outBuf.remaining, len) + outBuf.get(cbuf, off, available) + available + } else { + // Try and decode directly into the destination array + val directOut = CharBuffer.wrap(cbuf, off, len) + val result = readImpl(directOut) + if (result != InputStreamReader.Overflow) { + result + } else { + /* There's not enough space in the destination array to receive even + * a tiny bit of output from the decoder. We need to decode to the + * outBuf instead. + * This happens typically when the next code point to decode is a + * supplementary character, and the given `len` is 1. + */ + readMoreThroughOutBuf(cbuf, off, len) + } + } + } + + // In a separate method because this is (hopefully) not a common case + private def readMoreThroughOutBuf(cbuf: Array[Char], off: Int, len: Int): Int = { + // Return outBuf to its full capacity + outBuf.limit(outBuf.capacity) + outBuf.position(0) + + @tailrec // but not inline, this is not a common path + def loopWithOutBuf(desiredOutBufSize: Int): Int = { + if (outBuf.capacity < desiredOutBufSize) + outBuf = CharBuffer.allocate(desiredOutBufSize) + val charsRead = readImpl(outBuf) + if (charsRead == InputStreamReader.Overflow) + loopWithOutBuf(desiredOutBufSize*2) + else + charsRead + } + + val charsRead = loopWithOutBuf(2*len) + assert(charsRead != 0) // can be -1, though + outBuf.flip() + + if (charsRead == -1) -1 + else { + val available = Math.min(charsRead, len) + outBuf.get(cbuf, off, available) + available + } + } + + @tailrec + private def readImpl(out: CharBuffer): Int = { + val initPos = out.position + val result = decoder.decode(inBuf, out, endOfInput) + + if (out.position != initPos) { + /* Good, we made progress, so we can return. + * Note that the `result` does not matter. Whether it's an underflow, + * an overflow, or even an error, if we read *something*, we can return + * that. + * The next invocation of read() will cause a new invocation of decode(), + * which will necessarily return the same result (but without advancing + * at all), which will cause one of the following cases to be handled. + */ + out.position - initPos + } else if (result.isUnderflow) { + if (endOfInput) { + assert(!inBuf.hasRemaining, + "CharsetDecoder.decode() should not have returned UNDERFLOW when "+ + "both endOfInput and inBuf.hasRemaining are true. It should have "+ + "returned a MalformedInput error instead.") + // Flush + if (decoder.flush(out).isOverflow) + InputStreamReader.Overflow + else { + // Done + if (out.position == initPos) -1 + else out.position - initPos + } + } else { + // We need to read more from the underlying input stream + if (inBuf.limit == inBuf.capacity) { + inBuf.compact() + if (!inBuf.hasRemaining) { + throw new AssertionError( + "Scala.js implementation restriction: " + + inBuf.capacity + " bytes do not seem to be enough for " + + getEncoding + " to decode a single code point. " + + "Please report this as a bug.") + } + inBuf.limit(inBuf.position) + inBuf.position(0) + } + + /* Note that this stores the new data after the limit of the buffer. + * Further, note that we may read more bytes than strictly necessary, + * according to the specification of InputStreamReader. + */ + val bytesRead = + in.read(inBuf.array, inBuf.limit, inBuf.capacity - inBuf.limit) + + if (bytesRead == -1) + endOfInput = true + else + inBuf.limit(inBuf.limit + bytesRead) + + readImpl(out) + } + } else if (result.isOverflow) { + InputStreamReader.Overflow + } else { + result.throwException() + throw new AssertionError("should not get here") + } + } + + /* In theory, `in.available() > 0` is incorrect. We should return true only + * if there are enough bytes available to read at least one code point. + * However, this is how the JDK behaves, and even the JavaDoc suggests this + * is the expected behavior. + */ + override def ready(): Boolean = + outBuf.hasRemaining || in.available() > 0 + + private def ensureOpen(): Unit = { + if (closed) + throw new IOException("Stream closed") + } + +} + +object InputStreamReader { + private final val Overflow = -2 + + /** Empty CharBuffer shared by all InputStreamReaders as long as they do + * not really need one. + * Since we do not use `mark()`, it is fine to share them, because `mark()` + * is the only piece of mutable state for an empty buffer. Everything else + * is effectively immutable (e.g., position and limit must always be 0). + */ + private val CommonEmptyCharBuffer = CharBuffer.allocate(0) +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/OutputStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/OutputStream.scala new file mode 100644 index 0000000..729e69b --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/OutputStream.scala @@ -0,0 +1,25 @@ +package java.io + +abstract class OutputStream extends Object with Closeable with Flushable { + def write(b: Int): Unit + + def write(b: Array[Byte]): Unit = + write(b, 0, b.length) + + def write(b: Array[Byte], off: Int, len: Int): Unit = { + if (off < 0 || len < 0 || len > b.length - off) + throw new IndexOutOfBoundsException() + + var n = off + val stop = off + len + while (n < stop) { + write(b(n)) + n += 1 + } + } + + def flush(): Unit = () + + def close(): Unit = () + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/OutputStreamWriter.scala b/examples/scala-js/javalib/src/main/scala/java/io/OutputStreamWriter.scala new file mode 100644 index 0000000..18c1c57 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/OutputStreamWriter.scala @@ -0,0 +1,160 @@ +package java.io + +import scala.annotation.tailrec + +import java.nio._ +import java.nio.charset._ + +class OutputStreamWriter(private[this] var out: OutputStream, + private[this] var enc: CharsetEncoder) extends Writer { + + private[this] var closed: Boolean = false + + /** Incoming buffer: pending Chars that have been written to this instance + * of OutputStreamWriter, but not yet encoded. + * Normally, this should always be at most 1 Char, if it is a high surrogate + * which ended up alone at the end of the input of a write(). + */ + private[this] var inBuf: String = "" + + /** Outgoing buffer: Bytes that have been decoded (from `inBuf`), but not + * yet written to the underlying output stream. + * The valid bytes are between 0 and outBuf.position. + */ + private[this] var outBuf: ByteBuffer = ByteBuffer.allocate(4096) + + def this(out: OutputStream, cs: Charset) = + this(out, + cs.newEncoder + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE)) + + def this(out: OutputStream) = + this(out, Charset.defaultCharset) + + def this(out: OutputStream, charsetName: String) = + this(out, Charset.forName(charsetName)) + + def getEncoding(): String = + if (closed) null else enc.charset.name + + override def write(c: Int): Unit = + write(c.toChar.toString, 0, 1) + + override def write(cbuf: Array[Char], off: Int, len: Int): Unit = + writeImpl(CharBuffer.wrap(cbuf, off, len)) + + override def write(str: String, off: Int, len: Int): Unit = + writeImpl(CharBuffer.wrap(str, off, len)) + + private def writeImpl(cbuf: CharBuffer): Unit = { + ensureOpen() + + val cbuf1 = if (inBuf != "") { + val fullInput = CharBuffer.wrap(inBuf + cbuf.toString) + inBuf = "" + fullInput + } else cbuf + + @inline + @tailrec + def loopEncode(): Unit = { + val result = enc.encode(cbuf1, outBuf, false) + if (result.isUnderflow) () + else if (result.isOverflow) { + makeRoomInOutBuf() + loopEncode() + } else { + result.throwException() + throw new AssertionError("should not get here") + } + } + + loopEncode() + if (cbuf1.hasRemaining) + inBuf = cbuf1.toString + } + + override def flush(): Unit = { + ensureOpen() + flushBuffer() + out.flush() + } + + override def close(): Unit = if (!closed) { + // Finish up the input + @inline + @tailrec + def loopEncode(): Unit = { + val cbuf = CharBuffer.wrap(inBuf) + val result = enc.encode(cbuf, outBuf, true) + if (result.isUnderflow) { + assert(!cbuf.hasRemaining, + "CharsetEncoder.encode() should not have returned UNDERFLOW when "+ + "both endOfInput and inBuf.hasRemaining are true. It should have "+ + "returned a MalformedInput error instead.") + } else if (result.isOverflow) { + makeRoomInOutBuf() + loopEncode() + } else { + result.throwException() + throw new AssertionError("should not get here") + } + } + + @inline + @tailrec + def loopFlush(): Unit = { + if (enc.flush(outBuf).isOverflow) { + makeRoomInOutBuf() + loopFlush() + } + } + + loopEncode() + loopFlush() + + // Flush before closing + flush() + + // Close the underlying stream + out.close() + + // Clean up all the resources + closed = true + out = null + enc = null + inBuf = null + outBuf = null + } + + private def ensureOpen(): Unit = { + if (closed) + throw new IOException("Closed writer.") + } + + private def makeRoomInOutBuf(): Unit = { + if (outBuf.position != 0) { + flushBuffer() + } else { + // Very unlikely (outBuf.capacity is not enough to encode a single code point) + outBuf.flip() + val newBuf = ByteBuffer.allocate(outBuf.capacity * 2) + newBuf.put(outBuf) + outBuf = newBuf + } + } + + /** Flushes the internal buffer of this writer, but not the underlying + * output stream. + */ + private[io] def flushBuffer(): Unit = { + ensureOpen() + + // Don't use outBuf.flip() first, in case out.write() throws + // Hence, use 0 instead of position, and position instead of limit + out.write(outBuf.array, outBuf.arrayOffset, outBuf.position) + outBuf.clear() + } + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/PrintStream.scala b/examples/scala-js/javalib/src/main/scala/java/io/PrintStream.scala new file mode 100644 index 0000000..68fa041 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/PrintStream.scala @@ -0,0 +1,218 @@ +package java.io + +import java.nio.charset.Charset +import java.util.Formatter + +class PrintStream private (_out: OutputStream, autoFlush: Boolean, + charset: Charset) + extends FilterOutputStream(_out) with Appendable with Closeable { + + /* The way we handle charsets here is a bit tricky, because we want to + * minimize the area of reachability for normal programs. + * + * First, if nobody uses the constructor taking an explicit encoding, we + * don't want to reach Charset.forName(), which pulls in all of the + * implemented charsets. + * + * Second, most programs will reach PrintStream only because of + * java.lang.System.{out,err}, which are subclasses of PrintStream that do + * not actually need to encode anything: they override all of PrintStream's + * methods to bypass the encoding altogether, and hence don't even need + * the default charset. + * + * This is why we have: + * * A private constructor taking the Charset directly, instead of its name. + * * Which is allowed to be `null`, which stands for the default charset. + * * The default charset is only loaded lazily in the initializer of the + * encoder field. + */ + + def this(out: OutputStream) = + this(out, false, null: Charset) + + def this(out: OutputStream, autoFlush: Boolean) = + this(out, autoFlush, null: Charset) + + def this(out: OutputStream, autoFlush: Boolean, encoding: String) = + this(out, autoFlush, Charset.forName(encoding)) + + /* The following constructors, although implemented, will not link, since + * File, FileOutputStream and BufferedOutputStream are not implemented. + * They're here just in case a third-party library on the classpath + * implements those. + */ + def this(file: File) = + this(new BufferedOutputStream(new FileOutputStream(file))) + def this(file: File, csn: String) = + this(new BufferedOutputStream(new FileOutputStream(file)), false, csn) + def this(fileName: String) = + this(new File(fileName)) + def this(fileName: String, csn: String) = + this(new File(fileName), csn) + + private lazy val encoder = { + val c = + if (charset == null) Charset.defaultCharset + else charset + /* We pass `this` as the output stream for the encoding writer so that + * we can apply auto-flushing. Note that this will flush() more often + * than required by the spec. It appears to be consistent with how the + * JDK behaves. + */ + new OutputStreamWriter(this, c) + } + + private var closing: Boolean = false + private var closed: Boolean = false + private var errorFlag: Boolean = false + + override def flush(): Unit = + ensureOpenAndTrapIOExceptions(out.flush()) + + override def close(): Unit = trapIOExceptions { + if (!closing) { + closing = true + encoder.close() + flush() + closed = true + out.close() + } + } + + def checkError(): Boolean = { + if (closed) { + /* Just check the error flag. + * Common sense would tell us to look at the underlying writer's + * checkError() result too (like we do in the not closed case below). + * But the JDK does not behave like that. So we don't either. + */ + errorFlag + } else { + flush() + /* If the underlying writer is also a PrintStream, we also check its + * checkError() result. This is not clearly specified by the JavaDoc, + * but, experimentally, the JDK seems to behave that way. + */ + errorFlag || (out match { + case out: PrintStream => out.checkError() + case _ => false + }) + } + } + + protected[io] def setError(): Unit = errorFlag = true + protected[io] def clearError(): Unit = errorFlag = false + + /* Note that calling directly the write() methods will happily bypass the + * potential lone high surrogate that is buffered in the underlying + * OutputStreamWriter. This means that the following sequence of operations: + * + * ps.print('\ud83d') // high surrogate of PILE OF POO + * ps.write('a') + * ps.print('\udca9') // low surrogate of PILE OF POO + * + * will result in the following bytes being emitted to the underlying stream: + * + * a\ud83d\udca9 + * + * i.e., first the 'a', then the PILE OF POO. + * + * This is consistent with the behavior of the JDK. + */ + + override def write(b: Int): Unit = { + ensureOpenAndTrapIOExceptions { + out.write(b) + if (autoFlush && b == '\n') + flush() + } + } + + override def write(buf: Array[Byte], off: Int, len: Int): Unit = { + ensureOpenAndTrapIOExceptions { + out.write(buf, off, len) + if (autoFlush) + flush() + } + } + + def print(b: Boolean): Unit = printString(String.valueOf(b)) + def print(c: Char): Unit = printString(String.valueOf(c)) + def print(i: Int): Unit = printString(String.valueOf(i)) + def print(l: Long): Unit = printString(String.valueOf(l)) + def print(f: Float): Unit = printString(String.valueOf(f)) + def print(d: Double): Unit = printString(String.valueOf(d)) + def print(s: String): Unit = printString(if (s == null) "null" else s) + def print(obj: AnyRef): Unit = printString(String.valueOf(obj)) + + private def printString(s: String): Unit = ensureOpenAndTrapIOExceptions { + encoder.write(s) + encoder.flushBuffer() + } + + def print(s: Array[Char]): Unit = ensureOpenAndTrapIOExceptions { + encoder.write(s) + encoder.flushBuffer() + } + + def println(): Unit = ensureOpenAndTrapIOExceptions { + encoder.write('\n') // In Scala.js the line separator is always LF + encoder.flushBuffer() + if (autoFlush) + flush() + } + + def println(b: Boolean): Unit = { print(b); println() } + def println(c: Char): Unit = { print(c); println() } + def println(i: Int): Unit = { print(i); println() } + def println(l: Long): Unit = { print(l); println() } + def println(f: Float): Unit = { print(f); println() } + def println(d: Double): Unit = { print(d); println() } + def println(s: Array[Char]): Unit = { print(s); println() } + def println(s: String): Unit = { print(s); println() } + def println(obj: AnyRef): Unit = { print(obj); println() } + + def printf(fmt: String, args: Array[Object]): PrintStream = + format(fmt, args) + + // Not implemented: + //def printf(l: java.util.Locale, fmt: String, args: Array[Object]): PrintStream = ??? + + def format(fmt: String, args: Array[Object]): PrintStream = { + new Formatter(this).format(fmt, args) + this + } + + // Not implemented: + //def format(l: java.util.Locale, fmt: String, args: Array[Object]): PrintStream = ??? + + def append(csq: CharSequence): PrintStream = { + print(if (csq == null) "null" else csq.toString) + this + } + + def append(csq: CharSequence, start: Int, end: Int): PrintStream = { + val csq1 = if (csq == null) "null" else csq + print(csq1.subSequence(start, end).toString) + this + } + + def append(c: Char): PrintStream = { + print(c) + this + } + + @inline private[this] def trapIOExceptions(body: => Unit): Unit = { + try { + body + } catch { + case _: IOException => setError() + } + } + + @inline private[this] def ensureOpenAndTrapIOExceptions(body: => Unit): Unit = { + if (closed) setError() + else trapIOExceptions(body) + } + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/PrintWriter.scala b/examples/scala-js/javalib/src/main/scala/java/io/PrintWriter.scala new file mode 100644 index 0000000..4e693e0 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/PrintWriter.scala @@ -0,0 +1,150 @@ +package java.io + +import java.util.Formatter + +class PrintWriter(protected[io] var out: Writer, + autoFlush: Boolean) extends Writer { + + def this(out: Writer) = this(out, false) + + def this(out: OutputStream, autoFlush: Boolean) = + this(new OutputStreamWriter(out), autoFlush) + def this(out: OutputStream) = + this(out, false) + + /* The following constructors, although implemented, will not link, since + * File, FileOutputStream and BufferedOutputStream are not implemented. + * They're here just in case a third-party library on the classpath + * implements those. + */ + def this(file: File) = + this(new BufferedOutputStream(new FileOutputStream(file))) + def this(file: File, csn: String) = + this(new OutputStreamWriter(new BufferedOutputStream( + new FileOutputStream(file)), csn)) + def this(fileName: String) = this(new File(fileName)) + def this(fileName: String, csn: String) = this(new File(fileName), csn) + + private var closed: Boolean = false + private var errorFlag: Boolean = false + + def flush(): Unit = + ensureOpenAndTrapIOExceptions(out.flush()) + + def close(): Unit = trapIOExceptions { + if (!closed) { + flush() + closed = true + out.close() + } + } + + def checkError(): Boolean = { + if (closed) { + /* Just check the error flag. + * Common sense would tell us to look at the underlying writer's + * checkError() result too (like we do in the not closed case below). + * But the JDK does not behave like that. So we don't either. + */ + errorFlag + } else { + flush() + /* If the underlying writer is also a PrintWriter, we also check its + * checkError() result. This is not clearly specified by the JavaDoc, + * but, experimentally, the JDK seems to behave that way. + */ + errorFlag || (out match { + case out: PrintWriter => out.checkError() + case _ => false + }) + } + } + + protected[io] def setError(): Unit = errorFlag = true + protected[io] def clearError(): Unit = errorFlag = false + + override def write(c: Int): Unit = + ensureOpenAndTrapIOExceptions(out.write(c)) + + override def write(buf: Array[Char], off: Int, len: Int): Unit = + ensureOpenAndTrapIOExceptions(out.write(buf, off, len)) + + override def write(buf: Array[Char]): Unit = + ensureOpenAndTrapIOExceptions(out.write(buf)) + + override def write(s: String, off: Int, len: Int): Unit = + ensureOpenAndTrapIOExceptions(out.write(s, off, len)) + + override def write(s: String): Unit = + ensureOpenAndTrapIOExceptions(out.write(s)) + + def print(b: Boolean): Unit = write(String.valueOf(b)) + def print(c: Char): Unit = write(c) + def print(i: Int): Unit = write(String.valueOf(i)) + def print(l: Long): Unit = write(String.valueOf(l)) + def print(f: Float): Unit = write(String.valueOf(f)) + def print(d: Double): Unit = write(String.valueOf(d)) + def print(s: Array[Char]): Unit = write(s) + def print(s: String): Unit = write(if (s == null) "null" else s) + def print(obj: AnyRef): Unit = write(String.valueOf(obj)) + + def println(): Unit = { + write('\n') // In Scala.js the line separator is always LF + if (autoFlush) + flush() + } + + def println(b: Boolean): Unit = { print(b); println() } + def println(c: Char): Unit = { print(c); println() } + def println(i: Int): Unit = { print(i); println() } + def println(l: Long): Unit = { print(l); println() } + def println(f: Float): Unit = { print(f); println() } + def println(d: Double): Unit = { print(d); println() } + def println(s: Array[Char]): Unit = { print(s); println() } + def println(s: String): Unit = { print(s); println() } + def println(obj: AnyRef): Unit = { print(obj); println() } + + def printf(fmt: String, args: Array[Object]): PrintWriter = + format(fmt, args) + + // Not implemented: + //def printf(l: java.util.Locale, fmt: String, args: Array[Object]): PrintWriter = ??? + + def format(fmt: String, args: Array[Object]): PrintWriter = { + new Formatter(this).format(fmt, args) + if (autoFlush) + flush() + this + } + + // Not implemented: + //def format(l: java.util.Locale, fmt: String, args: Array[Object]): PrintWriter = ??? + + override def append(csq: CharSequence): PrintWriter = { + super.append(csq) + this + } + + override def append(csq: CharSequence, start: Int, end: Int): PrintWriter = { + super.append(csq, start, end) + this + } + + override def append(c: Char): PrintWriter = { + super.append(c) + this + } + + @inline private[this] def trapIOExceptions(body: => Unit): Unit = { + try { + body + } catch { + case _: IOException => setError() + } + } + + @inline private[this] def ensureOpenAndTrapIOExceptions(body: => Unit): Unit = { + if (closed) setError() + else trapIOExceptions(body) + } +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/Reader.scala b/examples/scala-js/javalib/src/main/scala/java/io/Reader.scala new file mode 100644 index 0000000..97be140 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/Reader.scala @@ -0,0 +1,60 @@ +package java.io + +import java.nio.CharBuffer + +abstract class Reader private[this] (_lock: Option[Object]) + extends Readable with Closeable { + + protected val lock = _lock.getOrElse(this) + + protected def this(lock: Object) = this(Some(lock)) + protected def this() = this(None) + + def read(target: CharBuffer): Int = { + if (!target.hasRemaining) 0 + else if (target.hasArray) { + val charsRead = read(target.array, + target.position + target.arrayOffset, target.remaining) + if (charsRead != -1) + target.position(target.position + charsRead) + charsRead + } else { + val buf = new Array[Char](target.remaining) + val charsRead = read(buf) + if (charsRead != -1) + target.put(buf, 0, charsRead) + charsRead + } + } + + def read(): Int = { + val buf = new Array[Char](1) + if (read(buf) == -1) -1 + else buf(0).toInt + } + + def read(cbuf: Array[Char]): Int = + read(cbuf, 0, cbuf.length) + + def read(cbuf: Array[Char], off: Int, len: Int): Int + + def skip(n: Long): Long = { + if (n < 0) + throw new IllegalArgumentException("Cannot skip negative amount") + else if (read() == -1) 0 + else 1 + } + + def ready(): Boolean = false + + def markSupported(): Boolean = false + + def mark(readAheadLimit: Int): Unit = + throw new IOException("Mark not supported") + + def reset(): Unit = + throw new IOException("Reset not supported") + + def close(): Unit + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/Serializable.scala b/examples/scala-js/javalib/src/main/scala/java/io/Serializable.scala new file mode 100644 index 0000000..01dd228 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/Serializable.scala @@ -0,0 +1,3 @@ +package java.io + +trait Serializable diff --git a/examples/scala-js/javalib/src/main/scala/java/io/StringReader.scala b/examples/scala-js/javalib/src/main/scala/java/io/StringReader.scala new file mode 100644 index 0000000..2ca8f90 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/StringReader.scala @@ -0,0 +1,69 @@ +package java.io + +class StringReader(s: String) extends Reader { + + private[this] var closed = false + private[this] var pos = 0 + private[this] var mark = 0 + + override def close(): Unit = { + closed = true + } + + override def mark(readAheadLimit: Int): Unit = { + ensureOpen() + + mark = pos + } + + override def markSupported(): Boolean = true + + override def read(): Int = { + ensureOpen() + + if (pos < s.length) { + val res = s.charAt(pos).toInt + pos += 1 + res + } else -1 + } + + override def read(cbuf: Array[Char], off: Int, len: Int): Int = { + ensureOpen() + + if (off < 0 || len < 0 || len > cbuf.length - off) + throw new IndexOutOfBoundsException + + if (len == 0) 0 + else { + val count = Math.min(len, s.length - pos) + var i = 0 + while (i < count) { + cbuf(off + i) = s.charAt(pos + i) + i += 1 + } + pos += count + count + } + } + + override def ready(): Boolean = pos < s.length + + override def reset(): Unit = { + ensureOpen() + pos = mark + } + + override def skip(ns: Long): Long = { + // Apparently, StringReader.skip allows negative skips + val count = Math.max(Math.min(ns, s.length - pos).toInt, -pos) + pos += count + count.toLong + } + + private def ensureOpen(): Unit = { + if (closed) + throw new IOException("Operation on closed stream") + } + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/StringWriter.scala b/examples/scala-js/javalib/src/main/scala/java/io/StringWriter.scala new file mode 100644 index 0000000..13eca00 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/StringWriter.scala @@ -0,0 +1,44 @@ +package java.io + +class StringWriter extends Writer { + + private[this] val buf = new StringBuffer + + def this(initialSize: Int) = this() + + override def write(c: Int): Unit = + buf.append(c.toChar) + + def write(cbuf: Array[Char], off: Int, len: Int): Unit = + buf.append(cbuf, off, len) + + override def write(str: String): Unit = + buf.append(str) + + override def write(str: String, off: Int, len: Int): Unit = + buf.append(str, off, off + len) // Third param is 'end', not 'len' + + override def append(csq: CharSequence): StringWriter = { + buf.append(csq) + this + } + + override def append(csq: CharSequence, start: Int, end: Int): StringWriter = { + buf.append(csq, start, end) + this + } + + override def append(c: Char): StringWriter = { + buf.append(c) + this + } + + override def toString(): String = buf.toString + + def getBuffer(): StringBuffer = buf + + def flush(): Unit = () + + def close(): Unit = () + +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/Throwables.scala b/examples/scala-js/javalib/src/main/scala/java/io/Throwables.scala new file mode 100644 index 0000000..c312c4c --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/Throwables.scala @@ -0,0 +1,19 @@ +package java.io + +class IOException(s: String, e: Throwable) extends Exception(s, e) { + def this(e: Throwable) = this(null, e) + def this(s: String) = this(s, null) + def this() = this(null, null) +} + +class EOFException(s: String) extends IOException(s) { + def this() = this(null) +} + +class UTFDataFormatException(s: String) extends IOException(s) { + def this() = this(null) +} + +class UnsupportedEncodingException(s: String) extends IOException(s) { + def this() = this(null) +} diff --git a/examples/scala-js/javalib/src/main/scala/java/io/Writer.scala b/examples/scala-js/javalib/src/main/scala/java/io/Writer.scala new file mode 100644 index 0000000..d63b477 --- /dev/null +++ b/examples/scala-js/javalib/src/main/scala/java/io/Writer.scala @@ -0,0 +1,45 @@ +package java.io + +abstract class Writer private[this] (_lock: Option[Object]) extends + Appendable with Closeable with Flushable { + + protected val lock = _lock.getOrElse(this) + + protected def this(lock: Object) = this(Some(lock)) + protected def this() = this(None) + + def write(c: Int): Unit = + write(Array(c.toChar)) + + def write(cbuf: Array[Char]): Unit = + write(cbuf, 0, cbuf.length) + + def write(cbuf: Array[Char], off: Int, len: Int): Unit + + def write(str: String): Unit = + write(str.toCharArray) + + def write(str: String, off: Int, len: Int): Unit = + write(str.toCharArray, off, len) + + def append(csq: CharSequence): Writer = { + write(if (csq == null) "null" else csq.toString) + this + } + + def append(csq: CharSequence, start: Int, end: Int): Writer = { + val csq1 = if (csq == null) "null" else csq + write(csq1.subSequence(start, end).toString) + this + } + + def append(c: Char): Writer = { + write(c.toInt) + this + } + + def flush(): Unit + + def close(): Unit + +} |