From 156e9007fd1461f396bfc3bb4e9f665b8beec5a0 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Mon, 10 Jan 2022 12:23:09 -0500 Subject: [PATCH] Convert JsonValueSource to Kotlin (#1489) * Rename .java to .kt * Convert JsonValueSource to Kotlin --- .../com/squareup/moshi/JsonValueSource.java | 235 ------------------ .../com/squareup/moshi/JsonValueSource.kt | 208 ++++++++++++++++ 2 files changed, 208 insertions(+), 235 deletions(-) delete mode 100644 moshi/src/main/java/com/squareup/moshi/JsonValueSource.java create mode 100644 moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java deleted file mode 100644 index a824fec..0000000 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2020 Square, 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 - * - * https://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.squareup.moshi; - -import java.io.EOFException; -import java.io.IOException; -import okio.Buffer; -import okio.BufferedSource; -import okio.ByteString; -import okio.Source; -import okio.Timeout; - -/** - * This source reads a prefix of another source as a JSON value and then terminates. It can read - * top-level arrays, objects, or strings only. - * - *

It implements {@linkplain JsonReader#setLenient lenient parsing} and has no mechanism to - * enforce strict parsing. If the input is not valid or lenient JSON the behavior of this source is - * unspecified. - */ -final class JsonValueSource implements Source { - static final ByteString STATE_JSON = ByteString.encodeUtf8("[]{}\"'/#"); - static final ByteString STATE_SINGLE_QUOTED = ByteString.encodeUtf8("'\\"); - static final ByteString STATE_DOUBLE_QUOTED = ByteString.encodeUtf8("\"\\"); - static final ByteString STATE_END_OF_LINE_COMMENT = ByteString.encodeUtf8("\r\n"); - static final ByteString STATE_C_STYLE_COMMENT = ByteString.encodeUtf8("*"); - static final ByteString STATE_END_OF_JSON = ByteString.EMPTY; - - private final BufferedSource source; - private final Buffer buffer; - - /** If non-empty, data from this should be returned before data from {@link #source}. */ - private final Buffer prefix; - - /** - * The state indicates what kind of data is readable at {@link #limit}. This also serves - * double-duty as the type of bytes we're interested in while in this state. - */ - private ByteString state; - - /** - * The level of nesting of arrays and objects. When the end of string, array, or object is - * reached, this should be compared against 0. If it is zero, then we've read a complete value and - * this source is exhausted. - */ - private int stackSize; - - /** The number of bytes immediately returnable to the caller. */ - private long limit = 0; - - private boolean closed = false; - - JsonValueSource(BufferedSource source) { - this(source, new Buffer(), STATE_JSON, 0); - } - - JsonValueSource(BufferedSource source, Buffer prefix, ByteString state, int stackSize) { - this.source = source; - this.buffer = source.getBuffer(); - this.prefix = prefix; - this.state = state; - this.stackSize = stackSize; - } - - /** - * Advance {@link #limit} until any of these conditions are met: - * - *

- * - * @throws EOFException if the stream is exhausted before the JSON object completes. - */ - private void advanceLimit(long byteCount) throws IOException { - while (limit < byteCount) { - // If we've finished the JSON object, we're done. - if (state == STATE_END_OF_JSON) { - return; - } - - // If we can't return any bytes without more data in the buffer, grow the buffer. - if (limit == buffer.size()) { - if (limit > 0L) return; - source.require(1L); - } - - // Find the next interesting character for the current state. If the buffer doesn't have one, - // then we can read the entire buffer. - long index = buffer.indexOfElement(state, limit); - if (index == -1L) { - limit = buffer.size(); - continue; - } - - byte b = buffer.getByte(index); - - if (state == STATE_JSON) { - switch (b) { - case '[': - case '{': - stackSize++; - limit = index + 1; - break; - - case ']': - case '}': - stackSize--; - if (stackSize == 0) state = STATE_END_OF_JSON; - limit = index + 1; - break; - - case '\"': - state = STATE_DOUBLE_QUOTED; - limit = index + 1; - break; - - case '\'': - state = STATE_SINGLE_QUOTED; - limit = index + 1; - break; - - case '/': - source.require(index + 2); - byte b2 = buffer.getByte(index + 1); - if (b2 == '/') { - state = STATE_END_OF_LINE_COMMENT; - limit = index + 2; - } else if (b2 == '*') { - state = STATE_C_STYLE_COMMENT; - limit = index + 2; - } else { - limit = index + 1; - } - break; - - case '#': - state = STATE_END_OF_LINE_COMMENT; - limit = index + 1; - break; - } - - } else if (state == STATE_SINGLE_QUOTED || state == STATE_DOUBLE_QUOTED) { - if (b == '\\') { - source.require(index + 2); - limit = index + 2; - } else { - state = (stackSize > 0) ? STATE_JSON : STATE_END_OF_JSON; - limit = index + 1; - } - - } else if (state == STATE_C_STYLE_COMMENT) { - source.require(index + 2); - if (buffer.getByte(index + 1) == '/') { - limit = index + 2; - state = STATE_JSON; - } else { - limit = index + 1; - } - - } else if (state == STATE_END_OF_LINE_COMMENT) { - limit = index + 1; - state = STATE_JSON; - - } else { - throw new AssertionError(); - } - } - } - - /** - * Discards any remaining JSON data in this source that was left behind after it was closed. It is - * an error to call {@link #read} after calling this method. - */ - public void discard() throws IOException { - closed = true; - while (state != STATE_END_OF_JSON) { - advanceLimit(8192); - source.skip(limit); - } - } - - @Override - public long read(Buffer sink, long byteCount) throws IOException { - if (closed) throw new IllegalStateException("closed"); - if (byteCount == 0) return 0L; - - // If this stream has a prefix, consume that first. - if (!prefix.exhausted()) { - long prefixResult = prefix.read(sink, byteCount); - byteCount -= prefixResult; - if (buffer.exhausted()) return prefixResult; // Defer a blocking call. - long suffixResult = read(sink, byteCount); - return suffixResult != -1L ? suffixResult + prefixResult : prefixResult; - } - - advanceLimit(byteCount); - - if (limit == 0) { - if (state != STATE_END_OF_JSON) throw new AssertionError(); - return -1L; - } - - long result = Math.min(byteCount, limit); - sink.write(buffer, result); - limit -= result; - return result; - } - - @Override - public Timeout timeout() { - return source.timeout(); - } - - @Override - public void close() throws IOException { - // Note that this does not close the underlying source; that's the creator's responsibility. - closed = true; - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt new file mode 100644 index 0000000..3dde36e --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueSource.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2020 Square, 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 + * + * https://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.squareup.moshi + +import okio.Buffer +import okio.BufferedSource +import okio.ByteString +import okio.ByteString.Companion.EMPTY +import okio.ByteString.Companion.encodeUtf8 +import okio.EOFException +import okio.Source +import kotlin.jvm.JvmOverloads +import kotlin.math.min + +/** + * This source reads a prefix of another source as a JSON value and then terminates. It can read + * top-level arrays, objects, or strings only. + * + * It implements [lenient parsing][JsonReader.setLenient] and has no mechanism to + * enforce strict parsing. If the input is not valid or lenient JSON the behavior of this source is + * unspecified. + */ +internal class JsonValueSource @JvmOverloads constructor( + private val source: BufferedSource, + /** If non-empty, data from this should be returned before data from [source]. */ + private val prefix: Buffer = Buffer(), + /** + * The state indicates what kind of data is readable at [limit]. This also serves + * double-duty as the type of bytes we're interested in while in this state. + */ + private var state: ByteString = STATE_JSON, + /** + * The level of nesting of arrays and objects. When the end of string, array, or object is + * reached, this should be compared against 0. If it is zero, then we've read a complete value and + * this source is exhausted. + */ + private var stackSize: Int = 0 +) : Source { + private val buffer: Buffer = source.buffer + + /** The number of bytes immediately returnable to the caller. */ + private var limit: Long = 0 + private var closed = false + + /** + * Advance [limit] until any of these conditions are met: + * * Limit is at least `byteCount`. We can satisfy the caller's request! + * * The JSON value is complete. This stream is exhausted. + * * We have some data to return and returning more would require reloading the buffer. We prefer to return some + * data immediately when more data requires blocking. + * + * @throws EOFException if the stream is exhausted before the JSON object completes. + */ + private fun advanceLimit(byteCount: Long) { + while (limit < byteCount) { + // If we've finished the JSON object, we're done. + if (state === STATE_END_OF_JSON) { + return + } + + // If we can't return any bytes without more data in the buffer, grow the buffer. + if (limit == buffer.size) { + if (limit > 0L) return + source.require(1L) + } + + // Find the next interesting character for the current state. If the buffer doesn't have one, + // then we can read the entire buffer. + val index = buffer.indexOfElement(state, limit) + if (index == -1L) { + limit = buffer.size + continue + } + val b = buffer[index] + when { + state === STATE_JSON -> when (b.toInt().toChar()) { + '[', '{' -> { + stackSize++ + limit = index + 1 + } + ']', '}' -> { + stackSize-- + if (stackSize == 0) state = STATE_END_OF_JSON + limit = index + 1 + } + '\"' -> { + state = STATE_DOUBLE_QUOTED + limit = index + 1 + } + '\'' -> { + state = STATE_SINGLE_QUOTED + limit = index + 1 + } + '/' -> { + source.require(index + 2) + when (buffer[index + 1]) { + '/'.code.toByte() -> { + state = STATE_END_OF_LINE_COMMENT + limit = index + 2 + } + '*'.code.toByte() -> { + state = STATE_C_STYLE_COMMENT + limit = index + 2 + } + else -> { + limit = index + 1 + } + } + } + '#' -> { + state = STATE_END_OF_LINE_COMMENT + limit = index + 1 + } + } + state === STATE_SINGLE_QUOTED || state === STATE_DOUBLE_QUOTED -> { + if (b == '\\'.code.toByte()) { + source.require(index + 2) + limit = index + 2 + } else { + state = if (stackSize > 0) STATE_JSON else STATE_END_OF_JSON + limit = index + 1 + } + } + state === STATE_C_STYLE_COMMENT -> { + source.require(index + 2) + if (buffer[index + 1] == '/'.code.toByte()) { + limit = index + 2 + state = STATE_JSON + } else { + limit = index + 1 + } + } + state === STATE_END_OF_LINE_COMMENT -> { + limit = index + 1 + state = STATE_JSON + } + else -> { + throw AssertionError() + } + } + } + } + + /** + * Discards any remaining JSON data in this source that was left behind after it was closed. It is + * an error to call [read] after calling this method. + */ + fun discard() { + closed = true + while (state !== STATE_END_OF_JSON) { + advanceLimit(8192) + source.skip(limit) + } + } + + override fun read(sink: Buffer, byteCount: Long): Long { + var mutableByteCount = byteCount + check(!closed) { "closed" } + if (mutableByteCount == 0L) return 0L + + // If this stream has a prefix, consume that first. + if (!prefix.exhausted()) { + val prefixResult = prefix.read(sink, mutableByteCount) + mutableByteCount -= prefixResult + if (buffer.exhausted()) return prefixResult // Defer a blocking call. + val suffixResult = read(sink, mutableByteCount) + return if (suffixResult != -1L) suffixResult + prefixResult else prefixResult + } + advanceLimit(mutableByteCount) + if (limit == 0L) { + if (state !== STATE_END_OF_JSON) throw AssertionError() + return -1L + } + val result = min(mutableByteCount, limit) + sink.write(buffer, result) + limit -= result + return result + } + + override fun timeout() = source.timeout() + + override fun close() { + // Note that this does not close the underlying source; that's the creator's responsibility. + closed = true + } + + companion object { + val STATE_JSON: ByteString = "[]{}\"'/#".encodeUtf8() + val STATE_SINGLE_QUOTED: ByteString = "'\\".encodeUtf8() + val STATE_DOUBLE_QUOTED: ByteString = "\"\\".encodeUtf8() + val STATE_END_OF_LINE_COMMENT: ByteString = "\r\n".encodeUtf8() + val STATE_C_STYLE_COMMENT: ByteString = "*".encodeUtf8() + val STATE_END_OF_JSON: ByteString = EMPTY + } +}