/** Examine a RequestContext method to see if it returns a transportable type. */
  private boolean validateContextMethodAndSetDataType(
      RequestMethod.Builder methodBuilder, JMethod method, boolean allowSetters)
      throws UnableToCompleteException {
    JClassType requestReturnType = method.getReturnType().isInterface();
    JClassType invocationReturnType;
    if (requestReturnType == null) {
      // Primitive return type
      poison(badContextReturnType(method, requestInterface, instanceRequestInterface));
      return false;
    }

    if (instanceRequestInterface.isAssignableFrom(requestReturnType)) {
      // Instance method invocation
      JClassType[] params =
          ModelUtils.findParameterizationOf(instanceRequestInterface, requestReturnType);
      methodBuilder.setInstanceType(getEntityProxyType(params[0]));
      invocationReturnType = params[1];
    } else if (requestInterface.isAssignableFrom(requestReturnType)) {
      // Static method invocation
      JClassType[] params = ModelUtils.findParameterizationOf(requestInterface, requestReturnType);
      invocationReturnType = params[0];

    } else {
      // Unhandled return type, must be something random
      poison(badContextReturnType(method, requestInterface, instanceRequestInterface));
      return false;
    }

    // Validate the parameters
    boolean paramsOk = true;
    JParameter[] params = method.getParameters();
    for (int i = 0; i < params.length; ++i) {
      JParameter param = params[i];
      paramsOk =
          validateTransportableType(new RequestMethod.Builder(), param.getType(), false)
              && paramsOk;
    }

    // Validate any extra properties on the request type
    for (JMethod maybeSetter : requestReturnType.getInheritableMethods()) {
      if (JBeanMethod.SET.matches(maybeSetter) || JBeanMethod.SET_BUILDER.matches(maybeSetter)) {
        if (allowSetters) {
          methodBuilder.addExtraSetter(maybeSetter);
        } else {
          poison(noSettersAllowed(maybeSetter));
        }
      }
    }
    return validateTransportableType(methodBuilder, invocationReturnType, true);
  }
  /** Examines a type to see if it can be transported. */
  private boolean validateTransportableType(
      RequestMethod.Builder methodBuilder, JType type, boolean requireObject)
      throws UnableToCompleteException {
    JClassType transportedClass = type.isClassOrInterface();
    if (transportedClass == null) {
      if (requireObject) {
        poison(
            "The type %s cannot be transported by RequestFactory as" + " a return type",
            type.getQualifiedSourceName());
        return false;
      } else {
        // Primitives always ok
        return true;
      }
    }

    if (ModelUtils.isValueType(oracle, transportedClass)
        || splittableType.equals(transportedClass)) {
      // Simple values, like Integer and String
      methodBuilder.setValueType(true);
    } else if (entityProxyInterface.isAssignableFrom(transportedClass)
        || valueProxyInterface.isAssignableFrom(transportedClass)) {
      // EntityProxy and ValueProxy return types
      methodBuilder.setEntityType(getEntityProxyType(transportedClass));
    } else if (collectionInterface.isAssignableFrom(transportedClass)) {
      // Only allow certain collections for now
      JParameterizedType parameterized = transportedClass.isParameterized();
      if (parameterized == null) {
        poison("Requests that return collections of List or Set must be parameterized");
        return false;
      }
      if (listInterface.equals(parameterized.getBaseType())) {
        methodBuilder.setCollectionType(CollectionType.LIST);
      } else if (setInterface.equals(parameterized.getBaseType())) {
        methodBuilder.setCollectionType(CollectionType.SET);
      } else {
        poison(
            "Requests that return collections may be declared with" + " %s or %s only",
            listInterface.getQualifiedSourceName(), setInterface.getQualifiedSourceName());
        return false;
      }
      // Also record the element type in the method builder
      JClassType elementType =
          ModelUtils.findParameterizationOf(collectionInterface, transportedClass)[0];
      methodBuilder.setCollectionElementType(elementType);
      validateTransportableType(methodBuilder, elementType, requireObject);
    } else if (mapInterface.isAssignableFrom(transportedClass)) {
      JParameterizedType parameterized = transportedClass.isParameterized();
      if (parameterized == null) {
        poison("Requests that return Maps must be parameterized");
        return false;
      }
      if (mapInterface.equals(parameterized.getBaseType())) {
        methodBuilder.setCollectionType(CollectionType.MAP);
      } else {
        poison(
            "Requests that return maps may be declared with" + " %s only",
            mapInterface.getQualifiedSourceName());
        return false;
      }
      // Also record the element type in the method builder
      JClassType[] params = ModelUtils.findParameterizationOf(mapInterface, transportedClass);
      JClassType keyType = params[0];
      JClassType valueType = params[1];
      methodBuilder.setMapKeyType(keyType);
      methodBuilder.setMapValueType(valueType);
      validateTransportableType(methodBuilder, keyType, requireObject);
      validateTransportableType(methodBuilder, valueType, requireObject);
    } else {
      // Unknown type, fail
      poison("Invalid Request parameterization %s", transportedClass.getQualifiedSourceName());
      return false;
    }
    methodBuilder.setDataType(transportedClass);
    return true;
  }
  private EntityProxyModel getEntityProxyType(JClassType entityProxyType)
      throws UnableToCompleteException {
    entityProxyType = ModelUtils.ensureBaseType(entityProxyType);
    EntityProxyModel toReturn = peers.get(entityProxyType);
    if (toReturn == null) {
      EntityProxyModel.Builder inProgress = peerBuilders.get(entityProxyType);
      if (inProgress != null) {
        toReturn = inProgress.peek();
      }
    }
    if (toReturn == null) {
      EntityProxyModel.Builder builder = new EntityProxyModel.Builder();
      peerBuilders.put(entityProxyType, builder);

      // Validate possible super-proxy types first
      for (JClassType supertype : entityProxyType.getFlattenedSupertypeHierarchy()) {
        List<EntityProxyModel> superTypes = new ArrayList<EntityProxyModel>();
        if (supertype != entityProxyType && shouldAttemptProxyValidation(supertype)) {
          superTypes.add(getEntityProxyType(supertype));
        }
        builder.setSuperProxyTypes(superTypes);
      }

      builder.setQualifiedBinaryName(ModelUtils.getQualifiedBaseBinaryName(entityProxyType));
      builder.setQualifiedSourceName(ModelUtils.getQualifiedBaseSourceName(entityProxyType));
      if (entityProxyInterface.isAssignableFrom(entityProxyType)) {
        builder.setType(Type.ENTITY);
      } else if (valueProxyInterface.isAssignableFrom(entityProxyType)) {
        builder.setType(Type.VALUE);
      } else {
        poison(
            "The type %s is not assignable to either %s or %s",
            entityProxyInterface.getQualifiedSourceName(),
            valueProxyInterface.getQualifiedSourceName());
        // Cannot continue, since knowing the behavior is crucial
        die(poisonedMessage());
      }

      // Get the server domain object type
      ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class);
      ProxyForName proxyForName = entityProxyType.getAnnotation(ProxyForName.class);
      JsonRpcProxy jsonRpcProxy = entityProxyType.getAnnotation(JsonRpcProxy.class);
      if (proxyFor == null && proxyForName == null && jsonRpcProxy == null) {
        poison(
            "The %s type does not have a @%s, @%s, or @%s annotation",
            entityProxyType.getQualifiedSourceName(),
            ProxyFor.class.getSimpleName(),
            ProxyForName.class.getSimpleName(),
            JsonRpcProxy.class.getSimpleName());
      }

      // Look at the methods declared on the EntityProxy
      List<RequestMethod> requestMethods = new ArrayList<RequestMethod>();
      Map<String, JMethod> duplicatePropertyGetters = new HashMap<String, JMethod>();
      for (JMethod method : entityProxyType.getInheritableMethods()) {
        if (method.getEnclosingType().equals(entityProxyInterface)) {
          // Ignore methods on EntityProxy
          continue;
        }
        RequestMethod.Builder methodBuilder = new RequestMethod.Builder();
        methodBuilder.setDeclarationMethod(entityProxyType, method);

        JType transportedType;
        String name = method.getName();
        if (JBeanMethod.GET.matches(method)) {
          transportedType = method.getReturnType();
          String propertyName = JBeanMethod.GET.inferName(method);
          JMethod previouslySeen = duplicatePropertyGetters.get(propertyName);
          if (previouslySeen == null) {
            duplicatePropertyGetters.put(propertyName, method);
          } else {
            poison(
                "Duplicate accessors for property %s: %s() and %s()",
                propertyName, previouslySeen.getName(), method.getName());
          }

        } else if (JBeanMethod.SET.matches(method) || JBeanMethod.SET_BUILDER.matches(method)) {
          transportedType = method.getParameters()[0].getType();

        } else if (name.equals("stableId") && method.getParameters().length == 0) {
          // Ignore any overload of stableId
          continue;
        } else {
          poison("The method %s is neither a getter nor a setter", method.getReadableDeclaration());
          continue;
        }
        validateTransportableType(methodBuilder, transportedType, false);
        RequestMethod requestMethod = methodBuilder.build();
        requestMethods.add(requestMethod);
      }
      builder
          .setExtraTypes(checkExtraTypes(entityProxyType, false))
          .setRequestMethods(requestMethods);

      toReturn = builder.build();
      peers.put(entityProxyType, toReturn);
      peerBuilders.remove(entityProxyType);
    }
    return toReturn;
  }
  private RequestQueueModel collectModel(
      TreeLogger logger, GeneratorContext context, JClassType toGenerate)
      throws UnableToCompleteException {
    RequestQueueModel.Builder rqBuilder = new RequestQueueModel.Builder();

    TypeOracle typeOracle = context.getTypeOracle();
    JClassType requestQueue = typeOracle.findType(RequestQueue.class.getName());
    JClassType asyncCallback = typeOracle.findType(AsyncCallback.class.getName());
    JClassType voidType = typeOracle.findType(Void.class.getName());
    rqBuilder.setRequestQueueInterfaceName(toGenerate.getParameterizedQualifiedSourceName());

    AsyncServiceModel.Builder serviceBuilder = new AsyncServiceModel.Builder();
    for (JMethod m : toGenerate.getMethods()) {
      TreeLogger serviceLogger =
          logger.branch(Type.DEBUG, "Reading async service " + m.getReadableDeclaration());

      // Skip those defined at RequestQueue
      if (m.getEnclosingType().equals(requestQueue)) {
        continue;
      }
      JClassType returnType = m.getReturnType().isClassOrInterface();
      if (returnType == null) {
        serviceLogger.log(Type.ERROR, "Unexpected method return type " + returnType);
        throw new UnableToCompleteException();
      }

      serviceBuilder.setAsyncServiceInterfaceName(returnType.getParameterizedQualifiedSourceName());
      serviceBuilder.setDeclaredMethodName(m.getName());

      Service serviceAnnotation = m.getAnnotation(Service.class);
      if (serviceAnnotation == null) {
        serviceLogger.log(Type.ERROR, "Missing @Service annotation");
        throw new UnableToCompleteException();
      }
      serviceBuilder.setService(serviceAnnotation.value().getName());

      AsyncServiceMethodModel.Builder methodBuilder = new AsyncServiceMethodModel.Builder();
      for (JMethod asyncMethod : m.getReturnType().isClassOrInterface().getMethods()) {
        TreeLogger methodLogger =
            serviceLogger.branch(
                Type.DEBUG, "Reading service method " + asyncMethod.getReadableDeclaration());

        List<JType> types = new ArrayList<JType>();
        methodBuilder.setReturnTypeName(voidType.getParameterizedQualifiedSourceName());
        boolean asyncFound = false;
        for (JType param : asyncMethod.getParameterTypes()) {
          if (asyncFound) {
            methodLogger.log(
                Type.WARN, "Already passed an AsyncCallback param - is that what you meant?");
          }
          if (param.isClassOrInterface() != null
              && param.isClassOrInterface().isAssignableTo(asyncCallback)) {
            JClassType boxedReturnType =
                ModelUtils.findParameterizationOf(asyncCallback, param.isClassOrInterface())[0];
            methodBuilder
                .setHasCallback(true)
                .setReturnTypeName(boxedReturnType.getParameterizedQualifiedSourceName());
            asyncFound = true;
            continue; // should be last, check for this...
          }
          types.add(param);
        }
        Set<JClassType> throwables = new HashSet<JClassType>();
        Throws t = asyncMethod.getAnnotation(Throws.class);
        if (t != null) {
          for (Class<? extends Throwable> throwable : t.value()) {
            throwables.add(typeOracle.findType(throwable.getName()));
          }
        }

        methodBuilder
            .setMethodName(asyncMethod.getName())
            .setArgTypes(types)
            .setThrowables(throwables);

        serviceBuilder.addMethod(methodBuilder.build());
      }
      rqBuilder.addService(serviceBuilder.build());
    }

    return rqBuilder.build();
  }