Allow EntityProxy, ValueProxy, or any simple value type to be used as an entity's id and version values.
Allow RequestFactory to work with inner classes to make self-contained tests easier to write.
Resolves issue 5368.
Patch by: bobv
Review by: rchandia, rjrjr

Review at http://gwt-code-reviews.appspot.com/1127801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9261 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
index 6d0877e..fec733f 100644
--- a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
+++ b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
@@ -108,7 +108,7 @@
     StringBuilder parameters = new StringBuilder();
     for (JParameter param : jmethod.getParameters()) {
       parameters.append(",").append(
-          ModelUtils.getQualifiedBaseName(param.getType())).append(" ").append(
+          ModelUtils.getQualifiedBaseSourceName(param.getType())).append(" ").append(
           param.getName());
     }
     if (parameters.length() > 0) {
@@ -119,12 +119,12 @@
     if (jmethod.getThrows().length > 0) {
       for (JType thrown : jmethod.getThrows()) {
         throwsDeclaration.append(". ").append(
-            ModelUtils.getQualifiedBaseName(thrown));
+            ModelUtils.getQualifiedBaseSourceName(thrown));
       }
       throwsDeclaration.deleteCharAt(0);
       throwsDeclaration.insert(0, "throws ");
     }
-    String returnName = ModelUtils.getQualifiedBaseName(jmethod.getReturnType());
+    String returnName = ModelUtils.getQualifiedBaseSourceName(jmethod.getReturnType());
     assert !returnName.contains("extends");
     return String.format("%s %s(%s) %s", returnName, jmethod.getName(),
         parameters, throwsDeclaration);
@@ -248,7 +248,7 @@
           } else {
             // return (ReturnType) values.get(\"foo\");
             sw.println("return (%s) values.get(\"%s\");",
-                ModelUtils.getQualifiedBaseName(jmethod.getReturnType()),
+                ModelUtils.getQualifiedBaseSourceName(jmethod.getReturnType()),
                 method.getPropertyName());
           }
         }
@@ -482,7 +482,7 @@
           // Foo toReturn=FooAutoBean.this.get("getFoo", getWrapped().getFoo());
           sw.println(
               "%s toReturn = %3$s.this.get(\"%2$s\", getWrapped().%2$s());",
-              ModelUtils.getQualifiedBaseName(jmethod.getReturnType()),
+              ModelUtils.getQualifiedBaseSourceName(jmethod.getReturnType()),
               methodName, type.getSimpleSourceName());
 
           // Non-value types might need to be wrapped
diff --git a/user/src/com/google/gwt/autobean/server/AutoBeanFactoryMagic.java b/user/src/com/google/gwt/autobean/server/AutoBeanFactoryMagic.java
index f3d6a87..7fc9f00 100644
--- a/user/src/com/google/gwt/autobean/server/AutoBeanFactoryMagic.java
+++ b/user/src/com/google/gwt/autobean/server/AutoBeanFactoryMagic.java
@@ -94,7 +94,7 @@
       System.arraycopy(extraInterfaces, 0, intfs, 1, extraInterfaces.length);
     }
 
-    return intf.cast(Proxy.newProxyInstance(
-        Thread.currentThread().getContextClassLoader(), intfs, handler));
+    return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs,
+        handler));
   }
 }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
index 3960203..9d228f4 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
@@ -370,10 +370,9 @@
         PropertyContext ctx) {
       // Skip primitive types whose values are uninteresting.
       Class<?> type = ctx.getType();
-      if (value != null) {
-        if (value.equals(ValueCodex.getUninitializedFieldValue(type))) {
-          return false;
-        }
+      Object blankValue = ValueCodex.getUninitializedFieldValue(type);
+      if (value == blankValue || value != null && value.equals(blankValue)) {
+        return false;
       }
 
       // Special handling for enums if we have an obfuscation map
diff --git a/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java b/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java
index 5545cd1..583c49d 100644
--- a/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java
+++ b/user/src/com/google/gwt/editor/rebind/model/ModelUtils.java
@@ -88,11 +88,20 @@
   }
 
   /**
+   * Given a JType, return the binary name of the class that is most proximately
+   * assignable to the type. This method will resolve type parameters as well as
+   * wildcard types.
+   */
+  public static String getQualifiedBaseBinaryName(JType type) {
+    return ensureBaseType(type).getErasedType().getQualifiedBinaryName();
+  }
+
+  /**
    * Given a JType, return the source name of the class that is most proximately
    * assignable to the type. This method will resolve type parameters as well as
    * wildcard types.
    */
-  public static String getQualifiedBaseName(JType type) {
+  public static String getQualifiedBaseSourceName(JType type) {
     return ensureBaseType(type).getErasedType().getQualifiedSourceName();
   }
 
diff --git a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
index 916eee7..89c992a 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
@@ -268,11 +268,11 @@
     sw.indent();
     for (EntityProxyModel type : model.getAllProxyModels()) {
       // tokensToTypes.put("Foo", Foo.class);
-      sw.println("tokensToTypes.put(\"%1$s\", %1$s.class);",
-          type.getQualifiedSourceName());
+      sw.println("tokensToTypes.put(\"%s\", %s.class);",
+          type.getQualifiedBinaryName(), type.getQualifiedSourceName());
       // typesToTokens.put(Foo.class, Foo);
-      sw.println("typesToTokens.put(%1$s.class, \"%1$s\");",
-          type.getQualifiedSourceName());
+      sw.println("typesToTokens.put(%s.class, \"%s\");",
+          type.getQualifiedSourceName(), type.getQualifiedBinaryName());
       // fooProxyTypes.add(MyFooProxy.class);
       sw.println("%s.add(%s.class);", type.getType().equals(Type.ENTITY)
           ? "entityProxyTypes" : "valueProxyTypes",
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java
index 7676035..da85d8c 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/EntityProxyModel.java
@@ -23,14 +23,6 @@
  */
 public class EntityProxyModel {
   /**
-   * The kind of proxy. This is an enum in case more proxy types are defined in
-   * the future.
-   */
-  public enum Type {
-    ENTITY, VALUE
-  }
-
-  /**
    * Builds {@link EntityProxyModel}.
    */
   public static class Builder {
@@ -56,6 +48,10 @@
       toReturn.proxyFor = value;
     }
 
+    public void setQualifiedBinaryName(String qualifiedBinaryName) {
+      toReturn.qualifiedBinaryName = qualifiedBinaryName;
+    }
+
     public void setQualifiedSourceName(String name) {
       assert !name.contains(" ");
       toReturn.qualifiedSourceName = name;
@@ -70,7 +66,16 @@
     }
   }
 
+  /**
+   * The kind of proxy. This is an enum in case more proxy types are defined in
+   * the future.
+   */
+  public enum Type {
+    ENTITY, VALUE
+  }
+
   private Class<?> proxyFor;
+  private String qualifiedBinaryName;
   private String qualifiedSourceName;
   private List<RequestMethod> requestMethods;
   private Type type;
@@ -82,6 +87,10 @@
     return proxyFor;
   }
 
+  public String getQualifiedBinaryName() {
+    return qualifiedBinaryName;
+  }
+
   public String getQualifiedSourceName() {
     return qualifiedSourceName;
   }
@@ -101,5 +110,4 @@
   public String toString() {
     return qualifiedSourceName;
   }
-
 }
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
index 56d859a..2ea756b 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
@@ -209,7 +209,8 @@
       EntityProxyModel.Builder builder = new EntityProxyModel.Builder();
       peerBuilders.put(entityProxyType, builder);
 
-      builder.setQualifiedSourceName(ModelUtils.getQualifiedBaseName(entityProxyType));
+      builder.setQualifiedBinaryName(ModelUtils.getQualifiedBaseBinaryName(entityProxyType));
+      builder.setQualifiedSourceName(ModelUtils.getQualifiedBaseSourceName(entityProxyType));
       if (entityProxyInterface.isAssignableFrom(entityProxyType)) {
         builder.setType(Type.ENTITY);
       } else if (valueProxyInterface.isAssignableFrom(entityProxyType)) {
diff --git a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
index 6ffb4c2..b17acb4 100644
--- a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
+++ b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
@@ -29,9 +29,9 @@
 import com.google.gwt.requestfactory.shared.Service;
 import com.google.gwt.requestfactory.shared.ServiceName;
 import com.google.gwt.requestfactory.shared.ValueProxy;
-import com.google.gwt.requestfactory.shared.messages.EntityCodex;
 import com.google.gwt.requestfactory.shared.messages.EntityCodex.EntitySource;
 
+import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -86,11 +86,21 @@
   public Object createDomainObject(Class<?> clazz) {
     Throwable ex;
     try {
-      return clazz.newInstance();
+      Constructor<?> c = clazz.getConstructor();
+      c.setAccessible(true);
+      return c.newInstance();
     } catch (InstantiationException e) {
       return report("Could not create a new instance of the requested type");
+    } catch (NoSuchMethodException e) {
+      return report("The requested type is not default-instantiable");
+    } catch (InvocationTargetException e) {
+      return report(e);
     } catch (IllegalAccessException e) {
       ex = e;
+    } catch (SecurityException e) {
+      ex = e;
+    } catch (IllegalArgumentException e) {
+      ex = e;
     }
     return die(ex, "Could not create a new instance of domain type %s",
         clazz.getCanonicalName());
@@ -138,16 +148,12 @@
         clazz.getCanonicalName());
   }
 
-  public String getFlatId(EntitySource source, Object domainObject) {
-    Object id = getProperty(domainObject, "id");
-    if (id == null) {
-      return null;
-    }
-    if (!isKeyType(id.getClass())) {
-      die(null, "The type %s is not a valid key type",
-          id.getClass().getCanonicalName());
-    }
-    return EntityCodex.encode(source, id).getPayload();
+  public Object getId(Object domainObject) {
+    return getProperty(domainObject, "id");
+  }
+
+  public Class<?> getIdType(Class<?> domainType) {
+    return getFind(domainType).getParameterTypes()[0];
   }
 
   public Object getProperty(Object domainObject, String property) {
@@ -155,6 +161,7 @@
     try {
       Method method = domainObject.getClass().getMethod(
           "get" + capitalize(property));
+      method.setAccessible(true);
       Object value = method.invoke(domainObject);
       return value;
     } catch (SecurityException e) {
@@ -192,22 +199,14 @@
     return clazz.getName();
   }
 
-  public int getVersion(Object domainObject) {
-    // TODO: Make version any value type
-    Object version = getProperty(domainObject, "version");
-    if (version == null) {
-      return 0;
-    }
-    if (!(version instanceof Integer)) {
-      die(null, "The getVersion() method on type %s did not return"
-          + " int or Integer", domainObject.getClass().getCanonicalName());
-    }
-    return ((Integer) version).intValue();
+  public Object getVersion(Object domainObject) {
+    return getProperty(domainObject, "version");
   }
 
-  public Object invoke(Method domainMethod, Object[] args) {
+  public Object invoke(Method domainMethod, Object... args) {
     Throwable ex;
     try {
+      domainMethod.setAccessible(true);
       if (Modifier.isStatic(domainMethod.getModifiers())) {
         return domainMethod.invoke(null, args);
       } else {
@@ -225,46 +224,19 @@
     return die(ex, "Could not invoke method %s", domainMethod.getName());
   }
 
-  public Object loadDomainObject(EntitySource source, Class<?> clazz,
-      String flatId) {
-    String searchFor = "find" + clazz.getSimpleName();
-    Method found = null;
-    for (Method method : clazz.getMethods()) {
-      if (!Modifier.isStatic(method.getModifiers())) {
-        continue;
-      }
-      if (!searchFor.equals(method.getName())) {
-        continue;
-      }
-      if (method.getParameterTypes().length != 1) {
-        continue;
-      }
-      if (!isKeyType(method.getParameterTypes()[0])) {
-        continue;
-      }
-      found = method;
-      break;
-    }
-    if (found == null) {
-      die(null, "Could not find static method %s with a single"
-          + " parameter of a key type", searchFor);
-    }
-    Object id = EntityCodex.decode(source, found.getParameterTypes()[0], null,
-        flatId);
+  /**
+   * This implementation attempts to re-load the object from the backing store.
+   */
+  public boolean isLive(EntitySource source, Object domainObject) {
+    Object id = getId(domainObject);
+    return invoke(getFind(domainObject.getClass()), id) != null;
+  }
+
+  public Object loadDomainObject(EntitySource source, Class<?> clazz, Object id) {
     if (id == null) {
-      report("Cannot load a domain object with a null id");
+      die(null, "Cannot invoke find method with a null id");
     }
-    Throwable ex;
-    try {
-      return found.invoke(null, id);
-    } catch (IllegalArgumentException e) {
-      ex = e;
-    } catch (IllegalAccessException e) {
-      ex = e;
-    } catch (InvocationTargetException e) {
-      return report(e);
-    }
-    return die(ex, "Cauld not load domain object using id", id.toString());
+    return invoke(getFind(clazz), id);
   }
 
   public Class<? extends BaseProxy> resolveClass(String typeToken) {
@@ -288,14 +260,6 @@
 
   public Method resolveDomainMethod(Method requestContextMethod) {
     Class<?> enclosing = requestContextMethod.getDeclaringClass();
-    synchronized (validator) {
-      validator.antidote();
-      validator.validateRequestContext(enclosing.getName());
-      if (validator.isPoisoned()) {
-        die(null, "The type %s did not pass RequestFactory validation",
-            enclosing.getCanonicalName());
-      }
-    }
 
     Class<?> searchIn = null;
     Service s = enclosing.getAnnotation(Service.class);
@@ -340,6 +304,14 @@
 
   public Method resolveRequestContextMethod(String requestContextClass,
       String methodName) {
+    synchronized (validator) {
+      validator.antidote();
+      validator.validateRequestContext(requestContextClass);
+      if (validator.isPoisoned()) {
+        die(null, "The RequestContext type %s did not pass validation",
+            requestContextClass);
+      }
+    }
     Class<?> searchIn = forName(requestContextClass);
     for (Method method : searchIn.getMethods()) {
       if (method.getName().equals(methodName)) {
@@ -352,12 +324,13 @@
 
   public void setProperty(Object domainObject, String property,
       Class<?> expectedType, Object value) {
-    Method getId;
+    Method setter;
     Throwable ex;
     try {
-      getId = domainObject.getClass().getMethod("set" + capitalize(property),
+      setter = domainObject.getClass().getMethod("set" + capitalize(property),
           expectedType);
-      getId.invoke(domainObject, value);
+      setter.setAccessible(true);
+      setter.invoke(domainObject, value);
       return;
     } catch (SecurityException e) {
       ex = e;
@@ -395,6 +368,10 @@
     throw new UnexpectedException(msg, e);
   }
 
+  /**
+   * Call {@link Class#forName(String)} and report any errors through
+   * {@link #die()}.
+   */
   private Class<?> forName(String name) {
     try {
       return Class.forName(name, false,
@@ -404,9 +381,41 @@
     }
   }
 
-  private boolean isKeyType(Class<?> clazz) {
-    return ValueCodex.canDecode(clazz)
-        || BaseProxy.class.isAssignableFrom(clazz);
+  private Method getFind(Class<?> clazz) {
+    if (clazz == null) {
+      return die(null, "Could not find static method with a single"
+          + " parameter of a key type");
+    }
+    String searchFor = "find" + clazz.getSimpleName();
+    for (Method method : clazz.getMethods()) {
+      if (!Modifier.isStatic(method.getModifiers())) {
+        continue;
+      }
+      if (!searchFor.equals(method.getName())) {
+        continue;
+      }
+      if (method.getParameterTypes().length != 1) {
+        continue;
+      }
+      if (!isKeyType(method.getParameterTypes()[0])) {
+        continue;
+      }
+      return method;
+    }
+    return getFind(clazz.getSuperclass());
+  }
+
+  /**
+   * Returns <code>true</code> if the given class can be used as an id or
+   * version key.
+   */
+  private boolean isKeyType(Class<?> domainClass) {
+    if (ValueCodex.canDecode(domainClass)) {
+      return true;
+    }
+
+    return BaseProxy.class.isAssignableFrom(getClientType(domainClass,
+        BaseProxy.class));
   }
 
   /**
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java b/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java
index ed21fc4..e65603a 100644
--- a/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java
+++ b/user/src/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidator.java
@@ -140,7 +140,6 @@
   private class DomainMapper extends EmptyVisitor {
     private final ErrorContext logger;
     private String domainInternalName;
-    private boolean isValueType;
 
     public DomainMapper(ErrorContext logger) {
       this.logger = logger;
@@ -151,10 +150,6 @@
       return domainInternalName;
     }
 
-    public boolean isValueType() {
-      return isValueType;
-    }
-
     @Override
     public void visit(int version, int access, String name, String signature,
         String superName, String[] interfaces) {
@@ -177,8 +172,6 @@
           public void visit(String name, Object value) {
             if ("value".equals(name)) {
               domainInternalName = ((Type) value).getInternalName();
-            } else if ("isValueType".equals(name)) {
-              isValueType = (Boolean) value;
             }
           }
         };
@@ -208,9 +201,6 @@
 
               domainInternalName = desc.toString();
               logger.spam(domainInternalName);
-            } else if ("isValueType".equals(name)) {
-              isValueType = (Boolean) value;
-              logger.spam("isValueType: %s", isValueType);
             }
           }
         };
@@ -569,6 +559,11 @@
    */
   private final Set<Type> valueTypes = new HashSet<Type>();
 
+  /**
+   * Maps a domain object to the type returned from its getId method.
+   */
+  private final Map<Type, Type> unresolvedKeyTypes = new HashMap<Type, Type>();
+
   public RequestFactoryInterfaceValidator(Logger logger, Loader loader) {
     this.parentLogger = new ErrorContext(logger);
     this.loader = loader;
@@ -702,6 +697,8 @@
       checkClientMethodInDomain(logger, method, domainServiceType);
       maybeCheckReferredProxies(logger, method);
     }
+
+    checkUnresolvedKeyTypes(logger);
   }
 
   /**
@@ -887,18 +884,55 @@
       return;
     }
     logger = logger.setType(domainType);
-    Method getIdString = new Method("getId", "()Ljava/lang/String;");
-    Method getIdLong = new Method("getId", "()Ljava/lang/Long;");
-    if (findCompatibleMethod(logger, domainType, getIdString, false, true) == null
-        && findCompatibleMethod(logger, domainType, getIdLong, false, true) == null) {
-      logger.poison("Did not find a getId() method that"
-          + " returns a String or a Long");
+    String findMethodName = "find"
+        + BinaryName.getShortClassName(domainType.getClassName());
+    Type keyType = null;
+    RFMethod findMethod = null;
+
+    boolean foundFind = false;
+    boolean foundId = false;
+    boolean foundVersion = false;
+    for (RFMethod method : getMethodsInHierarchy(logger, domainType)) {
+      if ("getId".equals(method.getName())
+          && method.getArgumentTypes().length == 0) {
+        foundId = true;
+        keyType = method.getReturnType();
+        if (!isResolvedKeyType(logger, keyType)) {
+          unresolvedKeyTypes.put(domainType, keyType);
+        }
+      } else if ("getVersion".equals(method.getName())
+          && method.getArgumentTypes().length == 0) {
+        foundVersion = true;
+        if (!isResolvedKeyType(logger, method.getReturnType())) {
+          unresolvedKeyTypes.put(domainType, method.getReturnType());
+        }
+      } else if (findMethodName.equals(method.getName())
+          && method.getArgumentTypes().length == 1) {
+        foundFind = true;
+        findMethod = method;
+      }
+      if (foundFind && foundId && foundVersion) {
+        break;
+      }
+    }
+    if (!foundId) {
+      logger.poison("There is no getId() method in type %s", print(domainType));
+    }
+    if (!foundVersion) {
+      logger.poison("There is no getVersion() method in type %s",
+          print(domainType));
     }
 
-    Method getVersion = new Method("getVersion", "()Ljava/lang/Integer;");
-    if (findCompatibleMethod(logger, domainType, getVersion) == null) {
-      logger.poison("Did not find a getVersion() method"
-          + " that returns an Integer");
+    if (foundFind) {
+      if (keyType != null
+          && !isAssignable(logger, findMethod.getArgumentTypes()[0], keyType)) {
+        logger.poison("The key type returned by %s getId()"
+            + " cannot be used as the argument to %s(%s)", print(keyType),
+            findMethod.getName(), print(findMethod.getArgumentTypes()[0]));
+      }
+    } else {
+      logger.poison("There is no %s method in type %s that returns %2$s",
+          findMethodName, print(domainType));
     }
   }
 
@@ -914,6 +948,21 @@
         createDomainMethod(logger, clientPropertyMethod));
   }
 
+  private void checkUnresolvedKeyTypes(ErrorContext logger) {
+    unresolvedKeyTypes.values().removeAll(domainToClientType.keySet());
+    if (unresolvedKeyTypes.isEmpty()) {
+      return;
+    }
+
+    for (Map.Entry<Type, Type> type : unresolvedKeyTypes.entrySet()) {
+      logger.setType(type.getKey()).poison(
+          "The domain type %s uses  a non-simple key type (%s)"
+              + " in its getId() or getVersion() method that"
+              + " does not have a proxy mapping.", print(type.getKey()),
+          print(type.getValue()));
+    }
+  }
+
   /**
    * Convert a method declaration using client types (e.g. FooProxy) to domain
    * types (e.g. Foo).
@@ -1091,12 +1140,10 @@
         domainType = errorType;
       } else {
         domainType = Type.getObjectType(pv.getDomainInternalName());
-        if (pv.isValueType()) {
-          valueTypes.add(clientType);
-        }
       }
     }
     addToDomainMap(logger, domainType, clientType);
+    maybeCheckProxyType(logger, clientType);
     return domainType;
   }
 
@@ -1218,6 +1265,22 @@
         || "java/util/Set".equals(type.getInternalName());
   }
 
+  /**
+   * Keep in sync with {@code ReflectiveServiceLayer.isKeyType()}.
+   */
+  private boolean isResolvedKeyType(ErrorContext logger, Type type) {
+    if (isValueType(logger, type)) {
+      return true;
+    }
+
+    // We have already seen a mapping for the key type
+    if (domainToClientType.containsKey(type)) {
+      return true;
+    }
+
+    return false;
+  }
+
   private boolean isValueType(ErrorContext logger, Type type) {
     if (type.getSort() != Type.OBJECT) {
       return true;
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestState.java b/user/src/com/google/gwt/requestfactory/server/RequestState.java
index 9dcccdc..5cb6c6c 100644
--- a/user/src/com/google/gwt/requestfactory/server/RequestState.java
+++ b/user/src/com/google/gwt/requestfactory/server/RequestState.java
@@ -17,6 +17,10 @@
 
 import com.google.gwt.autobean.server.AutoBeanFactoryMagic;
 import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBeanCodex;
+import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.autobean.shared.ValueCodex;
+import com.google.gwt.autobean.shared.impl.StringQuoter;
 import com.google.gwt.requestfactory.server.SimpleRequestProcessor.IdToEntityMap;
 import com.google.gwt.requestfactory.server.SimpleRequestProcessor.ServiceLayer;
 import com.google.gwt.requestfactory.shared.BaseProxy;
@@ -24,10 +28,13 @@
 import com.google.gwt.requestfactory.shared.ValueProxy;
 import com.google.gwt.requestfactory.shared.impl.Constants;
 import com.google.gwt.requestfactory.shared.impl.IdFactory;
+import com.google.gwt.requestfactory.shared.impl.MessageFactoryHolder;
 import com.google.gwt.requestfactory.shared.impl.SimpleProxyId;
 import com.google.gwt.requestfactory.shared.messages.EntityCodex;
-import com.google.gwt.requestfactory.shared.messages.IdUtil;
+import com.google.gwt.requestfactory.shared.messages.IdMessage;
+import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength;
 
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Map;
 
@@ -77,6 +84,31 @@
     resolver = new Resolver(this);
   }
 
+  public Splittable flatten(Object domainValue) {
+    Splittable flatValue;
+    if (ValueCodex.canDecode(domainValue.getClass())) {
+      flatValue = ValueCodex.encode(domainValue);
+    } else {
+      flatValue = new SimpleRequestProcessor(service).createOobMessage(Collections.singletonList(domainValue));
+    }
+    return flatValue;
+  }
+
+  public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(IdMessage idMessage) {
+    SimpleProxyId<Q> id;
+    if (idMessage.getSyntheticId() > 0) {
+      @SuppressWarnings("unchecked")
+      Class<Q> clazz = (Class<Q>) service.resolveClass(idMessage.getTypeToken());
+      id = idFactory.allocateSyntheticId(clazz, idMessage.getSyntheticId());
+    } else {
+      String decodedId = idMessage.getServerId() == null ? null
+          : SimpleRequestProcessor.fromBase64(idMessage.getServerId());
+      id = idFactory.getId(idMessage.getTypeToken(), decodedId,
+          idMessage.getClientId());
+    }
+    return getBeanForPayload(id);
+  }
+
   public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
       SimpleProxyId<Q> id, Object domainObject) {
     @SuppressWarnings("unchecked")
@@ -91,18 +123,10 @@
    * EntityCodex support.
    */
   public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
-      String serializedProxyId) {
-    String typeToken = IdUtil.getTypeToken(serializedProxyId);
-    SimpleProxyId<Q> id;
-    if (IdUtil.isEphemeral(serializedProxyId)) {
-      id = idFactory.getId(typeToken, null,
-          IdUtil.getClientId(serializedProxyId));
-    } else {
-      id = idFactory.getId(
-          typeToken,
-          SimpleRequestProcessor.fromBase64(IdUtil.getServerId(serializedProxyId)));
-    }
-    return getBeanForPayload(id);
+      Splittable serializedProxyId) {
+    IdMessage idMessage = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
+        IdMessage.class, serializedProxyId).as();
+    return getBeanForPayload(idMessage);
   }
 
   public IdFactory getIdFactory() {
@@ -118,20 +142,23 @@
    * {@link IdFactory#getHistoryToken(SimpleProxyId)} except that it
    * base64-encodes the server ids.
    * <p>
-   * XXX: Fix the above
+   * XXX: Merge this with AbstsractRequestContext's implementation
    */
-  public String getSerializedProxyId(SimpleProxyId<?> stableId) {
-    if (stableId.isEphemeral()) {
-      return IdUtil.ephemeralId(stableId.getClientId(),
-          service.getTypeToken(stableId.getProxyClass()));
-    } else if (stableId.isSynthetic()) {
-      return IdUtil.syntheticId(stableId.getSyntheticId(),
-          service.getTypeToken(stableId.getProxyClass()));
+  public Splittable getSerializedProxyId(SimpleProxyId<?> stableId) {
+    AutoBean<IdMessage> bean = MessageFactoryHolder.FACTORY.id();
+    IdMessage ref = bean.as();
+    ref.setTypeToken(service.getTypeToken(stableId.getProxyClass()));
+    if (stableId.isSynthetic()) {
+      ref.setStrength(Strength.SYNTHETIC);
+      ref.setSyntheticId(stableId.getSyntheticId());
+    } else if (stableId.isEphemeral()) {
+      ref.setStrength(Strength.EPHEMERAL);
+      ref.setClientId(stableId.getClientId());
     } else {
-      return IdUtil.persistedId(
-          SimpleRequestProcessor.toBase64(stableId.getServerId()),
-          service.getTypeToken(stableId.getProxyClass()));
+      ref.setStrength(Strength.PERSISTED);
+      ref.setServerId(SimpleRequestProcessor.toBase64(stableId.getServerId()));
     }
+    return AutoBeanCodex.encode(bean);
   }
 
   public ServiceLayer getServiceLayer() {
@@ -185,11 +212,19 @@
       // Resolve the domain object
       Class<?> domainClass = service.getDomainClass(id.getProxyClass());
       Object domain;
-      if (id.isEphemeral()) {
+      if (id.isEphemeral() || id.isSynthetic()) {
         domain = service.createDomainObject(domainClass);
       } else {
-        String address = id.getServerId();
-        domain = service.loadDomainObject(this, domainClass, address);
+        Splittable address = StringQuoter.split(id.getServerId());
+        Class<?> param = service.getIdType(domainClass);
+        Object domainParam;
+        if (ValueCodex.canDecode(param)) {
+          domainParam = ValueCodex.decode(param, address);
+        } else {
+          domainParam = new SimpleRequestProcessor(service).decodeOobMessage(
+              param, address).get(0);
+        }
+        domain = service.loadDomainObject(this, domainClass, domainParam);
       }
       domainObjectsToId.put(domain, id);
       toReturn = createEntityProxyBean(id, domain);
diff --git a/user/src/com/google/gwt/requestfactory/server/Resolver.java b/user/src/com/google/gwt/requestfactory/server/Resolver.java
index 20abe2c..24a7fed 100644
--- a/user/src/com/google/gwt/requestfactory/server/Resolver.java
+++ b/user/src/com/google/gwt/requestfactory/server/Resolver.java
@@ -19,6 +19,7 @@
 import com.google.gwt.autobean.shared.AutoBean;
 import com.google.gwt.autobean.shared.AutoBeanUtils;
 import com.google.gwt.autobean.shared.AutoBeanVisitor;
+import com.google.gwt.autobean.shared.Splittable;
 import com.google.gwt.autobean.shared.ValueCodex;
 import com.google.gwt.requestfactory.server.SimpleRequestProcessor.ServiceLayer;
 import com.google.gwt.requestfactory.shared.BaseProxy;
@@ -187,7 +188,8 @@
    * @param domainValue the domain object to be converted into a client-side
    *          value
    * @param assignableTo the type in the client to which the resolved value
-   *          should be assignable
+   *          should be assignable. A value of {@code null} indicates that any
+   *          resolution will suffice.
    * @param propertyRefs the property references requested by the client
    */
   public Object resolveClientValue(Object domainValue, Type assignableTo,
@@ -266,22 +268,22 @@
 
     boolean isEntityProxy = state.isEntityType(proxyType);
     final boolean isOwnerValueProxy = state.isValueType(proxyType);
-    int version;
+    Object domainVersion;
 
     // Create the id or update an ephemeral id by calculating its address
     if (id == null || id.isEphemeral()) {
       // The address is an id or an id plus a path
-      String address;
+      Object domainId;
       if (isEntityProxy) {
         // Compute data needed to return id to the client
-        address = service.getFlatId(state, domainEntity);
-        version = service.getVersion(domainEntity);
+        domainId = service.getId(domainEntity);
+        domainVersion = service.getVersion(domainEntity);
       } else {
-        address = null;
-        version = 0;
+        domainId = null;
+        domainVersion = null;
       }
       if (id == null) {
-        if (address == null) {
+        if (domainId == null) {
           /*
            * This will happen when server code attempts to return an unpersisted
            * object to the client. In this case, we'll assign a synthetic id
@@ -293,24 +295,32 @@
           id = state.getIdFactory().allocateSyntheticId(proxyType,
               ++syntheticId);
         } else {
-          id = state.getIdFactory().getId(proxyType, address, null);
+          Splittable flatValue = state.flatten(domainId);
+          id = state.getIdFactory().getId(proxyType, flatValue.getPayload(), 0);
         }
-      } else {
-        id.setServerId(address);
+      } else if (domainId != null) {
+        // Mark an ephemeral id as having been persisted
+        Splittable flatValue = state.flatten(domainId);
+        id.setServerId(flatValue.getPayload());
       }
     } else if (isEntityProxy) {
       // Already have the id, just pull the current version
-      version = service.getVersion(domainEntity);
+      domainVersion = service.getVersion(domainEntity);
     } else {
-      // The version of a value object is always 0
-      version = 0;
+      // The version of a value object is always null
+      domainVersion = null;
     }
 
     @SuppressWarnings("unchecked")
     AutoBean<T> bean = (AutoBean<T>) state.getBeanForPayload(id, domainEntity);
     resolved.put(key, bean.as());
     bean.setTag(Constants.IN_RESPONSE, true);
-    bean.setTag(Constants.ENCODED_VERSION_PROPERTY, version);
+    if (domainVersion != null) {
+      Splittable flatVersion = state.flatten(domainVersion);
+      bean.setTag(Constants.VERSION_PROPERTY_B64,
+          SimpleRequestProcessor.toBase64(flatVersion.getPayload()));
+    }
+
     bean.accept(new AutoBeanVisitor() {
 
       @Override
@@ -376,6 +386,11 @@
       return null;
     }
 
+    boolean anyType = returnType == null;
+    if (anyType) {
+      returnType = Object.class;
+    }
+
     Class<?> assignableTo = TypeUtils.ensureBaseType(returnType);
     ResolutionKey key = new ResolutionKey(domainValue, returnType);
 
@@ -387,6 +402,10 @@
     Class<?> returnClass = service.getClientType(domainValue.getClass(),
         assignableTo);
 
+    if (anyType) {
+      assignableTo = returnClass;
+    }
+
     // Pass simple values through
     if (ValueCodex.canDecode(returnClass)) {
       return assignableTo.cast(domainValue);
@@ -396,8 +415,9 @@
     boolean isProxy = BaseProxy.class.isAssignableFrom(returnClass);
     boolean isId = EntityProxyId.class.isAssignableFrom(returnClass);
     if (isProxy || isId) {
-      BaseProxy entity = resolveClientProxy(domainValue,
-          assignableTo.asSubclass(BaseProxy.class), propertyRefs, key, prefix);
+      Class<? extends BaseProxy> proxyClass = assignableTo.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/gwt/requestfactory/server/SimpleRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
index 8637cf6..03c830a 100644
--- a/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
+++ b/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
@@ -37,7 +37,6 @@
 import com.google.gwt.requestfactory.shared.impl.ValueProxyCategory;
 import com.google.gwt.requestfactory.shared.messages.EntityCodex;
 import com.google.gwt.requestfactory.shared.messages.EntityCodex.EntitySource;
-import com.google.gwt.requestfactory.shared.messages.IdUtil;
 import com.google.gwt.requestfactory.shared.messages.InvocationMessage;
 import com.google.gwt.requestfactory.shared.messages.MessageFactory;
 import com.google.gwt.requestfactory.shared.messages.OperationMessage;
@@ -84,7 +83,13 @@
      * May return {@code null} to indicate that the domain object has not been
      * persisted.
      */
-    String getFlatId(EntitySource source, Object domainObject);
+    Object getId(Object domainObject);
+
+    /**
+     * Returns the type of object the domain type's {@code findFoo()} expects to
+     * receive.
+     */
+    Class<?> getIdType(Class<?> domainType);
 
     Object getProperty(Object domainObject, String property);
 
@@ -96,11 +101,21 @@
 
     String getTypeToken(Class<?> domainClass);
 
-    int getVersion(Object domainObject);
+    /**
+     * May return {@code null} to indicate that the domain object has not been
+     * persisted.
+     */
+    Object getVersion(Object domainObject);
 
-    Object invoke(Method domainMethod, Object[] args);
+    Object invoke(Method domainMethod, Object... args);
 
-    Object loadDomainObject(EntitySource source, Class<?> clazz, String flatId);
+    /**
+     * Returns {@code true} if the given domain object is still live (i.e. not
+     * deleted) in the backing store.
+     */
+    boolean isLive(EntitySource source, Object domainObject);
+
+    Object loadDomainObject(EntitySource source, Class<?> clazz, Object domainId);
 
     Class<? extends BaseProxy> resolveClass(String typeToken);
 
@@ -154,7 +169,6 @@
   }
 
   private ExceptionHandler exceptionHandler = new DefaultExceptionHandler();
-
   private final ServiceLayer service;
 
   public SimpleRequestProcessor(ServiceLayer serviceLayer) {
@@ -182,6 +196,61 @@
   }
 
   /**
+   * Encode a list of objects into a self-contained message that can be used for
+   * out-of-band communication.
+   */
+  <T> Splittable createOobMessage(List<T> domainValues) {
+    RequestState state = new RequestState(service);
+
+    List<Splittable> encodedValues = new ArrayList<Splittable>(
+        domainValues.size());
+    for (T domainValue : domainValues) {
+      Object clientValue;
+      if (domainValue == null) {
+        clientValue = null;
+      } else {
+        Class<?> clientType = service.getClientType(domainValue.getClass(),
+            BaseProxy.class);
+        clientValue = state.getResolver().resolveClientValue(domainValue,
+            clientType, Collections.<String> emptySet());
+      }
+      encodedValues.add(EntityCodex.encode(state, clientValue));
+    }
+
+    IdToEntityMap map = new IdToEntityMap();
+    map.putAll(state.beans);
+    List<OperationMessage> operations = new ArrayList<OperationMessage>();
+    createReturnOperations(operations, state, map);
+
+    InvocationMessage invocation = FACTORY.invocation().as();
+    invocation.setParameters(encodedValues);
+
+    AutoBean<RequestMessage> bean = FACTORY.request();
+    RequestMessage resp = bean.as();
+    resp.setInvocations(Collections.singletonList(invocation));
+    resp.setOperations(operations);
+    return AutoBeanCodex.encode(bean);
+  }
+
+  /**
+   * Decode an out-of-band message.
+   */
+  <T> List<T> decodeOobMessage(Class<T> domainClass, Splittable payload) {
+    Class<?> proxyType = service.getClientType(domainClass, BaseProxy.class);
+    RequestState state = new RequestState(service);
+    RequestMessage message = AutoBeanCodex.decode(FACTORY,
+        RequestMessage.class, payload).as();
+    processOperationMessages(state, message);
+    List<Object> decoded = decodeInvocationArguments(state,
+        message.getInvocations().get(0).getParameters(),
+        new Class<?>[] {proxyType}, new Type[] {domainClass});
+
+    @SuppressWarnings("unchecked")
+    List<T> toReturn = (List<T>) decoded;
+    return toReturn;
+  }
+
+  /**
    * Main processing method.
    */
   void process(RequestMessage req, ResponseMessage resp) {
@@ -246,21 +315,24 @@
             id.getProxyClass(), Collections.<String> emptySet());
       }
 
-      int version;
-      if (id.isEphemeral() || id.isSynthetic()) {
+      if (id.isEphemeral() || id.isSynthetic() || domainObject == null) {
         // If the object isn't persistent, there's no reason to send an update
         writeOperation = null;
-        version = 0;
-      } else if (service.loadDomainObject(returnState,
-          service.getDomainClass(id.getProxyClass()), id.getServerId()) == null) {
+      } else if (!service.isLive(returnState, domainObject)) {
         writeOperation = WriteOperation.DELETE;
-        version = 0;
       } else if (id.wasEphemeral()) {
         writeOperation = WriteOperation.PERSIST;
-        version = service.getVersion(domainObject);
       } else {
         writeOperation = WriteOperation.UPDATE;
-        version = service.getVersion(domainObject);
+      }
+
+      Splittable version = null;
+      if (writeOperation == WriteOperation.PERSIST
+          || writeOperation == WriteOperation.UPDATE) {
+        Object domainVersion = service.getVersion(domainObject);
+        if (domainVersion != null) {
+          version = returnState.flatten(domainVersion);
+        }
       }
 
       boolean inResponse = bean.getTag(Constants.IN_RESPONSE) != null;
@@ -271,8 +343,9 @@
        * the domain version.
        */
       if (WriteOperation.UPDATE.equals(writeOperation) && !inResponse) {
-        if (Integer.valueOf(version).equals(
-            bean.getTag(Constants.ENCODED_VERSION_PROPERTY))) {
+        String previousVersion = bean.<String> getTag(Constants.VERSION_PROPERTY_B64);
+        if (version != null && previousVersion != null
+            && version.equals(fromBase64(previousVersion))) {
           continue;
         }
       }
@@ -310,44 +383,15 @@
 
       op.setSyntheticId(id.getSyntheticId());
       op.setTypeToken(service.getTypeToken(id.getProxyClass()));
-      op.setVersion(version);
+      if (version != null) {
+        op.setVersion(toBase64(version.getPayload()));
+      }
 
       operations.add(op);
     }
   }
 
   /**
-   * Handles instance invocations as the instance at the 0th parameter.
-   */
-  private List<Object> decodeInvocationArguments(RequestState source,
-      InvocationMessage invocation, Class<?>[] contextArgs, Type[] genericArgs) {
-    if (invocation.getParameters() == null) {
-      return Collections.emptyList();
-    }
-
-    assert invocation.getParameters().size() == contextArgs.length;
-    List<Object> args = new ArrayList<Object>(contextArgs.length);
-    for (int i = 0, j = contextArgs.length; i < j; i++) {
-      Class<?> type = contextArgs[i];
-      Class<?> elementType = null;
-      Splittable split;
-      if (Collection.class.isAssignableFrom(type)) {
-        elementType = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
-            Collection.class, genericArgs[i]));
-        split = invocation.getParameters().get(i);
-      } else {
-        split = invocation.getParameters().get(i);
-      }
-      Object arg = EntityCodex.decode(source, type, elementType, split);
-      arg = source.getResolver().resolveDomainValue(arg,
-          !EntityProxyId.class.equals(contextArgs[i]));
-      args.add(arg);
-    }
-
-    return args;
-  }
-
-  /**
    * Decode the arguments to pass into the domain method. If the domain method
    * is not static, the instance object will be in the 0th position.
    */
@@ -370,8 +414,39 @@
     System.arraycopy(contextMethod.getGenericParameterTypes(), 0, genericArgs,
         offset, baseLength);
 
-    List<Object> args = decodeInvocationArguments(source, invocation,
-        contextArgs, genericArgs);
+    List<Object> args = decodeInvocationArguments(source,
+        invocation.getParameters(), contextArgs, genericArgs);
+    return args;
+  }
+
+  /**
+   * Handles instance invocations as the instance at the 0th parameter.
+   */
+  private List<Object> decodeInvocationArguments(RequestState source,
+      List<Splittable> parameters, Class<?>[] contextArgs, Type[] genericArgs) {
+    if (parameters == null) {
+      return Collections.emptyList();
+    }
+
+    assert parameters.size() == contextArgs.length;
+    List<Object> args = new ArrayList<Object>(contextArgs.length);
+    for (int i = 0, j = contextArgs.length; i < j; i++) {
+      Class<?> type = contextArgs[i];
+      Class<?> elementType = null;
+      Splittable split;
+      if (Collection.class.isAssignableFrom(type)) {
+        elementType = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
+            Collection.class, genericArgs[i]));
+        split = parameters.get(i);
+      } else {
+        split = parameters.get(i);
+      }
+      Object arg = EntityCodex.decode(source, type, elementType, split);
+      arg = source.getResolver().resolveDomainValue(arg,
+          !EntityProxyId.class.equals(contextArgs[i]));
+      args.add(arg);
+    }
+
     return args;
   }
 
@@ -415,13 +490,11 @@
 
     for (final OperationMessage operation : operations) {
       // Unflatten properties
-      String payloadId = operation.getOperation().equals(WriteOperation.PERSIST)
-          ? IdUtil.ephemeralId(operation.getClientId(),
-              operation.getTypeToken()) : IdUtil.persistedId(
-              operation.getServerId(), operation.getTypeToken());
-      AutoBean<? extends EntityProxy> bean = state.getBeanForPayload(payloadId);
+      AutoBean<? extends EntityProxy> bean = state.getBeanForPayload(operation);
       // Use the version later to know which objects need to be sent back
-      bean.setTag(Constants.ENCODED_VERSION_PROPERTY, operation.getVersion());
+      if (operation.getVersion() != null) {
+        bean.setTag(Constants.VERSION_PROPERTY_B64, operation.getVersion());
+      }
 
       // Load the domain object with properties, if it exists
       final Object domain = bean.getTag(Constants.DOMAIN_OBJECT);
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
index 1b3befb..8d90add 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
@@ -38,7 +38,7 @@
 import com.google.gwt.requestfactory.shared.impl.posers.DatePoser;
 import com.google.gwt.requestfactory.shared.messages.EntityCodex;
 import com.google.gwt.requestfactory.shared.messages.IdMessage;
-import com.google.gwt.requestfactory.shared.messages.IdUtil;
+import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength;
 import com.google.gwt.requestfactory.shared.messages.InvocationMessage;
 import com.google.gwt.requestfactory.shared.messages.MessageFactory;
 import com.google.gwt.requestfactory.shared.messages.OperationMessage;
@@ -220,14 +220,11 @@
    * EntityCodex support.
    */
   public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
-      String serializedProxyId) {
-    SimpleProxyId<Q> id;
-    if (IdUtil.isSynthetic(serializedProxyId)) {
-      id = allocateSyntheticId(IdUtil.getTypeToken(serializedProxyId),
-          IdUtil.getSyntheticId(serializedProxyId));
-    } else {
-      id = requestFactory.getBaseProxyId(serializedProxyId);
-    }
+      Splittable serializedProxyId) {
+    IdMessage ref = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
+        IdMessage.class, serializedProxyId).as();
+    @SuppressWarnings("unchecked")
+    SimpleProxyId<Q> id = (SimpleProxyId<Q>) getId(ref);
     return getProxyForReturnPayloadGraph(id);
   }
 
@@ -238,8 +235,21 @@
   /**
    * EntityCodex support.
    */
-  public String getSerializedProxyId(SimpleProxyId<?> stableId) {
-    return requestFactory.getHistoryToken(stableId);
+  public Splittable getSerializedProxyId(SimpleProxyId<?> stableId) {
+    AutoBean<IdMessage> bean = MessageFactoryHolder.FACTORY.id();
+    IdMessage ref = bean.as();
+    ref.setServerId(stableId.getServerId());
+    ref.setTypeToken(getRequestFactory().getTypeToken(stableId.getProxyClass()));
+    if (stableId.isSynthetic()) {
+      ref.setStrength(Strength.SYNTHETIC);
+      ref.setSyntheticId(stableId.getSyntheticId());
+    } else if (stableId.isEphemeral()) {
+      ref.setStrength(Strength.EPHEMERAL);
+      ref.setClientId(stableId.getClientId());
+    } else {
+      ref.setStrength(Strength.PERSISTED);
+    }
+    return AutoBeanCodex.encode(bean);
   }
 
   public boolean isChanged() {
@@ -514,11 +524,8 @@
     if (op.getSyntheticId() > 0) {
       return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId());
     }
-    if (op.getClientId() > 0) {
-      return requestFactory.getId(op.getTypeToken(), op.getServerId(),
-          op.getClientId());
-    }
-    return requestFactory.getId(op.getTypeToken(), op.getServerId());
+    return requestFactory.getId(op.getTypeToken(), op.getServerId(),
+        op.getClientId());
   }
 
   /**
@@ -592,7 +599,7 @@
       assert parent != null;
 
       // Send our version number to the server to cut down on future payloads
-      Integer version = currentView.getTag(Constants.ENCODED_VERSION_PROPERTY);
+      String version = currentView.getTag(Constants.VERSION_PROPERTY_B64);
       if (version != null) {
         operation.setVersion(version);
       }
@@ -667,7 +674,7 @@
       OperationMessage op, WriteOperation... operations) {
 
     AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id);
-    toMutate.setTag(Constants.ENCODED_VERSION_PROPERTY, op.getVersion());
+    toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion());
 
     final Map<String, Splittable> properties = op.getPropertyMap();
     if (properties != null) {
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
index 48cfab3..b9c95ce 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
@@ -26,7 +26,6 @@
 import com.google.gwt.requestfactory.shared.Request;
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.RequestTransport;
-import com.google.gwt.requestfactory.shared.messages.IdUtil;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -42,10 +41,10 @@
   private EventBus eventBus;
 
   @SuppressWarnings("serial")
-  private final Map<String, Integer> version = new LinkedHashMap<String, Integer>(
+  private final Map<String, String> version = new LinkedHashMap<String, String>(
       16, 0.75f, true) {
     @Override
-    protected boolean removeEldestEntry(Entry<String, Integer> eldest) {
+    protected boolean removeEldestEntry(Entry<String, String> eldest) {
       return size() > MAX_VERSION_ENTRIES;
     }
   };
@@ -135,9 +134,10 @@
    * Used by {@link AbstractRequestContext} to quiesce update events for objects
    * that haven't truly changed.
    */
-  protected boolean hasVersionChanged(SimpleProxyId<?> id, int observedVersion) {
+  protected boolean hasVersionChanged(SimpleProxyId<?> id,
+      String observedVersion) {
     String key = getHistoryToken(id);
-    Integer existingVersion = version.get(key);
+    String existingVersion = version.get(key);
     // Return true if we haven't seen this before or the versions differ
     boolean toReturn = existingVersion == null
         || !existingVersion.equals(observedVersion);
@@ -146,5 +146,4 @@
     }
     return toReturn;
   }
-
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java b/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java
index 58f3543..5524e05 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/Constants.java
@@ -20,7 +20,7 @@
  */
 public interface Constants {
   String DOMAIN_OBJECT = "domainObject";
-  String ENCODED_VERSION_PROPERTY = "version";
+  String VERSION_PROPERTY_B64 = "version";
   String IN_RESPONSE = "inResponse";
   String REQUEST_CONTEXT = "requestContext";
   String STABLE_ID = "stableId";
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
index 2d9ac74..eaae080 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/IdFactory.java
@@ -18,7 +18,6 @@
 import com.google.gwt.requestfactory.shared.BaseProxy;
 import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.ValueProxy;
-import com.google.gwt.requestfactory.shared.messages.IdUtil;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -62,7 +61,8 @@
   @SuppressWarnings("unchecked")
   public <P extends EntityProxy> Class<P> asEntityProxy(
       Class<? extends BaseProxy> clazz) {
-    assert isEntityType(clazz);
+    assert isEntityType(clazz) : clazz.getName()
+        + " is not an EntityProxy type";
     return (Class<P>) clazz;
   }
 
@@ -73,7 +73,7 @@
   @SuppressWarnings("unchecked")
   public <P extends ValueProxy> Class<P> asValueProxy(
       Class<? extends BaseProxy> clazz) {
-    assert isEntityType(clazz);
+    assert isValueType(clazz) : clazz.getName() + " is not a ValueProxy type";
     return (Class<P>) clazz;
   }
 
@@ -120,19 +120,11 @@
   }
 
   /**
-   * Create or retrieve a SimpleProxyId.
-   */
-  public <P extends BaseProxy> SimpleProxyId<P> getId(Class<P> clazz,
-      String serverId) {
-    return getId(getTypeToken(clazz), serverId);
-  }
-
-  /**
    * Create or retrieve a SimpleProxyId. If both the serverId and clientId are
    * specified and the id is ephemeral, it will be updated with the server id.
    */
   public <P extends BaseProxy> SimpleProxyId<P> getId(Class<P> clazz,
-      String serverId, Integer clientId) {
+      String serverId, int clientId) {
     return getId(getTypeToken(clazz), serverId, clientId);
   }
 
@@ -141,7 +133,7 @@
    */
   public <P extends BaseProxy> SimpleProxyId<P> getId(String typeToken,
       String serverId) {
-    return getId(typeToken, serverId, null);
+    return getId(typeToken, serverId, 0);
   }
 
   /**
@@ -150,12 +142,12 @@
    * id.
    */
   public <P extends BaseProxy> SimpleProxyId<P> getId(String typeToken,
-      String serverId, Integer clientId) {
+      String serverId, int clientId) {
     /*
      * If there's a clientId, that probably means we've just created a brand-new
      * EntityProxy or have just persisted something on the server.
      */
-    if (clientId != null) {
+    if (clientId > 0) {
       // Try a cache lookup for the ephemeral key
       String ephemeralKey = IdUtil.ephemeralId(clientId, typeToken);
       @SuppressWarnings("unchecked")
@@ -203,6 +195,7 @@
      * case for read-dominated applications.
      */
     Class<P> clazz = getTypeFromToken(typeToken);
+    assert clazz != null : "No class literal for " + typeToken;
     return createId(clazz, serverId);
   }
 
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java b/user/src/com/google/gwt/requestfactory/shared/impl/IdUtil.java
similarity index 97%
rename from user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java
rename to user/src/com/google/gwt/requestfactory/shared/impl/IdUtil.java
index 5efd614..fd71302 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/IdUtil.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/IdUtil.java
@@ -13,12 +13,12 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.requestfactory.shared.messages;
+package com.google.gwt.requestfactory.shared.impl;
 
 /**
  * Common functions for slicing and dicing EntityProxy ids.
  */
-public class IdUtil {
+class IdUtil {
   private static final String ANY_SEPARATOR_PATTERN = "@[012]@";
   private static final String EPHEMERAL_SEPARATOR = "@1@";
   private static final String TOKEN_SEPARATOR = "@0@";
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java b/user/src/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
index 069509d..a36a6ae 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/MessageFactoryHolder.java
@@ -21,6 +21,6 @@
 /**
  * This class has a super-source version with a client-only implementation.
  */
-interface MessageFactoryHolder {
+public interface MessageFactoryHolder {
   MessageFactory FACTORY = AutoBeanFactoryMagic.create(MessageFactory.class);
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java b/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java
index 943c970..dce3a15 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/SimpleProxyId.java
@@ -16,7 +16,6 @@
 package com.google.gwt.requestfactory.shared.impl;
 
 import com.google.gwt.requestfactory.shared.BaseProxy;
-import com.google.gwt.requestfactory.shared.messages.IdUtil;
 
 /**
  * The base implementation of id objects in the RequestFactory system. This type
@@ -82,8 +81,7 @@
    */
   SimpleProxyId(Class<P> proxyClass, String encodedAddress) {
     assert proxyClass != null;
-    assert encodedAddress != null && !encodedAddress.contains("@")
-        && !"null".equals(encodedAddress);
+    assert encodedAddress != null;
     setServerId(encodedAddress);
     clientId = NEVER_EPHEMERAL;
     hashCode = encodedAddress.hashCode();
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java b/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java
index bcf5245..82de771 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/EntityCodex.java
@@ -41,9 +41,18 @@
    * Abstracts the process by which EntityProxies are created.
    */
   public interface EntitySource {
-    <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(String serializedProxyId);
+    /**
+     * Expects an encoded
+     * {@link com.google.gwt.requestfactory.shared.messages.IdMessage}.
+     */
+    <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
+        Splittable serializedIdMessage);
 
-    String getSerializedProxyId(SimpleProxyId<?> stableId);
+    /**
+     * Should return an encoded
+     * {@link com.google.gwt.requestfactory.shared.messages.IdMessage}.
+     */
+    Splittable getSerializedProxyId(SimpleProxyId<?> stableId);
 
     boolean isEntityType(Class<?> clazz);
 
@@ -95,7 +104,7 @@
 
     if (source.isEntityType(type) || source.isValueType(type)
         || EntityProxyId.class.equals(type)) {
-      return source.getBeanForPayload(split.asString()).as();
+      return source.getBeanForPayload(split).as();
     }
 
     // Fall back to values
@@ -119,6 +128,10 @@
       return LazySplittable.NULL;
     }
 
+    if (value instanceof Poser<?>) {
+      value = ((Poser<?>) value).getPosedValue();
+    }
+
     if (value instanceof Iterable<?>) {
       StringBuffer toReturn = new StringBuffer();
       toReturn.append('[');
@@ -145,11 +158,7 @@
     }
 
     if (value instanceof SimpleProxyId<?>) {
-      value = source.getSerializedProxyId((SimpleProxyId<?>) value);
-    }
-
-    if (value instanceof Poser<?>) {
-      value = ((Poser<?>) value).getPosedValue();
+      return source.getSerializedProxyId((SimpleProxyId<?>) value);
     }
 
     return ValueCodex.encode(value);
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java b/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java
index 7be22c8..2b8919b 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/IdMessage.java
@@ -21,9 +21,35 @@
  * Used as a base type for messages that are about a particular id.
  */
 public interface IdMessage {
+  /**
+   * Describes the longevity of the id.
+   */
+  public enum Strength {
+    /**
+     * The id is indefinitely persistent and can be freely reused by the client
+     * and the server.
+     */
+    @PropertyName("0")
+    PERSISTED,
+
+    /**
+     * The id is managed by the client and is generally unknown to the server.
+     */
+    @PropertyName("1")
+    EPHEMERAL,
+
+    /**
+     * The id not not managed by the client or server and is valid only for the
+     * duration of a single request or response.
+     */
+    @PropertyName("2")
+    SYNTHETIC;
+  }
+
   String CLIENT_ID = "C";
   String SERVER_ID = "S";
   String TYPE_TOKEN = "T";
+  String STRENGTH = "R";
   String SYNTHETIC_ID = "Y";
 
   @PropertyName(CLIENT_ID)
@@ -32,6 +58,9 @@
   @PropertyName(SERVER_ID)
   String getServerId();
 
+  @PropertyName(STRENGTH)
+  Strength getStrength();
+
   @PropertyName(SYNTHETIC_ID)
   int getSyntheticId();
 
@@ -44,6 +73,9 @@
   @PropertyName(SERVER_ID)
   void setServerId(String value);
 
+  @PropertyName(STRENGTH)
+  void setStrength(Strength value);
+
   @PropertyName(SYNTHETIC_ID)
   void setSyntheticId(int value);
 
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
index 5742010..a6201f5 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
@@ -24,6 +24,8 @@
 public interface MessageFactory extends AutoBeanFactory {
   AutoBean<ServerFailureMessage> failure();
 
+  AutoBean<IdMessage> id();
+
   AutoBean<InvocationMessage> invocation();
 
   AutoBean<OperationMessage> operation();
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/VersionedMessage.java b/user/src/com/google/gwt/requestfactory/shared/messages/VersionedMessage.java
index 6563ec1..e216e37 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/VersionedMessage.java
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/VersionedMessage.java
@@ -24,8 +24,8 @@
   String VERSION = "V";
 
   @PropertyName(VERSION)
-  int getVersion();
+  String getVersion();
 
   @PropertyName(VERSION)
-  void setVersion(int version);
+  void setVersion(String version);
 }
diff --git a/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java b/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
index 19ea814..342aaa3 100644
--- a/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
+++ b/user/test/com/google/gwt/requestfactory/RequestFactoryJreSuite.java
@@ -16,6 +16,7 @@
 package com.google.gwt.requestfactory;
 
 import com.google.gwt.requestfactory.rebind.model.RequestFactoryModelTest;
+import com.google.gwt.requestfactory.server.ComplexKeysJreTest;
 import com.google.gwt.requestfactory.server.FindServiceJreTest;
 import com.google.gwt.requestfactory.server.RequestFactoryInterfaceValidatorTest;
 import com.google.gwt.requestfactory.server.RequestFactoryJreTest;
@@ -31,6 +32,7 @@
   public static Test suite() {
     TestSuite suite = new TestSuite(
         "requestfactory package tests that require the JRE");
+    suite.addTestSuite(ComplexKeysJreTest.class);
     suite.addTestSuite(FindServiceJreTest.class);
     suite.addTestSuite(RequestFactoryJreTest.class);
     suite.addTestSuite(SimpleEntityProxyIdTest.class);
diff --git a/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java b/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java
index 2efef63..9eff4c3 100644
--- a/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java
+++ b/user/test/com/google/gwt/requestfactory/RequestFactorySuite.java
@@ -21,6 +21,7 @@
 import com.google.gwt.requestfactory.client.RequestFactoryPolymorphicTest;
 import com.google.gwt.requestfactory.client.RequestFactoryTest;
 import com.google.gwt.requestfactory.client.ui.EditorTest;
+import com.google.gwt.requestfactory.shared.ComplexKeysTest;
 
 import junit.framework.Test;
 
@@ -31,11 +32,12 @@
   public static Test suite() {
     GWTTestSuite suite = new GWTTestSuite(
         "Test suite for requestfactory gwt code.");
+    suite.addTestSuite(ComplexKeysTest.class);
     suite.addTestSuite(EditorTest.class);
+    suite.addTestSuite(FindServiceTest.class);
     suite.addTestSuite(RequestFactoryTest.class);
     suite.addTestSuite(RequestFactoryExceptionHandlerTest.class);
     suite.addTestSuite(RequestFactoryPolymorphicTest.class);
-    suite.addTestSuite(FindServiceTest.class);
     return suite;
   }
 }
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
index b50b784..a52ab68 100644
--- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -172,13 +172,14 @@
   private static final int DELAY_TEST_FINISH = 5000;
 
   public <T extends EntityProxy> void assertContains(Collection<T> col, T value) {
+    EntityProxyId<?> lookFor = value.stableId();
     for (T x : col) {
-      if (x.stableId().equals(value.stableId())) {
+      EntityProxyId<?> found = x.stableId();
+      if (found.equals(lookFor)) {
         return;
       }
     }
-    assertTrue(
-        ("Value " + value + " not found in collection ") + col.toString(),
+    assertTrue("Value " + value + " not found in collection " + col.toString(),
         false);
   }
 
diff --git a/user/test/com/google/gwt/requestfactory/server/ComplexKeysJreTest.java b/user/test/com/google/gwt/requestfactory/server/ComplexKeysJreTest.java
new file mode 100644
index 0000000..e7bcaeb
--- /dev/null
+++ b/user/test/com/google/gwt/requestfactory/server/ComplexKeysJreTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010 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.requestfactory.server;
+
+import com.google.gwt.requestfactory.shared.ComplexKeysTest;
+
+/**
+ * JRE version of ComplexKeysTest.
+ */
+public class ComplexKeysJreTest extends ComplexKeysTest {
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+
+  @Override
+  protected Factory createFactory() {
+    return RequestFactoryJreTest.createInProcess(Factory.class);
+  }
+}
diff --git a/user/test/com/google/gwt/requestfactory/server/FindServiceJreTest.java b/user/test/com/google/gwt/requestfactory/server/FindServiceJreTest.java
index de8028b..1169ec7 100644
--- a/user/test/com/google/gwt/requestfactory/server/FindServiceJreTest.java
+++ b/user/test/com/google/gwt/requestfactory/server/FindServiceJreTest.java
@@ -30,6 +30,6 @@
 
   @Override
   protected SimpleRequestFactory createFactory() {
-    return RequestFactoryJreTest.createInProcess();
+    return RequestFactoryJreTest.createInProcess(SimpleRequestFactory.class);
   }
 }
diff --git a/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java b/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
index 46218c4..d8eca03 100644
--- a/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
+++ b/user/test/com/google/gwt/requestfactory/server/RequestFactoryInterfaceValidatorTest.java
@@ -30,6 +30,7 @@
 import junit.framework.TestCase;
 
 import java.util.List;
+import java.util.Random;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -38,6 +39,10 @@
  */
 public class RequestFactoryInterfaceValidatorTest extends TestCase {
   static class ClinitEntity {
+    static ClinitEntity findClinitEntity(String key) {
+      return null;
+    }
+
     static ClinitEntity request() {
       return null;
     }
@@ -111,6 +116,42 @@
   class Foo {
   }
 
+  @ProxyFor(HasListDomain.class)
+  interface HasList extends EntityProxy {
+    List<ReachableOnlyThroughReturnedList> getList();
+
+    void setList(List<ReachableOnlyThroughParamaterList> list);
+  }
+
+  static class HasListDomain extends Domain {
+    public String getId() {
+      return null;
+    }
+
+    public int getVersion() {
+      return 0;
+    }
+
+    List<Domain> getList() {
+      return null;
+    }
+
+    void setList(List<Domain> value) {
+    }
+  }
+
+  @ProxyFor(value = Value.class)
+  interface MyValueProxy extends ValueProxy {
+  }
+
+  @ProxyFor(Domain.class)
+  interface ReachableOnlyThroughParamaterList extends EntityProxy {
+  }
+
+  @ProxyFor(Domain.class)
+  interface ReachableOnlyThroughReturnedList extends EntityProxy {
+  }
+
   interface RequestContextMissingAnnotation extends RequestContext {
   }
 
@@ -125,7 +166,6 @@
   interface ServiceRequestMismatchedParam extends RequestContext {
     Request<Integer> foo(long a);
   }
-
   @Service(Domain.class)
   interface ServiceRequestMismatchedReturn extends RequestContext {
     Request<Long> foo(int a);
@@ -143,15 +183,27 @@
     Request<Integer> doesNotExist(int a);
   }
 
+  static class UnexpectedIdAndVersionDomain {
+    Random getId() {
+      return null;
+    }
+
+    Random getVersion() {
+      return null;
+    }
+  }
+
+  @ProxyFor(UnexpectedIdAndVersionDomain.class)
+  interface UnexpectedIdAndVersionProxy extends EntityProxy {
+  }
+
   static class Value {
   }
 
-  @ProxyFor(value = Value.class)
-  interface MyValueProxy extends ValueProxy {
-  }
-
   RequestFactoryInterfaceValidator v;
 
+  private static final boolean DUMP_PAYLOAD = Boolean.getBoolean("gwt.rpc.dumpPayload");;
+
   /**
    * Ensure that calling {@link RequestFactoryInterfaceValidator#antidote()}
    * doesn't cause information to be lost.
@@ -163,7 +215,7 @@
     assertFalse(v.isPoisoned());
     v.validateRequestContext(RequestContextMissingAnnotation.class.getName());
     assertTrue(v.isPoisoned());
-  }
+  };
 
   /**
    * Test the {@link FindRequest} context used to implement find().
@@ -172,37 +224,6 @@
     v.validateRequestContext(FindRequest.class.getName());
   }
 
-  @ProxyFor(HasListDomain.class)
-  interface HasList extends EntityProxy {
-    List<ReachableOnlyThroughReturnedList> getList();
-
-    void setList(List<ReachableOnlyThroughParamaterList> list);
-  }
-
-  static class HasListDomain extends Domain {
-    List<Domain> getList() {
-      return null;
-    }
-
-    void setList(List<Domain> value) {
-    }
-
-    public String getId() {
-      return null;
-    }
-
-    public int getVersion() {
-      return 0;
-    }
-  }
-
-  @ProxyFor(Domain.class)
-  interface ReachableOnlyThroughReturnedList extends EntityProxy {
-  };
-  @ProxyFor(Domain.class)
-  interface ReachableOnlyThroughParamaterList extends EntityProxy {
-  };
-
   /**
    * Make sure that proxy types referenced through type parameters of method
    * return types and paramaters types are examined.
@@ -278,6 +299,11 @@
     assertFalse(v.isPoisoned());
   }
 
+  public void testUnexpectedIdAndVersion() {
+    v.validateEntityProxy(UnexpectedIdAndVersionProxy.class.getName());
+    assertTrue(v.isPoisoned());
+  }
+
   public void testValueType() {
     v.validateValueProxy(MyValueProxy.class.getName());
     assertFalse(v.isPoisoned());
@@ -286,7 +312,7 @@
   @Override
   protected void setUp() throws Exception {
     Logger logger = Logger.getLogger("");
-    logger.setLevel(Level.OFF);
+    logger.setLevel(DUMP_PAYLOAD ? Level.ALL : Level.OFF);
     v = new RequestFactoryInterfaceValidator(logger, new ClassLoaderLoader(
         Thread.currentThread().getContextClassLoader()));
   }
diff --git a/user/test/com/google/gwt/requestfactory/server/RequestFactoryJreTest.java b/user/test/com/google/gwt/requestfactory/server/RequestFactoryJreTest.java
index d0d69b1..0e33457 100644
--- a/user/test/com/google/gwt/requestfactory/server/RequestFactoryJreTest.java
+++ b/user/test/com/google/gwt/requestfactory/server/RequestFactoryJreTest.java
@@ -21,6 +21,7 @@
 import com.google.gwt.requestfactory.server.SimpleRequestProcessor.ServiceLayer;
 import com.google.gwt.requestfactory.server.testing.InProcessRequestTransport;
 import com.google.gwt.requestfactory.server.testing.RequestFactoryMagic;
+import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.SimpleRequestFactory;
 
 /**
@@ -28,9 +29,9 @@
  */
 public class RequestFactoryJreTest extends RequestFactoryTest {
 
-  public static SimpleRequestFactory createInProcess() {
+  public static <T extends RequestFactory> T createInProcess(Class<T> clazz) {
     EventBus eventBus = new SimpleEventBus();
-    SimpleRequestFactory req = RequestFactoryMagic.create(SimpleRequestFactory.class);
+    T req = RequestFactoryMagic.create(clazz);
     ServiceLayer serviceLayer = new ReflectiveServiceLayer();
     SimpleRequestProcessor processor = new SimpleRequestProcessor(serviceLayer);
     req.initialize(eventBus, new InProcessRequestTransport(processor));
@@ -44,6 +45,6 @@
 
   @Override
   protected SimpleRequestFactory createFactory() {
-    return createInProcess();
+    return createInProcess(SimpleRequestFactory.class);
   }
 }
diff --git a/user/test/com/google/gwt/requestfactory/shared/ComplexKeysTest.java b/user/test/com/google/gwt/requestfactory/shared/ComplexKeysTest.java
new file mode 100644
index 0000000..60af61d
--- /dev/null
+++ b/user/test/com/google/gwt/requestfactory/shared/ComplexKeysTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2010 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.requestfactory.shared;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Tests the use of non-trivial EntityProxy and ValueProxy key types.
+ */
+public class ComplexKeysTest extends GWTTestCase {
+
+  /**
+   * The factory being tested.
+   */
+  protected interface Factory extends RequestFactory {
+    Context context();
+  }
+
+  @Service(ContextImpl.class)
+  interface Context extends RequestContext {
+    Request<DomainWithEntityKeyProxy> createEntity(String key);
+
+    Request<DomainWithValueKeyProxy> createValue(String key);
+  }
+
+  static class ContextImpl {
+    public static DomainWithEntityKey createEntity(String key) {
+      return new DomainWithEntityKey(new EntityKey(key));
+    }
+
+    public static DomainWithValueKey createValue(String key) {
+      return new DomainWithValueKey(new ValueKey(key));
+    }
+  }
+
+  static class DomainWithEntityKey {
+
+    public static DomainWithEntityKey findDomainWithEntityKey(EntityKey key) {
+      return new DomainWithEntityKey(key);
+    }
+
+    private final EntityKey key;
+
+    public DomainWithEntityKey(EntityKey key) {
+      if (key == null) {
+        throw new IllegalArgumentException("Key key");
+      }
+      this.key = key;
+    }
+
+    public EntityKey getId() {
+      return key;
+    }
+
+    public Void getVersion() {
+      return null;
+    }
+  }
+
+  @ProxyFor(DomainWithEntityKey.class)
+  interface DomainWithEntityKeyProxy extends EntityProxy {
+    EntityKeyProxy getId();
+
+    EntityProxyId<DomainWithEntityKeyProxy> stableId();
+  }
+
+  static class DomainWithValueKey {
+    public static DomainWithValueKey create(String key) {
+      return new DomainWithValueKey(new ValueKey(key));
+    }
+
+    public static DomainWithValueKey findDomainWithValueKey(ValueKey key) {
+      return new DomainWithValueKey(key);
+    }
+
+    private final ValueKey key;
+
+    public DomainWithValueKey(ValueKey key) {
+      if (key == null) {
+        throw new IllegalArgumentException("Key key");
+      }
+      this.key = key;
+    }
+
+    public ValueKey getId() {
+      return key;
+    }
+
+    public Void getVersion() {
+      return null;
+    }
+  }
+
+  @ProxyFor(DomainWithValueKey.class)
+  interface DomainWithValueKeyProxy extends EntityProxy {
+    ValueKeyProxy getId();
+
+    EntityProxyId<DomainWithValueKeyProxy> stableId();
+  }
+
+  static class EntityKey {
+    public static EntityKey findEntityKey(String key) {
+      return new EntityKey(key);
+    }
+
+    private final String key;
+
+    public EntityKey(String key) {
+      assertEquals("key", key);
+      this.key = key;
+    }
+
+    public String getId() {
+      return key;
+    }
+
+    public Void getVersion() {
+      return null;
+    }
+  }
+
+  @ProxyFor(EntityKey.class)
+  interface EntityKeyProxy extends EntityProxy {
+  }
+
+  static class ValueKey {
+    private String key;
+
+    public ValueKey() {
+    }
+
+    public ValueKey(String key) {
+      setKey(key);
+    }
+
+    public String getKey() {
+      return key;
+    }
+
+    public void setKey(String key) {
+      assertEquals("key", key);
+      this.key = key;
+    }
+  }
+
+  @ProxyFor(ValueKey.class)
+  interface ValueKeyProxy extends ValueProxy {
+    String getKey();
+  }
+
+  private static final int TEST_DELAY = 5000;
+  private Factory factory;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.requestfactory.RequestFactorySuite";
+  }
+
+  public void testEntityKey() {
+    delayTestFinish(TEST_DELAY);
+    context().createEntity("key").fire(
+        new Receiver<DomainWithEntityKeyProxy>() {
+          @Override
+          public void onSuccess(final DomainWithEntityKeyProxy response) {
+            factory.find(response.stableId()).fire(
+                new Receiver<DomainWithEntityKeyProxy>() {
+                  @Override
+                  public void onSuccess(DomainWithEntityKeyProxy found) {
+                    assertEquals(response.stableId(), found.stableId());
+                    finishTest();
+                  }
+                });
+          }
+        });
+  }
+
+  public void testValueKey() {
+    delayTestFinish(TEST_DELAY);
+    context().createValue("key").fire(new Receiver<DomainWithValueKeyProxy>() {
+      @Override
+      public void onSuccess(final DomainWithValueKeyProxy response) {
+        factory.find(response.stableId()).fire(
+            new Receiver<DomainWithValueKeyProxy>() {
+              @Override
+              public void onSuccess(DomainWithValueKeyProxy found) {
+                assertEquals(response.stableId(), found.stableId());
+                finishTest();
+              }
+            });
+      }
+    });
+  }
+
+  protected Factory createFactory() {
+    Factory toReturn = GWT.create(Factory.class);
+    toReturn.initialize(new SimpleEventBus());
+    return toReturn;
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    factory = createFactory();
+  }
+
+  private Context context() {
+    return factory.context();
+  }
+}