/*
 * Copyright 2008 Google 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.google.gwt.user.rebind.rpc;

import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JGenericType;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JRealClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.JTypeParameter;
import com.google.gwt.core.ext.typeinfo.JWildcardType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.user.client.rpc.GwtTransient;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.rebind.rpc.ProblemReport.Priority;
import com.google.gwt.user.rebind.rpc.TypeParameterExposureComputer.TypeParameterFlowInfo;
import com.google.gwt.user.rebind.rpc.TypePaths.TypePath;

import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

/**
 * Builds a {@link SerializableTypeOracle} for a given set of root types.
 *
 * <p>
 * There are two goals for this builder. First, discover the set of serializable
 * types that can be serialized if you serialize one of the root types. Second,
 * to make sure that all root types can actually be serialized by GWT.
 * </p>
 *
 * <p>
 * To find the serializable types, it includes the root types, and then it
 * iteratively traverses the type hierarchy and the fields of any type already
 * deemed serializable. To improve the accuracy of the traversal there is a
 * computations of the exposure of type parameters. When the traversal reaches a
 * parameterized type, these exposure values are used to determine how to treat
 * the arguments.
 * </p>
 *
 * <p>
 * A type qualifies for serialization if it or one of its subtypes is
 * automatically or manually serializable. Automatic serialization is selected
 * if the type is assignable to {@link IsSerializable} or {@link Serializable}
 * or if the type is a primitive type such as int, boolean, etc. Manual
 * serialization is selected if there exists another type with the same fully
 * qualified name concatenated with "_CustomFieldSerializer". If a type
 * qualifies for both manual and automatic serialization, manual serialization
 * is preferred.
 * </p>
 *
 * <p>
 * Some types may be marked as "enhanced," either automatically by the presence
 * of a JDO <code>@PersistenceCapable</code> or JPA <code>@Entity</code> tag on
 * the class definition, or manually by extending the 'rpc.enhancedClasses'
 * configuration property in the GWT module XML file. For example, to manually
 * mark the class com.google.myapp.MyPersistentClass as enhanced, use:
 *
 * <pre>
 * <extend-configuration-property name='rpc.enhancedClasses'
 *     value='com.google.myapp.MyPersistentClass'/>
 * </pre>
 *
 * <p>
 * Enhanced classes are checked for the presence of additional serializable
 * fields on the server that were not defined in client code as seen by the GWT
 * compiler. If it is possible for an instance of such a class to be transmitted
 * bidrectionally between server and client, a special RPC rule is used. The
 * server-only fields are serialized using standard Java serialization and sent
 * between the client and server as a blob of opaque base-64 encoded binary
 * data. When an instance is sent from client to server, the server instance is
 * populated by invoking setter methods where possible rather than by setting
 * fields directly. This allows APIs such as JDO the opportunity to update the
 * object state properly to take into account changes that may have occurred to
 * the object's state while resident on the client.
 * </p>
 */
public class SerializableTypeOracleBuilder {

  static class TypeInfoComputed {

    /**
     * <code>true</code> if the type is automatically or manually serializable
     * and the corresponding checks succeed.
     */
    private boolean fieldSerializable = false;

    /**
     * <code>true</code> if this type might be instantiated.
     */
    private boolean instantiable = false;

    /**
     * <code>true</code> if there are instantiable subtypes assignable to this
     * one.
     */
    private boolean instantiableSubtypes;

    /**
     * All instantiable types found when this type was queried, including the
     * type itself. (Null until calculated.)
     */
    private Set<JClassType> instantiableTypes;

    /**
     * Custom field serializer or <code>null</code> if there isn't one.
     */
    private final JClassType manualSerializer;

    /**
     * <code>true</code> if this class might be enhanced on the server to
     * contain extra fields.
     */
    private final boolean maybeEnhanced;

    /**
     * Path used to discover this type.
     */
    private final TypePath path;

    /**
     * The state that this type is currently in.
     */
    private TypeState state = TypeState.NOT_CHECKED;

    /**
     * {@link JClassType} associated with this metadata.
     */
    private final JType type;

    private TypeInfoComputed(JType type, TypePath path, TypeOracle typeOracle) {
      this.type = type;
      this.path = path;
      if (type instanceof JClassType) {
        JClassType typeClass = (JClassType) type;
        manualSerializer = findCustomFieldSerializer(typeOracle, typeClass);
        maybeEnhanced = hasJdoAnnotation(typeClass) || hasJpaAnnotation(typeClass);
      } else {
        manualSerializer = null;
        maybeEnhanced = false;
      }
    }

    public TypePath getPath() {
      return path;
    }

    public JType getType() {
      return type;
    }

    public boolean hasInstantiableSubtypes() {
      return instantiable || instantiableSubtypes || state == TypeState.CHECK_IN_PROGRESS;
    }

    public boolean isDone() {
      return state == TypeState.CHECK_DONE;
    }

    public boolean isFieldSerializable() {
      return fieldSerializable;
    }

    public boolean isInstantiable() {
      return instantiable;
    }

    public boolean isManuallySerializable() {
      return manualSerializer != null;
    }

    public boolean isPendingInstantiable() {
      return state == TypeState.CHECK_IN_PROGRESS;
    }

    public boolean maybeEnhanced() {
      return maybeEnhanced;
    }

    public void setFieldSerializable() {
      fieldSerializable = true;
    }

    public void setInstantiable(boolean instantiable) {
      this.instantiable = instantiable;
      if (instantiable) {
        fieldSerializable = true;
      }
      state = TypeState.CHECK_DONE;
    }

    public void setInstantiableSubtypes(boolean instantiableSubtypes) {
      this.instantiableSubtypes = instantiableSubtypes;
    }

    public void setPendingInstantiable() {
      state = TypeState.CHECK_IN_PROGRESS;
    }
  }

  private enum TypeState {
    /**
     * The instantiability of a type has been determined.
     */
    CHECK_DONE("Check succeeded"),
    /**
     * The instantiability of a type is being checked.
     */
    CHECK_IN_PROGRESS("Check in progress"),
    /**
     * The instantiability of a type has not been checked.
     */
    NOT_CHECKED("Not checked");

    private final String message;

    TypeState(String message) {
      this.message = message;
    }

    @Override
    public String toString() {
      return message;
    }
  }

  /**
   * Compares {@link JType}s according to their qualified source names.
   */
  static final Comparator<JType> JTYPE_COMPARATOR = new Comparator<JType>() {
    @Override
    public int compare(JType t1, JType t2) {
      return t1.getQualifiedSourceName().compareTo(t2.getQualifiedSourceName());
    }
  };

  /**
   * No type filtering by default..
   */
  private static final TypeFilter DEFAULT_TYPE_FILTER = new TypeFilter() {
    @Override
    public String getName() {
      return "Default";
    }

    @Override
    public boolean isAllowed(JClassType type) {
      return true;
    }
  };

  /**
   * A reference to the annotation class
   * javax.jdo.annotations.PersistenceCapable used by the JDO API. May be null
   * if JDO is not present in the runtime environment.
   */
  private static Class<? extends Annotation> JDO_PERSISTENCE_CAPABLE_ANNOTATION = null;

  /**
   * A reference to the method 'String
   * javax.jdo.annotations.PersistenceCapable.detachable()'.
   */
  private static Method JDO_PERSISTENCE_CAPABLE_DETACHABLE_METHOD;

  /**
   * A reference to the annotation class javax.persistence.Entity used by the
   * JPA API. May be null if JPA is not present in the runtime environment.
   */
  private static Class<? extends Annotation> JPA_ENTITY_ANNOTATION = null;

  static {
    try {
      JDO_PERSISTENCE_CAPABLE_ANNOTATION =
          Class.forName("javax.jdo.annotations.PersistenceCapable").asSubclass(Annotation.class);
      JDO_PERSISTENCE_CAPABLE_DETACHABLE_METHOD =
          JDO_PERSISTENCE_CAPABLE_ANNOTATION.getDeclaredMethod("detachable", (Class[]) null);
    } catch (ClassNotFoundException e) {
      // Ignore, JDO_PERSISTENCE_CAPABLE_ANNOTATION will be null
    } catch (NoSuchMethodException e) {
      JDO_PERSISTENCE_CAPABLE_ANNOTATION = null;
    }

    try {
      JPA_ENTITY_ANNOTATION =
          Class.forName("javax.persistence.Entity").asSubclass(Annotation.class);
    } catch (ClassNotFoundException e) {
      // Ignore, JPA_ENTITY_CAPABLE_ANNOTATION will be null
    }
  }

  static boolean canBeInstantiated(JClassType type, ProblemReport problems) {
    if (type.isEnum() == null) {
      if (type.isAbstract()) {
        // Abstract types will be picked up if there is an instantiable
        // subtype.
        return false;
      }

      if (!type.isDefaultInstantiable() && !isManuallySerializable(type)) {
        // Warn and return false.
        problems.add(type, type.getParameterizedQualifiedSourceName()
            + " is not default instantiable (it must have a zero-argument "
            + "constructor or no constructors at all) and has no custom " + "serializer.",
            Priority.DEFAULT);
        return false;
      }
    } else {
      /*
       * Enums are always instantiable regardless of abstract or default
       * instantiability.
       */
    }

    return true;
  }

  /**
   * Finds the custom field serializer for a given type.
   *
   * @param typeOracle
   * @param type
   * @return the custom field serializer for a type or <code>null</code> if
   *         there is not one
   */
  public static JClassType findCustomFieldSerializer(TypeOracle typeOracle, JType type) {
    JClassType classOrInterface = type.isClassOrInterface();
    if (classOrInterface == null) {
      return null;
    }

    String customFieldSerializerName = getCustomFieldSerializerName(type.getQualifiedSourceName());
    return findCustomFieldSerializer(typeOracle, customFieldSerializerName);
  }

  /**
   * Finds the custom field serializer for a given qualified source name.
   *
   * @param typeOracle
   * @param customFieldSerializerName
   * @return the custom field serializer for a type of <code>null</code> if
   *         there is not one
   */
  public static JClassType findCustomFieldSerializer(TypeOracle typeOracle,
      String customFieldSerializerName) {
    JClassType customSerializer = typeOracle.findType(customFieldSerializerName);
    if (customSerializer == null) {
      // If the type is in the java.lang or java.util packages then it will be
      // mapped into com.google.gwt.user.client.rpc.core package
      customSerializer =
          typeOracle.findType("com.google.gwt.user.client.rpc.core." + customFieldSerializerName);
    }

    return customSerializer;
  }

  /**
   * Returns the name for a custom field serializer, given a source name.
   *
   * @param sourceName
   * @return the custom field serializer type name for a given source name.
   */
  public static String getCustomFieldSerializerName(String sourceName) {
    return sourceName + "_CustomFieldSerializer";
  }

  static JRealClassType getBaseType(JClassType type) {
    if (type.isParameterized() != null) {
      return type.isParameterized().getBaseType();
    } else if (type.isRawType() != null) {
      return type.isRawType().getBaseType();
    }

    return (JRealClassType) type;
  }

  private static boolean hasGwtTransientAnnotation(JField field) {
    for (Annotation a : field.getAnnotations()) {
      if (a.annotationType().getSimpleName().equals(GwtTransient.class.getSimpleName())) {
        return true;
      }
    }
    return false;
  }

  /**
   * @param type the type to query
   * @return true if the type is annotated with @PersistenceCapable(...,
   *         detachable="true")
   */
  static boolean hasJdoAnnotation(JClassType type) {
    if (JDO_PERSISTENCE_CAPABLE_ANNOTATION == null) {
      return false;
    }
    Annotation annotation = type.getAnnotation(JDO_PERSISTENCE_CAPABLE_ANNOTATION);
    if (annotation == null) {
      return false;
    }
    try {
      Object value = JDO_PERSISTENCE_CAPABLE_DETACHABLE_METHOD.invoke(annotation, (Object[]) null);
      if (value instanceof String) {
        return "true".equalsIgnoreCase((String) value);
      } else {
        return false;
      }
    } catch (IllegalAccessException e) {
      // will return false
    } catch (InvocationTargetException e) {
      // will return false
    }

    return false;
  }

  /**
   * @param type the type to query
   * @return true if the type is annotated with @Entity
   */
  static boolean hasJpaAnnotation(JClassType type) {
    if (JPA_ENTITY_ANNOTATION == null) {
      return false;
    }
    Annotation annotation = type.getAnnotation(JPA_ENTITY_ANNOTATION);
    return annotation != null;
  }

  static boolean isAutoSerializable(JClassType type) {
    try {
      JClassType isSerializable = getIsSerializableMarkerInterface(type);
      JClassType serializable = getSerializableMarkerInterface(type);
      return type.isAssignableTo(isSerializable) || type.isAssignableTo(serializable);
    } catch (NotFoundException e) {
      return false;
    }
  }

  /**
   * Returns <code>true</code> if this type is part of the standard java
   * packages.
   */
  static boolean isInStandardJavaPackage(String className) {
    if (className.startsWith("java.")) {
      return true;
    }

    if (className.startsWith("javax.")) {
      return true;
    }

    return false;
  }

  static void recordTypeParametersIn(JType type, Set<JTypeParameter> params) {
    JTypeParameter isTypeParameter = type.isTypeParameter();
    if (isTypeParameter != null) {
      params.add(isTypeParameter);
    }

    JArrayType isArray = type.isArray();
    if (isArray != null) {
      recordTypeParametersIn(isArray.getComponentType(), params);
    }

    JWildcardType isWildcard = type.isWildcard();
    if (isWildcard != null) {
      for (JClassType bound : isWildcard.getUpperBounds()) {
        recordTypeParametersIn(bound, params);
      }
    }

    JParameterizedType isParameterized = type.isParameterized();
    if (isParameterized != null) {
      for (JClassType arg : isParameterized.getTypeArgs()) {
        recordTypeParametersIn(arg, params);
      }
    }
  }

  /**
   * Return <code>true</code> if a class's fields should be considered for
   * serialization. If it returns <code>false</code> then none of the fields of
   * this class should be serialized.
   */
  static boolean shouldConsiderFieldsForSerialization(JClassType type, TypeFilter filter,
      ProblemReport problems) {
    if (!isAllowedByFilter(filter, type, problems)) {
      return false;
    }

    if (!isDeclaredSerializable(type)) {
      problems.add(type, type.getParameterizedQualifiedSourceName() + " is not assignable to '"
          + IsSerializable.class.getName() + "' or '" + Serializable.class.getName()
          + "' nor does it have a custom field serializer", Priority.DEFAULT);
      return false;
    }

    if (isManuallySerializable(type)) {
      JClassType manualSerializer = findCustomFieldSerializer(type.getOracle(), type);
      assert (manualSerializer != null);

      List<String> fieldProblems = CustomFieldSerializerValidator.validate(manualSerializer, type);
      if (!fieldProblems.isEmpty()) {
        for (String problem : fieldProblems) {
          problems.add(type, problem, Priority.FATAL);
        }
        return false;
      }
    } else {
      assert (isAutoSerializable(type));

      if (!isAccessibleToSerializer(type)) {
        // Class is not visible to a serializer class in the same package
        problems.add(type, type.getParameterizedQualifiedSourceName()
            + " is not accessible from a class in its same package; it "
            + "will be excluded from the set of serializable types", Priority.DEFAULT);
        return false;
      }

      if (type.isMemberType() && !type.isStatic()) {
        // Non-static member types cannot be serialized
        problems.add(type, type.getParameterizedQualifiedSourceName() + " is nested but "
            + "not static; it will be excluded from the set of serializable " + "types",
            Priority.DEFAULT);
        return false;
      }
    }

    return true;
  }

  /**
   * Returns <code>true</code> if the field qualifies for serialization without
   * considering its type.
   */
  static boolean shouldConsiderForSerialization(TreeLogger logger, GeneratorContext context,
      JField field) {
    if (field.isStatic() || field.isTransient() || hasGwtTransientAnnotation(field)) {
      return false;
    }

    if (field.isFinal() && !Shared.shouldSerializeFinalFields(logger, context)) {
      logFinalField(logger, context, field);
      return false;
    }
    return true;
  }

  private static void logFinalField(TreeLogger logger, GeneratorContext context, JField field) {
    TreeLogger.Type logLevel;
    if (Shared.shouldSuppressNonStaticFinalFieldWarnings(logger, context)) {
      logLevel = TreeLogger.DEBUG;
    } else if (isManuallySerializable(field.getEnclosingType())) {
      // If the type has a custom serializer, assume the programmer knows best.
      logLevel = TreeLogger.DEBUG;
    } else {
      logLevel = TreeLogger.WARN;
    }
    logger.branch(logLevel, "Field '" + field + "' will not be serialized because it is final");
  }

  private static boolean directlyImplementsMarkerInterface(JClassType type) {
    try {
      return TypeHierarchyUtils.directlyImplementsInterface(type,
          getIsSerializableMarkerInterface(type))
          || TypeHierarchyUtils.directlyImplementsInterface(type,
              getSerializableMarkerInterface(type));
    } catch (NotFoundException e) {
      return false;
    }
  }

  private static JArrayType getArrayType(TypeOracle typeOracle, int rank, JType component) {
    assert (rank > 0);

    JArrayType array = null;
    JType currentComponent = component;
    for (int i = 0; i < rank; ++i) {
      array = typeOracle.getArrayType(currentComponent);
      currentComponent = array;
    }

    return array;
  }

  private static JClassType getIsSerializableMarkerInterface(JClassType type)
      throws NotFoundException {
    return type.getOracle().getType(IsSerializable.class.getName());
  }

  private static JClassType getSerializableMarkerInterface(JClassType type)
      throws NotFoundException {
    return type.getOracle().getType(Serializable.class.getName());
  }

  /**
   * Returns <code>true</code> if a serializer class could access this type.
   */
  private static boolean isAccessibleToSerializer(JClassType type) {
    if (type.isPrivate()) {
      return false;
    }

    if (isInStandardJavaPackage(type.getQualifiedSourceName())) {
      if (!type.isPublic()) {
        return false;
      }
    }

    if (type.isMemberType()) {
      return isAccessibleToSerializer(type.getEnclosingType());
    }

    return true;
  }

  private static boolean isAllowedByFilter(TypeFilter filter, JClassType classType,
      ProblemReport problems) {
    if (!filter.isAllowed(classType)) {
      problems.add(classType, classType.getParameterizedQualifiedSourceName()
          + " is excluded by type filter ", Priority.AUXILIARY);
      return false;
    }

    return true;
  }

  private static boolean isDeclaredSerializable(JClassType type) {
    return isAutoSerializable(type) || isManuallySerializable(type);
  }

  private static boolean isDirectlySerializable(JClassType type) {
    return directlyImplementsMarkerInterface(type) || isManuallySerializable(type);
  }

  private static boolean isManuallySerializable(JClassType type) {
    return findCustomFieldSerializer(type.getOracle(), type) != null;
  }

  private static void logSerializableTypes(TreeLogger logger, Set<JClassType> fieldSerializableTypes) {
    if (!logger.isLoggable(TreeLogger.DEBUG)) {
      return;
    }
    TreeLogger localLogger =
        logger.branch(TreeLogger.DEBUG, "Identified " + fieldSerializableTypes.size()
            + " serializable type" + ((fieldSerializableTypes.size() == 1) ? "" : "s"), null);

    for (JClassType fieldSerializableType : fieldSerializableTypes) {
      localLogger.branch(TreeLogger.DEBUG, fieldSerializableType
          .getParameterizedQualifiedSourceName(), null);
    }
  }

  private boolean alreadyCheckedObject;

  /**
   * Cache of the {@link JClassType} for {@link Collection}.
   */
  private final JGenericType collectionClass;

  private final GeneratorContext context;

  private Set<String> enhancedClasses = null;

  private PrintWriter logOutputWriter;

  /**
   * Cache of the {@link JClassType} for {@link Map}.
   */
  private final JGenericType mapClass;

  private final Map<JClassType, TreeLogger> rootTypes = new LinkedHashMap<JClassType, TreeLogger>();

  private final TypeConstrainer typeConstrainer;
  private TypeFilter typeFilter = DEFAULT_TYPE_FILTER;

  private final TypeOracle typeOracle;

  private final TypeParameterExposureComputer typeParameterExposureComputer;

  /**
   * The set of type parameters that appear in one of the root types.
   * TODO(spoon): It would be cleaner to delete this field, and instead to have
   * {@link #addRootType(TreeLogger, JType)} replace parameters with wildcard
   * types. Then the root types would not contain any parameters.
   */
  private Set<JTypeParameter> typeParametersInRootTypes = new HashSet<JTypeParameter>();

  /**
   * Map of {@link JType} to {@link TypeInfoComputed}.
   */
  private final Map<JType, TypeInfoComputed> typeToTypeInfoComputed =
      new HashMap<JType, TypeInfoComputed>();

  /**
   * Constructs a builder.
   *
   * @param logger
   * @param context
   *
   * @throws UnableToCompleteException if we fail to find one of our special
   *           types
   */
  public SerializableTypeOracleBuilder(TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    this.context = context;
    this.typeOracle = context.getTypeOracle();
    this.typeParameterExposureComputer = new TypeParameterExposureComputer(context, typeFilter);
    typeConstrainer = new TypeConstrainer(typeOracle);

    try {
      collectionClass = typeOracle.getType(Collection.class.getName()).isGenericType();
      mapClass = typeOracle.getType(Map.class.getName()).isGenericType();
    } catch (NotFoundException e) {
      logger.log(TreeLogger.ERROR, null, e);
      throw new UnableToCompleteException();
    }

    enhancedClasses = Shared.getEnhancedTypes(context.getPropertyOracle());
  }

  public void addRootType(TreeLogger logger, JType type) {
    if (type.isPrimitive() != null) {
      return;
    }

    JClassType clazz = (JClassType) type;
    if (!rootTypes.containsKey(clazz)) {
      recordTypeParametersIn(type, typeParametersInRootTypes);

      rootTypes.put(clazz, logger);
    } else {
      if (logger.isLoggable(TreeLogger.TRACE)) {
        logger.log(TreeLogger.TRACE, clazz.getParameterizedQualifiedSourceName()
            + " is already a root type.");
      }
    }
  }

  /**
   * Builds a {@link SerializableTypeOracle} for a given set of root types.
   *
   * @param logger
   * @return a {@link SerializableTypeOracle} for the specified set of root
   *         types
   *
   * @throws UnableToCompleteException if there was not at least one
   *           instantiable type assignable to each of the specified root types
   */
  public SerializableTypeOracle build(TreeLogger logger) throws UnableToCompleteException {
    alreadyCheckedObject = false;

    boolean allSucceeded = true;

    for (Entry<JClassType, TreeLogger> entry : rootTypes.entrySet()) {
      ProblemReport problems = new ProblemReport();
      problems.setContextType(entry.getKey());
      boolean entrySucceeded =
          computeTypeInstantiability(entry.getValue(), entry.getKey(),
              TypePaths.createRootPath(entry.getKey()), problems).hasInstantiableSubtypes();
      if (!entrySucceeded) {
        if (!problems.hasFatalProblems()) {
          logger.log(TreeLogger.ERROR, "'" + entry.getKey().getQualifiedSourceName() +
              "' has no instantiable subtypes");
        } else {
          problems.report(logger, TreeLogger.ERROR, TreeLogger.INFO);
        }
      } else {
        maybeReport(logger, problems);
      }
      allSucceeded &= entrySucceeded & !problems.hasFatalProblems();
    }

    if (!allSucceeded) {
      throw new UnableToCompleteException();
    }
    assertNothingPending();

    // Add covariant arrays in a separate pass. We want to ensure that nothing is pending
    // so that the leaf type's instantiableTypes variable is ready (if it's computed at all)
    // and all of the leaf's subtypes are ready.
    // (Copy values to avoid concurrent modification.)
    List<TypeInfoComputed> ticsToCheck = new ArrayList<TypeInfoComputed>();
    ticsToCheck.addAll(typeToTypeInfoComputed.values());
    for (TypeInfoComputed tic : ticsToCheck) {
      JArrayType type = tic.getType().isArray();
      if (type != null && tic.instantiable) {
        ProblemReport problems = new ProblemReport();
        problems.setContextType(type);

        markArrayTypes(logger, type, tic.getPath(), problems);

        maybeReport(logger, problems);
        allSucceeded &= !problems.hasFatalProblems();
      }
    }

    if (!allSucceeded) {
      throw new UnableToCompleteException();
    }
    assertNothingPending();

    pruneUnreachableTypes();

    logReachableTypes(logger);

    Set<JClassType> possiblyInstantiatedTypes = new TreeSet<JClassType>(JTYPE_COMPARATOR);

    Set<JClassType> fieldSerializableTypes = new TreeSet<JClassType>(JTYPE_COMPARATOR);

    for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) {
      if (!(tic.getType() instanceof JClassType)) {
        continue;
      }
      JClassType type = (JClassType) tic.getType();

      type = type.getErasedType();

      if (type.getLeafType().getQualifiedSourceName().equals("java.lang.JsException")) {
        // JsException is not considered serializable since it is never available at JVM.
        continue;
      }

      if (tic.isInstantiable()) {
        assert (!type.isAbstract() || type.isEnum() != null);

        possiblyInstantiatedTypes.add(type);
      }

      if (tic.isFieldSerializable()) {
        assert (type.isInterface() == null);

        fieldSerializableTypes.add(type);
      }

      if (tic.maybeEnhanced()
          || (enhancedClasses != null && enhancedClasses.contains(type.getQualifiedSourceName()))) {
        logger.log(TreeLogger.WARN, "The class " + type.getQualifiedSourceName() + " has " +
                "JPA/JDO annotations or is explicitly configured as an enhanced class using the " +
                "configuration property rpc.enhancedClasses. This makes the server vulnerable " +
                "to an issue with deserialization of unsafe data. See " +
                "https://github.com/gwtproject/gwt/issues/9709 for more information.");
        type.setEnhanced();
      }
    }

    logSerializableTypes(logger, fieldSerializableTypes);

    return new SerializableTypeOracleImpl(fieldSerializableTypes, possiblyInstantiatedTypes);
  }

  /**
   * Set the {@link PrintWriter} which will receive a detailed log of the types
   * which were examined in order to determine serializability.
   */
  public void setLogOutputWriter(PrintWriter logOutputWriter) {
    this.logOutputWriter = logOutputWriter;
  }

  public void setTypeFilter(TypeFilter typeFilter) {
    this.typeFilter = typeFilter;
    typeParameterExposureComputer.setTypeFilter(typeFilter);
  }

  /**
   * This method determines information about serializing a type with GWT. To do
   * so, it must traverse all subtypes as well as all field types of those
   * types, transitively.
   *
   * It returns a {@link TypeInfoComputed} with the information found.
   *
   * As a side effect, all types needed--plus some--to serialize this type are
   * accumulated in {@link #typeToTypeInfoComputed}. In particular, there will
   * be an entry for any type that has been validated by this method, as a
   * shortcircuit to avoid recomputation.
   *
   * The method is exposed using default access to enable testing.
   */
  TypeInfoComputed computeTypeInstantiability(TreeLogger logger, JType type, TypePath path,
      ProblemReport problems) {
    assert (type != null);
    if (type.isPrimitive() != null) {
      TypeInfoComputed tic = ensureTypeInfoComputed(type, path);
      tic.setInstantiableSubtypes(true);
      tic.setInstantiable(false);
      return tic;
    }

    assert (type instanceof JClassType);

    JClassType classType = (JClassType) type;

    TypeInfoComputed tic = typeToTypeInfoComputed.get(classType);
    if (tic != null && tic.isDone()) {
      // we have an answer already; use it.
      return tic;
    }

    TreeLogger localLogger =
        logger.branch(TreeLogger.DEBUG, classType.getParameterizedQualifiedSourceName(), null);

    JTypeParameter isTypeParameter = classType.isTypeParameter();
    if (isTypeParameter != null) {
      if (typeParametersInRootTypes.contains(isTypeParameter)) {
        return computeTypeInstantiability(localLogger, isTypeParameter.getFirstBound(), TypePaths
            .createTypeParameterInRootPath(path, isTypeParameter), problems);
      }

      /*
       * This type parameter was not in a root type and therefore it is the
       * caller's responsibility to deal with it. We assume that it is
       * indirectly instantiable here.
       */
      tic = ensureTypeInfoComputed(classType, path);
      tic.setInstantiableSubtypes(true);
      tic.setInstantiable(false);
      return tic;
    }

    JWildcardType isWildcard = classType.isWildcard();
    if (isWildcard != null) {
      boolean success = true;
      for (JClassType bound : isWildcard.getUpperBounds()) {
        success &=
            computeTypeInstantiability(localLogger, bound, path, problems)
                .hasInstantiableSubtypes();
      }
      tic = ensureTypeInfoComputed(classType, path);
      tic.setInstantiableSubtypes(success);
      tic.setInstantiable(false);
      return tic;
    }

    JArrayType isArray = classType.isArray();
    if (isArray != null) {
      TypeInfoComputed arrayTic = checkArrayInstantiable(localLogger, isArray, path, problems);
      assert typeToTypeInfoComputed.get(classType) != null;
      return arrayTic;
    }

    if (classType == typeOracle.getJavaLangObject()) {
      /*
       * Report an error if the type is or erases to Object since this violates
       * our restrictions on RPC. Should be fatal, but I worry users may have
       * Object-using code they can't readily get out of the class hierarchy.
       */
      problems.add(classType, "In order to produce smaller client-side code, 'Object' is not "
          + "allowed; please use a more specific type", Priority.DEFAULT);
      tic = ensureTypeInfoComputed(classType, path);
      tic.setInstantiable(false);
      return tic;
    }

    if (classType.isRawType() != null) {
      localLogger
          .log(
              TreeLogger.DEBUG,
              "Type '"
                  + classType.getQualifiedSourceName()
                  + "' should be parameterized to help the compiler produce the smallest code size possible for your module",
              null);
    }

    JClassType originalType = (JClassType) type;

    // TreeLogger subtypesLogger = localLogger.branch(TreeLogger.DEBUG,
    // "Analyzing subclasses:", null);
    tic = ensureTypeInfoComputed(classType, path);
    Set<JClassType> instantiableTypes = new HashSet<JClassType>();
    boolean anySubtypes =
        checkSubtypes(localLogger, originalType, instantiableTypes, path, problems);
    if (!tic.isDone()) {
      tic.setInstantiableSubtypes(anySubtypes);
      tic.setInstantiable(false);
    }
    // Don't publish this until complete to ensure nobody depends on partial results.
    tic.instantiableTypes = instantiableTypes;
    return tic;
  }

  int getTypeParameterExposure(JGenericType type, int index) {
    return getFlowInfo(type, index).getExposure();
  }

  /**
   * Returns <code>true</code> if the fields of the type should be considered
   * for serialization.
   *
   * Default access to allow for testing.
   */
  boolean shouldConsiderFieldsForSerialization(JClassType type, ProblemReport problems) {
    return shouldConsiderFieldsForSerialization(type, typeFilter, problems);
  }

  private void assertNothingPending() {
    if (getClass().desiredAssertionStatus()) {
      for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) {
        assert (!tic.isPendingInstantiable());
      }
    }
  }

  /**
   * Consider any subtype of java.lang.Object which qualifies for serialization.
   */
  private void checkAllSubtypesOfObject(TreeLogger logger, TypePath parent, ProblemReport problems) {
    if (alreadyCheckedObject) {
      return;
    }
    alreadyCheckedObject = true;

    /*
     * This will pull in the world and the set of serializable types will be
     * larger than it needs to be. We exclude types that do not qualify for
     * serialization to avoid generating false errors due to types that do not
     * qualify for serialization and have no serializable subtypes.
     */
    TreeLogger localLogger =
        logger.branch(TreeLogger.WARN,
            "Checking all subtypes of Object which qualify for serialization", null);
    JClassType[] allTypes = typeOracle.getJavaLangObject().getSubtypes();
    for (JClassType cls : allTypes) {
      if (isDeclaredSerializable(cls)) {
        computeTypeInstantiability(localLogger, cls, TypePaths.createSubtypePath(parent, cls,
            typeOracle.getJavaLangObject()), problems);
      }
    }
  }

  private TypeInfoComputed checkArrayInstantiable(TreeLogger logger, JArrayType array,
      TypePath path, ProblemReport problems) {

    JType leafType = array.getLeafType();
    JWildcardType leafWild = leafType.isWildcard();
    if (leafWild != null) {
      JArrayType arrayType = getArrayType(typeOracle, array.getRank(), leafWild.getUpperBound());
      return checkArrayInstantiable(logger, arrayType, path, problems);
    }

    TypeInfoComputed tic = ensureTypeInfoComputed(array, path);
    if (tic.isDone() || tic.isPendingInstantiable()) {
      return tic;
    }
    tic.setPendingInstantiable();

    JTypeParameter isLeafTypeParameter = leafType.isTypeParameter();
    if (isLeafTypeParameter != null && !typeParametersInRootTypes.contains(isLeafTypeParameter)) {
      // Don't deal with non root type parameters, but make a TIC entry to
      // save time if it recurs. We assume they're indirectly instantiable.
      tic.setInstantiableSubtypes(true);
      tic.setInstantiable(false);
      return tic;
    }

    if (!isAllowedByFilter(array, problems)) {
      // Don't deal with filtered out types either, but make a TIC entry to
      // save time if it recurs. We assume they're not instantiable.
      tic.setInstantiable(false);
      return tic;
    }

    // An array is instantiable provided that any leaf subtype is instantiable.
    // (Ignores the possibility of empty arrays of non-instantiable types.)

    TreeLogger branch = logger.branch(TreeLogger.DEBUG, "Analyzing component type:", null);

    TypeInfoComputed leafTic =
        computeTypeInstantiability(branch, leafType, TypePaths
            .createArrayComponentPath(array, path), problems);
    boolean succeeded = leafTic.hasInstantiableSubtypes();

    tic.setInstantiable(succeeded);
    return tic;
  }

  /**
   * Returns <code>true</code> if the declared fields of this type are all
   * instantiable. As a side-effect it fills in {@link TypeInfoComputed} for all
   * necessary types.
   */
  private boolean checkDeclaredFields(TreeLogger logger, TypeInfoComputed typeInfo,
      TypePath parent, ProblemReport problems) {

    JClassType classOrInterface = (JClassType) typeInfo.getType();
    if (classOrInterface.isEnum() != null) {
      // The fields of an enum are never serialized; they are always okay.
      return true;
    }

    JClassType baseType = getBaseType(classOrInterface);

    boolean allSucceeded = true;
    // TODO: Propagating the constraints will produce better results as long
    // as infinite expansion can be avoided in the process.
    JField[] fields = baseType.getFields();
    if (fields.length > 0) {
      TreeLogger localLogger =
          logger.branch(TreeLogger.DEBUG, "Analyzing the fields of type '"
              + classOrInterface.getParameterizedQualifiedSourceName()
              + "' that qualify for serialization", null);

      for (JField field : fields) {
        if (!shouldConsiderForSerialization(localLogger, context, field)) {
          continue;
        }

        TreeLogger fieldLogger = localLogger.branch(TreeLogger.DEBUG, field.toString(), null);
        JType fieldType = field.getType();

        TypePath path = TypePaths.createFieldPath(parent, field);
        if (typeInfo.isManuallySerializable()
            && fieldType.getLeafType() == typeOracle.getJavaLangObject()) {
          checkAllSubtypesOfObject(fieldLogger.branch(TreeLogger.WARN,
              "Object was reached from a manually serializable type", null), path, problems);
        } else {
          allSucceeded &=
              computeTypeInstantiability(fieldLogger, fieldType, path, problems)
                  .hasInstantiableSubtypes();
        }
      }
    }

    boolean succeeded = allSucceeded || typeInfo.isManuallySerializable();
    if (succeeded) {
      typeInfo.setFieldSerializable();
    }

    return succeeded;
  }

  private boolean checkSubtype(TreeLogger logger, JClassType classOrInterface,
      JClassType originalType, TypePath parent, ProblemReport problems) {
    if (classOrInterface.isEnum() != null) {
      // The fields of an enum are never serialized; they are always okay.
      return true;
    }

    JParameterizedType isParameterized = classOrInterface.isParameterized();
    if (isParameterized != null) {
      if (isRawMapOrRawCollection(classOrInterface)) {
        /*
         * Backwards compatibility. Raw collections or maps force all object
         * subtypes to be considered.
         */
        checkAllSubtypesOfObject(logger, parent, problems);
      } else {
        TreeLogger paramsLogger =
            logger.branch(TreeLogger.DEBUG, "Checking parameters of '"
                + isParameterized.getParameterizedQualifiedSourceName() + "'");

        for (JTypeParameter param : isParameterized.getBaseType().getTypeParameters()) {
          if (!checkTypeArgument(paramsLogger, isParameterized.getBaseType(), param.getOrdinal(),
              isParameterized.getTypeArgs()[param.getOrdinal()], parent, problems)) {
            return false;
          }
        }
      }
    }

    // Check all super type fields first (recursively).
    JClassType superType = classOrInterface.getSuperclass();
    if (superType != null && superType.isRawType() != null) {
      superType = superType.isRawType().asParameterizedByWildcards();
    }

    if (superType != null && isDeclaredSerializable(superType)) {
      superType = constrainTypeBy(superType, originalType);
      if (superType == null) {
        return false;
      }

      boolean superTypeOk = false;
      superTypeOk =
          checkSubtype(logger, superType, originalType, TypePaths.createSupertypePath(parent,
              superType, classOrInterface), problems);

      /*
       * If my super type did not check out, then I am not instantiable and we
       * should error out... UNLESS I amdirectly serializable myself, in which
       * case it's ok for me to be the root of a new instantiable hierarchy.
       */
      if (!superTypeOk && !isDirectlySerializable(classOrInterface)) {
        return false;
      }
    }

    TypeInfoComputed tic = ensureTypeInfoComputed(classOrInterface, parent);
    return checkDeclaredFields(logger, tic, parent, problems);
  }

  /**
   * Returns <code>true</code> if this type or one of its subtypes is
   * instantiable relative to a known base type.
   */
  private boolean checkSubtypes(TreeLogger logger, JClassType originalType,
      Set<JClassType> instSubtypes, TypePath path, ProblemReport problems) {
    JRealClassType baseType = getBaseType(originalType);
    TreeLogger computationLogger =
        logger.branch(TreeLogger.DEBUG, "Finding possibly instantiable subtypes");
    List<JClassType> candidates =
        getPossiblyInstantiableSubtypes(computationLogger, baseType, problems);
    boolean anySubtypes = false;

    TreeLogger verificationLogger = logger.branch(TreeLogger.DEBUG, "Verifying instantiability");
    for (JClassType candidate : candidates) {
      if (getBaseType(candidate) == baseType && originalType.isRawType() == null) {
        // Don't rely on the constrainer when we have perfect information.
        candidate = originalType;
      } else {
        candidate = constrainTypeBy(candidate, originalType);
        if (candidate == null) {
          continue;
        }
      }

      if (!isAllowedByFilter(candidate, problems)) {
        continue;
      }

      TypePath subtypePath = TypePaths.createSubtypePath(path, candidate, originalType);
      TypeInfoComputed tic = ensureTypeInfoComputed(candidate, subtypePath);
      if (tic.isDone()) {
        if (tic.isInstantiable()) {
          anySubtypes = true;
          instSubtypes.add(candidate);
        }
        continue;
      } else if (tic.isPendingInstantiable()) {
        anySubtypes = true;
        instSubtypes.add(candidate);
        continue;
      }
      tic.setPendingInstantiable();

      TreeLogger subtypeLogger =
          verificationLogger.branch(TreeLogger.DEBUG, candidate
              .getParameterizedQualifiedSourceName());
      boolean instantiable =
          checkSubtype(subtypeLogger, candidate, originalType, subtypePath, problems);
      anySubtypes |= instantiable;
      tic.setInstantiable(instantiable);

      if (instantiable) {
        subtypeLogger.branch(TreeLogger.DEBUG, "Is instantiable");
      }

      // Note we are leaving hasInstantiableSubtypes() as false which might be
      // wrong but it is only used by arrays and thus it will never be looked at
      // for this tic.
      if (instantiable) {
        instSubtypes.add(candidate);
      }
    }

    return anySubtypes;
  }

  /**
   * Check the argument to a parameterized type to see if it will make the type
   * it is applied to be serializable. As a side effect, populates
   * {@link #typeToTypeInfoComputed} in the same way as
   * {@link #computeTypeInstantiability}.
   *
   * @param baseType - The generic type the parameter is on
   * @param paramIndex - The index of the parameter in the generic type
   * @param typeArg - An upper bound on the actual argument being applied to the
   *          generic type
   *
   * @return Whether the a parameterized type can be serializable if
   *         <code>baseType</code> is the base type and the
   *         <code>paramIndex</code>th type argument is a subtype of
   *         <code>typeArg</code>.
   */
  private boolean checkTypeArgument(TreeLogger logger, JGenericType baseType, int paramIndex,
      JClassType typeArg, TypePath parent, ProblemReport problems) {
    JWildcardType isWildcard = typeArg.isWildcard();
    if (isWildcard != null) {
      return checkTypeArgument(logger, baseType, paramIndex, isWildcard.getUpperBound(), parent,
          problems);
    }

    JArrayType typeArgAsArray = typeArg.isArray();
    if (typeArgAsArray != null) {
      JTypeParameter parameterOfTypeArgArray = typeArgAsArray.getLeafType().isTypeParameter();
      if (parameterOfTypeArgArray != null) {
        JGenericType declaringClass = parameterOfTypeArgArray.getDeclaringClass();
        if (declaringClass != null) {
          TypeParameterFlowInfo flowInfoForArrayParam =
              getFlowInfo(declaringClass, parameterOfTypeArgArray.getOrdinal());
          TypeParameterFlowInfo otherFlowInfo = getFlowInfo(baseType, paramIndex);
          if (otherFlowInfo.getExposure() >= 0
              && otherFlowInfo.isTransitivelyAffectedBy(flowInfoForArrayParam)) {
            problems.add(baseType, "Cannot serialize type '"
                + baseType.getParameterizedQualifiedSourceName()
                + "' when given an argument of type '"
                + typeArg.getParameterizedQualifiedSourceName()
                + "' because it appears to require serializing arrays " + "of unbounded dimension",
                Priority.DEFAULT);
            return false;
          }
        }
      }
    }

    TypePath path = TypePaths.createTypeArgumentPath(parent, baseType, paramIndex, typeArg);
    int exposure = getTypeParameterExposure(baseType, paramIndex);
    switch (exposure) {
      case TypeParameterExposureComputer.EXPOSURE_DIRECT: {
        TreeLogger branch =
            logger.branch(TreeLogger.DEBUG, "Checking type argument " + paramIndex + " of type '"
                + baseType.getParameterizedQualifiedSourceName()
                + "' because it is directly exposed in this type or in one of its subtypes");
        return computeTypeInstantiability(branch, typeArg, path, problems)
            .hasInstantiableSubtypes()
            || mightNotBeExposed(baseType, paramIndex);
      }
      case TypeParameterExposureComputer.EXPOSURE_NONE:
        // Ignore this argument
        logger.log(TreeLogger.DEBUG, "Ignoring type argument " + paramIndex + " of type '"
            + baseType.getParameterizedQualifiedSourceName()
            + "' because it is not exposed in this or any subtype");
        return true;

      default: {
        assert (exposure >= TypeParameterExposureComputer.EXPOSURE_MIN_BOUNDED_ARRAY);
        problems.add(getArrayType(typeOracle, exposure, typeArg), "Checking type argument "
            + paramIndex + " of type '" + baseType.getParameterizedQualifiedSourceName()
            + "' because it is exposed as an array with a maximum dimension of " + exposure
            + " in this type or one of its subtypes", Priority.AUXILIARY);
        return computeTypeInstantiability(logger, getArrayType(typeOracle, exposure, typeArg),
            path, problems).hasInstantiableSubtypes()
            || mightNotBeExposed(baseType, paramIndex);
      }
    }
  }

  /**
   * If <code>type</code>'s base class does not inherit from
   * <code>superType</code>'s base class, then return <code>type</code>
   * unchanged. If it does, then return a subtype of <code>type</code> that
   * includes all values in both <code>type</code> and <code>superType</code>.
   * If there are definitely no such values, return <code>null</code>.
   */
  private JClassType constrainTypeBy(JClassType type, JClassType superType) {
    return typeConstrainer.constrainTypeBy(type, superType);
  }

  private TypeParameterFlowInfo getFlowInfo(JGenericType type, int index) {
    return typeParameterExposureComputer.computeTypeParameterExposure(type, index);
  }

  /**
   * Returns the subtypes of a given base type as parameterized by wildcards.
   */
  private List<JClassType> getPossiblyInstantiableSubtypes(TreeLogger logger,
      JRealClassType baseType, ProblemReport problems) {
    assert (baseType == getBaseType(baseType));

    List<JClassType> possiblyInstantiableTypes = new ArrayList<JClassType>();
    if (baseType == typeOracle.getJavaLangObject()) {
      return possiblyInstantiableTypes;
    }

    List<JClassType> candidates = new ArrayList<JClassType>();
    candidates.add(baseType);
    List<JClassType> subtypes = Arrays.asList(baseType.getSubtypes());
    candidates.addAll(subtypes);

    for (JClassType subtype : candidates) {
      JClassType subtypeBase = getBaseType(subtype);
      if (maybeInstantiable(logger, subtypeBase, problems)) {
        /*
         * Convert the generic type into a parameterization that only includes
         * wildcards.
         */
        JGenericType isGeneric = subtype.isGenericType();
        if (isGeneric != null) {
          subtype = isGeneric.asParameterizedByWildcards();
        } else {
          assert (subtype instanceof JRealClassType);
        }

        possiblyInstantiableTypes.add(subtype);
      }
    }

    if (possiblyInstantiableTypes.size() == 0) {
      String possibilities[] = new String[candidates.size()];
      for (int i = 0; i < possibilities.length; i++) {
        JClassType subtype = candidates.get(i);
        String worstMessage = problems.getWorstMessageForType(subtype);
        if (worstMessage == null) {
          possibilities[i] =
              "   subtype " + subtype.getParameterizedQualifiedSourceName()
                  + " is not instantiable";
        } else {
          // message with have the form "FQCN some-problem-description"
          possibilities[i] = "   subtype " + worstMessage;
        }
      }
      problems.add(baseType, baseType.getParameterizedQualifiedSourceName()
          + " has no available instantiable subtypes.", Priority.DEFAULT, possibilities);
    }
    return possiblyInstantiableTypes;
  }

  private TypeInfoComputed ensureTypeInfoComputed(JType type, TypePath path) {
    TypeInfoComputed tic = typeToTypeInfoComputed.get(type);
    if (tic == null) {
      tic = new TypeInfoComputed(type, path, typeOracle);
      typeToTypeInfoComputed.put(type, tic);
    }
    return tic;
  }

  private boolean isAllowedByFilter(JClassType classType, ProblemReport problems) {
    return isAllowedByFilter(typeFilter, classType, problems);
  }

  /**
   * Returns <code>true</code> if the type is Collection<? extends Object> or
   * Map<? extends Object, ? extends Object>.
   */
  private boolean isRawMapOrRawCollection(JClassType type) {
    if (type.asParameterizationOf(collectionClass) == collectionClass.asParameterizedByWildcards()) {
      return true;
    }

    if (type.asParameterizationOf(mapClass) == mapClass.asParameterizedByWildcards()) {
      return true;
    }

    return false;
  }

  private void logPath(TreeLogger logger, TypePath path) {
    if (path == null) {
      return;
    }

    if (logger.isLoggable(TreeLogger.DEBUG)) {
      logger.log(TreeLogger.DEBUG, path.toString());
    }
    logPath(logger, path.getParent());
  }

  private void logReachableTypes(TreeLogger logger) {
    if (!context.isProdMode() && !logger.isLoggable(TreeLogger.DEBUG)) {
      return;
    }

    if (logOutputWriter != null) {
      // Route the TreeLogger output to an output stream.
      PrintWriterTreeLogger printWriterTreeLogger = new PrintWriterTreeLogger(logOutputWriter);
      printWriterTreeLogger.setMaxDetail(TreeLogger.ALL);
      logger = printWriterTreeLogger;
    }

    if (logger.isLoggable(TreeLogger.DEBUG)) {
      logger.log(TreeLogger.DEBUG, "Reachable types computed on: " + new Date().toString());
    }
    Set<JType> keySet = typeToTypeInfoComputed.keySet();
    JType[] types = keySet.toArray(new JType[0]);
    Arrays.sort(types, JTYPE_COMPARATOR);

    for (JType type : types) {
      TypeInfoComputed tic = typeToTypeInfoComputed.get(type);
      assert (tic != null);

      TreeLogger typeLogger =
          logger.branch(TreeLogger.DEBUG, tic.getType().getParameterizedQualifiedSourceName());
      TreeLogger serializationStatus = typeLogger.branch(TreeLogger.DEBUG, "Serialization status");
      if (tic.isInstantiable()) {
        serializationStatus.branch(TreeLogger.DEBUG, "Instantiable");
      } else {
        if (tic.isFieldSerializable()) {
          serializationStatus.branch(TreeLogger.DEBUG, "Field serializable");
        } else {
          serializationStatus.branch(TreeLogger.DEBUG, "Not serializable");
        }
      }

      TreeLogger pathLogger = typeLogger.branch(TreeLogger.DEBUG, "Path");

      logPath(pathLogger, tic.getPath());
      logger.log(TreeLogger.DEBUG, "");
    }

    if (logOutputWriter != null) {
      logOutputWriter.flush();
    }
  }

  /**
   * Mark arrays of <code>leafType</code> as instantiable, for arrays of
   * dimension up to <code>maxRank</code>.
   */
  private void markArrayTypesInstantiable(JType leafType, int maxRank, TypePath path) {
    for (int rank = 1; rank <= maxRank; ++rank) {
      JArrayType covariantArray = getArrayType(typeOracle, rank, leafType);

      TypeInfoComputed covariantArrayTic = ensureTypeInfoComputed(covariantArray, path);
      covariantArrayTic.setInstantiable(true);
    }
  }

  /**
   * Marks all covariant and lesser-ranked arrays as instantiable for all leaf types between
   * the given array's leaf type and its instantiable subtypes. (Note: this adds O(S * R)
   * array types to the output where S is the number of subtypes and R is the rank.)
   * Prerequisite: The leaf type's tic and its subtypes must already be created.
   * @see #checkArrayInstantiable
   */
  private void markArrayTypes(TreeLogger logger, JArrayType array, TypePath path,
      ProblemReport problems) {
    logger = logger.branch(TreeLogger.DEBUG, "Adding array types for " + array);

    JType leafType = array.getLeafType();
    JTypeParameter isLeafTypeParameter = leafType.isTypeParameter();
    if (isLeafTypeParameter != null) {
      if (typeParametersInRootTypes.contains(isLeafTypeParameter)) {
        leafType = isLeafTypeParameter.getFirstBound(); // to match computeTypeInstantiability
      } else {
        // skip non-root leaf parameters, to match checkArrayInstantiable
        return;
      }
    }

    TypeInfoComputed leafTic = typeToTypeInfoComputed.get(leafType);
    if (leafTic == null) {
      problems.add(array, "internal error: leaf type not computed: " +
          leafType.getQualifiedSourceName(), Priority.FATAL);
      return;
    }

    if (leafType.isClassOrInterface() == null) {
      // Simple case: no covariance, just lower ranks.
      assert leafType.isPrimitive() != null;
      markArrayTypesInstantiable(leafType, array.getRank(), path);
      return;
    }

    JRealClassType baseClass = getBaseType(leafType.isClassOrInterface());

    TreeLogger covariantArrayLogger =
        logger.branch(TreeLogger.DEBUG, "Covariant array types:");

    Set<JClassType> instantiableTypes = leafTic.instantiableTypes;
    if (instantiableTypes == null) {
      // The types are there (due to a supertype) but the Set wasn't computed, so compute it now.
      instantiableTypes = new HashSet<JClassType>();
      List<JClassType> candidates =
          getPossiblyInstantiableSubtypes(logger, baseClass, problems);
      for (JClassType candidate : candidates) {
        TypeInfoComputed tic = typeToTypeInfoComputed.get(candidate);
        if (tic != null && tic.instantiable) {
          instantiableTypes.add(candidate);
        }
      }
    }
    for (JClassType instantiableType : TypeHierarchyUtils.getAllTypesBetweenRootTypeAndLeaves(
        baseClass, instantiableTypes)) {
      if (!isAccessibleToSerializer(instantiableType)) {
        // Skip types that are not accessible from a serializer
        continue;
      }

      if (covariantArrayLogger.isLoggable(TreeLogger.DEBUG)) {
        covariantArrayLogger.branch(TreeLogger.DEBUG, getArrayType(typeOracle, array.getRank(),
            instantiableType).getParameterizedQualifiedSourceName());
      }

      markArrayTypesInstantiable(instantiableType, array.getRank(), path);
    }
  }

  private boolean maybeInstantiable(TreeLogger logger, JClassType type, ProblemReport problems) {
    boolean success =
        canBeInstantiated(type, problems) && shouldConsiderFieldsForSerialization(type, problems);
    if (success) {
      if (logger.isLoggable(TreeLogger.DEBUG)) {
        logger.log(TreeLogger.DEBUG, type.getParameterizedQualifiedSourceName()
            + " might be instantiable");
      }
    }
    return success;
  }

  /**
   * Report problems if they are fatal or we're debugging.
   */
  private void maybeReport(TreeLogger logger, ProblemReport problems) {
    if (problems.hasFatalProblems()) {
      problems.reportFatalProblems(logger, TreeLogger.ERROR);
    }
    // if entrySucceeded, there may still be "warning" problems, but too
    // often they're expected (e.g. non-instantiable subtypes of List), so
    // we log them at DEBUG.
    // TODO(fabbott): we could blacklist or graylist those types here, so
    // instantiation during code generation would flag them for us.
    problems.report(logger, TreeLogger.DEBUG, TreeLogger.DEBUG);
  }

  private boolean mightNotBeExposed(JGenericType baseType, int paramIndex) {
    TypeParameterFlowInfo flowInfo = getFlowInfo(baseType, paramIndex);
    return flowInfo.getMightNotBeExposed() || isManuallySerializable(baseType);
  }

  /**
   * Remove serializable types that were visited due to speculative paths but
   * are not really needed for serialization.
   *
   * NOTE: This is currently much more limited than it should be. For example, a
   * path sensitive prune could remove instantiable types also.
   */
  private void pruneUnreachableTypes() {
    /*
     * Record all supertypes of any instantiable type, whether or not they are
     * field serialziable.
     */
    Set<JType> supersOfInstantiableTypes = new LinkedHashSet<JType>();
    for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) {
      if (tic.isInstantiable() && tic.getType() instanceof JClassType) {
        JClassType sup = (JClassType) tic.getType().getErasedType();
        while (sup != null) {
          supersOfInstantiableTypes.add(sup.getErasedType());
          sup = sup.getErasedType().getSuperclass();
        }
      }
    }

    /*
     * Record any field serializable type that is not in the supers of any
     * instantiable type.
     */
    Set<JType> toKill = new LinkedHashSet<JType>();
    for (TypeInfoComputed tic : typeToTypeInfoComputed.values()) {
      if (tic.isFieldSerializable()
          && !supersOfInstantiableTypes.contains(tic.getType().getErasedType())) {
        toKill.add(tic.getType());
      }
    }

    /*
     * Remove any field serializable supers that cannot be reached from an
     * instantiable type.
     */
    for (JType type : toKill) {
      typeToTypeInfoComputed.remove(type);
    }
  }
}
