Add RequestContext.append() to allow actions across different domain service types to be combined in a single HTTP request.
Patch by: bobv
Review by: rjrjr

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10052 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/RequestContext.java b/user/src/com/google/web/bindery/requestfactory/shared/RequestContext.java
index 91e45d0..6dc0186 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/RequestContext.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/RequestContext.java
@@ -20,6 +20,26 @@
  */
 public interface RequestContext {
   /**
+   * Joins another RequestContext to this RequestContext.
+   * 
+   * <pre>
+   * SomeContext ctx = myFactory.someContext();
+   * // Perform operations on ctx
+   * OtherContext other = ctx.append(myFactory.otherContext());
+   * // Perform operations on both other and ctx
+   * ctx.fire() // or other.fire() are equivalent 
+   * </pre>
+   * 
+   * @param other a freshly-constructed RequestContext whose state should be
+   *          bound to this RequestContext
+   * @return {@code other}
+   * @throws IllegalStateException if any methods have been called on
+   *           {@code other} or if {@code other} was constructed by a different
+   *           RequestFactory instance
+   */
+  <T extends RequestContext> T append(T other);
+
+  /**
    * Returns a new mutable proxy that this request can carry to the server,
    * perhaps to be persisted. If the object is succesfully persisted, a PERSIST
    * event will be posted including the EntityProxyId of this proxy.
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
index 0e10a3a..5e2a151 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
@@ -16,7 +16,7 @@
 package com.google.web.bindery.requestfactory.shared.impl;
 
 import static com.google.web.bindery.requestfactory.shared.impl.BaseProxyCategory.stableId;
-import static com.google.web.bindery.requestfactory.shared.impl.Constants.REQUEST_CONTEXT;
+import static com.google.web.bindery.requestfactory.shared.impl.Constants.REQUEST_CONTEXT_STATE;
 import static com.google.web.bindery.requestfactory.shared.impl.Constants.STABLE_ID;
 
 import com.google.web.bindery.autobean.shared.AutoBean;
@@ -88,6 +88,63 @@
     abstract DialectImpl create(AbstractRequestContext context);
   }
 
+  /**
+   * Encapsulates all state contained by the AbstractRequestContext.
+   */
+  protected static class State {
+    public final AbstractRequestContext canonical;
+    public final DialectImpl dialect;
+    public final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
+
+    public boolean locked;
+    /**
+     * A map of all EntityProxies that the RequestContext has interacted with.
+     * Objects are placed into this map by being returned from {@link #create},
+     * passed into {@link #edit}, or used as an invocation argument.
+     */
+    public final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies =
+        new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>();
+    /**
+     * A map that contains the canonical instance of an entity to return in the
+     * return graph, since this is built from scratch.
+     */
+    public final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies =
+        new HashMap<SimpleProxyId<?>, AutoBean<?>>();
+
+    public final AbstractRequestFactory requestFactory;
+
+    /**
+     * A map that allows us to handle the case where the server has sent back an
+     * unpersisted entity. Because we assume that the server is stateless, the
+     * client will need to swap out the request-local ids with a regular
+     * client-allocated id.
+     */
+    public final Map<Integer, SimpleProxyId<?>> syntheticIds =
+        new HashMap<Integer, SimpleProxyId<?>>();
+
+    public State(AbstractRequestFactory requestFactory, DialectImpl dialect,
+        AbstractRequestContext canonical) {
+      this.requestFactory = requestFactory;
+      this.dialect = dialect;
+      this.canonical = canonical;
+    }
+
+    public AbstractRequestContext getCanonicalContext() {
+      return canonical;
+    }
+
+    public boolean isClean() {
+      return editedProxies.isEmpty() && invocations.isEmpty() && !locked
+          && returnedProxies.isEmpty() && syntheticIds.isEmpty();
+    }
+
+    public boolean isCompatible(State state) {
+      // Object comparison intentional
+      return requestFactory == state.requestFactory
+          && dialect.getClass().equals(state.dialect.getClass());
+    }
+  }
+
   interface DialectImpl {
 
     void addInvocation(AbstractRequest<?> request);
@@ -107,17 +164,17 @@
        * ironed out. Once this is done, addInvocation() can be removed from the
        * DialectImpl interface and restored to to AbstractRequestContext.
        */
-      if (!invocations.isEmpty()) {
+      if (!state.invocations.isEmpty()) {
         throw new RuntimeException("Only one invocation per request, pending backend support");
       }
-      invocations.add(request);
+      state.invocations.add(request);
       for (Object arg : request.getRequestData().getOrderedParameters()) {
         retainArg(arg);
       }
     }
 
     public String makePayload() {
-      RequestData data = invocations.get(0).getRequestData();
+      RequestData data = state.invocations.get(0).getRequestData();
 
       AutoBean<JsonRpcRequest> bean = MessageFactoryHolder.FACTORY.jsonRpcRequest();
       JsonRpcRequest request = bean.as();
@@ -145,7 +202,7 @@
       Splittable raw = StringQuoter.split(payload);
 
       @SuppressWarnings("unchecked")
-      Receiver<Object> callback = (Receiver<Object>) invocations.get(0).getReceiver();
+      Receiver<Object> callback = (Receiver<Object>) state.invocations.get(0).getReceiver();
 
       if (!raw.isNull("error")) {
         Splittable error = raw.get("error");
@@ -159,7 +216,7 @@
       Splittable result = raw.get("result");
       @SuppressWarnings("unchecked")
       Class<BaseProxy> target =
-          (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType();
+          (Class<BaseProxy>) state.invocations.get(0).getRequestData().getReturnType();
 
       SimpleProxyId<BaseProxy> id = getRequestFactory().allocateId(target);
       AutoBean<BaseProxy> bean = createProxy(target, id);
@@ -197,7 +254,7 @@
      * Called by generated subclasses to enqueue a method invocation.
      */
     public void addInvocation(AbstractRequest<?> request) {
-      invocations.add(request);
+      state.invocations.add(request);
       for (Object arg : request.getRequestData().getOrderedParameters()) {
         retainArg(arg);
       }
@@ -260,15 +317,15 @@
 
       // Send return values
       Set<Throwable> causes = null;
-      for (int i = 0, j = invocations.size(); i < j; i++) {
+      for (int i = 0, j = state.invocations.size(); i < j; i++) {
         try {
           if (response.getStatusCodes().get(i)) {
-            invocations.get(i).onSuccess(response.getInvocationResults().get(i));
+            state.invocations.get(i).onSuccess(response.getInvocationResults().get(i));
           } else {
             ServerFailureMessage failure =
                 AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ServerFailureMessage.class,
                     response.getInvocationResults().get(i)).as();
-            invocations.get(i).onFail(
+            state.invocations.get(i).onFail(
                 new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure
                     .getStackTrace(), failure.isFatal()));
           }
@@ -291,9 +348,9 @@
         }
       }
       // After success, shut down the context
-      editedProxies.clear();
-      invocations.clear();
-      returnedProxies.clear();
+      state.editedProxies.clear();
+      state.invocations.clear();
+      state.returnedProxies.clear();
 
       if (causes != null) {
         throw new UmbrellaException(causes);
@@ -321,7 +378,7 @@
       AutoBean<BaseProxy> stub = getProxyForReturnPayloadGraph(baseId);
 
       // So pick up the instance that we just sent to the server
-      AutoBean<?> edited = editedProxies.get(BaseProxyCategory.stableId(stub));
+      AutoBean<?> edited = state.editedProxies.get(BaseProxyCategory.stableId(stub));
       currentProxy = (BaseProxy) edited.as();
 
       // Try to find the original, immutable version.
@@ -357,38 +414,24 @@
       WriteOperation.PERSIST, WriteOperation.UPDATE};
   private static final WriteOperation[] UPDATE_ONLY = {WriteOperation.UPDATE};
   private static int payloadId = 100;
-  protected final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
-  private boolean locked;
 
-  private final AbstractRequestFactory requestFactory;
-  /**
-   * A map of all EntityProxies that the RequestContext has interacted with.
-   * Objects are placed into this map by being passed into {@link #edit} or as
-   * an invocation argument.
-   */
-  private final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies =
-      new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>();
-  /**
-   * A map that contains the canonical instance of an entity to return in the
-   * return graph, since this is built from scratch.
-   */
-  private final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies =
-      new HashMap<SimpleProxyId<?>, AutoBean<?>>();
-
-  /**
-   * A map that allows us to handle the case where the server has sent back an
-   * unpersisted entity. Because we assume that the server is stateless, the
-   * client will need to swap out the request-local ids with a regular
-   * client-allocated id.
-   */
-  private final Map<Integer, SimpleProxyId<?>> syntheticIds =
-      new HashMap<Integer, SimpleProxyId<?>>();
-
-  private final DialectImpl dialect;
+  private State state;
 
   protected AbstractRequestContext(AbstractRequestFactory factory, Dialect dialect) {
-    this.requestFactory = factory;
-    this.dialect = dialect.create(this);
+    this.state = new State(factory, dialect.create(this), this);
+  }
+
+  public <T extends RequestContext> T append(T other) {
+    AbstractRequestContext child = (AbstractRequestContext) other;
+    if (!state.isCompatible(child.state)) {
+      throw new IllegalStateException(getClass().getName() + " and " + child.getClass().getName()
+          + " are not compatible");
+    }
+    if (!child.state.isClean()) {
+      throw new IllegalStateException("The provided RequestContext has been changed");
+    }
+    child.state = state;
+    return other;
   }
 
   /**
@@ -397,7 +440,7 @@
   public <T extends BaseProxy> T create(Class<T> clazz) {
     checkLocked();
 
-    SimpleProxyId<T> id = requestFactory.allocateId(clazz);
+    SimpleProxyId<T> id = state.requestFactory.allocateId(clazz);
     AutoBean<T> created = createProxy(clazz, id);
     return takeOwnership(created);
   }
@@ -426,7 +469,8 @@
     checkLocked();
 
     @SuppressWarnings("unchecked")
-    AutoBean<T> previouslySeen = (AutoBean<T>) editedProxies.get(BaseProxyCategory.stableId(bean));
+    AutoBean<T> previouslySeen =
+        (AutoBean<T>) state.editedProxies.get(BaseProxyCategory.stableId(bean));
     if (previouslySeen != null && !previouslySeen.isFrozen()) {
       /*
        * If we've seen the object before, it might be because it was passed in
@@ -450,7 +494,7 @@
    */
   public void fire() {
     boolean needsReceiver = true;
-    for (AbstractRequest<?> request : invocations) {
+    for (AbstractRequest<?> request : state.invocations) {
       if (request.hasReceiver()) {
         needsReceiver = false;
         break;
@@ -488,7 +532,7 @@
   }
 
   public AbstractRequestFactory getRequestFactory() {
-    return requestFactory;
+    return state.requestFactory;
   }
 
   /**
@@ -517,7 +561,7 @@
      * simple flag-check because of the possibility of "unmaking" a change, per
      * the JavaDoc.
      */
-    for (AutoBean<? extends BaseProxy> bean : editedProxies.values()) {
+    for (AutoBean<? extends BaseProxy> bean : state.editedProxies.values()) {
       AutoBean<?> previous = bean.getTag(Constants.PARENT_OBJECT);
       if (previous == null) {
         // Compare to empty object
@@ -535,26 +579,26 @@
    * EntityCodex support.
    */
   public boolean isEntityType(Class<?> clazz) {
-    return requestFactory.isEntityType(clazz);
+    return state.requestFactory.isEntityType(clazz);
   }
 
   public boolean isLocked() {
-    return locked;
+    return state.locked;
   }
 
   /**
    * EntityCodex support.
    */
   public boolean isValueType(Class<?> clazz) {
-    return requestFactory.isValueType(clazz);
-  }
+    return state.requestFactory.isValueType(clazz);
+  };
 
   /**
    * Called by generated subclasses to enqueue a method invocation.
    */
   protected void addInvocation(AbstractRequest<?> request) {
-    dialect.addInvocation(request);
-  };
+    state.dialect.addInvocation(request);
+  }
 
   /**
    * Invoke the appropriate {@code onFailure} callbacks, possibly throwing an
@@ -563,7 +607,7 @@
   protected void fail(Receiver<Void> receiver, ServerFailure failure) {
     reuse();
     Set<Throwable> causes = null;
-    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(invocations)) {
+    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(state.invocations)) {
       try {
         request.onFail(failure);
       } catch (Throwable t) {
@@ -602,7 +646,7 @@
   protected void violation(final Receiver<Void> receiver, Set<Violation> errors) {
     reuse();
     Set<Throwable> causes = null;
-    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(invocations)) {
+    for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(state.invocations)) {
       try {
         request.onViolation(errors);
       } catch (Throwable t) {
@@ -635,7 +679,7 @@
     if (Strength.SYNTHETIC.equals(op.getStrength())) {
       return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId());
     }
-    return requestFactory.getId(op.getTypeToken(), op.getServerId(), op.getClientId());
+    return state.requestFactory.getId(op.getTypeToken(), op.getServerId(), op.getClientId());
   }
 
   /**
@@ -644,11 +688,11 @@
    */
   <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(SimpleProxyId<Q> id) {
     @SuppressWarnings("unchecked")
-    AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id);
+    AutoBean<Q> bean = (AutoBean<Q>) state.returnedProxies.get(id);
     if (bean == null) {
       Class<Q> proxyClass = id.getProxyClass();
       bean = createProxy(proxyClass, id);
-      returnedProxies.put(id, bean);
+      state.returnedProxies.put(id, bean);
     }
 
     return bean;
@@ -664,7 +708,7 @@
     // The OperationMessages describes operations on exactly one entity
     AutoBean<OperationMessage> toReturn = MessageFactoryHolder.FACTORY.operation();
     OperationMessage operation = toReturn.as();
-    operation.setTypeToken(requestFactory.getTypeToken(stableId.getProxyClass()));
+    operation.setTypeToken(state.requestFactory.getTypeToken(stableId.getProxyClass()));
 
     // Find the object to compare against
     AutoBean<?> parent;
@@ -784,14 +828,14 @@
      * Notify subscribers if the object differs from when it first came into the
      * RequestContext.
      */
-    if (operations != null && requestFactory.isEntityType(id.getProxyClass())) {
+    if (operations != null && state.requestFactory.isEntityType(id.getProxyClass())) {
       for (WriteOperation writeOperation : operations) {
         if (writeOperation.equals(WriteOperation.UPDATE)
-            && !requestFactory.hasVersionChanged(id, op.getVersion())) {
+            && !state.requestFactory.hasVersionChanged(id, op.getVersion())) {
           // No updates if the server reports no change
           continue;
         }
-        requestFactory.getEventBus().fireEventFromSource(
+        state.requestFactory.getEventBus().fireEventFromSource(
             new EntityProxyChange<EntityProxy>((EntityProxy) proxy, writeOperation),
             id.getProxyClass());
       }
@@ -807,16 +851,17 @@
   private <Q extends BaseProxy> SimpleProxyId<Q> allocateSyntheticId(String typeToken,
       int syntheticId) {
     @SuppressWarnings("unchecked")
-    SimpleProxyId<Q> toReturn = (SimpleProxyId<Q>) syntheticIds.get(syntheticId);
+    SimpleProxyId<Q> toReturn = (SimpleProxyId<Q>) state.syntheticIds.get(syntheticId);
     if (toReturn == null) {
-      toReturn = requestFactory.allocateId(requestFactory.<Q> getTypeFromToken(typeToken));
-      syntheticIds.put(syntheticId, toReturn);
+      toReturn =
+          state.requestFactory.allocateId(state.requestFactory.<Q> getTypeFromToken(typeToken));
+      state.syntheticIds.put(syntheticId, toReturn);
     }
     return toReturn;
   }
 
   private void checkLocked() {
-    if (locked) {
+    if (state.locked) {
       throw new IllegalStateException("A request is already in progress");
     }
   }
@@ -832,13 +877,13 @@
       throw new IllegalArgumentException(object.getClass().getName());
     }
 
-    RequestContext context = bean.getTag(REQUEST_CONTEXT);
-    if (!bean.isFrozen() && context != this) {
+    State otherState = bean.getTag(REQUEST_CONTEXT_STATE);
+    if (!bean.isFrozen() && otherState != this.state) {
       /*
        * This means something is way off in the weeds. If a bean is editable,
        * it's supposed to be associated with a RequestContext.
        */
-      assert context != null : "Unfrozen bean with null RequestContext";
+      assert otherState != null : "Unfrozen bean with null RequestContext";
 
       /*
        * Already editing the object in another context or it would have been in
@@ -937,18 +982,18 @@
 
   private void doFire(final Receiver<Void> receiver) {
     checkLocked();
-    locked = true;
+    state.locked = true;
 
     freezeEntities(true);
 
-    String payload = dialect.makePayload();
-    requestFactory.getRequestTransport().send(payload, new TransportReceiver() {
+    String payload = state.dialect.makePayload();
+    state.requestFactory.getRequestTransport().send(payload, new TransportReceiver() {
       public void onTransportFailure(ServerFailure failure) {
         fail(receiver, failure);
       }
 
       public void onTransportSuccess(String payload) {
-        dialect.processPayload(receiver, payload);
+        state.dialect.processPayload(receiver, payload);
       }
     });
   }
@@ -957,7 +1002,7 @@
    * Set the frozen status of all EntityProxies owned by this context.
    */
   private void freezeEntities(boolean frozen) {
-    for (AutoBean<?> bean : editedProxies.values()) {
+    for (AutoBean<?> bean : state.editedProxies.values()) {
       bean.setFrozen(frozen);
     }
   }
@@ -969,7 +1014,7 @@
     // Always diff'ed against itself, producing a no-op
     toMutate.setTag(Constants.PARENT_OBJECT, toMutate);
     // Act with entity-identity semantics
-    toMutate.setTag(REQUEST_CONTEXT, null);
+    toMutate.setTag(REQUEST_CONTEXT_STATE, null);
     toMutate.setFrozen(true);
   }
 
@@ -981,7 +1026,7 @@
     MessageFactory f = MessageFactoryHolder.FACTORY;
 
     List<InvocationMessage> invocationMessages = new ArrayList<InvocationMessage>();
-    for (AbstractRequest<?> invocation : invocations) {
+    for (AbstractRequest<?> invocation : state.invocations) {
       // RequestData is produced by the generated subclass
       RequestData data = invocation.getRequestData();
       InvocationMessage message = f.invocation().as();
@@ -1014,7 +1059,7 @@
    */
   private List<OperationMessage> makePayloadOperations() {
     List<OperationMessage> operations = new ArrayList<OperationMessage>();
-    for (AutoBean<? extends BaseProxy> currentView : editedProxies.values()) {
+    for (AutoBean<? extends BaseProxy> currentView : state.editedProxies.values()) {
       OperationMessage operation =
           makeOperationMessage(BaseProxyCategory.stableId(currentView), currentView, true).as();
       operations.add(operation);
@@ -1079,15 +1124,15 @@
    */
   private void reuse() {
     freezeEntities(false);
-    locked = false;
+    state.locked = false;
   }
 
   /**
    * Make the EnityProxy bean edited and owned by this RequestContext.
    */
   private <T extends BaseProxy> T takeOwnership(AutoBean<T> bean) {
-    editedProxies.put(stableId(bean), bean);
-    bean.setTag(REQUEST_CONTEXT, this);
+    state.editedProxies.put(stableId(bean), bean);
+    bean.setTag(REQUEST_CONTEXT_STATE, this.state);
     return bean.as();
   }
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/BaseProxyCategory.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/BaseProxyCategory.java
index 481f4c8..1772203 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/BaseProxyCategory.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/BaseProxyCategory.java
@@ -15,12 +15,13 @@
  */
 package com.google.web.bindery.requestfactory.shared.impl;
 
-import static com.google.web.bindery.requestfactory.shared.impl.Constants.REQUEST_CONTEXT;
+import static com.google.web.bindery.requestfactory.shared.impl.Constants.REQUEST_CONTEXT_STATE;
 import static com.google.web.bindery.requestfactory.shared.impl.Constants.STABLE_ID;
 
 import com.google.web.bindery.autobean.shared.AutoBean;
 import com.google.web.bindery.autobean.shared.AutoBeanUtils;
 import com.google.web.bindery.requestfactory.shared.BaseProxy;
+import com.google.web.bindery.requestfactory.shared.impl.AbstractRequestContext.State;
 
 /**
  * Contains behaviors common to all proxy instances.
@@ -65,17 +66,17 @@
      */
     AutoBean<T> otherBean = AutoBeanUtils.getAutoBean(returnValue);
     if (otherBean != null) {
-      otherBean.setTag(REQUEST_CONTEXT, bean.getTag(REQUEST_CONTEXT));
+      otherBean.setTag(REQUEST_CONTEXT_STATE, bean.getTag(REQUEST_CONTEXT_STATE));
     }
     return returnValue;
   }
 
   public static AbstractRequestContext requestContext(AutoBean<?> bean) {
-    return bean.getTag(REQUEST_CONTEXT);
+    State state = bean.<AbstractRequestContext.State> getTag(REQUEST_CONTEXT_STATE);
+    return state == null ? null : state.getCanonicalContext();
   }
 
-  public static <T extends BaseProxy> SimpleProxyId<T> stableId(
-      AutoBean<? extends T> bean) {
+  public static <T extends BaseProxy> SimpleProxyId<T> stableId(AutoBean<? extends T> bean) {
     return bean.getTag(STABLE_ID);
   }
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/Constants.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/Constants.java
index 33d914e..279bc86 100644
--- a/user/src/com/google/web/bindery/requestfactory/shared/impl/Constants.java
+++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/Constants.java
@@ -22,7 +22,7 @@
   String DOMAIN_OBJECT = "domainObject";
   String IN_RESPONSE = "inResponse";
   String PARENT_OBJECT = "parentObject";
-  String REQUEST_CONTEXT = "requestContext";
+  String REQUEST_CONTEXT_STATE = "requestContext";
   String STABLE_ID = "stableId";
   String VERSION_PROPERTY_B64 = "version";
 }
diff --git a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
index bcdb434..d912119 100644
--- a/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
+++ b/user/src/com/google/web/bindery/requestfactory/vm/InProcessRequestContext.java
@@ -42,8 +42,11 @@
  */
 class InProcessRequestContext extends AbstractRequestContext {
   class RequestContextHandler implements InvocationHandler {
-    public Object invoke(Object proxy, Method method, final Object[] args)
-        throws Throwable {
+    public InProcessRequestContext getContext() {
+      return InProcessRequestContext.this;
+    }
+
+    public Object invoke(Object proxy, Method method, final Object[] args) throws Throwable {
       // Maybe delegate to superclass
       Class<?> owner = method.getDeclaringClass();
       if (Object.class.equals(owner) || RequestContext.class.equals(owner)
@@ -63,9 +66,9 @@
       Type returnGenericType;
       boolean isInstance = InstanceRequest.class.isAssignableFrom(method.getReturnType());
       if (isInstance) {
-        returnGenericType = TypeUtils.getParameterization(
-            InstanceRequest.class, method.getGenericReturnType(),
-            method.getReturnType())[1];
+        returnGenericType =
+            TypeUtils.getParameterization(InstanceRequest.class, method.getGenericReturnType(),
+                method.getReturnType())[1];
         if (args == null) {
           actualArgs = new Object[1];
         } else {
@@ -74,8 +77,9 @@
           System.arraycopy(args, 0, actualArgs, 1, args.length);
         }
       } else {
-        returnGenericType = TypeUtils.getSingleParameterization(Request.class,
-            method.getGenericReturnType(), method.getReturnType());
+        returnGenericType =
+            TypeUtils.getSingleParameterization(Request.class, method.getGenericReturnType(),
+                method.getReturnType());
         if (args == null) {
           actualArgs = NO_ARGS;
         } else {
@@ -84,26 +88,23 @@
       }
 
       Class<?> returnType = TypeUtils.ensureBaseType(returnGenericType);
-      Class<?> elementType = Collection.class.isAssignableFrom(returnType)
-          ? TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
-              Collection.class, returnGenericType)) : null;
+      Class<?> elementType =
+          Collection.class.isAssignableFrom(returnType) ? TypeUtils.ensureBaseType(TypeUtils
+              .getSingleParameterization(Collection.class, returnGenericType)) : null;
 
       final RequestData data;
       if (dialect.equals(Dialect.STANDARD)) {
-        String operation = method.getDeclaringClass().getName() + "::"
-            + method.getName();
+        String operation = method.getDeclaringClass().getName() + "::" + method.getName();
 
         data = new RequestData(operation, actualArgs, returnType, elementType);
       } else {
         // Calculate request metadata
-        JsonRpcWireName wireInfo = method.getReturnType().getAnnotation(
-            JsonRpcWireName.class);
+        JsonRpcWireName wireInfo = method.getReturnType().getAnnotation(JsonRpcWireName.class);
         String apiVersion = wireInfo.version();
         String operation = wireInfo.value();
 
         int foundContent = -1;
-        final String[] parameterNames = args == null ? new String[0]
-            : new String[args.length];
+        final String[] parameterNames = args == null ? new String[0] : new String[args.length];
         Annotation[][] parameterAnnotations = method.getParameterAnnotations();
         parameter : for (int i = 0, j = parameterAnnotations.length; i < j; i++) {
           for (Annotation annotation : parameterAnnotations[i]) {
@@ -115,10 +116,8 @@
               continue parameter;
             }
           }
-          throw new UnsupportedOperationException("No "
-              + PropertyName.class.getCanonicalName()
-              + " annotation on parameter " + i + " of method "
-              + method.toString());
+          throw new UnsupportedOperationException("No " + PropertyName.class.getCanonicalName()
+              + " annotation on parameter " + i + " of method " + method.toString());
         }
         final int contentIdx = foundContent;
 
@@ -134,14 +133,14 @@
       }
 
       // Create the request, just filling in the RequestData details
-      final AbstractRequest<Object> req = new AbstractRequest<Object>(
-          InProcessRequestContext.this) {
-        @Override
-        protected RequestData makeRequestData() {
-          data.setPropertyRefs(propertyRefs);
-          return data;
-        }
-      };
+      final AbstractRequest<Object> req =
+          new AbstractRequest<Object>(InProcessRequestContext.this) {
+            @Override
+            protected RequestData makeRequestData() {
+              data.setPropertyRefs(propertyRefs);
+              return data;
+            }
+          };
 
       if (!isInstance) {
         // Instance invocations are enqueued when using() is called
@@ -153,19 +152,15 @@
       } else if (dialect.equals(Dialect.JSON_RPC)) {
         // Support optional parameters for JSON-RPC payloads
         Class<?> requestType = method.getReturnType().asSubclass(Request.class);
-        return Proxy.newProxyInstance(requestType.getClassLoader(),
-            new Class<?>[] {requestType}, new InvocationHandler() {
-              public Object invoke(Object proxy, Method method, Object[] args)
-                  throws Throwable {
+        return Proxy.newProxyInstance(requestType.getClassLoader(), new Class<?>[] {requestType},
+            new InvocationHandler() {
+              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                 if (Object.class.equals(method.getDeclaringClass())
                     || Request.class.equals(method.getDeclaringClass())) {
                   return method.invoke(req, args);
-                } else if (BeanMethod.SET.matches(method)
-                    || BeanMethod.SET_BUILDER.matches(method)) {
-                  req.getRequestData().setNamedParameter(
-                      BeanMethod.SET.inferName(method), args[0]);
-                  return Void.TYPE.equals(method.getReturnType()) ? null
-                      : proxy;
+                } else if (BeanMethod.SET.matches(method) || BeanMethod.SET_BUILDER.matches(method)) {
+                  req.getRequestData().setNamedParameter(BeanMethod.SET.inferName(method), args[0]);
+                  return Void.TYPE.equals(method.getReturnType()) ? null : proxy;
                 }
                 throw new UnsupportedOperationException(method.toString());
               }
@@ -179,13 +174,19 @@
   static final Object[] NO_ARGS = new Object[0];
   private final Dialect dialect;
 
-  protected InProcessRequestContext(AbstractRequestFactory factory,
-      Dialect dialect) {
+  protected InProcessRequestContext(AbstractRequestFactory factory, Dialect dialect) {
     super(factory, dialect);
     this.dialect = dialect;
   }
 
   @Override
+  public <T extends RequestContext> T append(T other) {
+    RequestContextHandler h = (RequestContextHandler) Proxy.getInvocationHandler(other);
+    super.append(h.getContext());
+    return other;
+  }
+
+  @Override
   protected AutoBeanFactory getAutoBeanFactory() {
     return ((InProcessRequestFactory) getRequestFactory()).getAutoBeanFactory();
   }
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java
index 6b6f4fd..b23c76e 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTest.java
@@ -223,6 +223,87 @@
     });
   }
 
+  public void testAppend() {
+    delayTestFinish(DELAY_TEST_FINISH);
+    SimpleFooRequest c1 = req.simpleFooRequest();
+    SimpleFooProxy foo1 = c1.create(SimpleFooProxy.class);
+    SimpleBarRequest c2 = c1.append(req.simpleBarRequest());
+    SimpleFooRequest c3 = c2.append(req.simpleFooRequest());
+
+    assertNotSame(c1, c3);
+    assertSame(foo1, c2.edit(foo1));
+    assertSame(foo1, c3.edit(foo1));
+
+    SimpleBarProxy foo2 = c2.create(SimpleBarProxy.class);
+    assertSame(foo2, c1.edit(foo2));
+    assertSame(foo2, c3.edit(foo2));
+
+    SimpleFooProxy foo3 = c3.create(SimpleFooProxy.class);
+    assertSame(foo3, c1.edit(foo3));
+    assertSame(foo3, c2.edit(foo3));
+
+    try {
+      // Throws exception because c3 has already accumulated some state
+      req.simpleValueContext().append(c3);
+      fail("Should have thrown IllegalStateException");
+    } catch (IllegalStateException expected) {
+    }
+
+    try {
+      // Throws exception because a different RequestFactory instance is used
+      c3.append(createFactory().simpleFooRequest());
+      fail("Should have thrown IllegalStateException");
+    } catch (IllegalStateException expected) {
+    }
+
+    // Queue up two invocations, and test that both Receivers are called
+    final boolean[] seen = {false, false};
+    c1.add(1, 2).to(new Receiver<Integer>() {
+      @Override
+      public void onSuccess(Integer response) {
+        seen[0] = true;
+        assertEquals(3, response.intValue());
+      }
+    });
+    c2.countSimpleBar().to(new Receiver<Long>() {
+      @Override
+      public void onSuccess(Long response) {
+        seen[1] = true;
+        assertEquals(2, response.longValue());
+      }
+    });
+
+    // It doesn't matter which context instance is fired
+    c2.fire(new Receiver<Void>() {
+      @Override
+      public void onSuccess(Void response) {
+        assertTrue(seen[0]);
+        assertTrue(seen[1]);
+        finishTestAndReset();
+      }
+    });
+
+    /*
+     * Since the common State has been locked, calling any other
+     * context-mutation methods should fail.
+     */
+    try {
+      c1.fire();
+      fail("Should have thrown exception");
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      c3.fire();
+      fail("Should have thrown exception");
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      c3.create(SimpleFooProxy.class);
+      fail("Should have thrown exception");
+    } catch (IllegalStateException expected) {
+    }
+  }
+
   /**
    * Test that we can commit child objects.
    */
diff --git a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTestBase.java b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTestBase.java
index cac062b..43f9681 100644
--- a/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTestBase.java
+++ b/user/test/com/google/web/bindery/requestfactory/gwt/client/RequestFactoryTestBase.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -27,6 +27,7 @@
 import com.google.web.bindery.requestfactory.shared.EntityProxyChange;
 import com.google.web.bindery.requestfactory.shared.ProxySerializer;
 import com.google.web.bindery.requestfactory.shared.Receiver;
+import com.google.web.bindery.requestfactory.shared.SimpleFooRequest;
 import com.google.web.bindery.requestfactory.shared.SimpleRequestFactory;
 import com.google.web.bindery.requestfactory.shared.impl.BaseProxyCategory;
 import com.google.web.bindery.requestfactory.shared.impl.Constants;
@@ -36,7 +37,7 @@
  * A base class for anything that makes use of the SimpleRequestFactory.
  * Subclasses must always use {@link #finishTestAndReset()} in order to allow
  * calls to the reset methods to complete before the next test starts.
- *
+ * 
  */
 public abstract class RequestFactoryTestBase extends GWTTestCase {
 
@@ -113,13 +114,11 @@
     assertTrue(AutoBeanUtils.deepEquals(originalBean, restoredBean));
 
     if (proxy instanceof EntityProxy && !id.isEphemeral()) {
-      assertEquals(((EntityProxy) proxy).stableId(),
-          ((EntityProxy) restored).stableId());
+      assertEquals(((EntityProxy) proxy).stableId(), ((EntityProxy) restored).stableId());
     }
 
     // In deference to testing stable ids, copy the original id into the clone
-    restoredBean.setTag(Constants.STABLE_ID,
-        originalBean.getTag(Constants.STABLE_ID));
+    restoredBean.setTag(Constants.STABLE_ID, originalBean.getTag(Constants.STABLE_ID));
     return restored;
   }
 
@@ -143,23 +142,13 @@
   }
 
   protected void finishTestAndReset() {
-    final boolean[] reallyDone = {false, false};
-    req.simpleFooRequest().reset().fire(new Receiver<Void>() {
+    SimpleFooRequest ctx = req.simpleFooRequest();
+    ctx.reset();
+    ctx.append(req.simpleBarRequest()).reset();
+    ctx.fire(new Receiver<Void>() {
       @Override
       public void onSuccess(Void response) {
-        reallyDone[0] = true;
-        if (reallyDone[0] && reallyDone[1]) {
-          finishTest();
-        }
-      }
-    });
-    req.simpleBarRequest().reset().fire(new Receiver<Void>() {
-      @Override
-      public void onSuccess(Void response) {
-        reallyDone[1] = true;
-        if (reallyDone[0] && reallyDone[1]) {
-          finishTest();
-        }
+        finishTest();
       }
     });
   }