Update to r4917, which has GWT.runAsync download
code via XHR instead of script tags.  This version
deals with two browser quirks that r4917 did
not: it avoids no-argument xhr.send(), and it
does not assume window.eval will run in the
scope of "window".

Review by: bobv



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@4983 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
index a02d487..b63b22f 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
@@ -155,13 +155,34 @@
     out.newlineOpt();
     if (supportRunAsync) {
       out.print("function __gwtStartLoadingFragment(frag) {");
-      out.newlineOpt();
       out.indentIn();
-      out.print("  var script = document.createElement('script');");
       out.newlineOpt();
-      out.print("  script.src = '" + FRAGMENT_SUBDIR + "/" + strongName
-          + "/' + frag + '" + FRAGMENT_EXTENSION + "';");
-      out.print("  document.getElementsByTagName('head').item(0).appendChild(script);");
+      out.print("  return $moduleBase + '" + FRAGMENT_SUBDIR
+          + "/'  + $strongName + '/' + frag + '" + FRAGMENT_EXTENSION + "';");
+      out.indentOut();
+      out.newlineOpt();
+      out.print("};");
+      out.newlineOpt();
+      out.print("function __gwtInstallCode(code) {");
+      /*
+       * Use a script tag on all platforms, for simplicity. It would be cleaner
+       * to use window.eval, but at the time of writing that only reliably works
+       * on Firefox. It would also be cleaner to use window.execScript on
+       * platforms that support it (IE and Chrome). However, trying this causes
+       * IE 6 (and possibly others) to emit "error 80020101", apparently due to
+       * something objectionable in the compiler's output JavaScript.
+       */
+      out.indentIn();
+      out.newlineOpt();
+      out.print("var head = document.getElementsByTagName('head').item(0);");
+      out.newlineOpt();
+      out.print("var script = document.createElement('script');");
+      out.newlineOpt();
+      out.print("script.type = 'text/javascript';");
+      out.newlineOpt();
+      out.print("script.text = code;");
+      out.newlineOpt();
+      out.print("head.appendChild(script);");
       out.indentOut();
       out.newlineOpt();
       out.print("};");
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
index ea097c9..c5344bd 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
@@ -180,7 +180,13 @@
     srcWriter.println("}");
     srcWriter.println("if (!loading) {");
     srcWriter.println("loading = true;");
-    srcWriter.println("AsyncFragmentLoader.inject(" + entryNumber + ");");
+    srcWriter.println("AsyncFragmentLoader.inject(" + entryNumber + ",");
+    srcWriter.println("  new AsyncFragmentLoader.LoadErrorHandler() {");
+    srcWriter.println("    public void loadFailed(Throwable reason) {");
+    srcWriter.println("      loading = false;");
+    srcWriter.println("      runCallbackOnFailures(reason);");
+    srcWriter.println("    }");
+    srcWriter.println("  });");
     srcWriter.println("}");
     srcWriter.println("}");
   }
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java
index ee1f6a2..bc80c03 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/ContentWidget.java
@@ -120,6 +120,21 @@
   private HTML styleWidget = null;
 
   /**
+   * Whether the demo widget has been initialized.
+   */
+  private boolean widgetInitialized;
+
+  /**
+   * Whether the demo widget is (asynchronously) initializing.
+   */
+  private boolean widgetInitializing;
+
+  /**
+   * A vertical panel that holds the demo widget once it is initialized.
+   */
+  private VerticalPanel widgetVpanel;
+
+  /**
    * Constructor.
    * 
    * @param constants the constants
@@ -141,6 +156,12 @@
     deckPanel.add(w);
   }
 
+  @Override
+  public void ensureWidget() {
+    super.ensureWidget();
+    ensureWidgetInitialized(widgetVpanel);
+  }
+
   /**
    * Get the description of this example.
    * 
@@ -255,6 +276,7 @@
    * Initialize this widget by creating the elements that should be added to the
    * page.
    */
+  @Override
   protected final Widget createWidget() {
     deckPanel = new DeckPanel();
 
@@ -264,18 +286,18 @@
     tabBar.addSelectionHandler(this);
 
     // Create a container for the main example
-    final VerticalPanel vPanel = new VerticalPanel();
-    add(vPanel, constants.contentWidgetExample());
+    widgetVpanel = new VerticalPanel();
+    add(widgetVpanel, constants.contentWidgetExample());
 
     // Add the name
     HTML nameWidget = new HTML(getName());
     nameWidget.setStyleName(DEFAULT_STYLE_NAME + "-name");
-    vPanel.add(nameWidget);
+    widgetVpanel.add(nameWidget);
 
     // Add the description
     HTML descWidget = new HTML(getDescription());
     descWidget.setStyleName(DEFAULT_STYLE_NAME + "-description");
-    vPanel.add(descWidget);
+    widgetVpanel.add(descWidget);
 
     // Add source code tab
     if (hasSource()) {
@@ -292,22 +314,6 @@
       add(styleWidget, constants.contentWidgetStyle());
     }
 
-    asyncOnInitialize(new AsyncCallback<Widget>() {
-
-      public void onFailure(Throwable caught) {
-        Window.alert("exception: " + caught);
-      }
-
-      public void onSuccess(Widget result) {
-        // Initialize the showcase widget (if any) and add it to the page
-        Widget widget = result;
-        if (widget != null) {
-          vPanel.add(widget);
-        }
-        onInitializeComplete();
-      }
-    });
-
     return deckPanel;
   }
 
@@ -367,4 +373,34 @@
       realCallback.onError(request, e);
     }
   }
+
+  /**
+   * Ensure that the demo widget has been initialized. Note that initialization
+   * can fail if there is a network failure.
+   */
+  private void ensureWidgetInitialized(final VerticalPanel vPanel) {
+    if (widgetInitializing || widgetInitialized) {
+      return;
+    }
+
+    widgetInitializing = true;
+
+    asyncOnInitialize(new AsyncCallback<Widget>() {
+      public void onFailure(Throwable reason) {
+        widgetInitializing = false;
+        Window.alert("Failed to download code for this widget (" + reason + ")");
+      }
+
+      public void onSuccess(Widget result) {
+        widgetInitializing = false;
+        widgetInitialized = true;
+
+        Widget widget = result;
+        if (widget != null) {
+          vPanel.add(widget);
+        }
+        onInitializeComplete();
+      }
+    });
+  }
 }
diff --git a/user/src/com/google/gwt/core/client/AsyncFragmentLoader.java b/user/src/com/google/gwt/core/client/AsyncFragmentLoader.java
index 829cfc6..21b4d08 100644
--- a/user/src/com/google/gwt/core/client/AsyncFragmentLoader.java
+++ b/user/src/com/google/gwt/core/client/AsyncFragmentLoader.java
@@ -15,7 +15,12 @@
  */
 package com.google.gwt.core.client;
 
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+import com.google.gwt.xhr.client.XMLHttpRequest;
+
+import java.util.ArrayList;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Queue;
 
 /**
@@ -41,14 +46,32 @@
  * </ul>
  * 
  * <p>
- * Different linkers have different requirements about how the code is
- * downloaded and installed. Thus, when it is time to actually download the
- * code, this class defers to a JavaScript function named
- * <code>__gwtStartLoadingFragment</code>. Linkers must arrange that a suitable
- * <code>__gwtStartLoadingFragment</code> function is in scope.
+ * Since the precise way to load code depends on the linker, each linker should
+ * provide functions for fragment loading for any compilation that includes more
+ * than one fragment. Linkers should always provide a function
+ * <code>__gwtStartLoadingFragment</code>. This function is called by
+ * AsyncFragmentLoader with an integer fragment number that needs to be
+ * downloaded. If the mechanism for loading the contents of fragments is
+ * provided by the linker, this function should return <code>null</code> or
+ * <code>undefined</code>.
+ * </p>
+ * <p>
+ * Alternatively, the function can return a URL designating from where the code
+ * for the requested fragment can be downloaded. In that case, the linker should
+ * also provide a function <code>__gwtInstallCode</code> for actually installing
+ * the code once it is downloaded. That function will be passed the loaded code
+ * once it has been downloaded.
+ * </p>
  */
 public class AsyncFragmentLoader {
   /**
+   * An interface for handlers of load errors.
+   */
+  public static interface LoadErrorHandler {
+    void loadFailed(Throwable reason);
+  }
+
+  /**
    * Labels used for runAsync lightweight metrics.
    */
   public static class LwmLabels {
@@ -68,6 +91,73 @@
   }
 
   /**
+   * Handles a failure to download a base fragment.
+   */
+  private static class BaseDownloadFailed implements LoadErrorHandler {
+    private final LoadErrorHandler chainedHandler;
+
+    public BaseDownloadFailed(LoadErrorHandler chainedHandler) {
+      this.chainedHandler = chainedHandler;
+    }
+
+    public void loadFailed(Throwable reason) {
+      baseLoading = false;
+      chainedHandler.loadFailed(reason);
+    }
+  }
+
+  /**
+   * An exception indicating than at HTTP download failed.
+   */
+  private static class HttpDownloadFailure extends RuntimeException {
+    private final int statusCode;
+
+    public HttpDownloadFailure(int statusCode) {
+      super("HTTP download failed with status " + statusCode);
+      this.statusCode = statusCode;
+    }
+    
+    public int getStatusCode() {
+      return statusCode;
+    }
+  }
+
+  /**
+   * Handles a failure to download a leftovers fragment.
+   */
+  private static class LeftoversDownloadFailed implements LoadErrorHandler {
+    public void loadFailed(Throwable reason) {
+      leftoversLoading = false;
+
+      /*
+       * Cancel all other pending downloads. If any exception is thrown while
+       * cancelling any of them, throw only the last one.
+       */
+      RuntimeException lastException = null;
+
+      assert waitingForLeftovers.size() == waitingForLeftoversErrorHandlers.size();
+
+      // Copy the list in case a handler makes another runAsync call
+      List<LoadErrorHandler> handlersToRun = new ArrayList<LoadErrorHandler>(
+          waitingForLeftoversErrorHandlers);
+      waitingForLeftoversErrorHandlers.clear();
+      waitingForLeftovers.clear();
+
+      for (LoadErrorHandler handler : handlersToRun) {
+        try {
+          handler.loadFailed(reason);
+        } catch (RuntimeException e) {
+          lastException = e;
+        }
+      }
+
+      if (lastException != null) {
+        throw lastException;
+      }
+    }
+  }
+
+  /**
    * The first entry point reached after the program started.
    */
   private static int base = -1;
@@ -77,6 +167,10 @@
    */
   private static boolean baseLoading = false;
 
+  private static final String HTTP_GET = "GET";
+
+  private static final int HTTP_STATUS_OK = 200;
+
   /**
    * Whether the leftovers fragment has loaded yet.
    */
@@ -101,6 +195,11 @@
   private static Queue<Integer> waitingForLeftovers = new LinkedList<Integer>();
 
   /**
+   * Error handlers for the above queue.
+   */
+  private static Queue<LoadErrorHandler> waitingForLeftoversErrorHandlers = new LinkedList<LoadErrorHandler>();
+
+  /**
    * Inform the loader that the code for an entry point has now finished
    * loading.
    * 
@@ -117,10 +216,7 @@
       baseLoading = false;
 
       // Go ahead and download the appropriate leftovers fragment
-      leftoversLoading = true;
-      logEventProgress(LwmLabels.LEFTOVERS_DOWNLOAD, LwmLabels.BEGIN,
-          leftoversFragmentNumber(), null);
-      startLoadingFragment(leftoversFragmentNumber());
+      startLoadingLeftovers();
     }
   }
 
@@ -129,7 +225,7 @@
    * 
    * @param splitPoint the fragment to load
    */
-  public static void inject(int splitPoint) {
+  public static void inject(int splitPoint, LoadErrorHandler loadErrorHandler) {
     if (leftoversLoaded) {
       /*
        * A base and a leftovers fragment have loaded. Load an exclusively live
@@ -137,7 +233,7 @@
        */
       logEventProgress(LwmLabels.downloadGroup(splitPoint), LwmLabels.BEGIN,
           splitPoint, null);
-      startLoadingFragment(splitPoint);
+      startLoadingFragment(splitPoint, loadErrorHandler);
       return;
     }
 
@@ -145,7 +241,17 @@
       /*
        * Wait until the leftovers fragment has loaded before loading this one.
        */
+      assert (waitingForLeftovers.size() == waitingForLeftoversErrorHandlers.size());
       waitingForLeftovers.add(splitPoint);
+      waitingForLeftoversErrorHandlers.add(loadErrorHandler);
+
+      /*
+       * Also, restart the leftovers download if it previously failed.
+       */
+      if (!leftoversLoading) {
+        startLoadingLeftovers();
+      }
+
       return;
     }
 
@@ -153,7 +259,8 @@
     baseLoading = true;
     logEventProgress(LwmLabels.downloadGroup(splitPoint), LwmLabels.BEGIN,
         baseFragmentNumber(splitPoint), null);
-    startLoadingFragment(baseFragmentNumber(splitPoint));
+    startLoadingFragment(baseFragmentNumber(splitPoint),
+        new BaseDownloadFailed(loadErrorHandler));
   }
 
   /**
@@ -165,8 +272,10 @@
     logEventProgress(LwmLabels.LEFTOVERS_DOWNLOAD, LwmLabels.END,
         leftoversFragmentNumber(), null);
 
+    assert (waitingForLeftovers.size() == waitingForLeftoversErrorHandlers.size());
     while (!waitingForLeftovers.isEmpty()) {
-      inject(waitingForLeftovers.remove());
+      inject(waitingForLeftovers.remove(),
+          waitingForLeftoversErrorHandlers.remove());
     }
   }
 
@@ -203,8 +312,18 @@
     return evt;
   }-*/;
 
-  private static native void gwtStartLoadingFragment(int fragment) /*-{
-    __gwtStartLoadingFragment(fragment);
+  /**
+   * Use the linker-supplied __gwtStartLoadingFragment function. It should
+   * either start the download and return null or undefined, or it should return
+   * a URL that should be downloaded to get the code. If it starts the download
+   * itself, it can synchronously load it, e.g. from cache, if that makes sense.
+   */
+  private static native String gwtStartLoadingFragment(int fragment) /*-{
+    return __gwtStartLoadingFragment(fragment);
+  }-*/;
+
+  private static native void installCode(String text) /*-{
+    __gwtInstallCode(text);
   }-*/;
 
   private static native boolean isStatsAvailable() /*-{
@@ -232,8 +351,43 @@
         && stats(createStatsEvent(eventGroup, type, fragment, size));
   }
 
-  private static void startLoadingFragment(int fragment) {
-    gwtStartLoadingFragment(fragment);
+  private static void startLoadingFragment(int fragment,
+      final LoadErrorHandler loadErrorHandler) {
+    String fragmentUrl = gwtStartLoadingFragment(fragment);
+
+    if (fragmentUrl != null) {
+      // use XHR
+      final XMLHttpRequest xhr = XMLHttpRequest.create();
+
+      xhr.open(HTTP_GET, fragmentUrl);
+
+      xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
+        public void onReadyStateChange(XMLHttpRequest xhr) {
+          if (xhr.getReadyState() == XMLHttpRequest.DONE) {
+            xhr.clearOnReadyStateChange();
+            if (xhr.getStatus() == HTTP_STATUS_OK) {
+              try {
+                installCode(xhr.getResponseText());
+              } catch (RuntimeException e) {
+                loadErrorHandler.loadFailed(e);
+              }
+            } else {
+              loadErrorHandler.loadFailed(new HttpDownloadFailure(xhr.getStatus()));
+            }
+          }
+        }
+      });
+
+      xhr.send();
+    }
+  }
+
+  private static void startLoadingLeftovers() {
+    leftoversLoading = true;
+    logEventProgress(LwmLabels.LEFTOVERS_DOWNLOAD, LwmLabels.BEGIN,
+        leftoversFragmentNumber(), null);
+    startLoadingFragment(leftoversFragmentNumber(),
+        new LeftoversDownloadFailed());
   }
 
   /**
diff --git a/user/src/com/google/gwt/core/client/RunAsyncCallback.java b/user/src/com/google/gwt/core/client/RunAsyncCallback.java
index 4c76817..0ec253a 100644
--- a/user/src/com/google/gwt/core/client/RunAsyncCallback.java
+++ b/user/src/com/google/gwt/core/client/RunAsyncCallback.java
@@ -22,9 +22,9 @@
 public interface RunAsyncCallback {
   /**
    * Called when, for some reason, the necessary code cannot be loaded. For
-   * example, the user might no longer be on the network.
+   * example, the web browser might no longer have network access.
    */
-  void onFailure(Throwable caught);
+  void onFailure(Throwable reason);
 
   /**
    * Called once the necessary code for it has been loaded.
diff --git a/user/src/com/google/gwt/xhr/client/XMLHttpRequest.java b/user/src/com/google/gwt/xhr/client/XMLHttpRequest.java
index ae63a6d..c7ca653 100644
--- a/user/src/com/google/gwt/xhr/client/XMLHttpRequest.java
+++ b/user/src/com/google/gwt/xhr/client/XMLHttpRequest.java
@@ -215,16 +215,16 @@
   }-*/;
 
   /**
-   * Initiates a request.
-   * 
-   * @see http://www.w3.org/TR/XMLHttpRequest/#send
+   * Initiates a request with no request data. This simply calls
+   * {@link #send(String)} with <code>null</code> as an argument, because the
+   * no-argument <code>send()</code> method is unavailable on Firefox.
    */
-  public final native void send() /*-{
-    this.send();
-  }-*/;
+  public final void send() {
+    send(null);
+  }
 
   /**
-   * Initiates a request with data.
+   * Initiates a request with data.  If there is no data, specify null.
    * 
    * @param requestData the data to be sent with the request
    * @see http://www.w3.org/TR/XMLHttpRequest/#send