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 <egor@squareup.com>

* Small little cleanups

Co-authored-by: Egor Andreevich <egor@squareup.com>
This commit is contained in:
Zac Sweers
2021-12-26 15:49:52 -06:00
committed by GitHub
parent ae421b1909
commit f3d2103ffb
4 changed files with 386 additions and 443 deletions

View File

@@ -289,7 +289,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
else -> error("Not possible!") else -> error("Not possible!")
} }
val resolvedPropertyType = resolve(type, rawType, propertyType) val resolvedPropertyType = resolve(type, rawType, propertyType)
val adapter = moshi.adapter<Any>( val adapter = moshi.adapter<Any?>(
resolvedPropertyType, resolvedPropertyType,
Util.jsonAnnotations(allAnnotations.toTypedArray()), Util.jsonAnnotations(allAnnotations.toTypedArray()),
property.name property.name

View File

@@ -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.
*
* <p>Moshi instances are thread-safe, meaning multiple threads can safely use a single instance
* concurrently.
*/
public final class Moshi {
static final List<JsonAdapter.Factory> 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<JsonAdapter.Factory> factories;
private final int lastOffset;
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
Moshi(Builder builder) {
List<JsonAdapter.Factory> 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 <T> JsonAdapter<T> adapter(Type type) {
return adapter(type, Util.NO_ANNOTATIONS);
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Class<T> type) {
return adapter(type, Util.NO_ANNOTATIONS);
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation> annotationType) {
if (annotationType == null) {
throw new NullPointerException("annotationType == null");
}
return adapter(
type, Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation>... annotationTypes) {
if (annotationTypes.length == 1) {
return adapter(type, annotationTypes[0]);
}
Set<Annotation> annotations = new LinkedHashSet<>(annotationTypes.length);
for (Class<? extends Annotation> annotationType : annotationTypes) {
annotations.add(Types.createJsonQualifierImplementation(annotationType));
}
return adapter(type, Collections.unmodifiableSet(annotations));
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> 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 <T> JsonAdapter<T> adapter(
Type type, Set<? extends Annotation> 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<T>) result;
}
LookupChain lookupChain = lookupChainThreadLocal.get();
if (lookupChain == null) {
lookupChain = new LookupChain();
lookupChainThreadLocal.set(lookupChain);
}
boolean success = false;
JsonAdapter<T> 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<T> result = (JsonAdapter<T>) 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 <T> JsonAdapter<T> nextAdapter(
JsonAdapter.Factory skipPast, Type type, Set<? extends Annotation> 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<T> result = (JsonAdapter<T>) 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<? extends Annotation> annotations) {
if (annotations.isEmpty()) return type;
return Arrays.asList(type, annotations);
}
public static final class Builder {
final List<JsonAdapter.Factory> factories = new ArrayList<>();
int lastOffset = 0;
public <T> Builder add(Type type, JsonAdapter<T> jsonAdapter) {
return add(newAdapterFactory(type, jsonAdapter));
}
public <T> Builder add(
Type type, Class<? extends Annotation> annotation, JsonAdapter<T> 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 <T> Builder addLast(Type type, JsonAdapter<T> jsonAdapter) {
return addLast(newAdapterFactory(type, jsonAdapter));
}
public <T> Builder addLast(
Type type, Class<? extends Annotation> annotation, JsonAdapter<T> 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 <T> JsonAdapter.Factory newAdapterFactory(
final Type type, final JsonAdapter<T> 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<? extends Annotation> annotations, Moshi moshi) {
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
}
};
}
static <T> JsonAdapter.Factory newAdapterFactory(
final Type type,
final Class<? extends Annotation> annotation,
final JsonAdapter<T> 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<? extends Annotation> 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.
*
* <p>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.
*
* <p>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<Employee>} field for an
* organization's management hierarchy.
*
* <p>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<Lookup<?>> callLookups = new ArrayList<>();
final Deque<Lookup<?>> 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.
*/
<T> JsonAdapter<T> 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<T> hit = (Lookup<T>) 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<Object> lookup = new Lookup<>(type, fieldName, cacheKey);
callLookups.add(lookup);
stack.add(lookup);
return null;
}
/** Sets the adapter result of the current lookup. */
<T> void adapterFound(JsonAdapter<T> result) {
Lookup<T> currentLookup = (Lookup<T>) 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<Object>) lookup).adapter = (JsonAdapter<Object>) 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<Lookup<?>> 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<T> extends JsonAdapter<T> {
final Type type;
final @Nullable String fieldName;
final Object cacheKey;
@Nullable JsonAdapter<T> 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();
}
}
}

View File

@@ -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<LookupChain>()
private val adapterCache = LinkedHashMap<Any?, JsonAdapter<*>?>()
/** Returns a JSON adapter for `type`, creating it if necessary. */
@CheckReturnValue
public fun <T> adapter(type: Type): JsonAdapter<T> = adapter(type, Util.NO_ANNOTATIONS)
@CheckReturnValue
public fun <T> adapter(type: Class<T>): JsonAdapter<T> = adapter(type, Util.NO_ANNOTATIONS)
@CheckReturnValue
public fun <T> adapter(type: Type, annotationType: Class<out Annotation>): JsonAdapter<T> =
adapter(type, setOf(createJsonQualifierImplementation(annotationType)))
@CheckReturnValue
public fun <T> adapter(type: Type, vararg annotationTypes: Class<out Annotation>): JsonAdapter<T> {
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 <T> adapter(type: Type, annotations: Set<Annotation>): JsonAdapter<T> =
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 <T> adapter(
type: Type,
annotations: Set<Annotation>,
fieldName: String?
): JsonAdapter<T> {
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<T>
}
var lookupChain = lookupChainThreadLocal.get()
if (lookupChain == null) {
lookupChain = LookupChain()
lookupChainThreadLocal.set(lookupChain)
}
var success = false
val adapterFromCall = lookupChain.push<T>(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<T>? ?: 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 <T> nextAdapter(
skipPast: JsonAdapter.Factory,
type: Type,
annotations: Set<Annotation>
): JsonAdapter<T> {
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<T>?
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<Annotation>): Any {
return if (annotations.isEmpty()) type else listOf(type, annotations)
}
public class Builder {
internal val factories = mutableListOf<JsonAdapter.Factory>()
internal var lastOffset = 0
public fun <T> add(type: Type, jsonAdapter: JsonAdapter<T>): Builder = apply {
add(newAdapterFactory(type, jsonAdapter))
}
public fun <T> add(
type: Type,
annotation: Class<out Annotation>,
jsonAdapter: JsonAdapter<T>
): 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 <T> addLast(type: Type, jsonAdapter: JsonAdapter<T>): Builder = apply {
addLast(newAdapterFactory(type, jsonAdapter))
}
@Suppress("unused")
public fun <T> addLast(
type: Type,
annotation: Class<out Annotation>,
jsonAdapter: JsonAdapter<T>
): 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<Employee>` 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<Lookup<*>>()
private val stack = ArrayDeque<Lookup<*>>()
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 <T> push(type: Type, fieldName: String?, cacheKey: Any): JsonAdapter<T>? {
// 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<T>
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<Any>(type, fieldName, cacheKey)
callLookups += lookup
stack += lookup
return null
}
/** Sets the adapter result of the current lookup. */
fun <T> adapterFound(result: JsonAdapter<T>) {
@Suppress("UNCHECKED_CAST")
val currentLookup = stack.last() as Lookup<T>
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<Any>).adapter = replaced as JsonAdapter<Any>
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<T>(val type: Type, val fieldName: String?, val cacheKey: Any) : JsonAdapter<T>() {
var adapter: JsonAdapter<T>? = 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 <R> withAdapter(body: JsonAdapter<T>.() -> 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<JsonAdapter.Factory> = buildList(6) {
add(StandardJsonAdapters.FACTORY)
add(CollectionJsonAdapter.FACTORY)
add(MapJsonAdapter.FACTORY)
add(ArrayJsonAdapter.FACTORY)
add(RecordJsonAdapter.FACTORY)
add(ClassJsonAdapter.FACTORY)
}
fun <T> newAdapterFactory(
type: Type,
jsonAdapter: JsonAdapter<T>
): JsonAdapter.Factory {
return JsonAdapter.Factory { targetType, annotations, _ ->
if (annotations.isEmpty() && typesMatch(type, targetType)) jsonAdapter else null
}
}
fun <T> newAdapterFactory(
type: Type,
annotation: Class<out Annotation>,
jsonAdapter: JsonAdapter<T>
): 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
}
}
}
}
}

View File

@@ -603,44 +603,44 @@ public final class MoshiTest {
try { try {
builder.add((null)); builder.add((null));
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("factory == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
builder.add((Object) null); builder.add((Object) null);
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("adapter == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
builder.add(null, null); builder.add(null, null);
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("type == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
builder.add(type, null); builder.add(type, null);
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("jsonAdapter == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
builder.add(null, null, null); builder.add(null, null, null);
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("type == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
builder.add(type, null, null); builder.add(type, null, null);
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("annotation == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
builder.add(type, annotation, null); builder.add(type, annotation, null);
fail(); fail();
} catch (IllegalArgumentException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("jsonAdapter == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
} }
@@ -771,7 +771,7 @@ public final class MoshiTest {
moshi.adapter(null, Collections.<Annotation>emptySet()); moshi.adapter(null, Collections.<Annotation>emptySet());
fail(); fail();
} catch (NullPointerException expected) { } 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<? extends Annotation>) null); moshi.adapter(String.class, (Class<? extends Annotation>) null);
fail(); fail();
} catch (NullPointerException expected) { } catch (NullPointerException expected) {
assertThat(expected).hasMessageThat().isEqualTo("annotationType == null"); assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null");
} }
try { try {
moshi.adapter(String.class, (Set<? extends Annotation>) null); moshi.adapter(String.class, (Set<? extends Annotation>) null);
fail(); fail();
} catch (NullPointerException expected) { } 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); moshi.adapter(Object.class);
fail(); fail();
} catch (NullPointerException expected) { } 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 moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build();
Moshi.Builder newBuilder = moshi.newBuilder(); Moshi.Builder newBuilder = moshi.newBuilder();
for (JsonAdapter.Factory factory : Moshi.BUILT_IN_FACTORIES) { 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());
} }
} }