diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java deleted file mode 100644 index 5f889c6..0000000 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java +++ /dev/null @@ -1,469 +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.CLOSED; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; -import okio.Buffer; -import okio.BufferedSource; - -/** - * This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON - * primitives. It does depth-first traversal keeping a stack starting with the root object. During - * traversal a stack tracks the current position in the document: - * - * - */ -final class JsonValueReader extends JsonReader { - /** Sentinel object pushed on {@link #stack} when the reader is closed. */ - private static final Object JSON_READER_CLOSED = new Object(); - - private Object[] stack; - - JsonValueReader(Object root) { - scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT; - stack = new Object[32]; - stack[stackSize++] = root; - } - - /** Copy-constructor makes a deep copy for peeking. */ - JsonValueReader(JsonValueReader copyFrom) { - super(copyFrom); - - stack = copyFrom.stack.clone(); - for (int i = 0; i < stackSize; i++) { - if (stack[i] instanceof JsonIterator) { - stack[i] = ((JsonIterator) stack[i]).clone(); - } - } - } - - @Override - public void beginArray() throws IOException { - List peeked = require(List.class, Token.BEGIN_ARRAY); - - JsonIterator iterator = - new JsonIterator(Token.END_ARRAY, peeked.toArray(new Object[peeked.size()]), 0); - stack[stackSize - 1] = iterator; - scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY; - pathIndices[stackSize - 1] = 0; - - // If the iterator isn't empty push its first value onto the stack. - if (iterator.hasNext()) { - push(iterator.next()); - } - } - - @Override - public void endArray() throws IOException { - JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY); - if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) { - throw typeMismatch(peeked, Token.END_ARRAY); - } - remove(); - } - - @Override - public void beginObject() throws IOException { - Map peeked = require(Map.class, Token.BEGIN_OBJECT); - - JsonIterator iterator = - new JsonIterator(Token.END_OBJECT, peeked.entrySet().toArray(new Object[peeked.size()]), 0); - stack[stackSize - 1] = iterator; - scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT; - - // If the iterator isn't empty push its first value onto the stack. - if (iterator.hasNext()) { - push(iterator.next()); - } - } - - @Override - public void endObject() throws IOException { - JsonIterator peeked = require(JsonIterator.class, Token.END_OBJECT); - if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) { - throw typeMismatch(peeked, Token.END_OBJECT); - } - pathNames[stackSize - 1] = null; - remove(); - } - - @Override - public boolean hasNext() throws IOException { - if (stackSize == 0) return false; - - Object peeked = stack[stackSize - 1]; - return !(peeked instanceof Iterator) || ((Iterator) peeked).hasNext(); - } - - @Override - public Token peek() throws IOException { - if (stackSize == 0) return Token.END_DOCUMENT; - - // If the top of the stack is an iterator, take its first element and push it on the stack. - Object peeked = stack[stackSize - 1]; - if (peeked instanceof JsonIterator) return ((JsonIterator) peeked).endToken; - if (peeked instanceof List) return Token.BEGIN_ARRAY; - if (peeked instanceof Map) return Token.BEGIN_OBJECT; - if (peeked instanceof Map.Entry) return Token.NAME; - if (peeked instanceof String) return Token.STRING; - if (peeked instanceof Boolean) return Token.BOOLEAN; - if (peeked instanceof Number) return Token.NUMBER; - if (peeked == null) return Token.NULL; - if (peeked == JSON_READER_CLOSED) throw new IllegalStateException("JsonReader is closed"); - - throw typeMismatch(peeked, "a JSON value"); - } - - @Override - public String nextName() throws IOException { - Map.Entry peeked = require(Map.Entry.class, Token.NAME); - - // Swap the Map.Entry for its value on the stack and return its key. - String result = stringKey(peeked); - stack[stackSize - 1] = peeked.getValue(); - pathNames[stackSize - 2] = result; - return result; - } - - @Override - public int selectName(Options options) throws IOException { - Map.Entry peeked = require(Map.Entry.class, Token.NAME); - String name = stringKey(peeked); - for (int i = 0, length = options.strings.length; i < length; i++) { - // Swap the Map.Entry for its value on the stack and return its key. - if (options.strings[i].equals(name)) { - stack[stackSize - 1] = peeked.getValue(); - pathNames[stackSize - 2] = name; - return i; - } - } - return -1; - } - - @Override - public void skipName() throws IOException { - if (failOnUnknown) { - // Capture the peeked value before nextName() since it will reset its value. - Token peeked = peek(); - nextName(); // Move the path forward onto the offending name. - throw new JsonDataException("Cannot skip unexpected " + peeked + " at " + getPath()); - } - - Map.Entry peeked = require(Map.Entry.class, Token.NAME); - - // Swap the Map.Entry for its value on the stack. - stack[stackSize - 1] = peeked.getValue(); - pathNames[stackSize - 2] = "null"; - } - - @Override - public String nextString() throws IOException { - Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null); - if (peeked instanceof String) { - remove(); - return (String) peeked; - } - if (peeked instanceof Number) { - remove(); - return peeked.toString(); - } - if (peeked == JSON_READER_CLOSED) { - throw new IllegalStateException("JsonReader is closed"); - } - throw typeMismatch(peeked, Token.STRING); - } - - @Override - public int selectString(Options options) throws IOException { - Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null); - - if (!(peeked instanceof String)) { - if (peeked == JSON_READER_CLOSED) { - throw new IllegalStateException("JsonReader is closed"); - } - return -1; - } - String peekedString = (String) peeked; - - for (int i = 0, length = options.strings.length; i < length; i++) { - if (options.strings[i].equals(peekedString)) { - remove(); - return i; - } - } - return -1; - } - - @Override - public boolean nextBoolean() throws IOException { - Boolean peeked = require(Boolean.class, Token.BOOLEAN); - remove(); - return peeked; - } - - @Override - public @Nullable T nextNull() throws IOException { - require(Void.class, Token.NULL); - remove(); - return null; - } - - @Override - public double nextDouble() throws IOException { - Object peeked = require(Object.class, Token.NUMBER); - - double result; - if (peeked instanceof Number) { - result = ((Number) peeked).doubleValue(); - } else if (peeked instanceof String) { - try { - result = Double.parseDouble((String) peeked); - } catch (NumberFormatException e) { - throw typeMismatch(peeked, Token.NUMBER); - } - } else { - throw typeMismatch(peeked, Token.NUMBER); - } - if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { - throw new JsonEncodingException( - "JSON forbids NaN and infinities: " + result + " at path " + getPath()); - } - remove(); - return result; - } - - @Override - public long nextLong() throws IOException { - Object peeked = require(Object.class, Token.NUMBER); - - long result; - if (peeked instanceof Number) { - result = ((Number) peeked).longValue(); - } else if (peeked instanceof String) { - try { - result = Long.parseLong((String) peeked); - } catch (NumberFormatException e) { - try { - BigDecimal asDecimal = new BigDecimal((String) peeked); - result = asDecimal.longValueExact(); - } catch (NumberFormatException e2) { - throw typeMismatch(peeked, Token.NUMBER); - } - } - } else { - throw typeMismatch(peeked, Token.NUMBER); - } - remove(); - return result; - } - - @Override - public int nextInt() throws IOException { - Object peeked = require(Object.class, Token.NUMBER); - - int result; - if (peeked instanceof Number) { - result = ((Number) peeked).intValue(); - } else if (peeked instanceof String) { - try { - result = Integer.parseInt((String) peeked); - } catch (NumberFormatException e) { - try { - BigDecimal asDecimal = new BigDecimal((String) peeked); - result = asDecimal.intValueExact(); - } catch (NumberFormatException e2) { - throw typeMismatch(peeked, Token.NUMBER); - } - } - } else { - throw typeMismatch(peeked, Token.NUMBER); - } - remove(); - return result; - } - - @Override - public void skipValue() throws IOException { - if (failOnUnknown) { - throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath()); - } - - // If this element is in an object clear out the key. - if (stackSize > 1) { - pathNames[stackSize - 2] = "null"; - } - - Object skipped = stackSize != 0 ? stack[stackSize - 1] : null; - - if (skipped instanceof JsonIterator) { - throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath()); - } - if (skipped instanceof Map.Entry) { - // We're skipping a name. Promote the map entry's value. - Map.Entry entry = (Map.Entry) stack[stackSize - 1]; - stack[stackSize - 1] = entry.getValue(); - } else if (stackSize > 0) { - // We're skipping a value. - remove(); - } else { - throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath()); - } - } - - @Override - public BufferedSource nextSource() throws IOException { - Object value = readJsonValue(); - Buffer result = new Buffer(); - try (JsonWriter jsonWriter = JsonWriter.of(result)) { - jsonWriter.jsonValue(value); - } - return result; - } - - @Override - public JsonReader peekJson() { - return new JsonValueReader(this); - } - - @Override - public void promoteNameToValue() throws IOException { - if (hasNext()) { - String name = nextName(); - push(name); - } - } - - @Override - public void close() throws IOException { - Arrays.fill(stack, 0, stackSize, null); - stack[0] = JSON_READER_CLOSED; - scopes[0] = CLOSED; - stackSize = 1; - } - - private void push(Object newTop) { - if (stackSize == stack.length) { - if (stackSize == 256) { - throw new JsonDataException("Nesting too deep at " + getPath()); - } - scopes = Arrays.copyOf(scopes, scopes.length * 2); - pathNames = Arrays.copyOf(pathNames, pathNames.length * 2); - pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2); - stack = Arrays.copyOf(stack, stack.length * 2); - } - stack[stackSize++] = newTop; - } - - /** - * Returns the top of the stack which is required to be a {@code type}. Throws if this reader is - * closed, or if the type isn't what was expected. - */ - private @Nullable T require(Class type, Token expected) throws IOException { - Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null); - - if (type.isInstance(peeked)) { - return type.cast(peeked); - } - if (peeked == null && expected == Token.NULL) { - return null; - } - if (peeked == JSON_READER_CLOSED) { - throw new IllegalStateException("JsonReader is closed"); - } - throw typeMismatch(peeked, expected); - } - - private String stringKey(Map.Entry entry) { - Object name = entry.getKey(); - if (name instanceof String) return (String) name; - throw typeMismatch(name, Token.NAME); - } - - /** - * Removes a value and prepares for the next. If we're iterating a map or list this advances the - * iterator. - */ - private void remove() { - stackSize--; - stack[stackSize] = null; - scopes[stackSize] = 0; - - // If we're iterating an array or an object push its next element on to the stack. - if (stackSize > 0) { - pathIndices[stackSize - 1]++; - - Object parent = stack[stackSize - 1]; - if (parent instanceof Iterator && ((Iterator) parent).hasNext()) { - push(((Iterator) parent).next()); - } - } - } - - static final class JsonIterator implements Iterator, Cloneable { - final Token endToken; - final Object[] array; - int next; - - JsonIterator(Token endToken, Object[] array, int next) { - this.endToken = endToken; - this.array = array; - this.next = next; - } - - @Override - public boolean hasNext() { - return next < array.length; - } - - @Override - public Object next() { - return array[next++]; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - - @Override - protected JsonIterator clone() { - // No need to copy the array; it's read-only. - return new JsonIterator(endToken, array, next); - } - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt new file mode 100644 index 0000000..b732b18 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.kt @@ -0,0 +1,393 @@ +/* + * 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.JsonValueReader.JsonIterator +import com.squareup.moshi.internal.knownNotNull +import okio.Buffer +import okio.BufferedSource +import java.math.BigDecimal + +/** Sentinel object pushed on [JsonValueReader.stack] when the reader is closed. */ +private val JSON_READER_CLOSED = Any() + +/** + * This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON + * primitives. It does depth-first traversal keeping a stack starting with the root object. During + * traversal a stack tracks the current position in the document: + * * The next element to act upon is on the top of the stack. + * * When the top of the stack is a [List], calling [beginArray] replaces the list with a [JsonIterator]. The first + * element of the iterator is pushed on top of the iterator. + * * Similarly, when the top of the stack is a [Map], calling [beginObject] replaces the map with an [JsonIterator] + * of its entries. The first element of the iterator is pushed on top of the iterator. + * * When the top of the stack is a [Map.Entry], calling [nextName] returns the entry's key and replaces the entry + * with its value on the stack. + * * When an element is consumed it is popped. If the new top of the stack has a non-exhausted iterator, the next + * element of that iterator is pushed. + * * If the top of the stack is an exhausted iterator, calling [endArray] or [endObject] will pop it. + */ +internal class JsonValueReader : JsonReader { + private var stack: Array + + constructor(root: Any?) { + scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT + stack = arrayOfNulls(32) + stack[stackSize++] = root + } + + /** Copy-constructor makes a deep copy for peeking. */ + constructor(copyFrom: JsonValueReader) : super(copyFrom) { + stack = copyFrom.stack.clone() + for (i in 0 until stackSize) { + val element = stack[i] + if (element is JsonIterator) { + stack[i] = element.clone() + } + } + } + + override fun beginArray() { + val peeked = require>(Token.BEGIN_ARRAY) + val iterator = JsonIterator(Token.END_ARRAY, peeked.toTypedArray(), 0) + stack[stackSize - 1] = iterator + scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY + pathIndices[stackSize - 1] = 0 + + // If the iterator isn't empty push its first value onto the stack. + if (iterator.hasNext()) { + push(iterator.next()) + } + } + + override fun endArray() { + val peeked = require(Token.END_ARRAY) + if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) { + throw typeMismatch(peeked, Token.END_ARRAY) + } + remove() + } + + override fun beginObject() { + val peeked = require>(Token.BEGIN_OBJECT) + val iterator = JsonIterator(Token.END_OBJECT, peeked.entries.toTypedArray(), 0) + stack[stackSize - 1] = iterator + scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT + + // If the iterator isn't empty push its first value onto the stack. + if (iterator.hasNext()) { + push(iterator.next()) + } + } + + override fun endObject() { + val peeked = require(Token.END_OBJECT) + if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) { + throw typeMismatch(peeked, Token.END_OBJECT) + } + pathNames[stackSize - 1] = null + remove() + } + + override fun hasNext(): Boolean { + if (stackSize == 0) return false + val peeked = stack[stackSize - 1] + return peeked !is Iterator<*> || peeked.hasNext() + } + + override fun peek(): Token { + if (stackSize == 0) return Token.END_DOCUMENT + + // If the top of the stack is an iterator, take its first element and push it on the stack. + return when (val peeked = stack[stackSize - 1]) { + is JsonIterator -> peeked.endToken + is List<*> -> Token.BEGIN_ARRAY + is Map<*, *> -> Token.BEGIN_OBJECT + is Map.Entry<*, *> -> Token.NAME + is String -> Token.STRING + is Boolean -> Token.BOOLEAN + is Number -> Token.NUMBER + null -> Token.NULL + else -> ifNotClosed(peeked) { + throw typeMismatch(peeked, "a JSON value") + } + } + } + + override fun nextName(): String { + val peeked = require>(Token.NAME) + + // Swap the Map.Entry for its value on the stack and return its key. + val result = stringKey(peeked) + stack[stackSize - 1] = peeked.value + pathNames[stackSize - 2] = result + return result + } + + override fun selectName(options: Options): Int { + val peeked = require>(Token.NAME) + val name = stringKey(peeked) + for (i in options.strings.indices) { + // Swap the Map.Entry for its value on the stack and return its key. + if (options.strings[i] == name) { + stack[stackSize - 1] = peeked.value + pathNames[stackSize - 2] = name + return i + } + } + return -1 + } + + override fun skipName() { + if (failOnUnknown) { + // Capture the peeked value before nextName() since it will reset its value. + val peeked = peek() + nextName() // Move the path forward onto the offending name. + throw JsonDataException("Cannot skip unexpected $peeked at $path") + } + val (_, value) = require>(Token.NAME) + + // Swap the Map.Entry for its value on the stack. + stack[stackSize - 1] = value + pathNames[stackSize - 2] = "null" + } + + override fun nextString(): String { + return when (val peeked = if (stackSize != 0) stack[stackSize - 1] else null) { + is String -> { + remove() + peeked + } + is Number -> { + remove() + peeked.toString() + } + else -> ifNotClosed(peeked) { + throw typeMismatch(peeked, Token.STRING) + } + } + } + + override fun selectString(options: Options): Int { + val peeked = if (stackSize != 0) stack[stackSize - 1] else null + if (peeked !is String) { + ifNotClosed(peeked) { + -1 + } + } + for (i in options.strings.indices) { + if (options.strings[i] == peeked) { + remove() + return i + } + } + return -1 + } + + override fun nextBoolean(): Boolean { + val peeked = require(Token.BOOLEAN) + remove() + return peeked + } + + override fun nextNull(): T? { + requireNull() + remove() + return null + } + + override fun nextDouble(): Double { + val result = when (val peeked = require(Token.NUMBER)) { + is Number -> peeked.toDouble() + is String -> { + try { + peeked.toDouble() + } catch (e: NumberFormatException) { + throw typeMismatch(peeked, Token.NUMBER) + } + } + else -> { + throw typeMismatch(peeked, Token.NUMBER) + } + } + if (!lenient && (result.isNaN() || result.isInfinite())) { + throw JsonEncodingException("JSON forbids NaN and infinities: $result at path $path") + } + remove() + return result + } + + override fun nextLong(): Long { + val result: Long = when (val peeked = require(Token.NUMBER)) { + is Number -> peeked.toLong() + is String -> try { + peeked.toLong() + } catch (e: NumberFormatException) { + try { + BigDecimal(peeked).longValueExact() + } catch (e2: NumberFormatException) { + throw typeMismatch(peeked, Token.NUMBER) + } + } + else -> throw typeMismatch(peeked, Token.NUMBER) + } + remove() + return result + } + + override fun nextInt(): Int { + val result = when (val peeked = require(Token.NUMBER)) { + is Number -> peeked.toInt() + is String -> try { + peeked.toInt() + } catch (e: NumberFormatException) { + try { + BigDecimal(peeked).intValueExact() + } catch (e2: NumberFormatException) { + throw typeMismatch(peeked, Token.NUMBER) + } + } + else -> throw typeMismatch(peeked, Token.NUMBER) + } + remove() + return result + } + + override fun skipValue() { + if (failOnUnknown) { + throw JsonDataException("Cannot skip unexpected ${peek()} at $path") + } + + // If this element is in an object clear out the key. + if (stackSize > 1) { + pathNames[stackSize - 2] = "null" + } + + val skipped = if (stackSize != 0) stack[stackSize - 1] else null + if (skipped is JsonIterator) { + throw JsonDataException("Expected a value but was ${peek()} at path $path") + } + if (skipped is Map.Entry<*, *>) { + // We're skipping a name. Promote the map entry's value. + val entry = stack[stackSize - 1] as Map.Entry<*, *> + stack[stackSize - 1] = entry.value + } else if (stackSize > 0) { + // We're skipping a value. + remove() + } else { + throw JsonDataException("Expected a value but was ${peek()} at path $path") + } + } + + override fun nextSource(): BufferedSource { + val value = readJsonValue() + val result = Buffer() + JsonWriter.of(result).use { jsonWriter -> jsonWriter.jsonValue(value) } + return result + } + + override fun peekJson(): JsonReader = JsonValueReader(this) + + override fun promoteNameToValue() { + if (hasNext()) { + val name = nextName() + push(name) + } + } + + override fun close() { + stack.fill(null, 0, stackSize) + stack[0] = JSON_READER_CLOSED + scopes[0] = JsonScope.CLOSED + stackSize = 1 + } + + private fun push(newTop: Any?) { + if (stackSize == stack.size) { + if (stackSize == 256) { + throw JsonDataException("Nesting too deep at $path") + } + scopes = scopes.copyOf(scopes.size * 2) + pathNames = pathNames.copyOf(pathNames.size * 2) + pathIndices = pathIndices.copyOf(pathIndices.size * 2) + stack = stack.copyOf(stack.size * 2) + } + stack[stackSize++] = newTop + } + + private inline fun require(expected: Token): T = knownNotNull(require(T::class.java, expected)) + + private fun requireNull() = require(Void::class.java, Token.NULL) + + /** + * Returns the top of the stack which is required to be a `type`. Throws if this reader is + * closed, or if the type isn't what was expected. + */ + private fun require(type: Class, expected: Token): T? { + val peeked = if (stackSize != 0) stack[stackSize - 1] else null + if (type.isInstance(peeked)) { + return type.cast(peeked) + } + if (peeked == null && expected == Token.NULL) { + return null + } + ifNotClosed(peeked) { + throw typeMismatch(peeked, expected) + } + } + + private fun stringKey(entry: Map.Entry<*, *>): String { + val name = entry.key + if (name is String) return name + throw typeMismatch(name, Token.NAME) + } + + private inline fun ifNotClosed(peeked: Any?, body: () -> T): T { + check(peeked !== JSON_READER_CLOSED) { "JsonReader is closed" } + return body() + } + + /** + * Removes a value and prepares for the next. If we're iterating a map or list this advances the + * iterator. + */ + private fun remove() { + stackSize-- + stack[stackSize] = null + scopes[stackSize] = 0 + + // If we're iterating an array or an object push its next element on to the stack. + if (stackSize > 0) { + pathIndices[stackSize - 1]++ + val parent = stack[stackSize - 1] + if (parent is Iterator<*> && parent.hasNext()) { + push(parent.next()) + } + } + } + + internal class JsonIterator( + val endToken: Token, + val array: Array, + var next: Int + ) : Iterator, Cloneable { + override fun hasNext() = next < array.size + + override fun next() = array[next++] + + // No need to copy the array; it's read-only. + public override fun clone() = JsonIterator(endToken, array, next) + } +}