From f3d2103ffbee189d428b92eb96d2e1b8963f51f6 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Sun, 26 Dec 2021 15:49:52 -0600 Subject: [PATCH] Convert Moshi.java to Kotlin (#1462) * Rename .java to .kt * Migrate Moshi.java to Moshi.kt * Idiomatic cleanups * Move comments down * Apply suggestions from code review Co-authored-by: Egor Andreevich * Small little cleanups Co-authored-by: Egor Andreevich --- .../moshi/kotlin/reflect/KotlinJsonAdapter.kt | 2 +- .../main/java/com/squareup/moshi/Moshi.java | 423 ------------------ .../src/main/java/com/squareup/moshi/Moshi.kt | 365 +++++++++++++++ .../java/com/squareup/moshi/MoshiTest.java | 39 +- 4 files changed, 386 insertions(+), 443 deletions(-) delete mode 100644 moshi/src/main/java/com/squareup/moshi/Moshi.java create mode 100644 moshi/src/main/java/com/squareup/moshi/Moshi.kt diff --git a/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt b/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt index 9d98142..0da0d91 100644 --- a/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt +++ b/moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt @@ -289,7 +289,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory { else -> error("Not possible!") } val resolvedPropertyType = resolve(type, rawType, propertyType) - val adapter = moshi.adapter( + val adapter = moshi.adapter( resolvedPropertyType, Util.jsonAnnotations(allAnnotations.toTypedArray()), property.name diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.java b/moshi/src/main/java/com/squareup/moshi/Moshi.java deleted file mode 100644 index 312e520..0000000 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.java +++ /dev/null @@ -1,423 +0,0 @@ -/* - * Copyright (C) 2014 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.internal.Util.canonicalize; -import static com.squareup.moshi.internal.Util.removeSubtypeWildcard; -import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations; - -import com.squareup.moshi.internal.Util; -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.CheckReturnValue; -import javax.annotation.Nullable; - -/** - * Coordinates binding between JSON values and Java objects. - * - *

Moshi instances are thread-safe, meaning multiple threads can safely use a single instance - * concurrently. - */ -public final class Moshi { - static final List BUILT_IN_FACTORIES = new ArrayList<>(5); - - static { - BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY); - BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY); - BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY); - BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY); - BUILT_IN_FACTORIES.add(RecordJsonAdapter.FACTORY); - BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY); - } - - private final List factories; - private final int lastOffset; - private final ThreadLocal lookupChainThreadLocal = new ThreadLocal<>(); - private final Map> adapterCache = new LinkedHashMap<>(); - - Moshi(Builder builder) { - List factories = - new ArrayList<>(builder.factories.size() + BUILT_IN_FACTORIES.size()); - factories.addAll(builder.factories); - factories.addAll(BUILT_IN_FACTORIES); - this.factories = Collections.unmodifiableList(factories); - this.lastOffset = builder.lastOffset; - } - - /** Returns a JSON adapter for {@code type}, creating it if necessary. */ - @CheckReturnValue - public JsonAdapter adapter(Type type) { - return adapter(type, Util.NO_ANNOTATIONS); - } - - @CheckReturnValue - public JsonAdapter adapter(Class type) { - return adapter(type, Util.NO_ANNOTATIONS); - } - - @CheckReturnValue - public JsonAdapter adapter(Type type, Class annotationType) { - if (annotationType == null) { - throw new NullPointerException("annotationType == null"); - } - return adapter( - type, Collections.singleton(Types.createJsonQualifierImplementation(annotationType))); - } - - @CheckReturnValue - public JsonAdapter adapter(Type type, Class... annotationTypes) { - if (annotationTypes.length == 1) { - return adapter(type, annotationTypes[0]); - } - Set annotations = new LinkedHashSet<>(annotationTypes.length); - for (Class annotationType : annotationTypes) { - annotations.add(Types.createJsonQualifierImplementation(annotationType)); - } - return adapter(type, Collections.unmodifiableSet(annotations)); - } - - @CheckReturnValue - public JsonAdapter adapter(Type type, Set annotations) { - return adapter(type, annotations, null); - } - - /** - * @param fieldName An optional field name associated with this type. The field name is used as a - * hint for better adapter lookup error messages for nested structures. - */ - @CheckReturnValue - @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. - public JsonAdapter adapter( - Type type, Set annotations, @Nullable String fieldName) { - if (type == null) { - throw new NullPointerException("type == null"); - } - if (annotations == null) { - throw new NullPointerException("annotations == null"); - } - - type = removeSubtypeWildcard(canonicalize(type)); - - // If there's an equivalent adapter in the cache, we're done! - Object cacheKey = cacheKey(type, annotations); - synchronized (adapterCache) { - JsonAdapter result = adapterCache.get(cacheKey); - if (result != null) return (JsonAdapter) result; - } - - LookupChain lookupChain = lookupChainThreadLocal.get(); - if (lookupChain == null) { - lookupChain = new LookupChain(); - lookupChainThreadLocal.set(lookupChain); - } - - boolean success = false; - JsonAdapter adapterFromCall = lookupChain.push(type, fieldName, cacheKey); - try { - if (adapterFromCall != null) return adapterFromCall; - - // Ask each factory to create the JSON adapter. - for (int i = 0, size = factories.size(); i < size; i++) { - JsonAdapter result = (JsonAdapter) factories.get(i).create(type, annotations, this); - if (result == null) continue; - - // Success! Notify the LookupChain so it is cached and can be used by re-entrant calls. - lookupChain.adapterFound(result); - success = true; - return result; - } - - throw new IllegalArgumentException( - "No JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations)); - } catch (IllegalArgumentException e) { - throw lookupChain.exceptionWithLookupStack(e); - } finally { - lookupChain.pop(success); - } - } - - @CheckReturnValue - @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. - public JsonAdapter nextAdapter( - JsonAdapter.Factory skipPast, Type type, Set annotations) { - if (annotations == null) throw new NullPointerException("annotations == null"); - - type = removeSubtypeWildcard(canonicalize(type)); - - int skipPastIndex = factories.indexOf(skipPast); - if (skipPastIndex == -1) { - throw new IllegalArgumentException("Unable to skip past unknown factory " + skipPast); - } - for (int i = skipPastIndex + 1, size = factories.size(); i < size; i++) { - JsonAdapter result = (JsonAdapter) factories.get(i).create(type, annotations, this); - if (result != null) return result; - } - throw new IllegalArgumentException( - "No next JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations)); - } - - /** Returns a new builder containing all custom factories used by the current instance. */ - @CheckReturnValue - public Moshi.Builder newBuilder() { - Builder result = new Builder(); - for (int i = 0, limit = lastOffset; i < limit; i++) { - result.add(factories.get(i)); - } - for (int i = lastOffset, limit = factories.size() - BUILT_IN_FACTORIES.size(); i < limit; i++) { - result.addLast(factories.get(i)); - } - return result; - } - - /** Returns an opaque object that's equal if the type and annotations are equal. */ - private Object cacheKey(Type type, Set annotations) { - if (annotations.isEmpty()) return type; - return Arrays.asList(type, annotations); - } - - public static final class Builder { - final List factories = new ArrayList<>(); - int lastOffset = 0; - - public Builder add(Type type, JsonAdapter jsonAdapter) { - return add(newAdapterFactory(type, jsonAdapter)); - } - - public Builder add( - Type type, Class annotation, JsonAdapter jsonAdapter) { - return add(newAdapterFactory(type, annotation, jsonAdapter)); - } - - public Builder add(JsonAdapter.Factory factory) { - if (factory == null) throw new IllegalArgumentException("factory == null"); - factories.add(lastOffset++, factory); - return this; - } - - public Builder add(Object adapter) { - if (adapter == null) throw new IllegalArgumentException("adapter == null"); - return add(AdapterMethodsFactory.get(adapter)); - } - - public Builder addLast(Type type, JsonAdapter jsonAdapter) { - return addLast(newAdapterFactory(type, jsonAdapter)); - } - - public Builder addLast( - Type type, Class annotation, JsonAdapter jsonAdapter) { - return addLast(newAdapterFactory(type, annotation, jsonAdapter)); - } - - public Builder addLast(JsonAdapter.Factory factory) { - if (factory == null) throw new IllegalArgumentException("factory == null"); - factories.add(factory); - return this; - } - - public Builder addLast(Object adapter) { - if (adapter == null) throw new IllegalArgumentException("adapter == null"); - return addLast(AdapterMethodsFactory.get(adapter)); - } - - @CheckReturnValue - public Moshi build() { - return new Moshi(this); - } - } - - static JsonAdapter.Factory newAdapterFactory( - final Type type, final JsonAdapter jsonAdapter) { - if (type == null) throw new IllegalArgumentException("type == null"); - if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null"); - - return new JsonAdapter.Factory() { - @Override - public @Nullable JsonAdapter create( - Type targetType, Set annotations, Moshi moshi) { - return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null; - } - }; - } - - static JsonAdapter.Factory newAdapterFactory( - final Type type, - final Class annotation, - final JsonAdapter jsonAdapter) { - if (type == null) throw new IllegalArgumentException("type == null"); - if (annotation == null) throw new IllegalArgumentException("annotation == null"); - if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null"); - if (!annotation.isAnnotationPresent(JsonQualifier.class)) { - throw new IllegalArgumentException(annotation + " does not have @JsonQualifier"); - } - if (annotation.getDeclaredMethods().length > 0) { - throw new IllegalArgumentException("Use JsonAdapter.Factory for annotations with elements"); - } - - return new JsonAdapter.Factory() { - @Override - public @Nullable JsonAdapter create( - Type targetType, Set annotations, Moshi moshi) { - if (Util.typesMatch(type, targetType) - && annotations.size() == 1 - && Util.isAnnotationPresent(annotations, annotation)) { - return jsonAdapter; - } - return null; - } - }; - } - - /** - * A possibly-reentrant chain of lookups for JSON adapters. - * - *

We keep track of the current stack of lookups: we may start by looking up the JSON adapter - * for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking - * up the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace - * with all of the lookups. - * - *

Sometimes a JSON adapter factory depends on its own product; either directly or indirectly. - * To make this work, we offer a JSON adapter stub while the final adapter is being computed. When - * it is ready, we wire the stub to that finished adapter. This is necessary in self-referential - * object models, such as an {@code Employee} class that has a {@code List} field for an - * organization's management hierarchy. - * - *

This class defers putting any JSON adapters in the cache until the topmost JSON adapter has - * successfully been computed. That way we don't pollute the cache with incomplete stubs, or - * adapters that may transitively depend on incomplete stubs. - */ - final class LookupChain { - final List> callLookups = new ArrayList<>(); - final Deque> stack = new ArrayDeque<>(); - boolean exceptionAnnotated; - - /** - * Returns a JSON adapter that was already created for this call, or null if this is the first - * time in this call that the cache key has been requested in this call. This may return a - * lookup that isn't yet ready if this lookup is reentrant. - */ - JsonAdapter push(Type type, @Nullable String fieldName, Object cacheKey) { - // Try to find a lookup with the same key for the same call. - for (int i = 0, size = callLookups.size(); i < size; i++) { - Lookup lookup = callLookups.get(i); - if (lookup.cacheKey.equals(cacheKey)) { - Lookup hit = (Lookup) lookup; - stack.add(hit); - return hit.adapter != null ? hit.adapter : hit; - } - } - - // We might need to know about this cache key later in this call. Prepare for that. - Lookup lookup = new Lookup<>(type, fieldName, cacheKey); - callLookups.add(lookup); - stack.add(lookup); - return null; - } - - /** Sets the adapter result of the current lookup. */ - void adapterFound(JsonAdapter result) { - Lookup currentLookup = (Lookup) stack.getLast(); - currentLookup.adapter = result; - } - - /** - * Completes the current lookup by removing a stack frame. - * - * @param success true if the adapter cache should be populated if this is the topmost lookup. - */ - void pop(boolean success) { - stack.removeLast(); - if (!stack.isEmpty()) return; - - lookupChainThreadLocal.remove(); - - if (success) { - synchronized (adapterCache) { - for (int i = 0, size = callLookups.size(); i < size; i++) { - Lookup lookup = callLookups.get(i); - JsonAdapter replaced = adapterCache.put(lookup.cacheKey, lookup.adapter); - if (replaced != null) { - ((Lookup) lookup).adapter = (JsonAdapter) replaced; - adapterCache.put(lookup.cacheKey, replaced); - } - } - } - } - } - - IllegalArgumentException exceptionWithLookupStack(IllegalArgumentException e) { - // Don't add the lookup stack to more than one exception; the deepest is sufficient. - if (exceptionAnnotated) return e; - exceptionAnnotated = true; - - int size = stack.size(); - if (size == 1 && stack.getFirst().fieldName == null) return e; - - StringBuilder errorMessageBuilder = new StringBuilder(e.getMessage()); - for (Iterator> i = stack.descendingIterator(); i.hasNext(); ) { - Lookup lookup = i.next(); - errorMessageBuilder.append("\nfor ").append(lookup.type); - if (lookup.fieldName != null) { - errorMessageBuilder.append(' ').append(lookup.fieldName); - } - } - - return new IllegalArgumentException(errorMessageBuilder.toString(), e); - } - } - - /** This class implements {@code JsonAdapter} so it can be used as a stub for re-entrant calls. */ - static final class Lookup extends JsonAdapter { - final Type type; - final @Nullable String fieldName; - final Object cacheKey; - @Nullable JsonAdapter adapter; - - Lookup(Type type, @Nullable String fieldName, Object cacheKey) { - this.type = type; - this.fieldName = fieldName; - this.cacheKey = cacheKey; - } - - @Override - public T fromJson(JsonReader reader) throws IOException { - if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready"); - return adapter.fromJson(reader); - } - - @Override - public void toJson(JsonWriter writer, T value) throws IOException { - if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready"); - adapter.toJson(writer, value); - } - - @Override - public String toString() { - return adapter != null ? adapter.toString() : super.toString(); - } - } -} diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.kt b/moshi/src/main/java/com/squareup/moshi/Moshi.kt new file mode 100644 index 0000000..72b414e --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.kt @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2014 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.Types.createJsonQualifierImplementation +import com.squareup.moshi.internal.Util +import com.squareup.moshi.internal.Util.canonicalize +import com.squareup.moshi.internal.Util.isAnnotationPresent +import com.squareup.moshi.internal.Util.removeSubtypeWildcard +import com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations +import com.squareup.moshi.internal.Util.typesMatch +import java.lang.reflect.Type +import javax.annotation.CheckReturnValue + +/** + * Coordinates binding between JSON values and Java objects. + * + * Moshi instances are thread-safe, meaning multiple threads can safely use a single instance + * concurrently. + */ +public class Moshi internal constructor(builder: Builder) { + private val factories = buildList { + addAll(builder.factories) + addAll(BUILT_IN_FACTORIES) + } + private val lastOffset = builder.lastOffset + private val lookupChainThreadLocal = ThreadLocal() + private val adapterCache = LinkedHashMap?>() + + /** Returns a JSON adapter for `type`, creating it if necessary. */ + @CheckReturnValue + public fun adapter(type: Type): JsonAdapter = adapter(type, Util.NO_ANNOTATIONS) + + @CheckReturnValue + public fun adapter(type: Class): JsonAdapter = adapter(type, Util.NO_ANNOTATIONS) + + @CheckReturnValue + public fun adapter(type: Type, annotationType: Class): JsonAdapter = + adapter(type, setOf(createJsonQualifierImplementation(annotationType))) + + @CheckReturnValue + public fun adapter(type: Type, vararg annotationTypes: Class): JsonAdapter { + if (annotationTypes.size == 1) { + return adapter(type, annotationTypes[0]) + } + val annotations = buildSet(annotationTypes.size) { + for (annotationType in annotationTypes) { + add(createJsonQualifierImplementation(annotationType)!!) + } + } + return adapter(type, annotations) + } + + @CheckReturnValue + public fun adapter(type: Type, annotations: Set): JsonAdapter = + adapter(type, annotations, fieldName = null) + + /** + * @param fieldName An optional field name associated with this type. The field name is used as a + * hint for better adapter lookup error messages for nested structures. + */ + @CheckReturnValue + public fun adapter( + type: Type, + annotations: Set, + fieldName: String? + ): JsonAdapter { + val cleanedType = removeSubtypeWildcard(canonicalize(type)) + + // If there's an equivalent adapter in the cache, we're done! + val cacheKey = cacheKey(cleanedType, annotations) + synchronized(adapterCache) { + val result = adapterCache[cacheKey] + @Suppress("UNCHECKED_CAST") + if (result != null) return result as JsonAdapter + } + var lookupChain = lookupChainThreadLocal.get() + if (lookupChain == null) { + lookupChain = LookupChain() + lookupChainThreadLocal.set(lookupChain) + } + var success = false + val adapterFromCall = lookupChain.push(cleanedType, fieldName, cacheKey) + try { + if (adapterFromCall != null) return adapterFromCall + + // Ask each factory to create the JSON adapter. + for (i in factories.indices) { + @Suppress("UNCHECKED_CAST") // Factories are required to return only matching JsonAdapters. + val result = factories[i].create(cleanedType, annotations, this) as JsonAdapter? ?: continue + + // Success! Notify the LookupChain so it is cached and can be used by re-entrant calls. + lookupChain.adapterFound(result) + success = true + return result + } + throw IllegalArgumentException("No JsonAdapter for ${typeAnnotatedWithAnnotations(type, annotations)}") + } catch (e: IllegalArgumentException) { + throw lookupChain.exceptionWithLookupStack(e) + } finally { + lookupChain.pop(success) + } + } + + @CheckReturnValue + public fun nextAdapter( + skipPast: JsonAdapter.Factory, + type: Type, + annotations: Set + ): JsonAdapter { + val cleanedType = removeSubtypeWildcard(canonicalize(type)) + val skipPastIndex = factories.indexOf(skipPast) + require(skipPastIndex != -1) { "Unable to skip past unknown factory $skipPast" } + for (i in (skipPastIndex + 1) until factories.size) { + @Suppress("UNCHECKED_CAST") // Factories are required to return only matching JsonAdapters. + val result = factories[i].create(cleanedType, annotations, this) as JsonAdapter? + if (result != null) return result + } + throw IllegalArgumentException("No next JsonAdapter for ${typeAnnotatedWithAnnotations(cleanedType, annotations)}") + } + + /** Returns a new builder containing all custom factories used by the current instance. */ + @CheckReturnValue + public fun newBuilder(): Builder { + val result = Builder() + // Runs to reuse var names + run { + val limit = lastOffset + for (i in 0 until limit) { + result.add(factories[i]) + } + } + run { + val limit = factories.size - BUILT_IN_FACTORIES.size + for (i in lastOffset until limit) { + result.addLast(factories[i]) + } + } + return result + } + + /** Returns an opaque object that's equal if the type and annotations are equal. */ + private fun cacheKey(type: Type, annotations: Set): Any { + return if (annotations.isEmpty()) type else listOf(type, annotations) + } + + public class Builder { + internal val factories = mutableListOf() + internal var lastOffset = 0 + + public fun add(type: Type, jsonAdapter: JsonAdapter): Builder = apply { + add(newAdapterFactory(type, jsonAdapter)) + } + + public fun add( + type: Type, + annotation: Class, + jsonAdapter: JsonAdapter + ): Builder = apply { + add(newAdapterFactory(type, annotation, jsonAdapter)) + } + + public fun add(factory: JsonAdapter.Factory): Builder = apply { + factories.add(lastOffset++, factory) + } + + public fun add(adapter: Any): Builder = apply { + add(AdapterMethodsFactory.get(adapter)) + } + + @Suppress("unused") + public fun addLast(type: Type, jsonAdapter: JsonAdapter): Builder = apply { + addLast(newAdapterFactory(type, jsonAdapter)) + } + + @Suppress("unused") + public fun addLast( + type: Type, + annotation: Class, + jsonAdapter: JsonAdapter + ): Builder = apply { + addLast(newAdapterFactory(type, annotation, jsonAdapter)) + } + + public fun addLast(factory: JsonAdapter.Factory): Builder = apply { + factories.add(factory) + } + + @Suppress("unused") + public fun addLast(adapter: Any): Builder = apply { + addLast(AdapterMethodsFactory.get(adapter)) + } + + @CheckReturnValue + public fun build(): Moshi = Moshi(this) + } + + /** + * A possibly-reentrant chain of lookups for JSON adapters. + * + * We keep track of the current stack of lookups: we may start by looking up the JSON adapter + * for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking + * up the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace + * with all of the lookups. + * + * Sometimes a JSON adapter factory depends on its own product; either directly or indirectly. + * To make this work, we offer a JSON adapter stub while the final adapter is being computed. When + * it is ready, we wire the stub to that finished adapter. This is necessary in self-referential + * object models, such as an `Employee` class that has a `List` field for an + * organization's management hierarchy. + * + * This class defers putting any JSON adapters in the cache until the topmost JSON adapter has + * successfully been computed. That way we don't pollute the cache with incomplete stubs, or + * adapters that may transitively depend on incomplete stubs. + */ + internal inner class LookupChain { + private val callLookups = mutableListOf>() + private val stack = ArrayDeque>() + private var exceptionAnnotated = false + + /** + * Returns a JSON adapter that was already created for this call, or null if this is the first + * time in this call that the cache key has been requested in this call. This may return a + * lookup that isn't yet ready if this lookup is reentrant. + */ + fun push(type: Type, fieldName: String?, cacheKey: Any): JsonAdapter? { + // Try to find a lookup with the same key for the same call. + var i = 0 + val size = callLookups.size + while (i < size) { + val lookup = callLookups[i] + if (lookup.cacheKey == cacheKey) { + @Suppress("UNCHECKED_CAST") + val hit = lookup as Lookup + stack += hit + return if (hit.adapter != null) hit.adapter else hit + } + i++ + } + + // We might need to know about this cache key later in this call. Prepare for that. + val lookup = Lookup(type, fieldName, cacheKey) + callLookups += lookup + stack += lookup + return null + } + + /** Sets the adapter result of the current lookup. */ + fun adapterFound(result: JsonAdapter) { + @Suppress("UNCHECKED_CAST") + val currentLookup = stack.last() as Lookup + currentLookup.adapter = result + } + + /** + * Completes the current lookup by removing a stack frame. + * + * @param success true if the adapter cache should be populated if this is the topmost lookup. + */ + fun pop(success: Boolean) { + stack.removeLast() + if (!stack.isEmpty()) return + lookupChainThreadLocal.remove() + if (success) { + synchronized(adapterCache) { + var i = 0 + val size = callLookups.size + while (i < size) { + val lookup = callLookups[i] + val replaced = adapterCache.put(lookup.cacheKey, lookup.adapter) + if (replaced != null) { + @Suppress("UNCHECKED_CAST") + (lookup as Lookup).adapter = replaced as JsonAdapter + adapterCache[lookup.cacheKey] = replaced + } + i++ + } + } + } + } + + fun exceptionWithLookupStack(e: IllegalArgumentException): IllegalArgumentException { + // Don't add the lookup stack to more than one exception; the deepest is sufficient. + if (exceptionAnnotated) return e + exceptionAnnotated = true + val size = stack.size + if (size == 1 && stack.first().fieldName == null) return e + val errorMessage = buildString { + append(e.message) + for (lookup in stack.asReversed()) { + append("\nfor ").append(lookup.type) + if (lookup.fieldName != null) { + append(' ').append(lookup.fieldName) + } + } + } + return IllegalArgumentException(errorMessage, e) + } + } + + /** This class implements `JsonAdapter` so it can be used as a stub for re-entrant calls. */ + internal class Lookup(val type: Type, val fieldName: String?, val cacheKey: Any) : JsonAdapter() { + var adapter: JsonAdapter? = null + + override fun fromJson(reader: JsonReader) = withAdapter { fromJson(reader) } + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") // TODO remove after JsonAdapter is migrated + override fun toJson(writer: JsonWriter, value: T?) = withAdapter { toJson(writer, value) } + + private inline fun withAdapter(body: JsonAdapter.() -> R): R = + checkNotNull(adapter) { "JsonAdapter isn't ready" }.body() + + override fun toString() = adapter?.toString() ?: super.toString() + } + + internal companion object { + @JvmField + val BUILT_IN_FACTORIES: List = buildList(6) { + add(StandardJsonAdapters.FACTORY) + add(CollectionJsonAdapter.FACTORY) + add(MapJsonAdapter.FACTORY) + add(ArrayJsonAdapter.FACTORY) + add(RecordJsonAdapter.FACTORY) + add(ClassJsonAdapter.FACTORY) + } + + fun newAdapterFactory( + type: Type, + jsonAdapter: JsonAdapter + ): JsonAdapter.Factory { + return JsonAdapter.Factory { targetType, annotations, _ -> + if (annotations.isEmpty() && typesMatch(type, targetType)) jsonAdapter else null + } + } + + fun newAdapterFactory( + type: Type, + annotation: Class, + jsonAdapter: JsonAdapter + ): JsonAdapter.Factory { + require(annotation.isAnnotationPresent(JsonQualifier::class.java)) { "$annotation does not have @JsonQualifier" } + require(annotation.declaredMethods.isEmpty()) { "Use JsonAdapter.Factory for annotations with elements" } + return JsonAdapter.Factory { targetType, annotations, _ -> + if (typesMatch(type, targetType) && annotations.size == 1 && isAnnotationPresent(annotations, annotation)) { + jsonAdapter + } else { + null + } + } + } + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java index 392a194..f00d52d 100644 --- a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java @@ -603,44 +603,44 @@ public final class MoshiTest { try { builder.add((null)); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("factory == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add((Object) null); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("adapter == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(null, null); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("type == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(type, null); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("jsonAdapter == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(null, null, null); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("type == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(type, null, null); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("annotation == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(type, annotation, null); fail(); - } catch (IllegalArgumentException expected) { - assertThat(expected).hasMessageThat().isEqualTo("jsonAdapter == null"); + } catch (NullPointerException expected) { + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @@ -771,7 +771,7 @@ public final class MoshiTest { moshi.adapter(null, Collections.emptySet()); fail(); } catch (NullPointerException expected) { - assertThat(expected).hasMessageThat().isEqualTo("type == null"); + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @@ -782,13 +782,13 @@ public final class MoshiTest { moshi.adapter(String.class, (Class) null); fail(); } catch (NullPointerException expected) { - assertThat(expected).hasMessageThat().isEqualTo("annotationType == null"); + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { moshi.adapter(String.class, (Set) null); fail(); } catch (NullPointerException expected) { - assertThat(expected).hasMessageThat().isEqualTo("annotations == null"); + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @@ -808,7 +808,7 @@ public final class MoshiTest { moshi.adapter(Object.class); fail(); } catch (NullPointerException expected) { - assertThat(expected).hasMessageThat().isEqualTo("annotations == null"); + assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @@ -1235,7 +1235,8 @@ public final class MoshiTest { Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build(); Moshi.Builder newBuilder = moshi.newBuilder(); for (JsonAdapter.Factory factory : Moshi.BUILT_IN_FACTORIES) { - assertThat(factory).isNotIn(newBuilder.factories); + // Awkward but java sources don't know about the internal-ness of this + assertThat(factory).isNotIn(newBuilder.getFactories$moshi()); } }