Rework the way OperationMessages are processed to allow all domain objects to be loaded at once.
http://code.google.com/p/google-web-toolkit/wiki/RequestFactory_2_1_1
Patch by: bobv
Review by: rchandia,rjrjr

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9271 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
index 2c2da00..4f20c7b 100644
--- a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
+++ b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
@@ -26,7 +26,10 @@
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Type;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -188,6 +191,23 @@
   }
 
   @Override
+  public List<Object> loadDomainObjects(List<Class<?>> classes,
+      List<Object> domainIds) {
+    if (classes.size() != domainIds.size()) {
+      die(null,
+          "Size mismatch in paramaters. classes.size() = %d domainIds.size=%d",
+          classes.size(), domainIds.size());
+    }
+    List<Object> toReturn = new ArrayList<Object>(classes.size());
+    Iterator<Class<?>> classIt = classes.iterator();
+    Iterator<Object> idIt = domainIds.iterator();
+    while (classIt.hasNext()) {
+      toReturn.add(getTop().loadDomainObject(classIt.next(), idIt.next()));
+    }
+    return toReturn;
+  }
+
+  @Override
   public void setProperty(Object domainObject, String property,
       Class<?> expectedType, Object value) {
     Method setter;
diff --git a/user/src/com/google/gwt/requestfactory/server/RequestState.java b/user/src/com/google/gwt/requestfactory/server/RequestState.java
index 3682f1e..520e6d7 100644
--- a/user/src/com/google/gwt/requestfactory/server/RequestState.java
+++ b/user/src/com/google/gwt/requestfactory/server/RequestState.java
@@ -33,8 +33,11 @@
 import com.google.gwt.requestfactory.shared.messages.IdMessage;
 import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -83,6 +86,9 @@
     resolver = new Resolver(this);
   }
 
+  /**
+   * Turn a domain value into a wire format message.
+   */
   public Splittable flatten(Object domainValue) {
     Splittable flatValue;
     if (ValueCodex.canDecode(domainValue.getClass())) {
@@ -93,27 +99,15 @@
     return flatValue;
   }
 
-  public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(IdMessage idMessage) {
-    SimpleProxyId<Q> id;
-    if (Strength.SYNTHETIC.equals(idMessage.getStrength())) {
-      @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);
-  }
-
+  /**
+   * Get or create a BaseProxy AutoBean for the given id.
+   */
   public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
       SimpleProxyId<Q> id, Object domainObject) {
     @SuppressWarnings("unchecked")
     AutoBean<Q> toReturn = (AutoBean<Q>) beans.get(id);
     if (toReturn == null) {
-      toReturn = createEntityProxyBean(id, domainObject);
+      toReturn = createProxyBean(id, domainObject);
     }
     return toReturn;
   }
@@ -125,7 +119,33 @@
       Splittable serializedProxyId) {
     IdMessage idMessage = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY,
         IdMessage.class, serializedProxyId).as();
-    return getBeanForPayload(idMessage);
+    @SuppressWarnings("unchecked")
+    AutoBean<Q> toReturn = (AutoBean<Q>) getBeansForPayload(
+        Collections.singletonList(idMessage)).get(0);
+    return toReturn;
+  }
+
+  /**
+   * Get or create BaseProxy AutoBeans for a list of id-bearing messages.
+   */
+  public List<AutoBean<? extends BaseProxy>> getBeansForPayload(
+      List<? extends IdMessage> idMessages) {
+    List<SimpleProxyId<?>> ids = new ArrayList<SimpleProxyId<?>>(
+        idMessages.size());
+    for (IdMessage idMessage : idMessages) {
+      SimpleProxyId<?> id;
+      if (Strength.SYNTHETIC.equals(idMessage.getStrength())) {
+        Class<? extends BaseProxy> clazz = 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());
+      }
+      ids.add(id);
+    }
+    return getBeansForIds(ids);
   }
 
   public IdFactory getIdFactory() {
@@ -188,7 +208,7 @@
   /**
    * Creates an AutoBean for the given id, tracking a domain object.
    */
-  private <Q extends BaseProxy> AutoBean<Q> createEntityProxyBean(
+  private <Q extends BaseProxy> AutoBean<Q> createProxyBean(
       SimpleProxyId<Q> id, Object domainObject) {
     AutoBean<Q> toReturn = AutoBeanFactoryMagic.createBean(id.getProxyClass(),
         SimpleRequestProcessor.CONFIGURATION);
@@ -199,37 +219,76 @@
   }
 
   /**
-   * Returns the AutoBean corresponding to the given id, or creates if it does
-   * not yet exist.
+   * Returns the AutoBeans corresponding to the given ids, or creates them if
+   * they do not yet exist.
    */
-  private <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(
-      SimpleProxyId<Q> id) {
-    @SuppressWarnings("unchecked")
-    AutoBean<Q> toReturn = (AutoBean<Q>) beans.get(id);
-    if (toReturn == null) {
-      // Resolve the domain object
+  private List<AutoBean<? extends BaseProxy>> getBeansForIds(
+      List<SimpleProxyId<?>> ids) {
+    List<Class<?>> domainClasses = new ArrayList<Class<?>>(ids.size());
+    List<Object> domainIds = new ArrayList<Object>(ids.size());
+    List<SimpleProxyId<?>> idsToLoad = new ArrayList<SimpleProxyId<?>>();
+
+    /*
+     * Create proxies for ephemeral or synthetic ids that we haven't seen. Queue
+     * up the domain ids for entities that need to be loaded.
+     */
+    for (SimpleProxyId<?> id : ids) {
       Class<?> domainClass = service.resolveDomainClass(id.getProxyClass());
-      Object domain;
-      if (id.isEphemeral() || id.isSynthetic()) {
-        domain = service.createDomainObject(domainClass);
+      if (beans.containsKey(id)) {
+        // Already have a proxy for this id, no-op
+      } else if (id.isEphemeral() || id.isSynthetic()) {
+        // Create a new domain object for the short-lived id
+        Object domain = service.createDomainObject(domainClass);
         if (domain == null) {
           throw new UnexpectedException("Could not create instance of "
               + domainClass.getCanonicalName(), null);
         }
+        AutoBean<? extends BaseProxy> bean = createProxyBean(id, domain);
+        beans.put(id, bean);
+        domainObjectsToId.put(domain, id);
       } else {
-        Splittable address = StringQuoter.split(id.getServerId());
+        // Decode the domain parameter
+        Splittable split = StringQuoter.split(id.getServerId());
         Class<?> param = service.getIdType(domainClass);
         Object domainParam;
         if (ValueCodex.canDecode(param)) {
-          domainParam = ValueCodex.decode(param, address);
+          domainParam = ValueCodex.decode(param, split);
         } else {
           domainParam = new SimpleRequestProcessor(service).decodeOobMessage(
-              param, address).get(0);
+              param, split).get(0);
         }
-        domain = service.loadDomainObject(domainClass, domainParam);
+
+        // Enqueue
+        domainClasses.add(service.resolveDomainClass(id.getProxyClass()));
+        domainIds.add(domainParam);
+        idsToLoad.add(id);
       }
-      domainObjectsToId.put(domain, id);
-      toReturn = createEntityProxyBean(id, domain);
+    }
+
+    // Actually load the data
+    if (!domainClasses.isEmpty()) {
+      assert domainClasses.size() == domainIds.size()
+          && domainClasses.size() == idsToLoad.size();
+      List<Object> loaded = service.loadDomainObjects(domainClasses, domainIds);
+      if (idsToLoad.size() != loaded.size()) {
+        throw new UnexpectedException("Expected " + idsToLoad.size()
+            + " objects to be loaded, got " + loaded.size(), null);
+      }
+
+      Iterator<Object> itLoaded = loaded.iterator();
+      for (SimpleProxyId<?> id : idsToLoad) {
+        Object domain = itLoaded.next();
+        domainObjectsToId.put(domain, id);
+        AutoBean<? extends BaseProxy> bean = createProxyBean(id, domain);
+        beans.put(id, bean);
+      }
+    }
+
+    // Construct the return value
+    List<AutoBean<? extends BaseProxy>> toReturn = new ArrayList<AutoBean<? extends BaseProxy>>(
+        ids.size());
+    for (SimpleProxyId<?> id : ids) {
+      toReturn.add(beans.get(id));
     }
     return toReturn;
   }
diff --git a/user/src/com/google/gwt/requestfactory/server/ServiceLayer.java b/user/src/com/google/gwt/requestfactory/server/ServiceLayer.java
index 9a0ba62..a906e9f 100644
--- a/user/src/com/google/gwt/requestfactory/server/ServiceLayer.java
+++ b/user/src/com/google/gwt/requestfactory/server/ServiceLayer.java
@@ -193,6 +193,22 @@
   public abstract <T> T loadDomainObject(Class<T> clazz, Object domainId);
 
   /**
+   * Load multiple objects from the backing store. This method is intended to
+   * allow more efficient access to the backing store by providing all objects
+   * referenced in an incoming payload.
+   * <p>
+   * The default implementation of this method will delegate to
+   * {@link #loadDomainObject(Class, Object)}.
+   * 
+   * @param classes type type of each object to load
+   * @param domainIds the ids previously returned from {@link #getId(Object)}
+   * @return the requested objects, elements of which may be {@code null} if the
+   *         requested objects were irretrievable
+   */
+  public abstract List<Object> loadDomainObjects(List<Class<?>> classes,
+      List<Object> domainIds);
+
+  /**
    * Given a type token previously returned from
    * {@link #resolveTypeToken(Class)}, return the Class literal associated with
    * the token.
diff --git a/user/src/com/google/gwt/requestfactory/server/ServiceLayerDecorator.java b/user/src/com/google/gwt/requestfactory/server/ServiceLayerDecorator.java
index 5acd2bd..b9a2248 100644
--- a/user/src/com/google/gwt/requestfactory/server/ServiceLayerDecorator.java
+++ b/user/src/com/google/gwt/requestfactory/server/ServiceLayerDecorator.java
@@ -21,6 +21,7 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Type;
+import java.util.List;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -93,6 +94,12 @@
   }
 
   @Override
+  public List<Object> loadDomainObjects(List<Class<?>> classes,
+      List<Object> domainIds) {
+    return getNext().loadDomainObjects(classes, domainIds);
+  }
+
+  @Override
   public Class<? extends BaseProxy> resolveClass(String typeToken) {
     return getNext().resolveClass(typeToken);
   }
diff --git a/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java b/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
index f54e62e..4be0769 100644
--- a/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
+++ b/user/src/com/google/gwt/requestfactory/server/SimpleRequestProcessor.java
@@ -25,7 +25,6 @@
 import com.google.gwt.autobean.shared.Splittable;
 import com.google.gwt.autobean.shared.ValueCodex;
 import com.google.gwt.requestfactory.shared.BaseProxy;
-import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.EntityProxyId;
 import com.google.gwt.requestfactory.shared.InstanceRequest;
 import com.google.gwt.requestfactory.shared.ServerFailure;
@@ -54,6 +53,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -445,13 +445,14 @@
       return;
     }
 
-    for (final OperationMessage operation : operations) {
-      // Unflatten properties
-      AutoBean<? extends EntityProxy> bean = state.getBeanForPayload(operation);
-      // Use the version later to know which objects need to be sent back
-      if (operation.getVersion() != null) {
-        bean.setTag(Constants.VERSION_PROPERTY_B64, operation.getVersion());
-      }
+    List<AutoBean<? extends BaseProxy>> beans = state.getBeansForPayload(operations);
+    assert operations.size() == beans.size();
+
+    Iterator<OperationMessage> itOp = operations.iterator();
+    for (AutoBean<? extends BaseProxy> bean : beans) {
+      OperationMessage operation = itOp.next();
+      // Save the client's version information to reduce payload size later
+      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);