All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squareup.moshi.Moshi Maven / Gradle / Ivy

There is a newer version: 1.15.1
Show newest version
/*
 * 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
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.moshi;

import com.squareup.moshi.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;

import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;

/**
 * Coordinates binding between JSON values and Java objects.
 */
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(ClassJsonAdapter.FACTORY);
  }

  private final List factories;
  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);
  }

  /** 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() {
    int fullSize = factories.size();
    int tailSize = BUILT_IN_FACTORIES.size();
    List customFactories = factories.subList(0, fullSize - tailSize);
    return new Builder().addAll(customFactories);
  }

  /** 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<>();

    public  Builder add(final Type type, final JsonAdapter jsonAdapter) {
      if (type == null) throw new IllegalArgumentException("type == null");
      if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");

      return add(new JsonAdapter.Factory() {
        @Override public @Nullable JsonAdapter create(
            Type targetType, Set annotations, Moshi moshi) {
          return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
        }
      });
    }

    public  Builder add(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 add(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;
        }
      });
    }

    public Builder add(JsonAdapter.Factory factory) {
      if (factory == null) throw new IllegalArgumentException("factory == null");
      factories.add(factory);
      return this;
    }

    public Builder add(Object adapter) {
      if (adapter == null) throw new IllegalArgumentException("adapter == null");
      return add(AdapterMethodsFactory.get(adapter));
    }

    Builder addAll(List factories) {
      this.factories.addAll(factories);
      return this;
    }

    @CheckReturnValue public Moshi build() {
      return new 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 {@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(); } } }