diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java deleted file mode 100644 index b104afa..0000000 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright (C) 2017 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 static com.squareup.moshi.JsonScope.EMPTY_ARRAY; -import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT; -import static com.squareup.moshi.JsonScope.EMPTY_OBJECT; -import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT; -import static com.squareup.moshi.JsonScope.STREAMING_VALUE; -import static java.lang.Double.NEGATIVE_INFINITY; -import static java.lang.Double.POSITIVE_INFINITY; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; -import okio.Buffer; -import okio.BufferedSink; -import okio.ForwardingSink; -import okio.Okio; - -/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */ -final class JsonValueWriter extends JsonWriter { - Object[] stack = new Object[32]; - private @Nullable String deferredName; - - JsonValueWriter() { - pushScope(EMPTY_DOCUMENT); - } - - public Object root() { - int size = stackSize; - if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) { - throw new IllegalStateException("Incomplete document"); - } - return stack[0]; - } - - @Override - public JsonWriter beginArray() throws IOException { - if (promoteValueToName) { - throw new IllegalStateException( - "Array cannot be used as a map key in JSON at path " + getPath()); - } - if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) { - // Cancel this open. Invert the flatten stack size until this is closed. - flattenStackSize = ~flattenStackSize; - return this; - } - checkStack(); - List list = new ArrayList<>(); - add(list); - stack[stackSize] = list; - pathIndices[stackSize] = 0; - pushScope(EMPTY_ARRAY); - return this; - } - - @Override - public JsonWriter endArray() throws IOException { - if (peekScope() != EMPTY_ARRAY) { - throw new IllegalStateException("Nesting problem."); - } - if (stackSize == ~flattenStackSize) { - // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! - flattenStackSize = ~flattenStackSize; - return this; - } - stackSize--; - stack[stackSize] = null; - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter beginObject() throws IOException { - if (promoteValueToName) { - throw new IllegalStateException( - "Object cannot be used as a map key in JSON at path " + getPath()); - } - if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) { - // Cancel this open. Invert the flatten stack size until this is closed. - flattenStackSize = ~flattenStackSize; - return this; - } - checkStack(); - Map map = new LinkedHashTreeMap<>(); - add(map); - stack[stackSize] = map; - pushScope(EMPTY_OBJECT); - return this; - } - - @Override - public JsonWriter endObject() throws IOException { - if (peekScope() != EMPTY_OBJECT) { - throw new IllegalStateException("Nesting problem."); - } - if (deferredName != null) { - throw new IllegalStateException("Dangling name: " + deferredName); - } - if (stackSize == ~flattenStackSize) { - // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! - flattenStackSize = ~flattenStackSize; - return this; - } - promoteValueToName = false; - stackSize--; - stack[stackSize] = null; - pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter name(String name) throws IOException { - if (name == null) { - throw new NullPointerException("name == null"); - } - if (stackSize == 0) { - throw new IllegalStateException("JsonWriter is closed."); - } - if (peekScope() != EMPTY_OBJECT || deferredName != null || promoteValueToName) { - throw new IllegalStateException("Nesting problem."); - } - deferredName = name; - pathNames[stackSize - 1] = name; - return this; - } - - @Override - public JsonWriter value(@Nullable String value) throws IOException { - if (promoteValueToName) { - promoteValueToName = false; - return name(value); - } - add(value); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter nullValue() throws IOException { - if (promoteValueToName) { - throw new IllegalStateException( - "null cannot be used as a map key in JSON at path " + getPath()); - } - add(null); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter value(boolean value) throws IOException { - if (promoteValueToName) { - throw new IllegalStateException( - "Boolean cannot be used as a map key in JSON at path " + getPath()); - } - add(value); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter value(@Nullable Boolean value) throws IOException { - if (promoteValueToName) { - throw new IllegalStateException( - "Boolean cannot be used as a map key in JSON at path " + getPath()); - } - add(value); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter value(double value) throws IOException { - if (!lenient - && (Double.isNaN(value) || value == NEGATIVE_INFINITY || value == POSITIVE_INFINITY)) { - throw new IllegalArgumentException("Numeric values must be finite, but was " + value); - } - if (promoteValueToName) { - promoteValueToName = false; - return name(Double.toString(value)); - } - add(value); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter value(long value) throws IOException { - if (promoteValueToName) { - promoteValueToName = false; - return name(Long.toString(value)); - } - add(value); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public JsonWriter value(@Nullable Number value) throws IOException { - // If it's trivially converted to a long, do that. - if (value instanceof Byte - || value instanceof Short - || value instanceof Integer - || value instanceof Long) { - return value(value.longValue()); - } - - // If it's trivially converted to a double, do that. - if (value instanceof Float || value instanceof Double) { - return value(value.doubleValue()); - } - - if (value == null) { - return nullValue(); - } - - // Everything else gets converted to a BigDecimal. - BigDecimal bigDecimalValue = - value instanceof BigDecimal ? ((BigDecimal) value) : new BigDecimal(value.toString()); - if (promoteValueToName) { - promoteValueToName = false; - return name(bigDecimalValue.toString()); - } - add(bigDecimalValue); - pathIndices[stackSize - 1]++; - return this; - } - - @Override - public BufferedSink valueSink() { - if (promoteValueToName) { - throw new IllegalStateException( - "BufferedSink cannot be used as a map key in JSON at path " + getPath()); - } - if (peekScope() == STREAMING_VALUE) { - throw new IllegalStateException("Sink from valueSink() was not closed"); - } - pushScope(STREAMING_VALUE); - - final Buffer buffer = new Buffer(); - return Okio.buffer( - new ForwardingSink(buffer) { - @Override - public void close() throws IOException { - if (peekScope() != STREAMING_VALUE || stack[stackSize] != null) { - throw new AssertionError(); - } - stackSize--; // Remove STREAMING_VALUE from the stack. - - Object value = JsonReader.of(buffer).readJsonValue(); - boolean serializeNulls = JsonValueWriter.this.serializeNulls; - JsonValueWriter.this.serializeNulls = true; - try { - add(value); - } finally { - JsonValueWriter.this.serializeNulls = serializeNulls; - } - pathIndices[stackSize - 1]++; - } - }); - } - - @Override - public void close() throws IOException { - int size = stackSize; - if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) { - throw new IOException("Incomplete document"); - } - stackSize = 0; - } - - @Override - public void flush() throws IOException { - if (stackSize == 0) { - throw new IllegalStateException("JsonWriter is closed."); - } - } - - private JsonValueWriter add(@Nullable Object newTop) { - int scope = peekScope(); - - if (stackSize == 1) { - if (scope != EMPTY_DOCUMENT) { - throw new IllegalStateException("JSON must have only one top-level value."); - } - scopes[stackSize - 1] = NONEMPTY_DOCUMENT; - stack[stackSize - 1] = newTop; - - } else if (scope == EMPTY_OBJECT && deferredName != null) { - if (newTop != null || serializeNulls) { - @SuppressWarnings("unchecked") // Our maps always have string keys and object values. - Map map = (Map) stack[stackSize - 1]; - Object replaced = map.put(deferredName, newTop); - if (replaced != null) { - throw new IllegalArgumentException( - "Map key '" - + deferredName - + "' has multiple values at path " - + getPath() - + ": " - + replaced - + " and " - + newTop); - } - } - deferredName = null; - - } else if (scope == EMPTY_ARRAY) { - @SuppressWarnings("unchecked") // Our lists always have object values. - List list = (List) stack[stackSize - 1]; - list.add(newTop); - - } else if (scope == STREAMING_VALUE) { - throw new IllegalStateException("Sink from valueSink() was not closed"); - - } else { - throw new IllegalStateException("Nesting problem."); - } - - return this; - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt new file mode 100644 index 0000000..19d2af7 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2017 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 com.squareup.moshi.JsonScope.EMPTY_ARRAY +import com.squareup.moshi.JsonScope.EMPTY_DOCUMENT +import com.squareup.moshi.JsonScope.EMPTY_OBJECT +import com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT +import com.squareup.moshi.JsonScope.STREAMING_VALUE +import com.squareup.moshi.internal.knownNotNull +import okio.Buffer +import okio.BufferedSink +import okio.ForwardingSink +import okio.IOException +import okio.buffer +import java.math.BigDecimal + +/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */ +internal class JsonValueWriter : JsonWriter() { + @JvmField // TODO remove once JsonWriter is Kotlin + var stack = arrayOfNulls(32) + private var deferredName: String? = null + + init { + pushScope(EMPTY_DOCUMENT) + } + + fun root(): Any? { + val size = stackSize + check(size <= 1 && (size != 1 || scopes[0] == NONEMPTY_DOCUMENT)) { "Incomplete document" } + return stack[0] + } + + override fun beginArray(): JsonWriter { + check(!promoteValueToName) { "Array cannot be used as a map key in JSON at path $path" } + if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) { + // Cancel this open. Invert the flatten stack size until this is closed. + flattenStackSize = flattenStackSize.inv() + return this + } + checkStack() + val list = mutableListOf() + add(list) + stack[stackSize] = list + pathIndices[stackSize] = 0 + pushScope(EMPTY_ARRAY) + return this + } + + override fun endArray(): JsonWriter { + check(peekScope() == EMPTY_ARRAY) { "Nesting problem." } + if (stackSize == flattenStackSize.inv()) { + // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! + flattenStackSize = flattenStackSize.inv() + return this + } + stackSize-- + stack[stackSize] = null + pathIndices[stackSize - 1]++ + return this + } + + override fun beginObject(): JsonWriter { + check(!promoteValueToName) { "Object cannot be used as a map key in JSON at path $path" } + if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) { + // Cancel this open. Invert the flatten stack size until this is closed. + flattenStackSize = flattenStackSize.inv() + return this + } + checkStack() + val map = LinkedHashTreeMap() + add(map) + stack[stackSize] = map + pushScope(EMPTY_OBJECT) + return this + } + + override fun endObject(): JsonWriter { + check(peekScope() == EMPTY_OBJECT) { "Nesting problem." } + check(deferredName == null) { "Dangling name: $deferredName" } + if (stackSize == flattenStackSize.inv()) { + // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! + flattenStackSize = flattenStackSize.inv() + return this + } + promoteValueToName = false + stackSize-- + stack[stackSize] = null + pathNames[stackSize] = null // Free the last path name so that it can be garbage collected! + pathIndices[stackSize - 1]++ + return this + } + + override fun name(name: String): JsonWriter { + check(stackSize != 0) { "JsonWriter is closed." } + check(peekScope() == EMPTY_OBJECT && deferredName == null && !promoteValueToName) { "Nesting problem." } + deferredName = name + pathNames[stackSize - 1] = name + return this + } + + override fun value(value: String?): JsonWriter { + if (promoteValueToName) { + promoteValueToName = false + return name(value!!) + } + add(value) + pathIndices[stackSize - 1]++ + return this + } + + override fun nullValue(): JsonWriter { + check(!promoteValueToName) { "null cannot be used as a map key in JSON at path $path" } + add(null) + pathIndices[stackSize - 1]++ + return this + } + + override fun value(value: Boolean): JsonWriter { + check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" } + add(value) + pathIndices[stackSize - 1]++ + return this + } + + override fun value(value: Boolean?): JsonWriter { + check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" } + add(value) + pathIndices[stackSize - 1]++ + return this + } + + override fun value(value: Double): JsonWriter { + require(lenient || !value.isNaN() && value != Double.NEGATIVE_INFINITY && value != Double.POSITIVE_INFINITY) { + "Numeric values must be finite, but was $value" + } + if (promoteValueToName) { + promoteValueToName = false + return name(value.toString()) + } + add(value) + pathIndices[stackSize - 1]++ + return this + } + + override fun value(value: Long): JsonWriter { + if (promoteValueToName) { + promoteValueToName = false + return name(value.toString()) + } + add(value) + pathIndices[stackSize - 1]++ + return this + } + + override fun value(value: Number?): JsonWriter = apply { + when (value) { + null -> nullValue() + // If it's trivially converted to a long, do that. + is Byte, is Short, is Int, is Long -> value(value.toLong()) + // If it's trivially converted to a double, do that. + is Float, is Double -> value(value.toDouble()) + else -> { + // Everything else gets converted to a BigDecimal. + val bigDecimalValue = if (value is BigDecimal) value else BigDecimal(value.toString()) + if (promoteValueToName) { + promoteValueToName = false + return name(bigDecimalValue.toString()) + } + add(bigDecimalValue) + pathIndices[stackSize - 1]++ + } + } + } + + override fun valueSink(): BufferedSink { + check(!promoteValueToName) { "BufferedSink cannot be used as a map key in JSON at path $path" } + check(peekScope() != STREAMING_VALUE) { "Sink from valueSink() was not closed" } + pushScope(STREAMING_VALUE) + val buffer = Buffer() + return object : ForwardingSink(buffer) { + override fun close() { + if (peekScope() != STREAMING_VALUE || stack[stackSize] != null) { + throw AssertionError() + } + stackSize-- // Remove STREAMING_VALUE from the stack. + val value = JsonReader.of(buffer).readJsonValue() + val serializeNulls = serializeNulls + this@JsonValueWriter.serializeNulls = true + try { + add(value) + } finally { + this@JsonValueWriter.serializeNulls = serializeNulls + } + pathIndices[stackSize - 1]++ + } + }.buffer() + } + + override fun close() { + val size = stackSize + if (size > 1 || size == 1 && scopes[0] != NONEMPTY_DOCUMENT) { + throw IOException("Incomplete document") + } + stackSize = 0 + } + + override fun flush() { + check(stackSize != 0) { "JsonWriter is closed." } + } + + private fun add(newTop: Any?): JsonValueWriter { + val scope = peekScope() + when { + stackSize == 1 -> { + check(scope == EMPTY_DOCUMENT) { "JSON must have only one top-level value." } + scopes[stackSize - 1] = NONEMPTY_DOCUMENT + stack[stackSize - 1] = newTop + } + scope == EMPTY_OBJECT && deferredName != null -> { + if (newTop != null || serializeNulls) { + // Our maps always have string keys and object values. + @Suppress("UNCHECKED_CAST") + val map = stack[stackSize - 1] as MutableMap + // Safe to assume not null as this is single-threaded and smartcast just can't handle it + val replaced = map.put(knownNotNull(deferredName), newTop) + require(replaced == null) { + "Map key '$deferredName' has multiple values at path $path: $replaced and $newTop" + } + } + deferredName = null + } + scope == EMPTY_ARRAY -> { + // Our lists always have object values. + @Suppress("UNCHECKED_CAST") + val list = stack[stackSize - 1] as MutableList + list.add(newTop) + } + scope == STREAMING_VALUE -> throw IllegalStateException("Sink from valueSink() was not closed") + else -> throw IllegalStateException("Nesting problem.") + } + return this + } +}