From 9ac2ddf2dba716be63cfbd503215e772c52ffd65 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Wed, 12 Jan 2022 13:47:35 -0500 Subject: [PATCH] Finish up JsonWriter Kotlin conversion (#1501) Co-authored-by: Zac Sweers Co-authored-by: Goooler --- .../java/com/squareup/moshi/JsonAdapter.kt | 12 +- .../java/com/squareup/moshi/JsonUtf8Writer.kt | 20 +- .../com/squareup/moshi/JsonValueWriter.kt | 2 +- .../java/com/squareup/moshi/JsonWriter.java | 588 ------------------ .../java/com/squareup/moshi/JsonWriter.kt | 538 ++++++++++++++++ .../java/com/squareup/moshi/MapJsonAdapter.kt | 12 +- .../src/main/java/com/squareup/moshi/Moshi.kt | 2 +- 7 files changed, 563 insertions(+), 611 deletions(-) delete mode 100644 moshi/src/main/java/com/squareup/moshi/JsonWriter.java create mode 100644 moshi/src/main/java/com/squareup/moshi/JsonWriter.kt diff --git a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt index 7c099ae..243128e 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt @@ -139,12 +139,12 @@ public abstract class JsonAdapter { override fun fromJson(reader: JsonReader) = delegate.fromJson(reader) override fun toJson(writer: JsonWriter, value: T?) { - val serializeNulls = writer.getSerializeNulls() - writer.setSerializeNulls(true) + val serializeNulls = writer.serializeNulls + writer.serializeNulls = true try { delegate.toJson(writer, value) } finally { - writer.setSerializeNulls(serializeNulls) + writer.serializeNulls = serializeNulls } } @@ -262,12 +262,12 @@ public abstract class JsonAdapter { } override fun toJson(writer: JsonWriter, value: T?) { - val originalIndent = writer.getIndent() - writer.setIndent(indent) + val originalIndent = writer.indent + writer.indent = indent try { delegate.toJson(writer, value) } finally { - writer.setIndent(originalIndent) + writer.indent = originalIndent } } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt index 5173232..5ffccd4 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.kt @@ -44,15 +44,17 @@ internal class JsonUtf8Writer( private var separator = ":" private var deferredName: String? = null + override var indent: String + get() = super.indent + set(value) { + super.indent = value + separator = if (value.isNotEmpty()) ": " else ":" + } + init { pushScope(JsonScope.EMPTY_DOCUMENT) } - override fun setIndent(indent: String) { - super.setIndent(indent) - separator = if (indent.isNotEmpty()) ": " else ":" - } - override fun beginArray(): JsonWriter { check(!promoteValueToName) { "Array cannot be used as a map key in JSON at path $path" } writeDeferredName() @@ -172,7 +174,7 @@ internal class JsonUtf8Writer( } override fun value(value: Double): JsonWriter = apply { - require(lenient || !value.isNaN() && !value.isInfinite()) { + require(isLenient || !value.isNaN() && !value.isInfinite()) { "Numeric values must be finite, but was $value" } if (promoteValueToName) { @@ -201,7 +203,7 @@ internal class JsonUtf8Writer( return nullValue() } val string = value.toString() - val isFinite = lenient || string != "-Infinity" && string != "Infinity" && string != "NaN" + val isFinite = isLenient || string != "-Infinity" && string != "Infinity" && string != "NaN" require(isFinite) { "Numeric values must be finite, but was $value" } if (promoteValueToName) { promoteValueToName = false @@ -258,7 +260,7 @@ internal class JsonUtf8Writer( } private fun newline() { - if (indent == null) { + if (_indent == null) { return } sink.writeByte('\n'.code) @@ -295,7 +297,7 @@ internal class JsonUtf8Writer( val nextTop: Int when (peekScope()) { JsonScope.NONEMPTY_DOCUMENT -> { - if (!lenient) { + if (!isLenient) { throw IllegalStateException("JSON must have only one top-level value.") } nextTop = JsonScope.NONEMPTY_DOCUMENT diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt index 19d2af7..8de3921 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.kt @@ -144,7 +144,7 @@ internal class JsonValueWriter : JsonWriter() { } override fun value(value: Double): JsonWriter { - require(lenient || !value.isNaN() && value != Double.NEGATIVE_INFINITY && value != Double.POSITIVE_INFINITY) { + require(isLenient || !value.isNaN() && value != Double.NEGATIVE_INFINITY && value != Double.POSITIVE_INFINITY) { "Numeric values must be finite, but was $value" } if (promoteValueToName) { diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java deleted file mode 100644 index 49606e6..0000000 --- a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java +++ /dev/null @@ -1,588 +0,0 @@ -/* - * Copyright (C) 2010 Google 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 - * - * 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 com.squareup.moshi; - -import static com.squareup.moshi.JsonScope.EMPTY_ARRAY; -import static com.squareup.moshi.JsonScope.EMPTY_OBJECT; -import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY; -import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; - -import java.io.Closeable; -import java.io.Flushable; -import java.io.IOException; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.CheckReturnValue; -import javax.annotation.Nullable; -import okio.BufferedSink; -import okio.BufferedSource; - -/** - * Writes a JSON (RFC 7159) encoded value to a - * stream, one token at a time. The stream includes both literal values (strings, numbers, booleans - * and nulls) as well as the begin and end delimiters of objects and arrays. - * - *

Encoding JSON

- * - * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON document must contain one - * top-level array or object. Call methods on the writer as you walk the structure's contents, - * nesting arrays and objects as necessary: - * - *
    - *
  • To write arrays, first call {@link #beginArray()}. Write each of the - * array's elements with the appropriate {@link #value} methods or by nesting other arrays and - * objects. Finally close the array using {@link #endArray()}. - *
  • To write objects, first call {@link #beginObject()}. Write each of the - * object's properties by alternating calls to {@link #name} with the property's value. Write - * property values with the appropriate {@link #value} method or by nesting other objects or - * arrays. Finally close the object using {@link #endObject()}. - *
- * - *

Example

- * - * Suppose we'd like to encode a stream of messages such as the following: - * - *
{@code
- * [
- *   {
- *     "id": 912345678901,
- *     "text": "How do I stream JSON in Java?",
- *     "geo": null,
- *     "user": {
- *       "name": "json_newb",
- *       "followers_count": 41
- *      }
- *   },
- *   {
- *     "id": 912345678902,
- *     "text": "@json_newb just use JsonWriter!",
- *     "geo": [50.454722, -104.606667],
- *     "user": {
- *       "name": "jesse",
- *       "followers_count": 2
- *     }
- *   }
- * ]
- * }
- * - * This code encodes the above structure: - * - *
{@code
- * public void writeJsonStream(BufferedSink sink, List messages) throws IOException {
- *   JsonWriter writer = JsonWriter.of(sink);
- *   writer.setIndent("  ");
- *   writeMessagesArray(writer, messages);
- *   writer.close();
- * }
- *
- * public void writeMessagesArray(JsonWriter writer, List messages) throws IOException {
- *   writer.beginArray();
- *   for (Message message : messages) {
- *     writeMessage(writer, message);
- *   }
- *   writer.endArray();
- * }
- *
- * public void writeMessage(JsonWriter writer, Message message) throws IOException {
- *   writer.beginObject();
- *   writer.name("id").value(message.getId());
- *   writer.name("text").value(message.getText());
- *   if (message.getGeo() != null) {
- *     writer.name("geo");
- *     writeDoublesArray(writer, message.getGeo());
- *   } else {
- *     writer.name("geo").nullValue();
- *   }
- *   writer.name("user");
- *   writeUser(writer, message.getUser());
- *   writer.endObject();
- * }
- *
- * public void writeUser(JsonWriter writer, User user) throws IOException {
- *   writer.beginObject();
- *   writer.name("name").value(user.getName());
- *   writer.name("followers_count").value(user.getFollowersCount());
- *   writer.endObject();
- * }
- *
- * public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException {
- *   writer.beginArray();
- *   for (Double value : doubles) {
- *     writer.value(value);
- *   }
- *   writer.endArray();
- * }
- * }
- * - *

Each {@code JsonWriter} may be used to write a single JSON stream. Instances of this class are - * not thread safe. Calls that would result in a malformed JSON string will fail with an {@link - * IllegalStateException}. - */ -public abstract class JsonWriter implements Closeable, Flushable { - // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will - // grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is - // prone to trigger StackOverflowErrors. - int stackSize = 0; - int[] scopes = new int[32]; - String[] pathNames = new String[32]; - int[] pathIndices = new int[32]; - - /** - * A string containing a full set of spaces for a single level of indentation, or null for no - * pretty printing. - */ - String indent; - - boolean lenient; - boolean serializeNulls; - boolean promoteValueToName; - - /** - * Controls the deepest stack size that has begin/end pairs flattened: - * - *

    - *
  • If -1, no begin/end pairs are being suppressed. - *
  • If positive, this is the deepest stack size whose begin/end pairs are eligible to be - * flattened. - *
  • If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end - * pairs have been flattened. - *
- * - *

We differentiate between what layer would be flattened (positive) from what layer is being - * flattened (negative) so that we don't double-flatten. - * - *

To accommodate nested flattening we require callers to track the previous state when they - * provide a new state. The previous state is returned from {@link #beginFlatten} and restored - * with {@link #endFlatten}. - */ - int flattenStackSize = -1; - - private Map, Object> tags; - - /** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */ - @CheckReturnValue - public static JsonWriter of(BufferedSink sink) { - return new JsonUtf8Writer(sink); - } - - JsonWriter() { - // Package-private to control subclasses. - } - - /** Returns the scope on the top of the stack. */ - final int peekScope() { - if (stackSize == 0) { - throw new IllegalStateException("JsonWriter is closed."); - } - return scopes[stackSize - 1]; - } - - /** Before pushing a value on the stack this confirms that the stack has capacity. */ - final boolean checkStack() { - if (stackSize != scopes.length) return false; - - if (stackSize == 256) { - throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?"); - } - - scopes = Arrays.copyOf(scopes, scopes.length * 2); - pathNames = Arrays.copyOf(pathNames, pathNames.length * 2); - pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2); - if (this instanceof JsonValueWriter) { - ((JsonValueWriter) this).stack = - Arrays.copyOf(((JsonValueWriter) this).stack, ((JsonValueWriter) this).stack.length * 2); - } - - return true; - } - - final void pushScope(int newTop) { - scopes[stackSize++] = newTop; - } - - /** Replace the value on the top of the stack with the given value. */ - final void replaceTop(int topOfStack) { - scopes[stackSize - 1] = topOfStack; - } - - /** - * Sets the indentation string to be repeated for each level of indentation in the encoded - * document. If {@code indent.isEmpty()} the encoded document will be compact. Otherwise the - * encoded document will be more human-readable. - * - * @param indent a string containing only whitespace. - */ - public void setIndent(String indent) { - this.indent = !indent.isEmpty() ? indent : null; - } - - /** - * Returns a string containing only whitespace, used for each level of indentation. If empty, the - * encoded document will be compact. - */ - @CheckReturnValue - public final String getIndent() { - return indent != null ? indent : ""; - } - - /** - * Configure this writer to relax its syntax rules. By default, this writer only emits well-formed - * JSON as specified by RFC 7159. Setting the - * writer to lenient permits the following: - * - *

    - *
  • Top-level values of any type. With strict writing, the top-level value must be an object - * or an array. - *
  • Numbers may be {@linkplain Double#isNaN() NaNs} or {@linkplain Double#isInfinite() - * infinities}. - *
- */ - public final void setLenient(boolean lenient) { - this.lenient = lenient; - } - - /** Returns true if this writer has relaxed syntax rules. */ - @CheckReturnValue - public final boolean isLenient() { - return lenient; - } - - /** - * Sets whether object members are serialized when their value is null. This has no impact on - * array elements. The default is false. - */ - public final void setSerializeNulls(boolean serializeNulls) { - this.serializeNulls = serializeNulls; - } - - /** - * Returns true if object members are serialized when their value is null. This has no impact on - * array elements. The default is false. - */ - @CheckReturnValue - public final boolean getSerializeNulls() { - return serializeNulls; - } - - /** - * Begins encoding a new array. Each call to this method must be paired with a call to {@link - * #endArray}. - * - * @return this writer. - */ - public abstract JsonWriter beginArray() throws IOException; - - /** - * Ends encoding the current array. - * - * @return this writer. - */ - public abstract JsonWriter endArray() throws IOException; - - /** - * Begins encoding a new object. Each call to this method must be paired with a call to {@link - * #endObject}. - * - * @return this writer. - */ - public abstract JsonWriter beginObject() throws IOException; - - /** - * Ends encoding the current object. - * - * @return this writer. - */ - public abstract JsonWriter endObject() throws IOException; - - /** - * Encodes the property name. - * - * @param name the name of the forthcoming value. Must not be null. - * @return this writer. - */ - public abstract JsonWriter name(String name) throws IOException; - - /** - * Encodes {@code value}. - * - * @param value the literal string value, or null to encode a null literal. - * @return this writer. - */ - public abstract JsonWriter value(@Nullable String value) throws IOException; - - /** - * Encodes {@code null}. - * - * @return this writer. - */ - public abstract JsonWriter nullValue() throws IOException; - - /** - * Encodes {@code value}. - * - * @return this writer. - */ - public abstract JsonWriter value(boolean value) throws IOException; - - /** - * Encodes {@code value}. - * - * @return this writer. - */ - public abstract JsonWriter value(@Nullable Boolean value) throws IOException; - - /** - * Encodes {@code value}. - * - * @param value a finite value. May not be {@linkplain Double#isNaN() NaNs} or {@linkplain - * Double#isInfinite() infinities}. - * @return this writer. - */ - public abstract JsonWriter value(double value) throws IOException; - - /** - * Encodes {@code value}. - * - * @return this writer. - */ - public abstract JsonWriter value(long value) throws IOException; - - /** - * Encodes {@code value}. - * - * @param value a finite value. May not be {@linkplain Double#isNaN() NaNs} or {@linkplain - * Double#isInfinite() infinities}. - * @return this writer. - */ - public abstract JsonWriter value(@Nullable Number value) throws IOException; - - /** - * Writes {@code source} directly without encoding its contents. Equivalent to {@code try - * (BufferedSink sink = writer.valueSink()) { source.readAll(sink): }} - * - * @see #valueSink() - */ - public final JsonWriter value(BufferedSource source) throws IOException { - if (promoteValueToName) { - throw new IllegalStateException( - "BufferedSource cannot be used as a map key in JSON at path " + getPath()); - } - try (BufferedSink sink = valueSink()) { - source.readAll(sink); - } - return this; - } - - /** - * Returns a {@link BufferedSink} into which arbitrary data can be written without any additional - * encoding. You must call {@link BufferedSink#close()} before interacting with this {@code - * JsonWriter} instance again. - * - *

Since no validation is performed, options like {@link #setSerializeNulls} and other writer - * configurations are not respected. - */ - @CheckReturnValue - public abstract BufferedSink valueSink() throws IOException; - - /** - * Encodes the value which may be a string, number, boolean, null, map, or list. - * - * @return this writer. - * @see JsonReader#readJsonValue() - */ - public final JsonWriter jsonValue(@Nullable Object value) throws IOException { - if (value instanceof Map) { - beginObject(); - for (Map.Entry entry : ((Map) value).entrySet()) { - Object key = entry.getKey(); - if (!(key instanceof String)) { - throw new IllegalArgumentException( - key == null - ? "Map keys must be non-null" - : "Map keys must be of type String: " + key.getClass().getName()); - } - name(((String) key)); - jsonValue(entry.getValue()); - } - endObject(); - - } else if (value instanceof List) { - beginArray(); - for (Object element : ((List) value)) { - jsonValue(element); - } - endArray(); - - } else if (value instanceof String) { - value(((String) value)); - - } else if (value instanceof Boolean) { - value(((Boolean) value).booleanValue()); - - } else if (value instanceof Double) { - value(((Double) value).doubleValue()); - - } else if (value instanceof Long) { - value(((Long) value).longValue()); - - } else if (value instanceof Number) { - value(((Number) value)); - - } else if (value == null) { - nullValue(); - - } else { - throw new IllegalArgumentException("Unsupported type: " + value.getClass().getName()); - } - return this; - } - - /** - * Changes the writer to treat the next value as a string name. This is useful for map adapters so - * that arbitrary type adapters can use {@link #value} to write a name value. - * - *

In this example, calling this method allows two sequential calls to {@link #value(String)} - * to produce the object, {@code {"a": "b"}}. - * - *

{@code
-   * JsonWriter writer = JsonWriter.of(...);
-   * writer.beginObject();
-   * writer.promoteValueToName();
-   * writer.value("a");
-   * writer.value("b");
-   * writer.endObject();
-   * }
- */ - public final void promoteValueToName() throws IOException { - int context = peekScope(); - if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { - throw new IllegalStateException("Nesting problem."); - } - promoteValueToName = true; - } - - /** - * Cancels immediately-nested calls to {@link #beginArray()} or {@link #beginObject()} and their - * matching calls to {@link #endArray} or {@link #endObject()}. Use this to compose JSON adapters - * without nesting. - * - *

For example, the following creates JSON with nested arrays: {@code [1,[2,3,4],5]}. - * - *

{@code
-   * JsonAdapter> integersAdapter = ...
-   *
-   * public void writeNumbers(JsonWriter writer) {
-   *   writer.beginArray();
-   *   writer.value(1);
-   *   integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
-   *   writer.value(5);
-   *   writer.endArray();
-   * }
-   * }
- * - *

With flattening we can create JSON with a single array {@code [1,2,3,4,5]}: - * - *

{@code
-   * JsonAdapter> integersAdapter = ...
-   *
-   * public void writeNumbers(JsonWriter writer) {
-   *   writer.beginArray();
-   *   int token = writer.beginFlatten();
-   *   writer.value(1);
-   *   integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
-   *   writer.value(5);
-   *   writer.endFlatten(token);
-   *   writer.endArray();
-   * }
-   * }
- * - *

This method flattens arrays within arrays: - * - *

{@code
-   * Emit:       [1, [2, 3, 4], 5]
-   * To produce: [1, 2, 3, 4, 5]
-   * }
- * - * It also flattens objects within objects. Do not call {@link #name} before writing a flattened - * object. - * - *
{@code
-   * Emit:       {"a": 1, {"b": 2}, "c": 3}
-   * To Produce: {"a": 1, "b": 2, "c": 3}
-   * }
- * - * Other combinations are permitted but do not perform flattening. For example, objects inside of - * arrays are not flattened: - * - *
{@code
-   * Emit:       [1, {"b": 2}, 3, [4, 5], 6]
-   * To Produce: [1, {"b": 2}, 3, 4, 5, 6]
-   * }
- * - *

This method returns an opaque token. Callers must match all calls to this method with a call - * to {@link #endFlatten} with the matching token. - */ - @CheckReturnValue - public final int beginFlatten() { - int context = peekScope(); - if (context != NONEMPTY_OBJECT - && context != EMPTY_OBJECT - && context != NONEMPTY_ARRAY - && context != EMPTY_ARRAY) { - throw new IllegalStateException("Nesting problem."); - } - int token = flattenStackSize; - flattenStackSize = stackSize; - return token; - } - - /** Ends nested call flattening created by {@link #beginFlatten}. */ - public final void endFlatten(int token) { - flattenStackSize = token; - } - - /** - * Returns a JsonPath to the current location - * in the JSON value. - */ - @CheckReturnValue - public final String getPath() { - return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices); - } - - /** Returns the tag value for the given class key. */ - @SuppressWarnings("unchecked") - @CheckReturnValue - public final @Nullable T tag(Class clazz) { - if (tags == null) { - return null; - } - return (T) tags.get(clazz); - } - - /** Assigns the tag value using the given class key and value. */ - public final void setTag(Class clazz, T value) { - if (!clazz.isAssignableFrom(value.getClass())) { - throw new IllegalArgumentException("Tag value must be of type " + clazz.getName()); - } - if (tags == null) { - tags = new LinkedHashMap<>(); - } - tags.put(clazz, value); - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt b/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt new file mode 100644 index 0000000..a94050e --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/JsonWriter.kt @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2010 Google 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 + * + * 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 com.squareup.moshi + +import com.squareup.moshi.JsonScope.EMPTY_ARRAY +import com.squareup.moshi.JsonScope.EMPTY_OBJECT +import com.squareup.moshi.JsonScope.NONEMPTY_ARRAY +import com.squareup.moshi.JsonScope.NONEMPTY_OBJECT +import okio.BufferedSink +import okio.BufferedSource +import okio.Closeable +import okio.IOException +import java.io.Flushable +import javax.annotation.CheckReturnValue +import kotlin.Throws + +/** + * Writes a JSON ([RFC 7159](http://www.ietf.org/rfc/rfc7159.txt)) encoded value to a + * stream, one token at a time. The stream includes both literal values (strings, numbers, booleans + * and nulls) as well as the begin and end delimiters of objects and arrays. + * + * ## Encoding JSON + * + * To encode your data as JSON, create a new `JsonWriter`. Each JSON document must contain one + * top-level array or object. Call methods on the writer as you walk the structure's contents, + * nesting arrays and objects as necessary: + * + * * To write **arrays**, first call [beginArray]. Write each of the + * array's elements with the appropriate [value] methods or by nesting other arrays and + * objects. Finally close the array using [endArray]. + * * To write **objects**, first call [beginObject]. Write each of the + * object's properties by alternating calls to [name] with the property's value. Write + * property values with the appropriate [value] method or by nesting other objects or + * arrays. Finally close the object using [endObject]. + * + * ## Example + * + * Suppose we'd like to encode a stream of messages such as the following: + * + * ```json + * [ + * { + * "id": 912345678901, + * "text": "How do I stream JSON in Java?", + * "geo": null, + * "user": { + * "name": "json_newb", + * "followers_count": 41 + * } + * }, + * { + * "id": 912345678902, + * "text": "@json_newb just use JsonWriter!", + * "geo": [ + * 50.454722, + * -104.606667 + * ], + * "user": { + * "name": "jesse", + * "followers_count": 2 + * } + * } + * ] + * ``` + * + * This code encodes the above structure: + * + * ```java + * public void writeJsonStream(BufferedSink sink, List messages) throws IOException { + * JsonWriter writer = JsonWriter.of(sink); + * writer.setIndent(" "); + * writeMessagesArray(writer, messages); + * writer.close(); + * } + * + * public void writeMessagesArray(JsonWriter writer, List messages) throws IOException { + * writer.beginArray(); + * for (Message message : messages) { + * writeMessage(writer, message); + * } + * writer.endArray(); + * } + * + * public void writeMessage(JsonWriter writer, Message message) throws IOException { + * writer.beginObject(); + * writer.name("id").value(message.getId()); + * writer.name("text").value(message.getText()); + * if (message.getGeo() != null) { + * writer.name("geo"); + * writeDoublesArray(writer, message.getGeo()); + * } else { + * writer.name("geo").nullValue(); + * } + * writer.name("user"); + * writeUser(writer, message.getUser()); + * writer.endObject(); + * } + * + * public void writeUser(JsonWriter writer, User user) throws IOException { + * writer.beginObject(); + * writer.name("name").value(user.getName()); + * writer.name("followers_count").value(user.getFollowersCount()); + * writer.endObject(); + * } + * + * public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException { + * writer.beginArray(); + * for (Double value : doubles) { + * writer.value(value); + * } + * writer.endArray(); + * } + * ``` + * + * Each `JsonWriter` may be used to write a single JSON stream. Instances of this class are + * not thread safe. Calls that would result in a malformed JSON string will fail with an [IllegalStateException]. + */ +public sealed class JsonWriter : Closeable, Flushable { + /** + * The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will + * grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is + * prone to trigger StackOverflowErrors. + */ + @JvmField + protected var stackSize: Int = 0 + + @JvmField + protected var scopes: IntArray = IntArray(32) + + @JvmField + protected var pathNames: Array = arrayOfNulls(32) + + @JvmField + protected var pathIndices: IntArray = IntArray(32) + + /** + * A string containing a full set of spaces for a single level of indentation, or null for no + * pretty printing. + */ + @JvmField + protected var _indent: String? = null + public open var indent: String + /** + * Returns a string containing only whitespace, used for each level of indentation. If empty, + * the encoded document will be compact. + */ + get() = _indent.orEmpty() + /** + * Sets the indentation string to be repeated for each level of indentation in the encoded + * document. If `indent.isEmpty()` the encoded document will be compact. Otherwise, the + * encoded document will be more human-readable. + * + * @param value a string containing only whitespace. + */ + set(value) { + _indent = value.ifEmpty { null } + } + + /** + * Configure this writer to relax its syntax rules. By default, this writer only emits well-formed + * JSON as specified by [RFC 7159](http://www.ietf.org/rfc/rfc7159.txt). Setting the + * writer to lenient permits the following: + * + * - Top-level values of any type. With strict writing, the top-level value must be an object + * or an array. + * - Numbers may be [NaNs][Double.isNaN] or [infinities][Double.isInfinite]. + * + * Returns true if this writer has relaxed syntax rules. + */ + @get:CheckReturnValue + public var isLenient: Boolean = false + + /** + * Sets whether object members are serialized when their value is null. This has no impact on + * array elements. The default is false. + * + * Returns true if object members are serialized when their value is null. This has no impact on + * array elements. The default is false. + */ + @get:CheckReturnValue + public var serializeNulls: Boolean = false + + @JvmField + protected var promoteValueToName: Boolean = false + + /** + * Controls the deepest stack size that has begin/end pairs flattened: + * + * - If -1, no begin/end pairs are being suppressed. + * - If positive, this is the deepest stack size whose begin/end pairs are eligible to be flattened. + * - If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end pairs have been flattened. + * + * We differentiate between what layer would be flattened (positive) from what layer is being + * flattened (negative) so that we don't double-flatten. + * + * To accommodate nested flattening we require callers to track the previous state when they + * provide a new state. The previous state is returned from [beginFlatten] and restored + * with [endFlatten]. + */ + @JvmField + protected var flattenStackSize: Int = -1 + + private var tags: MutableMap, Any>? = null + + /** + * Returns a [JsonPath](http://goessner.net/articles/JsonPath/) to the current location + * in the JSON value. + */ + @get:CheckReturnValue + public val path: String + get() = JsonScope.getPath(stackSize, scopes, pathNames, pathIndices) + + /** Returns the scope on the top of the stack. */ + protected fun peekScope(): Int { + check(stackSize != 0) { "JsonWriter is closed." } + return scopes[stackSize - 1] + } + + /** Before pushing a value on the stack this confirms that the stack has capacity. */ + protected fun checkStack(): Boolean { + if (stackSize != scopes.size) return false + if (stackSize == 256) { + throw JsonDataException("Nesting too deep at $path: circular reference?") + } + scopes = scopes.copyOf(scopes.size * 2) + pathNames = pathNames.copyOf(pathNames.size * 2) + pathIndices = pathIndices.copyOf(pathIndices.size * 2) + if (this is JsonValueWriter) { + stack = stack.copyOf(stack.size * 2) + } + return true + } + + protected fun pushScope(newTop: Int) { + scopes[stackSize++] = newTop + } + + /** Replace the value on the top of the stack with the given value. */ + protected fun replaceTop(topOfStack: Int) { + scopes[stackSize - 1] = topOfStack + } + + /** + * Begins encoding a new array. Each call to this method must be paired with a call to [endArray]. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun beginArray(): JsonWriter + + /** + * Ends encoding the current array. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun endArray(): JsonWriter + + /** + * Begins encoding a new object. Each call to this method must be paired with a call to [endObject]. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun beginObject(): JsonWriter + + /** + * Ends encoding the current object. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun endObject(): JsonWriter + + /** + * Encodes the property name. + * + * @param name the name of the forthcoming value. Must not be null. + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun name(name: String): JsonWriter + + /** + * Encodes `value`. + * + * @param value the literal string value, or null to encode a null literal. + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun value(value: String?): JsonWriter + + /** + * Encodes `null`. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun nullValue(): JsonWriter + + /** + * Encodes `value`. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun value(value: Boolean): JsonWriter + + /** + * Encodes `value`. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun value(value: Boolean?): JsonWriter + + /** + * Encodes `value`. + * + * @param value a finite value. May not be [NaNs][Double.isNaN] or [infinities][Double.isInfinite]. + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun value(value: Double): JsonWriter + + /** + * Encodes `value`. + * + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun value(value: Long): JsonWriter + + /** + * Encodes `value`. + * + * @param value a finite value. May not be [NaNs][Double.isNaN] or [infinities][Double.isInfinite]. + * @return this writer. + */ + @Throws(IOException::class) + public abstract fun value(value: Number?): JsonWriter + + /** + * Writes `source` directly without encoding its contents. Equivalent to + * ```java + * try (BufferedSink sink = writer.valueSink()) { + * source.readAll(sink): + * } + * ``` + * + * @see valueSink + */ + @Throws(IOException::class) + public fun value(source: BufferedSource): JsonWriter { + check(!promoteValueToName) { "BufferedSource cannot be used as a map key in JSON at path $path" } + valueSink().use(source::readAll) + return this + } + + /** + * Returns a [BufferedSink] into which arbitrary data can be written without any additional + * encoding. You **must** call [BufferedSink.close] before interacting with this `JsonWriter` instance again. + * + * Since no validation is performed, options like [serializeNulls] and other writer + * configurations are not respected. + */ + @CheckReturnValue + @Throws(IOException::class) + public abstract fun valueSink(): BufferedSink + + /** + * Encodes the value which may be a string, number, boolean, null, map, or list. + * + * @return this writer. + * @see JsonReader.readJsonValue + */ + @Throws(IOException::class) + public fun jsonValue(value: Any?): JsonWriter { + when (value) { + is Map<*, *> -> { + beginObject() + for ((k, v) in value) { + requireNotNull(k) { "Map keys must be non-null" } + require(k is String) { "Map keys must be of type String: ${k.javaClass.name}" } + name(k) + jsonValue(v) + } + endObject() + } + is List<*> -> { + beginArray() + for (element in value) { + jsonValue(element) + } + endArray() + } + is String -> value(value as String?) + is Boolean -> value(value) + is Double -> value(value) + is Long -> value(value) + is Number -> value(value) + null -> nullValue() + else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}") + } + return this + } + + /** + * Changes the writer to treat the next value as a string name. This is useful for map adapters so + * that arbitrary type adapters can use [value] to write a name value. + * + * In this example, calling this method allows two sequential calls to [value] + * to produce the object, `{"a": "b"}`. + * + * ```java + * JsonWriter writer = JsonWriter.of(...); + * writer.beginObject(); + * writer.promoteValueToName(); + * writer.value("a"); + * writer.value("b"); + * writer.endObject(); + * ``` + */ + @Throws(IOException::class) + public fun promoteValueToName() { + val context = peekScope() + check(context == NONEMPTY_OBJECT || context == EMPTY_OBJECT) { + "Nesting problem." + } + promoteValueToName = true + } + + /** + * Cancels immediately-nested calls to [beginArray] or [beginObject] and their + * matching calls to [endArray] or [endObject]. Use this to compose JSON adapters + * without nesting. + * + * For example, the following creates JSON with nested arrays: `[1,[2,3,4],5]`. + * + * ```java + * JsonAdapter> integersAdapter = ... + * public void writeNumbers(JsonWriter writer) { + * writer.beginArray(); + * writer.value(1); + * integersAdapter.toJson(writer, Arrays.asList(2, 3, 4)); + * writer.value(5); + * writer.endArray(); + * } + * ``` + * + * With flattening we can create JSON with a single array `[1,2,3,4,5]`: + * + * ```java + * JsonAdapter> integersAdapter = ... + * + * public void writeNumbers(JsonWriter writer) { + * writer.beginArray(); + * int token = writer.beginFlatten(); + * writer.value(1); + * integersAdapter.toJson(writer, Arrays.asList(2, 3, 4)); + * writer.value(5); + * writer.endFlatten(token); + * writer.endArray(); + * } + * ``` + * + * This method flattens arrays within arrays: + * + * Emit: `[1, [2, 3, 4], 5]` + * To produce: `[1, 2, 3, 4, 5]` + * + * It also flattens objects within objects. Do not call [name] before writing a flattened + * object. + * + * Emit: `{"a": 1, {"b": 2}, "c": 3}` + * To Produce: `{"a": 1, "b": 2, "c": 3}` + * + * Other combinations are permitted but do not perform flattening. For example, objects inside of + * arrays are not flattened: + * + * Emit: ` [1, {"b": 2}, 3, [4, 5], 6]` + * To Produce: `[1, {"b": 2}, 3, 4, 5, 6]` + * + * This method returns an opaque token. Callers must match all calls to this method with a call + * to [endFlatten] with the matching token. + */ + @CheckReturnValue + public fun beginFlatten(): Int { + val context = peekScope() + check(context == NONEMPTY_OBJECT || context == EMPTY_OBJECT || context == NONEMPTY_ARRAY || context == EMPTY_ARRAY) { + "Nesting problem." + } + val token = flattenStackSize + flattenStackSize = stackSize + return token + } + + /** Ends nested call flattening created by [beginFlatten]. */ + public fun endFlatten(token: Int) { + flattenStackSize = token + } + + /** Returns the tag value for the given class key. */ + @CheckReturnValue + public fun tag(clazz: Class): T? { + @Suppress("UNCHECKED_CAST") + return tags?.get(clazz) as T? + } + + /** Assigns the tag value using the given class key and value. */ + public fun setTag(clazz: Class, value: T) { + require(clazz.isAssignableFrom(value::class.java)) { + "Tag value must be of type ${clazz.name}" + } + val localTags = tags ?: LinkedHashMap, Any>().also { tags = it } + localTags[clazz] = value + } + + public companion object { + /** Returns a new instance that writes UTF-8 encoded JSON to `sink`. */ + @JvmStatic + @CheckReturnValue + public fun of(sink: BufferedSink): JsonWriter = JsonUtf8Writer(sink) + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt index 432f70f..4c03f93 100644 --- a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt +++ b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.kt @@ -27,16 +27,16 @@ internal class MapJsonAdapter(moshi: Moshi, keyType: Type, valueType: Type private val keyAdapter: JsonAdapter = moshi.adapter(keyType) private val valueAdapter: JsonAdapter = moshi.adapter(valueType) - override fun toJson(writer: JsonWriter, map: Map?) { + override fun toJson(writer: JsonWriter, value: Map?) { writer.beginObject() // Never null because we wrap in nullSafe() - for ((key, value) in knownNotNull(map)) { - if (key == null) { - throw JsonDataException("Map key is null at " + writer.path) + for ((k, v) in knownNotNull(value)) { + if (k == null) { + throw JsonDataException("Map key is null at ${writer.path}") } writer.promoteValueToName() - keyAdapter.toJson(writer, key) - valueAdapter.toJson(writer, value) + keyAdapter.toJson(writer, k) + valueAdapter.toJson(writer, v) } writer.endObject() } diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.kt b/moshi/src/main/java/com/squareup/moshi/Moshi.kt index 1efc9c8..1fff301 100644 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.kt +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.kt @@ -63,7 +63,7 @@ public class Moshi internal constructor(builder: Builder) { } val annotations = buildSet(annotationTypes.size) { for (annotationType in annotationTypes) { - add(createJsonQualifierImplementation(annotationType)!!) + add(createJsonQualifierImplementation(annotationType)) } } return adapter(type, annotations)