Suport polymorphic return and parameter values in RequestFactory.
Issue 5367.
Review at http://gwt-code-reviews.appspot.com/1453811
Patch by: bobv
Review by: rjrjr


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10317 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java
index 718eaed..7bbd30a 100644
--- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java
+++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/RequestFactoryGenerator.java
@@ -43,6 +43,7 @@
 import com.google.web.bindery.requestfactory.gwt.rebind.model.ContextMethod;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.EntityProxyModel;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.EntityProxyModel.Type;
+import com.google.web.bindery.requestfactory.gwt.rebind.model.HasExtraTypes;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.ModelVisitor;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModel;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestMethod;
@@ -84,6 +85,24 @@
     }
 
     @Override
+    public boolean visit(ContextMethod x) {
+      visitExtraTypes(x);
+      return true;
+    }
+
+    @Override
+    public boolean visit(EntityProxyModel x) {
+      visitExtraTypes(x);
+      return true;
+    }
+
+    @Override
+    public boolean visit(RequestFactoryModel x) {
+      visitExtraTypes(x);
+      return true;
+    }
+
+    @Override
     void examineTypeOnce(JClassType type) {
       // Need this to handle List<Foo>, Map<Foo>
       JParameterizedType parameterized = type.isParameterized();
@@ -99,6 +118,14 @@
       }
       peer.accept(this);
     }
+
+    void visitExtraTypes(HasExtraTypes x) {
+      if (x.getExtraTypes() != null) {
+        for (EntityProxyModel extra : x.getExtraTypes()) {
+          extra.accept(this);
+        }
+      }
+    }
   }
 
   /**
@@ -235,6 +262,7 @@
       @Override
       public void endVisit(EntityProxyModel x) {
         models.add(x);
+        models.addAll(x.getSuperProxyTypes());
       }
     });
     return models;
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java
index 4de5093..efa6b4c 100644
--- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java
+++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/ContextMethod.java
@@ -26,7 +26,7 @@
 /**
  * Represents a service endpoint.
  */
-public class ContextMethod implements AcceptsModelVisitor {
+public class ContextMethod implements AcceptsModelVisitor, HasExtraTypes {
 
   /**
    * Builds a {@link ContextMethod}.
@@ -42,23 +42,31 @@
       }
     }
 
-    public void setDeclaredMethod(JMethod method) {
+    public Builder setDeclaredMethod(JMethod method) {
       toReturn.methodName = method.getName();
       JClassType returnClass = method.getReturnType().isClassOrInterface();
       toReturn.interfaceName = returnClass.getQualifiedSourceName();
       toReturn.packageName = returnClass.getPackage().getName();
-      toReturn.simpleSourceName = returnClass.getName().replace('.', '_')
-          + "Impl";
-      toReturn.dialect = returnClass.isAnnotationPresent(JsonRpcService.class)
-          ? Dialect.JSON_RPC : Dialect.STANDARD;
+      toReturn.simpleSourceName = returnClass.getName().replace('.', '_') + "Impl";
+      toReturn.dialect =
+          returnClass.isAnnotationPresent(JsonRpcService.class) ? Dialect.JSON_RPC
+              : Dialect.STANDARD;
+      return this;
     }
 
-    public void setRequestMethods(List<RequestMethod> requestMethods) {
+    public Builder setExtraTypes(List<EntityProxyModel> extraTypes) {
+      toReturn.extraTypes = extraTypes;
+      return this;
+    }
+
+    public Builder setRequestMethods(List<RequestMethod> requestMethods) {
       toReturn.requestMethods = requestMethods;
+      return this;
     }
   }
 
   private Dialect dialect;
+  private List<EntityProxyModel> extraTypes;
   private String interfaceName;
   private String methodName;
   private String packageName;
@@ -81,6 +89,10 @@
     return dialect;
   }
 
+  public List<EntityProxyModel> getExtraTypes() {
+    return Collections.unmodifiableList(extraTypes);
+  }
+
   /**
    * The qualified source name of the RequestContext sub-interface (i.e., the
    * return type of the method declaration).
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java
index 87c2eac..76756a4 100644
--- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java
+++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/EntityProxyModel.java
@@ -21,7 +21,7 @@
 /**
  * Represents an EntityProxy subtype.
  */
-public class EntityProxyModel implements AcceptsModelVisitor {
+public class EntityProxyModel implements AcceptsModelVisitor, HasExtraTypes {
   /**
    * Builds {@link EntityProxyModel}.
    */
@@ -29,6 +29,9 @@
     private EntityProxyModel toReturn = new EntityProxyModel();
 
     public EntityProxyModel build() {
+      if (toReturn.superProxyTypes == null) {
+        toReturn.superProxyTypes = Collections.emptyList();
+      }
       try {
         return toReturn;
       } finally {
@@ -44,21 +47,35 @@
       return toReturn;
     }
 
-    public void setQualifiedBinaryName(String qualifiedBinaryName) {
-      toReturn.qualifiedBinaryName = qualifiedBinaryName;
+    public Builder setExtraTypes(List<EntityProxyModel> extraTypes) {
+      toReturn.extraTypes = extraTypes;
+      return this;
     }
 
-    public void setQualifiedSourceName(String name) {
+    public Builder setQualifiedBinaryName(String qualifiedBinaryName) {
+      toReturn.qualifiedBinaryName = qualifiedBinaryName;
+      return this;
+    }
+
+    public Builder setQualifiedSourceName(String name) {
       assert !name.contains(" ");
       toReturn.qualifiedSourceName = name;
+      return this;
     }
 
-    public void setRequestMethods(List<RequestMethod> requestMethods) {
+    public Builder setRequestMethods(List<RequestMethod> requestMethods) {
       toReturn.requestMethods = requestMethods;
+      return this;
     }
 
-    public void setType(Type type) {
+    public Builder setSuperProxyTypes(List<EntityProxyModel> superTypes) {
+      toReturn.superProxyTypes = superTypes;
+      return this;
+    }
+
+    public Builder setType(Type type) {
       toReturn.type = type;
+      return this;
     }
   }
 
@@ -70,9 +87,11 @@
     ENTITY, VALUE
   }
 
+  private List<EntityProxyModel> extraTypes;
   private String qualifiedBinaryName;
   private String qualifiedSourceName;
   private List<RequestMethod> requestMethods;
+  private List<EntityProxyModel> superProxyTypes;
   private Type type;
 
   private EntityProxyModel() {
@@ -87,6 +106,10 @@
     visitor.endVisit(this);
   }
 
+  public List<EntityProxyModel> getExtraTypes() {
+    return Collections.unmodifiableList(extraTypes);
+  }
+
   public String getQualifiedBinaryName() {
     return qualifiedBinaryName;
   }
@@ -99,6 +122,13 @@
     return Collections.unmodifiableList(requestMethods);
   }
 
+  /**
+   * Returns the proxy types to which the EntityProxyModel is assignable.
+   */
+  public List<EntityProxyModel> getSuperProxyTypes() {
+    return superProxyTypes;
+  }
+
   public Type getType() {
     return type;
   }
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/HasExtraTypes.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/HasExtraTypes.java
new file mode 100644
index 0000000..6cd8a68
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/HasExtraTypes.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2011 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.web.bindery.requestfactory.gwt.rebind.model;
+
+import java.util.List;
+
+/**
+ * Capability interface for types that can be annotated with
+ * {@link com.google.web.bindery.requestfactory.shared.ExtraTypes}.
+ */
+public interface HasExtraTypes {
+  List<EntityProxyModel> getExtraTypes();
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java
index dc9a23c..3f08de3 100644
--- a/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java
+++ b/user/src/com/google/web/bindery/requestfactory/gwt/rebind/model/RequestFactoryModel.java
@@ -15,8 +15,6 @@
  */
 package com.google.web.bindery.requestfactory.gwt.rebind.model;
 
-import com.google.web.bindery.autobean.gwt.rebind.model.JBeanMethod;
-import com.google.web.bindery.autobean.shared.Splittable;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
@@ -26,9 +24,13 @@
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.editor.rebind.model.ModelUtils;
+import com.google.web.bindery.autobean.gwt.rebind.model.JBeanMethod;
+import com.google.web.bindery.autobean.shared.Splittable;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.EntityProxyModel.Type;
 import com.google.web.bindery.requestfactory.gwt.rebind.model.RequestMethod.CollectionType;
+import com.google.web.bindery.requestfactory.shared.BaseProxy;
 import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ExtraTypes;
 import com.google.web.bindery.requestfactory.shared.InstanceRequest;
 import com.google.web.bindery.requestfactory.shared.JsonRpcProxy;
 import com.google.web.bindery.requestfactory.shared.JsonRpcService;
@@ -46,6 +48,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -53,18 +56,16 @@
 /**
  * Represents a RequestFactory interface declaration.
  */
-public class RequestFactoryModel implements AcceptsModelVisitor {
+public class RequestFactoryModel implements AcceptsModelVisitor, HasExtraTypes {
   public static String poisonedMessage() {
     return "Unable to create RequestFactoryModel model due to previous errors";
   }
 
-  static String badContextReturnType(JMethod method,
-      JClassType requestInterface, JClassType instanceRequestInterface) {
-    return String.format(
-        "Return type %s in method %s must be an interface assignable"
-            + " to %s or %s", method.getReturnType(), method.getName(),
-        requestInterface.getSimpleSourceName(),
-        instanceRequestInterface.getSimpleSourceName());
+  static String badContextReturnType(JMethod method, JClassType requestInterface,
+      JClassType instanceRequestInterface) {
+    return String.format("Return type %s in method %s must be an interface assignable"
+        + " to %s or %s", method.getReturnType(), method.getName(), requestInterface
+        .getSimpleSourceName(), instanceRequestInterface.getSimpleSourceName());
   }
 
   static String noSettersAllowed(JMethod found) {
@@ -74,6 +75,7 @@
   private final JClassType collectionInterface;
   private final List<ContextMethod> contextMethods = new ArrayList<ContextMethod>();
   private final JClassType entityProxyInterface;
+  private final List<EntityProxyModel> extraTypes;
   private final JClassType factoryType;
   private final JClassType instanceRequestInterface;
   private final JClassType listInterface;
@@ -83,11 +85,13 @@
   /**
    * This map prevents cyclic type dependencies from overflowing the stack.
    */
-  private final Map<JClassType, EntityProxyModel.Builder> peerBuilders = new HashMap<JClassType, EntityProxyModel.Builder>();
+  private final Map<JClassType, EntityProxyModel.Builder> peerBuilders =
+      new HashMap<JClassType, EntityProxyModel.Builder>();
   /**
    * Iterated by {@link #getAllProxyModels()}.
    */
-  private final Map<JClassType, EntityProxyModel> peers = new LinkedHashMap<JClassType, EntityProxyModel>();
+  private final Map<JClassType, EntityProxyModel> peers =
+      new LinkedHashMap<JClassType, EntityProxyModel>();
   private boolean poisoned;
   private final JClassType requestContextInterface;
   private final JClassType requestFactoryInterface;
@@ -114,6 +118,7 @@
     splittableType = oracle.findType(Splittable.class.getCanonicalName());
     valueProxyInterface = oracle.findType(ValueProxy.class.getCanonicalName());
 
+    extraTypes = checkExtraTypes(factoryType, false);
     for (JMethod method : factoryType.getOverridableMethods()) {
       if (method.getEnclosingType().equals(requestFactoryInterface)) {
         // Ignore methods defined an RequestFactory itself
@@ -126,10 +131,8 @@
       }
 
       JClassType contextType = method.getReturnType().isInterface();
-      if (contextType == null
-          || !requestContextInterface.isAssignableFrom(contextType)) {
-        poison("Unexpected return type %s on method %s is not"
-            + " an interface assignable to %s",
+      if (contextType == null || !requestContextInterface.isAssignableFrom(contextType)) {
+        poison("Unexpected return type %s on method %s is not" + " an interface assignable to %s",
             method.getReturnType().getQualifiedSourceName(), method.getName(),
             requestContextInterface.getSimpleSourceName());
         continue;
@@ -162,6 +165,14 @@
     return Collections.unmodifiableCollection(peers.values());
   }
 
+  /**
+   * These extra types will have already been added to the extra types for each
+   * {@link ContextMethod} in the model.
+   */
+  public List<EntityProxyModel> getExtraTypes() {
+    return Collections.unmodifiableList(extraTypes);
+  }
+
   public JClassType getFactoryType() {
     return factoryType;
   }
@@ -185,16 +196,15 @@
   /**
    * Examine a RequestContext subtype to populate a ContextMethod.
    */
-  private void buildContextMethod(ContextMethod.Builder contextBuilder,
-      JClassType contextType) throws UnableToCompleteException {
+  private void buildContextMethod(ContextMethod.Builder contextBuilder, JClassType contextType)
+      throws UnableToCompleteException {
     Service serviceAnnotation = contextType.getAnnotation(Service.class);
     ServiceName serviceNameAnnotation = contextType.getAnnotation(ServiceName.class);
     JsonRpcService jsonRpcAnnotation = contextType.getAnnotation(JsonRpcService.class);
-    if (serviceAnnotation == null && serviceNameAnnotation == null
-        && jsonRpcAnnotation == null) {
-      poison("RequestContext subtype %s is missing a @%s or @%s annotation",
-          contextType.getQualifiedSourceName(), Service.class.getSimpleName(),
-          JsonRpcService.class.getSimpleName());
+    if (serviceAnnotation == null && serviceNameAnnotation == null && jsonRpcAnnotation == null) {
+      poison("RequestContext subtype %s is missing a @%s or @%s annotation", contextType
+          .getQualifiedSourceName(), Service.class.getSimpleName(), JsonRpcService.class
+          .getSimpleName());
       return;
     }
 
@@ -208,15 +218,48 @@
       RequestMethod.Builder methodBuilder = new RequestMethod.Builder();
       methodBuilder.setDeclarationMethod(contextType, method);
 
-      if (!validateContextMethodAndSetDataType(methodBuilder, method,
-          jsonRpcAnnotation != null)) {
+      if (!validateContextMethodAndSetDataType(methodBuilder, method, jsonRpcAnnotation != null)) {
         continue;
       }
 
       requestMethods.add(methodBuilder.build());
     }
 
-    contextBuilder.setRequestMethods(requestMethods);
+    contextBuilder.setExtraTypes(checkExtraTypes(contextType, true)).setRequestMethods(
+        requestMethods);
+  }
+
+  /**
+   * Checks type and its supertypes for {@link ExtraTypes} annotations.
+   * 
+   * @param type the type to examine
+   * @param addModelExtraTypes if {@code true} the contents of the
+   *          {@link #extraTypes} field will be added to the returned list.
+   */
+  private List<EntityProxyModel> checkExtraTypes(JClassType type, boolean addModelExtraTypes)
+      throws UnableToCompleteException {
+    Set<EntityProxyModel> toReturn = new LinkedHashSet<EntityProxyModel>();
+    if (addModelExtraTypes && extraTypes != null) {
+      toReturn.addAll(extraTypes);
+    }
+    for (JClassType toExamine : type.getFlattenedSupertypeHierarchy()) {
+      ExtraTypes proxyExtraTypes = toExamine.getAnnotation(ExtraTypes.class);
+      if (proxyExtraTypes != null) {
+        for (Class<? extends BaseProxy> clazz : proxyExtraTypes.value()) {
+          JClassType proxy = oracle.findType(clazz.getCanonicalName());
+          if (proxy == null) {
+            poison("Unknown class %s in @%s", clazz.getCanonicalName(), ExtraTypes.class
+                .getSimpleName());
+          } else {
+            toReturn.add(getEntityProxyType(proxy));
+          }
+        }
+      }
+    }
+    if (toReturn.isEmpty()) {
+      return Collections.emptyList();
+    }
+    return new ArrayList<EntityProxyModel>(toReturn);
   }
 
   private void die(String message) throws UnableToCompleteException {
@@ -238,6 +281,15 @@
       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)) {
@@ -245,9 +297,8 @@
       } 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());
+        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());
       }
@@ -257,10 +308,9 @@
       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());
+        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
@@ -283,28 +333,26 @@
           if (previouslySeen == null) {
             duplicatePropertyGetters.put(propertyName, method);
           } else {
-            poison("Duplicate accessors for property %s: %s() and %s()",
-                propertyName, previouslySeen.getName(), method.getName());
+            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)) {
+        } 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) {
+        } 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());
+          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.setRequestMethods(requestMethods);
+      builder.setExtraTypes(checkExtraTypes(entityProxyType, false)).setRequestMethods(
+          requestMethods);
 
       toReturn = builder.build();
       peers.put(entityProxyType, toReturn);
@@ -319,36 +367,50 @@
   }
 
   /**
+   * Returns {@code true} if the type is assignable to EntityProxy or ValueProxy
+   * and has a mapping to a domain type.
+   * 
+   * @see com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator#shouldAttemptProxyValidation()
+   */
+  private boolean shouldAttemptProxyValidation(JClassType maybeProxy) {
+    if (!entityProxyInterface.isAssignableFrom(maybeProxy)
+        && !valueProxyInterface.isAssignableFrom(maybeProxy)) {
+      return false;
+    }
+    if (maybeProxy.getAnnotation(ProxyFor.class) == null
+        && maybeProxy.getAnnotation(ProxyForName.class) == null) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
    * Examine a RequestContext method to see if it returns a transportable type.
    */
-  private boolean validateContextMethodAndSetDataType(
-      RequestMethod.Builder methodBuilder, JMethod method, boolean allowSetters)
-      throws UnableToCompleteException {
+  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));
+      poison(badContextReturnType(method, requestInterface, instanceRequestInterface));
       return false;
     }
 
     if (instanceRequestInterface.isAssignableFrom(requestReturnType)) {
       // Instance method invocation
-      JClassType[] params = ModelUtils.findParameterizationOf(
-          instanceRequestInterface, requestReturnType);
+      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);
+      JClassType[] params = ModelUtils.findParameterizationOf(requestInterface, requestReturnType);
       invocationReturnType = params[0];
 
     } else {
       // Unhandled return type, must be something random
-      poison(badContextReturnType(method, requestInterface,
-          instanceRequestInterface));
+      poison(badContextReturnType(method, requestInterface, instanceRequestInterface));
       return false;
     }
 
@@ -357,15 +419,14 @@
     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;
+      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 (JBeanMethod.SET.matches(maybeSetter) || JBeanMethod.SET_BUILDER.matches(maybeSetter)) {
         if (allowSetters) {
           methodBuilder.addExtraSetter(maybeSetter);
         } else {
@@ -379,14 +440,13 @@
   /**
    * Examines a type to see if it can be transported.
    */
-  private boolean validateTransportableType(
-      RequestMethod.Builder methodBuilder, JType type, boolean requireObject)
-      throws UnableToCompleteException {
+  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());
+        poison("The type %s cannot be transported by RequestFactory as" + " a return type", type
+            .getQualifiedSourceName());
         return false;
       } else {
         // Primitives always ok
@@ -394,8 +454,7 @@
       }
     }
 
-    if (ModelUtils.isValueType(oracle, transportedClass)
-        || splittableType.equals(transportedClass)) {
+    if (ModelUtils.isValueType(oracle, transportedClass) || splittableType.equals(transportedClass)) {
       // Simple values, like Integer and String
       methodBuilder.setValueType(true);
     } else if (entityProxyInterface.isAssignableFrom(transportedClass)
@@ -414,14 +473,13 @@
       } 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());
+        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];
+      JClassType elementType =
+          ModelUtils.findParameterizationOf(collectionInterface, transportedClass)[0];
       methodBuilder.setCollectionElementType(elementType);
       validateTransportableType(methodBuilder, elementType, requireObject);
     } else if (mapInterface.isAssignableFrom(transportedClass)) {
@@ -433,13 +491,12 @@
       if (mapInterface.equals(parameterized.getBaseType())) {
         methodBuilder.setCollectionType(CollectionType.MAP);
       } else {
-        poison("Requests that return maps may be declared with" + " %s only",
-            mapInterface.getQualifiedSourceName());
+        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[] params = ModelUtils.findParameterizationOf(mapInterface, transportedClass);
       JClassType keyType = params[0];
       JClassType valueType = params[1];
       methodBuilder.setMapKeyType(keyType);
@@ -448,8 +505,7 @@
       validateTransportableType(methodBuilder, valueType, requireObject);
     } else {
       // Unknown type, fail
-      poison("Invalid Request parameterization %s",
-          transportedClass.getQualifiedSourceName());
+      poison("Invalid Request parameterization %s", transportedClass.getQualifiedSourceName());
       return false;
     }
     methodBuilder.setDataType(transportedClass);
diff --git a/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java b/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java
index 683c6a6..448ded7 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/Deobfuscator.java
@@ -97,25 +97,30 @@
    * Returns a method descriptor that should be invoked on the service object.
    */
   public String getDomainMethodDescriptor(String operation) {
-    return operationData.get(new OperationKey(operation)).getDomainMethodDescriptor();
+    OperationData data = operationData.get(new OperationKey(operation));
+    return data == null ? null : data.getDomainMethodDescriptor();
   }
 
   public String getRequestContext(String operation) {
-    return operationData.get(new OperationKey(operation)).getRequestContext();
+    OperationData data = operationData.get(new OperationKey(operation));
+    return data == null ? null : data.getRequestContext();
   }
 
   public String getRequestContextMethodDescriptor(String operation) {
-    return operationData.get(new OperationKey(operation)).getClientMethodDescriptor();
+    OperationData data = operationData.get(new OperationKey(operation));
+    return data == null ? null : data.getClientMethodDescriptor();
   }
 
   public String getRequestContextMethodName(String operation) {
-    return operationData.get(new OperationKey(operation)).getMethodName();
+    OperationData data = operationData.get(new OperationKey(operation));
+    return data == null ? null : data.getMethodName();
   }
 
   /**
    * Returns a type's binary name based on an obfuscated token.
    */
   public String getTypeFromToken(String token) {
-    return typeTokens.get(token).getClassName();
+    Type type = typeTokens.get(token);
+    return type == null ? null : type.getClassName();
   }
 }
\ No newline at end of file
diff --git a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java
index b0933bc..9d19c99 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/RequestFactoryInterfaceValidator.java
@@ -31,6 +31,7 @@
 import com.google.web.bindery.autobean.shared.ValueCodex;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
 import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ExtraTypes;
 import com.google.web.bindery.requestfactory.shared.InstanceRequest;
 import com.google.web.bindery.requestfactory.shared.ProxyFor;
 import com.google.web.bindery.requestfactory.shared.ProxyForName;
@@ -48,6 +49,7 @@
 import java.lang.annotation.Annotation;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -55,6 +57,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -358,6 +362,47 @@
     }
   }
 
+  private class ExtraTypesCollector extends EmptyVisitor {
+    private final ErrorContext logger;
+    private final Set<Type> collected = new HashSet<Type>();
+
+    public ExtraTypesCollector(ErrorContext logger) {
+      this.logger = logger;
+      logger.spam("Collecting extra types");
+    }
+
+    public Type[] exec(Type type) {
+      for (Type toExamine : getSupertypes(logger, type)) {
+        RequestFactoryInterfaceValidator.this.visit(logger, toExamine.getInternalName(), this);
+      }
+      return collected.toArray(new Type[collected.size()]);
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      if (!desc.equals(Type.getDescriptor(ExtraTypes.class))) {
+        return null;
+      }
+
+      return new EmptyVisitor() {
+
+        @Override
+        public AnnotationVisitor visitArray(String name) {
+          if (!"value".equals(name)) {
+            return null;
+          }
+          return new EmptyVisitor() {
+            @Override
+            public void visit(String name, Object value) {
+              collected.add((Type) value);
+            }
+          };
+        }
+
+      };
+    }
+  }
+
   /**
    * Collects information about domain objects. This visitor is intended to be
    * iteratively applied to collect all methods in a type hierarchy.
@@ -626,7 +671,8 @@
    * Maps domain types (e.g Foo) to client proxy types (e.g. FooAProxy,
    * FooBProxy).
    */
-  private final Map<Type, List<Type>> domainToClientType = new HashMap<Type, List<Type>>();
+  private final Map<Type, SortedSet<Type>> domainToClientType =
+      new HashMap<Type, SortedSet<Type>>();
   /**
    * The type {@link EntityProxy}.
    */
@@ -650,6 +696,20 @@
    */
   private final Map<Type, Set<RFMethod>> methodsInHierarchy = new HashMap<Type, Set<RFMethod>>();
   /**
+   * Not static because it depends on {@link #parentLogger}.
+   */
+  private final Comparator<Type> typeNameComparator = new Comparator<Type>() {
+    @Override
+    public int compare(Type a, Type b) {
+      if (isAssignable(parentLogger, a, b)) {
+        return 1;
+      } else if (isAssignable(parentLogger, b, a)) {
+        return -1;
+      }
+      return a.getInternalName().compareTo(b.getInternalName());
+    }
+  };
+  /**
    * Used to resolve obfuscated type tokens.
    */
   private final Map<String, Type> typeTokens = new HashMap<String, Type>();
@@ -831,7 +891,7 @@
       return;
     }
 
-    Type domainServiceType = getDomainType(logger, requestContextType);
+    Type domainServiceType = getDomainType(logger, requestContextType, false);
     if (domainServiceType == errorType) {
       logger.poison("The type %s must be annotated with a @%s or @%s annotation", BinaryName
           .toSourceName(binaryName), Service.class.getSimpleName(), ServiceName.class
@@ -860,6 +920,7 @@
       maybeCheckReferredProxies(logger, method);
     }
 
+    maybeCheckExtraTypes(logger, requestContextType);
     checkUnresolvedKeyTypes(logger);
   }
 
@@ -903,6 +964,8 @@
         validateRequestContext(returnType.getClassName());
       }
     }
+
+    maybeCheckExtraTypes(logger, requestFactoryType);
   }
 
   /**
@@ -942,7 +1005,7 @@
    */
   String getEntityProxyTypeName(String domainTypeBinaryName, String clientTypeBinaryName) {
     Type key = Type.getObjectType(BinaryName.toInternalName(domainTypeBinaryName));
-    List<Type> found = domainToClientType.get(key);
+    SortedSet<Type> found = domainToClientType.get(key);
 
     /*
      * If nothing was found look for proxyable supertypes the domain object can
@@ -970,7 +1033,7 @@
 
     // Common case
     if (found.size() == 1) {
-      typeToReturn = found.get(0);
+      typeToReturn = found.first();
     } else {
       // Search for the first assignable type
       Type assignableTo = Type.getObjectType(BinaryName.toInternalName(clientTypeBinaryName));
@@ -993,10 +1056,9 @@
     clientToDomainType.put(clientType, domainType);
 
     if (isAssignable(logger, baseProxyIntf, clientType)) {
-      maybeCheckProxyType(logger, clientType);
-      List<Type> list = domainToClientType.get(domainType);
+      SortedSet<Type> list = domainToClientType.get(domainType);
       if (list == null) {
-        list = new ArrayList<Type>();
+        list = new TreeSet<Type>(typeNameComparator);
         domainToClientType.put(domainType, list);
       }
       list.add(clientType);
@@ -1127,9 +1189,9 @@
   private Method createDomainMethod(ErrorContext logger, Method clientMethod) {
     Type[] args = clientMethod.getArgumentTypes();
     for (int i = 0, j = args.length; i < j; i++) {
-      args[i] = getDomainType(logger, args[i]);
+      args[i] = getDomainType(logger, args[i], true);
     }
-    Type returnType = getDomainType(logger, clientMethod.getReturnType());
+    Type returnType = getDomainType(logger, clientMethod.getReturnType(), true);
     return new Method(clientMethod.getName(), returnType, args);
   }
 
@@ -1280,7 +1342,7 @@
    * domain object type. RequestContext types will be resolved to their service
    * object.
    */
-  private Type getDomainType(ErrorContext logger, Type clientType) {
+  private Type getDomainType(ErrorContext logger, Type clientType, boolean requireMapping) {
     Type domainType = clientToDomainType.get(clientType);
     if (domainType != null) {
       return domainType;
@@ -1294,8 +1356,10 @@
       DomainMapper pv = new DomainMapper(logger);
       visit(logger, clientType.getInternalName(), pv);
       if (pv.getDomainInternalName() == null) {
-        logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)", print(clientType),
-            ProxyFor.class.getSimpleName(), Service.class.getSimpleName());
+        if (requireMapping) {
+          logger.poison("%s has no mapping to a domain type (e.g. @%s or @%s)", print(clientType),
+              ProxyFor.class.getSimpleName(), Service.class.getSimpleName());
+        }
         domainType = errorType;
       } else {
         domainType = Type.getObjectType(pv.getDomainInternalName());
@@ -1306,7 +1370,9 @@
       }
     }
     addToDomainMap(logger, domainType, clientType);
-    maybeCheckProxyType(logger, clientType);
+    if (domainType != errorType) {
+      maybeCheckProxyType(logger, clientType);
+    }
     return domainType;
   }
 
@@ -1457,6 +1523,14 @@
   }
 
   /**
+   * Examines a type for an {@link ExtraTypes} annotation and processes the
+   * referred types.
+   */
+  private void maybeCheckExtraTypes(ErrorContext logger, Type type) {
+    maybeCheckProxyType(logger, new ExtraTypesCollector(logger.setType(type)).exec(type));
+  }
+
+  /**
    * Examine an array of Types and call {@link #validateEntityProxy(String)} or
    * {@link #validateValueProxy(String)} if the type is a proxy.
    */
@@ -1494,6 +1568,23 @@
     }
   }
 
+  /**
+   * Returns {@code true} if the type is assignable to EntityProxy or ValueProxy
+   * and has a mapping to a domain type.
+   * 
+   * @see com.google.web.bindery.requestfactory.gwt.rebind.model.RequestFactoryModel#shouldAttemptProxyValidation()
+   */
+  private boolean shouldAttemptProxyValidation(ErrorContext logger, Type type) {
+    logger = logger.setType(type);
+    if (!isAssignable(logger, entityProxyIntf, type) && !isAssignable(logger, valueProxyIntf, type)) {
+      return false;
+    }
+    if (getDomainType(logger, type, false) == errorType) {
+      return false;
+    }
+    return true;
+  }
+
   private void validateProxy(String binaryName, Type expectedType, boolean requireId) {
     if (fastFail(binaryName)) {
       return;
@@ -1509,8 +1600,15 @@
       return;
     }
 
+    // Check supertypes first
+    for (Type supertype : getSupertypes(logger, proxyType)) {
+      if (shouldAttemptProxyValidation(logger, supertype)) {
+        maybeCheckProxyType(logger, supertype);
+      }
+    }
+
     // Find the domain type
-    Type domainType = getDomainType(logger, proxyType);
+    Type domainType = getDomainType(logger, proxyType, false);
     if (domainType == errorType) {
       logger.poison("The type %s must be annotated with a @%s or @%s annotation", BinaryName
           .toSourceName(binaryName), ProxyFor.class.getSimpleName(), ProxyForName.class
@@ -1539,6 +1637,7 @@
       checkPropertyMethod(logger, clientPropertyMethod, domainType);
       maybeCheckReferredProxies(logger, clientPropertyMethod);
     }
+    maybeCheckExtraTypes(logger, proxyType);
   }
 
   /**
diff --git a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
index 7b8c5a6..38e5a97 100644
--- a/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
+++ b/user/src/com/google/web/bindery/requestfactory/server/Resolver.java
@@ -404,7 +404,7 @@
     boolean isProxy = BaseProxy.class.isAssignableFrom(returnClass);
     boolean isId = EntityProxyId.class.isAssignableFrom(returnClass);
     if (isProxy || isId) {
-      Class<? extends BaseProxy> proxyClass = assignableTo.asSubclass(BaseProxy.class);
+      Class<? extends BaseProxy> proxyClass = returnClass.asSubclass(BaseProxy.class);
       BaseProxy entity = resolveClientProxy(domainValue, proxyClass, propertyRefs, key, prefix);
       if (isId) {
         return assignableTo.cast(((EntityProxy) entity).stableId());
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java b/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java
index eaab4bc..8d93655 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/EntityProxy.java
@@ -18,6 +18,7 @@
 /**
  * A proxy for a server-side domain object.
  */
+@ProxyFor(Object.class)
 public interface EntityProxy extends BaseProxy {
   /**
    * Returns the {@link EntityProxyId} that identifies a particular instance of
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java b/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java
new file mode 100644
index 0000000..0273c97
--- /dev/null
+++ b/user/src/com/google/web/bindery/requestfactory/shared/ExtraTypes.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2011 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.web.bindery.requestfactory.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation can be applied to {@link EntityProxy}, {@link ValueProxy},
+ * {@link RequestContext}, and {@link RequestFactory} type declarations to
+ * include additional polymorphic proxy types that are not explicitly
+ * referenced.
+ */
+@Target(ElementType.TYPE)
+public @interface ExtraTypes {
+  Class<? extends BaseProxy>[] value();
+}
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java b/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java
index bbd7760..14e9aa2 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/ValueProxy.java
@@ -19,5 +19,6 @@
  * An analog to EntityProxy for domain types that do not have an identity
  * concept.
  */
+@ProxyFor(Object.class)
 public interface ValueProxy extends BaseProxy {
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
index 111e77f..ae23ac2 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
@@ -15,10 +15,12 @@
  */
 package com.google.web.bindery.requestfactory.vm;
 
+import com.google.web.bindery.autobean.shared.AutoBean;
 import com.google.web.bindery.autobean.shared.AutoBean.PropertyName;
 import com.google.web.bindery.autobean.shared.AutoBeanFactory;
 import com.google.web.bindery.autobean.vm.impl.BeanMethod;
 import com.google.web.bindery.autobean.vm.impl.TypeUtils;
+import com.google.web.bindery.requestfactory.shared.BaseProxy;
 import com.google.web.bindery.requestfactory.shared.InstanceRequest;
 import com.google.web.bindery.requestfactory.shared.JsonRpcContent;
 import com.google.web.bindery.requestfactory.shared.JsonRpcWireName;
@@ -26,9 +28,10 @@
 import com.google.web.bindery.requestfactory.shared.RequestContext;
 import com.google.web.bindery.requestfactory.shared.impl.AbstractRequest;
 import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestContext;
-import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestFactory;
 import com.google.web.bindery.requestfactory.shared.impl.RequestData;
+import com.google.web.bindery.requestfactory.shared.impl.SimpleProxyId;
 import com.google.web.bindery.requestfactory.vm.impl.OperationKey;
+import com.google.web.bindery.requestfactory.vm.impl.TypeTokenResolver;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.InvocationHandler;
@@ -182,11 +185,13 @@
   static final Object[] NO_ARGS = new Object[0];
   private final Class<? extends RequestContext> context;
   private final Dialect dialect;
+  private final TypeTokenResolver tokenResolver;
 
-  protected InProcessRequestContext(AbstractRequestFactory factory, Dialect dialect,
+  protected InProcessRequestContext(InProcessRequestFactory factory, Dialect dialect,
       Class<? extends RequestContext> context) {
     super(factory, dialect);
     this.context = context;
+    this.tokenResolver = factory.getTypeTokenResolver();
     this.dialect = dialect;
   }
 
@@ -198,6 +203,14 @@
   }
 
   @Override
+  public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id) {
+    if (tokenResolver.isReferencedType(clazz.getName())) {
+      return super.createProxy(clazz, id);
+    }
+    throw new IllegalArgumentException("Unknown proxy type " + clazz.getName());
+  }
+
+  @Override
   protected AutoBeanFactory getAutoBeanFactory() {
     return ((InProcessRequestFactory) getRequestFactory()).getAutoBeanFactory();
   }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
index da49296..fc3802b 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestFactory.java
@@ -131,4 +131,8 @@
   protected String getTypeToken(Class<? extends BaseProxy> clazz) {
     return isEntityType(clazz) || isValueType(clazz) ? OperationKey.hash(clazz.getName()) : null;
   }
+
+  TypeTokenResolver getTypeTokenResolver() {
+    return deobfuscator;
+  }
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
index 96caf85..4ed5112 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/impl/TypeTokenResolver.java
@@ -22,8 +22,10 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 
 /**
  * Resolves payload type tokens to binary class names.
@@ -46,6 +48,8 @@
     public TypeTokenResolver build() {
       TypeTokenResolver toReturn = d;
       toReturn.typeTokens = Collections.unmodifiableMap(toReturn.typeTokens);
+      toReturn.referencedTypes =
+          Collections.unmodifiableSet(new HashSet<String>(toReturn.typeTokens.values()));
       d = null;
       return toReturn;
     }
@@ -86,6 +90,11 @@
   }
 
   /**
+   * The values of {@link #typeTokens}.
+   */
+  private Set<String> referencedTypes;
+
+  /**
    * Map of obfuscated ids to binary class names.
    */
   private Map<String, String> typeTokens = new HashMap<String, String>();
@@ -94,6 +103,10 @@
     return typeTokens.get(typeToken);
   }
 
+  public boolean isReferencedType(String binaryName) {
+    return referencedTypes.contains(binaryName);
+  }
+
   /**
    * Closes the OutputStream.
    */
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
index 83baef3..ffd546d 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryPolymorphicTest.java
@@ -19,27 +19,751 @@
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.web.bindery.event.shared.EventBus;
 import com.google.web.bindery.event.shared.SimpleEventBus;
+import com.google.web.bindery.requestfactory.shared.EntityProxy;
+import com.google.web.bindery.requestfactory.shared.ExtraTypes;
+import com.google.web.bindery.requestfactory.shared.ProxyFor;
+import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.Request;
+import com.google.web.bindery.requestfactory.shared.RequestContext;
+import com.google.web.bindery.requestfactory.shared.RequestFactory;
+import com.google.web.bindery.requestfactory.shared.Service;
 import com.google.web.bindery.requestfactory.shared.SimpleFooProxy;
 import com.google.web.bindery.requestfactory.shared.TestRequestFactory;
 
+import java.util.Arrays;
+import java.util.List;
+
 /**
- * Just tests the
- * {@link com.google.web.bindery.requestfactory.gwt.rebind.RequestFactoryGenerator} to see
- * if polymorphic signatures are allowed.
+ * Tests behavior of RequestFactory when using polymorphic Request return types
+ * and other non-trivial type hierarchies.
  */
 public class RequestFactoryPolymorphicTest extends GWTTestCase {
 
+  /**
+   * Mandatory javadoc.
+   */
+  public static class A {
+    private static int idCount = 0;
+
+    public static A findA(int id) {
+      A toReturn = new A();
+      toReturn.id = id;
+      return toReturn;
+    }
+
+    private String a = "a";
+
+    protected int id = idCount++;
+
+    public String getA() {
+      return a;
+    }
+
+    public int getId() {
+      return id;
+    }
+
+    public int getVersion() {
+      return 0;
+    }
+
+    public void setA(String value) {
+      a = value;
+    }
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(A.class)
+  public interface AProxy extends EntityProxy, HasA {
+    // Mix in getA() from HasA
+    void setA(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class ASub extends A {
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class B extends A {
+    public static B findB(int id) {
+      B toReturn = new B();
+      toReturn.id = id;
+      return toReturn;
+    }
+
+    private String b = "b";
+
+    public String getB() {
+      return b;
+    }
+
+    public void setB(String value) {
+      b = value;
+    }
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(B.class)
+  public interface B1Proxy extends AProxy {
+    String getB();
+
+    void setB(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(B.class)
+  public interface B2Proxy extends AProxy {
+    String getB();
+
+    void setB(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class BSub extends B {
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class C extends B {
+    public static C findC(int id) {
+      C toReturn = new C();
+      toReturn.id = id;
+      return toReturn;
+    }
+
+    private String c = "c";
+
+    public String getC() {
+      return c;
+    }
+
+    public void setC(String value) {
+      c = value;
+    }
+  }
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(C.class)
+  public interface C1Proxy extends B1Proxy {
+    String getC();
+
+    void setC(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(C.class)
+  public interface C2Proxy extends B2Proxy {
+    String getC();
+
+    void setC(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class CSub extends C {
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class D extends A {
+    public static D findD(int id) {
+      D toReturn = new D();
+      toReturn.id = id;
+      return toReturn;
+    }
+
+    private String d = "d";
+
+    public String getD() {
+      return d;
+    }
+
+    public void setD(String value) {
+      d = value;
+    }
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ExtraTypes(D2Proxy.class)
+  @ProxyFor(D.class)
+  public interface D1Proxy extends AProxy {
+    String getD();
+
+    void setD(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(D.class)
+  public interface D2Proxy extends EntityProxy {
+    String getD();
+
+    void setD(String value);
+  }
+
+  /**
+   * This class should not be referenced except as a superclass of
+   * {@link MoreDerivedProxy}.
+   */
+  @ProxyFor(D.class)
+  public interface D3Proxy extends EntityProxy {
+    String getD();
+
+    void setD(String value);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class DSub extends D {
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public interface HasA {
+    String getA();
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  public static class Impl {
+    public static A AasA() {
+      return new A();
+    }
+
+    public static A BasA() {
+      return new B();
+    }
+
+    public static B BasB() {
+      return new B();
+    }
+
+    public static A CasA() {
+      return new C();
+    }
+
+    public static B CasB() {
+      return new C();
+    }
+
+    public static String checkA(Object obj) {
+      return A.class.equals(obj.getClass()) && "A".equals(((A) obj).getA()) ? "" : "checkA";
+    }
+
+    public static String checkB(Object obj) {
+      return B.class.equals(obj.getClass()) && "B".equals(((B) obj).getB()) ? "" : "checkB";
+    }
+
+    public static String checkC(Object obj) {
+      return C.class.equals(obj.getClass()) && "C".equals(((C) obj).getC()) ? "" : "checkC";
+    }
+
+    public static String checkD(Object obj) {
+      return D.class.equals(obj.getClass()) && "D".equals(((D) obj).getD()) ? "" : "checkD";
+    }
+
+    public static String checkList(List<Object> list) {
+      if (list.size() != 4) {
+        return "size";
+      }
+      String temp;
+      temp = checkA(list.get(0));
+      if (!temp.isEmpty()) {
+        return temp;
+      }
+      temp = checkB(list.get(1));
+      if (!temp.isEmpty()) {
+        return temp;
+      }
+      temp = checkC(list.get(2));
+      if (!temp.isEmpty()) {
+        return temp;
+      }
+      temp = checkD(list.get(3));
+      if (!temp.isEmpty()) {
+        return temp;
+      }
+      return "";
+    }
+
+    public static String checkW(Object obj) {
+      return W.class.equals(obj.getClass()) && "W".equals(((W) obj).getW()) ? "" : "checkW";
+    }
+
+    public static String checkZ(Object obj) {
+      return Z.class.equals(obj.getClass()) && "Z".equals(((Z) obj).getZ()) ? "" : "checkZ";
+    }
+
+    public static A DasA() {
+      return new D();
+    }
+
+    public static D DasD() {
+      return new D();
+    }
+
+    public static List<A> testCollection() {
+      return Arrays.asList(new A(), new B(), new C(), new D());
+    }
+
+    public static List<A> testCollectionSub() {
+      return Arrays.asList(new ASub(), new BSub(), new CSub(), new DSub());
+    }
+
+    public static W W() {
+      return new W();
+    }
+
+    public static W W2() {
+      return new W();
+    }
+
+    public static Z Z() {
+      return new Z();
+    }
+
+    public static Z Z2() {
+      return new Z();
+    }
+  }
+
+  /**
+   * Check diamond inheritance.
+   */
+  @ProxyFor(D.class)
+  public interface MoreDerivedProxy extends AProxy, D1Proxy, D2Proxy, D3Proxy {
+  }
+
+  /**
+   * The W and Z types are used with the WZProxy and ZWProxy to demonstrate
+   * proxy interface inheritance even when their proxy-for types aren't related.
+   */
+  public static class W {
+    private static int idCount;
+
+    public static W findW(int id) {
+      W toReturn = new W();
+      toReturn.id = id;
+      return toReturn;
+    }
+
+    private int id = idCount++;
+
+    private String w = "w";
+
+    private String z = "z";
+
+    public int getId() {
+      return id;
+    }
+
+    public int getVersion() {
+      return 0;
+    }
+
+    public String getW() {
+      return w;
+    }
+
+    public String getZ() {
+      return z;
+    }
+
+    public void setW(String w) {
+      this.w = w;
+    }
+
+    public void setZ(String z) {
+      this.z = z;
+    }
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(W.class)
+  public interface WProxy extends EntityProxy {
+    String getW();
+
+    void setW(String w);
+  }
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(W.class)
+  public interface WZProxy extends ZProxy {
+    String getW();
+
+    void setW(String w);
+  }
+  /**
+   * @see W
+   */
+  public static class Z extends A {
+    private static int idCount;
+
+    public static Z findZ(int id) {
+      Z toReturn = new Z();
+      toReturn.id = id;
+      return toReturn;
+    }
+
+    private int id = idCount++;
+
+    private String w = "w";
+
+    private String z = "z";
+
+    public int getId() {
+      return id;
+    }
+
+    public int getVersion() {
+      return 0;
+    }
+
+    public String getW() {
+      return w;
+    }
+
+    public String getZ() {
+      return z;
+    }
+
+    public void setW(String w) {
+      this.w = w;
+    }
+
+    public void setZ(String z) {
+      this.z = z;
+    }
+  }
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(Z.class)
+  public interface ZProxy extends EntityProxy {
+    String getZ();
+
+    void setZ(String z);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ProxyFor(Z.class)
+  public interface ZWProxy extends WProxy {
+    String getZ();
+
+    void setZ(String z);
+  }
+
+  /**
+   * Mandatory javadoc.
+   */
+  @ExtraTypes(B2Proxy.class)
+  protected interface Factory extends RequestFactory {
+    Context ctx();
+  }
+
+  /**
+   * Verifies that the received proxy's proxy type is exactly {@code clazz} and
+   * checks the values of the properties.
+   */
+  static class CastAndCheckReceiver extends Receiver<EntityProxy> {
+    public static CastAndCheckReceiver of(Class<?> clazz) {
+      return new CastAndCheckReceiver(clazz);
+    }
+
+    private final Class<?> clazz;
+
+    public CastAndCheckReceiver(Class<?> clazz) {
+      this.clazz = clazz;
+    }
+
+    @Override
+    public void onSuccess(EntityProxy response) {
+      assertNotNull(response);
+      assertEquals(clazz, response.stableId().getProxyClass());
+      if (response instanceof HasA) {
+        assertEquals("a", ((HasA) response).getA());
+      }
+      if (response instanceof B1Proxy) {
+        assertEquals("b", ((B1Proxy) response).getB());
+      }
+      if (response instanceof B2Proxy) {
+        assertEquals("b", ((B2Proxy) response).getB());
+      }
+      if (response instanceof C1Proxy) {
+        assertEquals("c", ((C1Proxy) response).getC());
+      }
+      if (response instanceof C2Proxy) {
+        assertEquals("c", ((C2Proxy) response).getC());
+      }
+      if (response instanceof D1Proxy) {
+        assertEquals("d", ((D1Proxy) response).getD());
+      }
+      if (response instanceof D2Proxy) {
+        assertEquals("d", ((D2Proxy) response).getD());
+      }
+    }
+  }
+
+  @ExtraTypes({C1Proxy.class, C2Proxy.class, MoreDerivedProxy.class})
+  @Service(Impl.class)
+  interface Context extends RequestContext {
+    Request<AProxy> AasA();
+
+    Request<AProxy> BasA();
+
+    Request<B1Proxy> BasB();
+
+    Request<AProxy> CasA();
+
+    Request<B1Proxy> CasB();
+
+    Request<String> checkA(EntityProxy proxy);
+
+    Request<String> checkB(EntityProxy proxy);
+
+    Request<String> checkC(EntityProxy proxy);
+
+    Request<String> checkD(EntityProxy proxy);
+
+    Request<String> checkList(List<EntityProxy> list);
+
+    Request<String> checkW(EntityProxy proxy);
+
+    Request<String> checkZ(EntityProxy proxy);
+
+    Request<AProxy> DasA();
+
+    Request<D1Proxy> DasD();
+
+    Request<List<AProxy>> testCollection();
+
+    Request<List<AProxy>> testCollectionSub();
+
+    Request<WProxy> W();
+
+    Request<WZProxy> W2();
+
+    Request<ZProxy> Z();
+
+    Request<ZWProxy> Z2();
+  }
+
+  /**
+   * Checks that the incoming list is {@code [ A, B, C, D ]}.
+   */
+  static class ListChecker extends Receiver<List<AProxy>> {
+    @Override
+    public void onSuccess(List<AProxy> response) {
+      new CastAndCheckReceiver(AProxy.class).onSuccess(response.get(0));
+      new CastAndCheckReceiver(B1Proxy.class).onSuccess(response.get(1));
+      new CastAndCheckReceiver(C1Proxy.class).onSuccess(response.get(2));
+      new CastAndCheckReceiver(MoreDerivedProxy.class).onSuccess(response.get(3));
+    }
+  }
+
+  private final Receiver<String> checkReceiver = new Receiver<String>() {
+
+    @Override
+    public void onSuccess(String response) {
+      assertEquals("", response);
+    }
+  };
+
+  private static final int TEST_DELAY = 5000;
+
+  protected Factory factory;
+
   @Override
   public String getModuleName() {
     return "com.google.web.bindery.requestfactory.gwt.RequestFactorySuite";
   }
 
-  public void testGenerator() {
+  public void testCreation() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    checkA(ctx, AProxy.class);
+    checkB(ctx, B1Proxy.class);
+    checkB(ctx, B2Proxy.class);
+    checkC(ctx, C1Proxy.class);
+    checkC(ctx, C2Proxy.class);
+    checkD(ctx, D1Proxy.class);
+    checkD(ctx, D2Proxy.class);
+    // D3Proxy is a proxy supertype, assignable to BaseProxy and has @ProxyFor
+    checkD(ctx, D3Proxy.class);
+    checkD(ctx, MoreDerivedProxy.class);
+    checkW(ctx, WProxy.class);
+    checkW(ctx, WZProxy.class);
+    checkZ(ctx, ZProxy.class);
+    checkZ(ctx, ZWProxy.class);
+    ctx.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        finishTest();
+      }
+    });
+  }
+
+  /**
+   * Ensure heterogeneous collections work.
+   */
+  public void testCreationList() {
+    Context ctx = factory.ctx();
+    ctx.checkList(
+        Arrays.asList(create(ctx, AProxy.class), create(ctx, B2Proxy.class), create(ctx,
+            C2Proxy.class), create(ctx, D2Proxy.class))).to(checkReceiver);
+    ctx.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        finishTest();
+      }
+    });
+  }
+
+  public void testGenericRequest() {
     TestRequestFactory rf = GWT.create(TestRequestFactory.class);
     EventBus eventBus = new SimpleEventBus();
     rf.initialize(eventBus);
-    SimpleFooProxy simpleFoo = rf.testFooPolymorphicRequest().create(
-        SimpleFooProxy.class);
+    SimpleFooProxy simpleFoo = rf.testGenericRequest().create(SimpleFooProxy.class);
     assertNull(simpleFoo.getUserName());
   }
+
+  public void testRetrieval() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    ctx.AasA().to(CastAndCheckReceiver.of(AProxy.class));
+    ctx.BasA().to(CastAndCheckReceiver.of(B1Proxy.class));
+    ctx.BasB().to(CastAndCheckReceiver.of(B1Proxy.class));
+    ctx.CasA().to(CastAndCheckReceiver.of(C1Proxy.class));
+    ctx.CasB().to(CastAndCheckReceiver.of(C1Proxy.class));
+    ctx.DasA().to(CastAndCheckReceiver.of(MoreDerivedProxy.class));
+    ctx.DasD().to(CastAndCheckReceiver.of(MoreDerivedProxy.class));
+    ctx.W().to(CastAndCheckReceiver.of(WProxy.class));
+    ctx.W2().to(CastAndCheckReceiver.of(WZProxy.class));
+    ctx.Z().to(CastAndCheckReceiver.of(ZProxy.class));
+    ctx.Z2().to(CastAndCheckReceiver.of(ZWProxy.class));
+    ctx.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        finishTest();
+      }
+    });
+  }
+
+  public void testRetrievalCollection() {
+    delayTestFinish(TEST_DELAY);
+    Context ctx = factory.ctx();
+    ctx.testCollection().to(new ListChecker());
+    ctx.testCollectionSub().to(new ListChecker());
+    ctx.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        finishTest();
+      }
+    });
+  }
+
+  protected Factory createFactory() {
+    Factory f = GWT.create(Factory.class);
+    f.initialize(new SimpleEventBus());
+    return f;
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    factory = createFactory();
+  }
+
+  private void checkA(Context ctx, Class<? extends AProxy> clazz) {
+    ctx.checkA(create(ctx, clazz)).to(checkReceiver);
+  }
+
+  private void checkB(Context ctx, Class<? extends EntityProxy> clazz) {
+    ctx.checkB(create(ctx, clazz)).to(checkReceiver);
+  }
+
+  private void checkC(Context ctx, Class<? extends EntityProxy> clazz) {
+    ctx.checkC(create(ctx, clazz)).to(checkReceiver);
+  }
+
+  private void checkD(Context ctx, Class<? extends EntityProxy> clazz) {
+    ctx.checkD(create(ctx, clazz)).to(checkReceiver);
+  }
+
+  private void checkW(Context ctx, Class<? extends EntityProxy> clazz) {
+    ctx.checkW(create(ctx, clazz)).to(checkReceiver);
+  }
+
+  private void checkZ(Context ctx, Class<? extends EntityProxy> clazz) {
+    ctx.checkZ(create(ctx, clazz)).to(checkReceiver);
+  }
+
+  private <T extends EntityProxy> T create(Context ctx, Class<T> clazz) {
+    T obj = ctx.create(clazz);
+    if (obj instanceof AProxy) {
+      ((AProxy) obj).setA("A");
+    }
+    if (obj instanceof B1Proxy) {
+      ((B1Proxy) obj).setB("B");
+    }
+    if (obj instanceof B2Proxy) {
+      ((B2Proxy) obj).setB("B");
+    }
+    if (obj instanceof C1Proxy) {
+      ((C1Proxy) obj).setC("C");
+    }
+    if (obj instanceof C2Proxy) {
+      ((C2Proxy) obj).setC("C");
+    }
+    if (obj instanceof D1Proxy) {
+      ((D1Proxy) obj).setD("D");
+    }
+    if (obj instanceof D2Proxy) {
+      ((D2Proxy) obj).setD("D");
+    }
+    if (obj instanceof D3Proxy) {
+      ((D3Proxy) obj).setD("D");
+    }
+    if (obj instanceof WProxy) {
+      ((WProxy) obj).setW("W");
+    }
+    if (obj instanceof ZProxy) {
+      ((ZProxy) obj).setZ("Z");
+    }
+    if (obj instanceof WZProxy) {
+      ((WZProxy) obj).setW("W");
+    }
+    if (obj instanceof ZWProxy) {
+      ((ZWProxy) obj).setZ("Z");
+    }
+    return obj;
+  }
 }
diff --git a/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java
new file mode 100644
index 0000000..6dc30c5
--- /dev/null
+++ b/user/test/com/google/web/bindery/requestfactory/server/RequestFactoryPolymorphicJreTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2011 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.web.bindery.requestfactory.server;
+
+import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryPolymorphicTest;
+import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidator.ClassLoaderLoader;
+import com.google.web.bindery.requestfactory.shared.BaseProxy;
+
+import java.util.logging.Logger;
+
+/**
+ * A JRE version of {@link RequestFactoryPolymorphicTest} that includes
+ * additional tests for the RequestFactoryInterfaceValidator that are specific
+ * to type-hierarchy mapping.
+ */
+public class RequestFactoryPolymorphicJreTest extends RequestFactoryPolymorphicTest {
+  RequestFactoryInterfaceValidator v;
+
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+
+  /**
+   * Disabling, since this is a test of the code Generator.
+   */
+  @Override
+  public void testGenericRequest() {
+  }
+
+  /**
+   * Check related proxy types with unrelated domain types.
+   * */
+  public void testUnrelatedDomainTypes() {
+    // Simple mappings
+    check(v, W.class, WProxy.class, WProxy.class);
+    check(v, W.class, WZProxy.class, WZProxy.class);
+    check(v, Z.class, ZProxy.class, ZProxy.class);
+    check(v, Z.class, ZWProxy.class, ZWProxy.class);
+
+    // Look for derived proxy types that map to the domain type
+    check(v, Z.class, WProxy.class, ZWProxy.class);
+    check(v, W.class, ZProxy.class, WZProxy.class);
+
+    /*
+     * This test is included to verify that the validator will fail gracefully
+     * when asked for a nonsensical assignment. For these two tests, the
+     * requested proxy type isn't mapped to the domain type, nor are there any
+     * *sub*-types of the requested proxy class that are assignable to the
+     * domaintype. The requested proxy type's supertype is assignable to the
+     * domain type, however the supertype isn't assignable to its subtype, so
+     * it's not a valid choice. Normally, the RequestFactoryInterfaceValidator
+     * would detect the mismatch between the RequestContext and the service
+     * method return types, so this shouldn't be a problem in practice. It would
+     * only ever crop up when the SkipInterfaceValidation annotation has been
+     * used.
+     */
+    check(v, Z.class, WZProxy.class, null);
+    check(v, W.class, ZWProxy.class, null);
+  }
+
+  /**
+   * Tests that the RequestFactoryInterfaceValidator is producing the correct
+   * mappings from domain types back to client types.
+   */
+  public void testValidator() {
+    /*
+     * Check explicit mappings. Not all of the types are directly referenced in
+     * the method declarations, so this also tests the ExtraTypes annotation.
+     */
+    check(v, A.class, AProxy.class, AProxy.class);
+    check(v, B.class, B1Proxy.class, B1Proxy.class);
+    check(v, B.class, B2Proxy.class, B2Proxy.class);
+    check(v, C.class, C1Proxy.class, C1Proxy.class);
+    check(v, C.class, C2Proxy.class, C2Proxy.class);
+
+    // Check types without explicit mappings.
+    check(v, ASub.class, AProxy.class, AProxy.class);
+    check(v, BSub.class, B1Proxy.class, B1Proxy.class);
+    check(v, BSub.class, B2Proxy.class, B2Proxy.class);
+    check(v, CSub.class, C1Proxy.class, C1Proxy.class);
+    check(v, CSub.class, C2Proxy.class, C2Proxy.class);
+
+    // Check assignments with proxies extending proxies
+    check(v, C.class, B1Proxy.class, C1Proxy.class);
+    check(v, C.class, B2Proxy.class, C2Proxy.class);
+
+    // Should prefer more-derived interfaces when possible
+    check(v, D.class, AProxy.class, MoreDerivedProxy.class);
+    check(v, D.class, D1Proxy.class, MoreDerivedProxy.class);
+    check(v, D.class, D2Proxy.class, MoreDerivedProxy.class);
+    check(v, D.class, D3Proxy.class, MoreDerivedProxy.class);
+    check(v, D.class, MoreDerivedProxy.class, MoreDerivedProxy.class);
+    check(v, DSub.class, AProxy.class, MoreDerivedProxy.class);
+    check(v, DSub.class, D1Proxy.class, MoreDerivedProxy.class);
+    check(v, DSub.class, D2Proxy.class, MoreDerivedProxy.class);
+    check(v, DSub.class, D3Proxy.class, MoreDerivedProxy.class);
+    check(v, DSub.class, MoreDerivedProxy.class, MoreDerivedProxy.class);
+  }
+
+  @Override
+  protected Factory createFactory() {
+    return RequestFactoryJreTest.createInProcess(Factory.class);
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    super.gwtSetUp();
+    Logger logger = Logger.getLogger(getClass().getName());
+    ClassLoaderLoader loader = new ClassLoaderLoader(getClass().getClassLoader());
+    v = new RequestFactoryInterfaceValidator(logger, loader);
+    v.validateRequestFactory(Factory.class.getName());
+  }
+
+  private void check(RequestFactoryInterfaceValidator v, Class<?> domainType,
+      Class<? extends BaseProxy> declaredReturnType, Class<? extends BaseProxy> expectedClientType) {
+    String type = v.getEntityProxyTypeName(domainType.getName(), declaredReturnType.getName());
+    if (expectedClientType == null) {
+      assertNull(type, type);
+    } else {
+      assertEquals(expectedClientType.getName(), type);
+    }
+  }
+}
diff --git a/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java b/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java
index 6ced0f6..8e4aefd 100644
--- a/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java
+++ b/user/test/com/google/web/bindery/requestfactory/shared/TestRequestFactory.java
@@ -16,8 +16,10 @@
 package com.google.web.bindery.requestfactory.shared;
 
 /**
- * Creates TestFooPolymorphicRequest.
+ * Creates TestFooPolymorphicRequest which verifies that the
+ * RequestFactoryGenerator can handle interface declarations that use generic
+ * type parameters.
  */
 public interface TestRequestFactory extends RequestFactory {
-  TestFooPolymorphicRequest testFooPolymorphicRequest();
+  TestFooPolymorphicRequest testGenericRequest();
 }
diff --git a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
index d714c79..f1193a7 100644
--- a/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
+++ b/user/test/com/google/web/bindery/requestfactory/vm/RequestFactoryJreSuite.java
@@ -23,6 +23,7 @@
 import com.google.web.bindery.requestfactory.server.RequestFactoryExceptionPropagationJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryInterfaceValidatorTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryJreTest;
+import com.google.web.bindery.requestfactory.server.RequestFactoryPolymorphicJreTest;
 import com.google.web.bindery.requestfactory.server.RequestFactoryUnicodeEscapingJreTest;
 import com.google.web.bindery.requestfactory.server.ServiceInheritanceJreTest;
 import com.google.web.bindery.requestfactory.server.ServiceLocatorTest;
@@ -47,6 +48,7 @@
     suite.addTestSuite(RequestFactoryExceptionPropagationJreTest.class);
     suite.addTestSuite(RequestFactoryInterfaceValidatorTest.class);
     suite.addTestSuite(RequestFactoryJreTest.class);
+    suite.addTestSuite(RequestFactoryPolymorphicJreTest.class);
     suite.addTestSuite(RequestFactoryUnicodeEscapingJreTest.class);
     suite.addTestSuite(ServiceInheritanceJreTest.class);
     suite.addTestSuite(ServiceLocatorTest.class);