Extract a RequestTransport interface and DefaultRequestTransport implementation from RequestFactory.
This will allow end-users to have arbitrary control over the way RF communicates with the server.
Patch by: bobv
Review by: rjrjr
Review at http://gwt-code-reviews.appspot.com/890802


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8812 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/requestfactory/client/DefaultRequestTransport.java b/user/src/com/google/gwt/requestfactory/client/DefaultRequestTransport.java
new file mode 100644
index 0000000..1fcc82a
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/client/DefaultRequestTransport.java
@@ -0,0 +1,155 @@
+/*
+ * 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.client;
+
+import static com.google.gwt.user.client.rpc.RpcRequestBuilder.STRONG_NAME_HEADER;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.requestfactory.shared.RequestEvent;
+import com.google.gwt.requestfactory.shared.RequestFactory;
+import com.google.gwt.requestfactory.shared.RequestTransport;
+import com.google.gwt.requestfactory.shared.RequestEvent.State;
+import com.google.gwt.user.client.Window.Location;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An implementation of {@link RequestTransport} that uses a
+ * {@link RequestBuilder}.
+ */
+public class DefaultRequestTransport implements RequestTransport {
+  /*
+   * A separate logger for wire activity, which does not get logged by the
+   * remote log handler, so we avoid infinite loops. All log messages that could
+   * happen every time a request is made from the server should be logged to
+   * this logger.
+   */
+  private static Logger wireLogger = Logger.getLogger("WireActivityLogger");
+  private static final String SERVER_ERROR = "Server Error";
+  private final EventBus eventBus;
+  private String requestUrl = GWT.getHostPageBaseURL() + RequestFactory.URL;
+
+  /**
+   * Construct a DefaultRequestTransport.
+   * 
+   * @param eventBus the same EventBus passed into {@link RequestFactory#init}.
+   */
+  public DefaultRequestTransport(EventBus eventBus) {
+    if (eventBus == null) {
+      throw new IllegalArgumentException("eventBus must not be null");
+    }
+    this.eventBus = eventBus;
+  }
+
+  /**
+   * Returns the current URL used by this transport.
+   */
+  public String getRequestUrl() {
+    return requestUrl;
+  }
+
+  public void send(String payload, Receiver receiver) {
+    RequestBuilder builder = createRequestBuilder();
+    configureRequestBuilder(builder);
+
+    builder.setRequestData(payload);
+    builder.setCallback(createRequestCallback(receiver));
+
+    try {
+      wireLogger.finest("Sending fire request");
+      builder.send();
+      postRequestEvent(State.SENT, null);
+    } catch (RequestException e) {
+      wireLogger.log(Level.SEVERE, SERVER_ERROR + " (" + e.getMessage() + ")",
+          e);
+    }
+  }
+
+  /**
+   * Override the default URL used by this transport.
+   */
+  public void setRequestUrl(String url) {
+    this.requestUrl = url;
+  }
+
+  /**
+   * Override to change the headers sent in the HTTP request.
+   */
+  protected void configureRequestBuilder(RequestBuilder builder) {
+    builder.setHeader("Content-Type", RequestFactory.JSON_CONTENT_TYPE_UTF8);
+    builder.setHeader("pageurl", Location.getHref());
+    builder.setHeader(STRONG_NAME_HEADER, GWT.getPermutationStrongName());
+  }
+
+  /**
+   * Constructs a RequestBuilder using the {@link RequestBuilder#POST} method
+   * sent to the URL returned from {@link #getRequestUrl()}.
+   */
+  protected RequestBuilder createRequestBuilder() {
+    return new RequestBuilder(RequestBuilder.POST, getRequestUrl());
+  }
+
+  /**
+   * Creates a RequestCallback that maps the HTTP response onto the
+   * {@link Receiver} interface.
+   */
+  protected RequestCallback createRequestCallback(final Receiver receiver) {
+    return new RequestCallback() {
+
+      public void onError(Request request, Throwable exception) {
+        postRequestEvent(State.RECEIVED, null);
+        wireLogger.log(Level.SEVERE, SERVER_ERROR, exception);
+        receiver.onFailure(exception.getMessage());
+      }
+
+      public void onResponseReceived(Request request, Response response) {
+        wireLogger.finest("Response received");
+        try {
+          if (200 == response.getStatusCode()) {
+            String text = response.getText();
+            receiver.onSuccess(text);
+          } else if (Response.SC_UNAUTHORIZED == response.getStatusCode()) {
+            String message = "Need to log in";
+            wireLogger.finest(message);
+            receiver.onFailure(message);
+          } else if (response.getStatusCode() > 0) {
+            /*
+             * During the redirection for logging in, we get a response with no
+             * status code, but it's not an error, so we only log errors with
+             * bad status codes here.
+             */
+            String message = SERVER_ERROR + " " + response.getStatusCode()
+                + " " + response.getText();
+            wireLogger.severe(message);
+            receiver.onFailure(message);
+          }
+        } finally {
+          postRequestEvent(State.RECEIVED, response);
+        }
+      }
+    };
+  }
+
+  private void postRequestEvent(State received, Response response) {
+    eventBus.fireEvent(new RequestEvent(received, response));
+  }
+}
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
index 99c84d1..e310b62 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
@@ -15,26 +15,20 @@
  */
 package com.google.gwt.requestfactory.client.impl;
 
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.http.client.Request;
-import com.google.gwt.http.client.RequestBuilder;
-import com.google.gwt.http.client.RequestCallback;
-import com.google.gwt.http.client.RequestException;
-import com.google.gwt.http.client.Response;
+import com.google.gwt.requestfactory.client.DefaultRequestTransport;
 import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.EntityProxyId;
 import com.google.gwt.requestfactory.shared.ProxyRequest;
-import com.google.gwt.requestfactory.shared.RequestEvent;
-import com.google.gwt.requestfactory.shared.RequestEvent.State;
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.RequestObject;
+import com.google.gwt.requestfactory.shared.RequestTransport;
+import com.google.gwt.requestfactory.shared.ServerFailure;
 import com.google.gwt.requestfactory.shared.WriteOperation;
-import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.requestfactory.shared.impl.RequestData;
 
 import java.util.HashMap;
 import java.util.Map;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
 /**
@@ -59,8 +53,9 @@
       return perSchemaMap.get(datastoreId);
     }
 
-    /* returns the previous futureId, if any*/
-    Object put(Object datastoreId, ProxySchema<? extends ProxyImpl> schema, Object futureId) {
+    /* returns the previous futureId, if any */
+    Object put(Object datastoreId, ProxySchema<? extends ProxyImpl> schema,
+        Object futureId) {
       Map<Object, Object> perSchemaMap = internalMap.get(schema);
       if (perSchemaMap == null) {
         perSchemaMap = new HashMap<Object, Object>();
@@ -69,25 +64,19 @@
       return perSchemaMap.put(datastoreId, futureId);
     }
   }
+
   static final boolean IS_FUTURE = true;
   static final boolean NOT_FUTURE = false;
 
   private static Logger logger = Logger.getLogger(RequestFactory.class.getName());
 
-  // A separate logger for wire activity, which does not get logged by the
-  // remote log handler, so we avoid infinite loops. All log messages that
-  // could happen every time a request is made from the server should be logged
-  // to this logger.
-  private static Logger wireLogger = Logger.getLogger("WireActivityLogger");
-
-  private static String SERVER_ERROR = "Server Error";
-  
   private final Integer initialVersion = 1;
   /*
    * Keeping these maps forever is not a desirable solution because of the
    * memory overhead but need these if want to provide stable {@EntityProxyId}.
    * 
-   * futureToDatastoreMap is currently not used, will be useful in find requests.
+   * futureToDatastoreMap is currently not used, will be useful in find
+   * requests.
    */
   final Map<Object, Object> futureToDatastoreMap = new HashMap<Object, Object>();
 
@@ -99,6 +88,8 @@
 
   private EventBus eventBus;
 
+  private RequestTransport transport;
+
   public <R extends ProxyImpl> R create(Class<R> token,
       ProxyToTypeMap recordToTypeMap) {
 
@@ -114,56 +105,31 @@
     return findRequest().find(proxyId);
   }
 
-  public void fire(final RequestObject<?> requestObject) {
-    RequestBuilder builder = new RequestBuilder(RequestBuilder.POST,
-        GWT.getHostPageBaseURL() + RequestFactory.URL);
-    builder.setHeader("Content-Type", RequestFactory.JSON_CONTENT_TYPE_UTF8);
-    builder.setHeader("pageurl", Location.getHref());
-    
-    builder.setRequestData(ClientRequestHelper.getRequestString(((AbstractRequest<?, ?>) requestObject).getRequestData().getRequestMap(
-        ((AbstractRequest<?, ?>) requestObject).deltaValueStore.toJson())));
-    builder.setCallback(new RequestCallback() {
-
-      public void onError(Request request, Throwable exception) {
-        postRequestEvent(State.RECEIVED, null);
-        wireLogger.log(Level.SEVERE, SERVER_ERROR, exception);
+  public void fire(RequestObject<?> requestObject) {
+    final AbstractRequest<?, ?> abstractRequest = (AbstractRequest<?, ?>) requestObject;
+    RequestData requestData = ((AbstractRequest<?, ?>) requestObject).getRequestData();
+    Map<String, String> requestMap = requestData.getRequestMap(abstractRequest.deltaValueStore.toJson());
+    String payload = ClientRequestHelper.getRequestString(requestMap);
+    transport.send(payload, new RequestTransport.Receiver() {
+      public void onFailure(String message) {
+        abstractRequest.receiver.onFailure(new ServerFailure(message, null,
+            null));
       }
 
-      public void onResponseReceived(Request request, Response response) {
-        wireLogger.finest("Response received");
-        try {
-          if (200 == response.getStatusCode()) {
-            String text = response.getText();
-            ((AbstractRequest<?, ?>) requestObject).handleResponseText(text);
-          } else if (Response.SC_UNAUTHORIZED == response.getStatusCode()) {
-            wireLogger.finest("Need to log in");
-          } else if (response.getStatusCode() > 0) {
-            // During the redirection for logging in, we get a response with no
-            // status code, but it's not an error, so we only log errors with
-            // bad status codes here.
-            wireLogger.severe(SERVER_ERROR + " " + response.getStatusCode()
-                + " " + response.getText());
-          }
-        } finally {
-          postRequestEvent(State.RECEIVED, response);
-        }
+      public void onSuccess(String payload) {
+        abstractRequest.handleResponseText(payload);
       }
     });
-
-    try {
-      wireLogger.finest("Sending fire request");
-      builder.send();
-      postRequestEvent(State.SENT, null);
-    } catch (RequestException e) {
-      wireLogger.log(Level.SEVERE, SERVER_ERROR + " (" + e.getMessage() + ")",
-          e);
-    }
   }
 
   public Class<? extends EntityProxy> getClass(EntityProxy proxy) {
     return ((ProxyImpl) proxy).getSchema().getProxyClass();
   }
 
+  public RequestTransport getRequestTransport() {
+    return transport;
+  }
+
   public abstract ProxySchema<?> getSchema(String token);
 
   public String getWireFormat(EntityProxyId proxyId) {
@@ -173,19 +139,22 @@
       // search for the datastore id for this futureId.
       Long datastoreId = (Long) futureToDatastoreMap.get(id);
       if (datastoreId == null) {
-        throw new IllegalArgumentException("Cannot call find on a proxyId before persisting");
+        throw new IllegalArgumentException(
+            "Cannot call find on a proxyId before persisting");
       }
       id = datastoreId;
     }
     return ProxyImpl.getWireFormatId(id, NOT_FUTURE, proxyIdImpl.schema);
   }
 
-  /**
-   * @param eventBus
-   */
   public void init(EventBus eventBus) {
+    init(eventBus, new DefaultRequestTransport(eventBus));
+  }
+
+  public void init(EventBus eventBus, RequestTransport transport) {
     this.valueStore = new ValueStoreJsonImpl();
     this.eventBus = eventBus;
+    this.transport = transport;
     logger.fine("Successfully initialized RequestFactory");
   }
 
@@ -200,7 +169,7 @@
     }
     return schema.getProxyClass();
   }
-  
+
   /**
    * TODO(amitmanjhi): remove this method, use getProxyId instead.
    */
@@ -229,7 +198,8 @@
     return schema.create(ProxyJsoImpl.create(id, -1, schema, this));
   }
 
-  protected EntityProxyId getProxyId(String token, ProxyToTypeMap recordToTypeMap) {
+  protected EntityProxyId getProxyId(String token,
+      ProxyToTypeMap recordToTypeMap) {
     String[] bits = token.split(EntityProxyIdImpl.SEPARATOR);
     if (bits.length != 2) {
       return null;
@@ -291,8 +261,4 @@
         schema, this);
     return schema.create(newRecord, IS_FUTURE);
   }
-
-  private void postRequestEvent(State received, Response response) {
-    eventBus.fireEvent(new RequestEvent(received, response));
-  }
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
index c7ef713..0b25860 100644
--- a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
@@ -86,7 +86,13 @@
   String getToken(EntityProxy proxy);
 
   /**
-   * Start this request factory.
+   * Start this request factory with a
+   * {@link com.google.gwt.requestfactory.client.DefaultRequestTransport}.
    */
   void init(EventBus eventBus);
+
+  /**
+   * Start this request factory with a user-provided transport.
+   */
+  void init(EventBus eventBus, RequestTransport transport);
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/RequestTransport.java b/user/src/com/google/gwt/requestfactory/shared/RequestTransport.java
new file mode 100644
index 0000000..f184f08
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/RequestTransport.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * Abstracts the mechanism by which a RequestFactory instance transmits its
+ * payload to the backend.
+ * 
+ * @see com.google.gwt.requestfactory.client.DefaultRequestTransport
+ */
+public interface RequestTransport {
+  /**
+   * A callback interface.
+   */
+  public interface Receiver {
+    void onSuccess(String payload);
+
+    void onFailure(String message);
+  }
+
+  /**
+   * Called by the RequestFactory implementation.
+   */
+  void send(String payload, Receiver receiver);
+}