mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
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:
@@ -289,7 +289,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
|
||||
else -> error("Not possible!")
|
||||
}
|
||||
val resolvedPropertyType = resolve(type, rawType, propertyType)
|
||||
val adapter = moshi.adapter<Any>(
|
||||
val adapter = moshi.adapter<Any?>(
|
||||
resolvedPropertyType,
|
||||
Util.jsonAnnotations(allAnnotations.toTypedArray()),
|
||||
property.name
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
365
moshi/src/main/java/com/squareup/moshi/Moshi.kt
Normal file
365
moshi/src/main/java/com/squareup/moshi/Moshi.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.<Annotation>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<? extends Annotation>) 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<? extends Annotation>) 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user