An async RPC method can now be declared to return a RequestBuilder, in which case a fully-baked RequestBuilder is returned, but no send is performed.  The caller can adjust the RequestBuilder and then call "send" to actually perform the call.

Suggested by: scottb, mmendez
Patch by: bobv
Review by: scottb, bruce


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2329 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java b/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
index d05eaec..5e32c56 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
@@ -228,33 +228,91 @@
    * 
    * @return a {@link Request} object that can be used to track the request
    */
-  @SuppressWarnings("unused")
   protected <T> Request doInvoke(ResponseReader responseReader,
       String methodName, int invocationCount, String requestData,
       AsyncCallback<T> callback) {
 
+    RequestBuilder rb = doPrepareRequestBuilderImpl(responseReader, methodName,
+        invocationCount, requestData, callback);
+
+    try {
+      return rb.send();
+    } catch (RequestException ex) {
+      InvocationException iex = new InvocationException(
+          "Unable to initiate the asynchronous service invocation -- check the network connection",
+          ex);
+      callback.onFailure(iex);
+    } finally {
+      if (RemoteServiceProxy.isStatsAvailable()
+          && RemoteServiceProxy.stats(methodName + ":" + invocationCount
+              + ":requestSent", RemoteServiceProxy.bytesStat(methodName,
+              invocationCount, requestData.length()))) {
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Configures a RequestBuilder to send an RPC request when the RequestBuilder
+   * is intended to be returned through the asynchronous proxy interface.
+   * 
+   * @param <T> return type for the AsyncCallback
+   * @param responseReader instance used to read the return value of the
+   *          invocation
+   * @param requestData payload that encodes the addressing and arguments of the
+   *          RPC call
+   * @param callback callback handler
+   * 
+   * @return a RequestBuilder object that is ready to have its
+   *         {@link RequestBuilder#send()} method invoked.
+   */
+  protected <T> RequestBuilder doPrepareRequestBuilder(
+      ResponseReader responseReader, String methodName, int invocationCount,
+      String requestData, AsyncCallback<T> callback) {
+
+    RequestBuilder rb = doPrepareRequestBuilderImpl(responseReader, methodName,
+        invocationCount, requestData, callback);
+
+    // We'll record when the request was configured...
+    if (RemoteServiceProxy.isStatsAvailable()
+        && RemoteServiceProxy.stats(methodName + ":" + invocationCount
+            + ":requestPrepared", RemoteServiceProxy.bytesStat(methodName,
+            invocationCount, requestData.length()))) {
+    }
+
+    return rb;
+  }
+
+  /**
+   * Configures a RequestBuilder to send an RPC request.
+   * 
+   * @param <T> return type for the AsyncCallback
+   * @param responseReader instance used to read the return value of the
+   *          invocation
+   * @param requestData payload that encodes the addressing and arguments of the
+   *          RPC call
+   * @param callback callback handler
+   * 
+   * @return a RequestBuilder object that is ready to have its
+   *         {@link RequestBuilder#send()} method invoked.
+   */
+  private <T> RequestBuilder doPrepareRequestBuilderImpl(
+      ResponseReader responseReader, String methodName, int invocationCount,
+      String requestData, AsyncCallback<T> callback) {
+
     if (getServiceEntryPoint() == null) {
       throw new NoServiceEntryPointSpecifiedException();
     }
 
     RequestCallbackAdapter<T> responseHandler = new RequestCallbackAdapter<T>(
         this, methodName, invocationCount, callback, responseReader);
+
     RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,
         getServiceEntryPoint());
-    rb.setHeader("Content-Type", "text/x-gwt-rpc; charset=utf-8");
 
-    try {
-      return rb.sendRequest(requestData, responseHandler);
-    } catch (RequestException ex) {
-      InvocationException iex = new InvocationException(
-          "Unable to initiate the asynchronous service invocation -- check the network connection",
-          ex);
-      callback.onFailure(iex);
-    } finally {
-      boolean toss = isStatsAvailable()
-          && stats(methodName + ":" + invocationCount + ":requestSent",
-              bytesStat(methodName, invocationCount, requestData.length()));
-    }
-    return null;
+    rb.setHeader("Content-Type", "text/x-gwt-rpc; charset=utf-8");
+    rb.setCallback(responseHandler);
+    rb.setRequestData(requestData);
+    return rb;
   }
 }
diff --git a/user/src/com/google/gwt/user/rebind/rpc/ProxyCreator.java b/user/src/com/google/gwt/user/rebind/rpc/ProxyCreator.java
index 8d35f8a..c70fd15 100644
--- a/user/src/com/google/gwt/user/rebind/rpc/ProxyCreator.java
+++ b/user/src/com/google/gwt/user/rebind/rpc/ProxyCreator.java
@@ -28,6 +28,8 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.dev.generator.NameFactory;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
 import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
 import com.google.gwt.user.client.rpc.SerializationException;
 import com.google.gwt.user.client.rpc.impl.ClientSerializationStreamReader;
@@ -312,12 +314,24 @@
         + getProxySimpleName() + "." + syncMethod.getName()
         + "\", getRequestId()));");
 
-    if (asyncReturnType != JPrimitiveType.VOID) {
-      w.print("return ");
+    /*
+     * Depending on the return type for the async method, return a
+     * RequestBuilder, a Request, or nothing at all.
+     */
+    if (asyncReturnType == JPrimitiveType.VOID) {
+      w.print("doInvoke(");
+    } else if (asyncReturnType.getQualifiedSourceName().equals(
+        RequestBuilder.class.getName())) {
+      w.print("return doPrepareRequestBuilder(");
+    } else if (asyncReturnType.getQualifiedSourceName().equals(
+        Request.class.getName())) {
+      w.print("return doInvoke(");
+    } else {
+      // This method should have been caught by RemoteServiceAsyncValidator
+      throw new RuntimeException("Unhandled return type "
+          + asyncReturnType.getQualifiedSourceName());
     }
 
-    // Call the doInvoke method to actually send the request.
-    w.print("doInvoke(");
     JType returnType = syncMethod.getReturnType();
     w.print("ResponseReader." + getResponseReaderFor(returnType).name());
     w.print(", \"" + getProxySimpleName() + "." + syncMethod.getName()
diff --git a/user/src/com/google/gwt/user/rebind/rpc/RemoteServiceAsyncValidator.java b/user/src/com/google/gwt/user/rebind/rpc/RemoteServiceAsyncValidator.java
index 2fa9811..655ee0e 100644
--- a/user/src/com/google/gwt/user/rebind/rpc/RemoteServiceAsyncValidator.java
+++ b/user/src/com/google/gwt/user/rebind/rpc/RemoteServiceAsyncValidator.java
@@ -26,6 +26,7 @@
 import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import java.util.HashMap;
@@ -145,6 +146,11 @@
   private final JClassType asyncCallbackClass;
 
   /**
+   * {@link JClassType} for the {@link RequestBuilder} class.
+   */
+  private final JClassType requestBuilderType;
+
+  /**
    * {@link JClassType} for the {@link Request} class.
    */
   private final JClassType requestType;
@@ -154,6 +160,7 @@
     try {
       asyncCallbackClass = typeOracle.getType(AsyncCallback.class.getName());
       requestType = typeOracle.getType(Request.class.getCanonicalName());
+      requestBuilderType = typeOracle.getType(RequestBuilder.class.getCanonicalName());
     } catch (NotFoundException e) {
       logger.log(TreeLogger.ERROR, null, e);
       throw new UnableToCompleteException();
@@ -212,12 +219,14 @@
         // TODO if async param is parameterized make sure that the sync return
         // type is assignable to the first type argument
         JType returnType = asyncMethod.getReturnType();
-        if (returnType != JPrimitiveType.VOID && returnType != requestType) {
+        if (returnType != JPrimitiveType.VOID && returnType != requestType
+            && returnType != requestBuilderType) {
           branch.branch(TreeLogger.ERROR,
               "The asynchronous version of the synchronous method '"
                   + syncMethod.getReadableDeclaration()
                   + "' must have a return type of 'void' or '"
-                  + Request.class.getCanonicalName() + "'", null);
+                  + Request.class.getCanonicalName() + "' or '"
+                  + RequestBuilder.class.getCanonicalName() + "'", null);
           failed = true;
         } else {
           syncMethodToAsyncMethodMap.put(syncMethod, asyncMethod);
@@ -231,4 +240,4 @@
 
     return syncMethodToAsyncMethodMap;
   }
-}
\ No newline at end of file
+}
diff --git a/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTest.java b/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTest.java
index 05bec77..6058f4c 100644
--- a/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTest.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestException;
 import com.google.gwt.junit.client.GWTTestCase;
 
 /**
@@ -33,8 +35,7 @@
  * </p>
  */
 public class RemoteServiceServletTest extends GWTTestCase {
-  private static final int TEST_DELAY = Integer.MAX_VALUE;
-  private Request req;
+  private static final int TEST_DELAY = 10000;
 
   private static RemoteServiceServletTestServiceAsync getAsyncService() {
     RemoteServiceServletTestServiceAsync service = (RemoteServiceServletTestServiceAsync) GWT.create(RemoteServiceServletTestService.class);
@@ -45,22 +46,46 @@
     return service;
   }
 
+  private Request req;
+
   public String getModuleName() {
     return "com.google.gwt.user.RPCSuite";
   }
 
+  public void testManualSend() throws RequestException {
+    RemoteServiceServletTestServiceAsync service = getAsyncService();
+
+    delayTestFinish(TEST_DELAY);
+
+    RequestBuilder builder = service.testExpectCustomHeader(new AsyncCallback<Void>() {
+
+      public void onFailure(Throwable caught) {
+        TestSetValidator.rethrowException(caught);
+      }
+
+      public void onSuccess(Void result) {
+        assertTrue(!req.isPending());
+        finishTest();
+      }
+    });
+
+    builder.setHeader("X-Custom-Header", "true");
+    req = builder.send();
+    assertTrue(req.isPending());
+  }
+
   public void testServiceInterfaceLocation() {
     RemoteServiceServletTestServiceAsync service = getAsyncService();
 
     delayTestFinish(TEST_DELAY);
 
-    req = service.test(new AsyncCallback<Object>() {
+    req = service.test(new AsyncCallback<Void>() {
 
       public void onFailure(Throwable caught) {
         TestSetValidator.rethrowException(caught);
       }
 
-      public void onSuccess(Object result) {
+      public void onSuccess(Void result) {
         assertTrue(!req.isPending());
         finishTest();
       }
diff --git a/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestService.java b/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestService.java
index 093241e..e55e36f 100644
--- a/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestService.java
+++ b/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 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
@@ -20,4 +20,6 @@
  */
 public interface RemoteServiceServletTestService extends RemoteService {
   void test();
+
+  void testExpectCustomHeader();
 }
diff --git a/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestServiceAsync.java b/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestServiceAsync.java
index 0576df3..154e091 100644
--- a/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestServiceAsync.java
+++ b/user/test/com/google/gwt/user/client/rpc/RemoteServiceServletTestServiceAsync.java
@@ -16,10 +16,13 @@
 package com.google.gwt.user.client.rpc;
 
 import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
 
 /**
  * TODO: document me.
  */
 public interface RemoteServiceServletTestServiceAsync {
-  Request test(AsyncCallback callback);
+  Request test(AsyncCallback<Void> callback);
+
+  RequestBuilder testExpectCustomHeader(AsyncCallback<Void> callback);
 }
diff --git a/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTestServiceImplBase.java b/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTestServiceImplBase.java
index 384eb27..39e1934 100644
--- a/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTestServiceImplBase.java
+++ b/user/test/com/google/gwt/user/server/rpc/RemoteServiceServletTestServiceImplBase.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.user.client.rpc.RemoteServiceServletTestService;
 
+import javax.servlet.http.HttpServletRequest;
+
 /**
  * TODO: document me.
  */
@@ -25,4 +27,11 @@
 
   public void test() {
   }
+
+  public void testExpectCustomHeader() {
+    HttpServletRequest req = getThreadLocalRequest();
+    if (!Boolean.parseBoolean(req.getHeader("X-Custom-Header"))) {
+      throw new RuntimeException("Missing header");
+    }
+  }
 }