Extend ScriptInjector to support removing the <script> tag after
loading a script from a URL, and rewrite ScriptTagLoadingStrategy to
use ScriptInjector.

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

Review by: skybrian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11389 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/core/client/ScriptInjector.java b/user/src/com/google/gwt/core/client/ScriptInjector.java
index 4c08242..d07ad4a 100644
--- a/user/src/com/google/gwt/core/client/ScriptInjector.java
+++ b/user/src/com/google/gwt/core/client/ScriptInjector.java
@@ -119,6 +119,7 @@
    */
   public static class FromUrl {
     private Callback<Void, Exception> callback;
+    private boolean removeTag = false;
     private final String scriptUrl;
     private JavaScriptObject window;
 
@@ -139,8 +140,8 @@
       assert doc != null;
       JavaScriptObject scriptElement = nativeMakeScriptElement(doc);
       assert scriptElement != null;
-      if (callback != null) {
-        attachListeners(scriptElement, callback);
+      if (callback != null || removeTag) {
+        attachListeners(scriptElement, callback, removeTag);
       }
       nativeSetSrc(scriptElement, scriptUrl);
       nativeAttachToHead(doc, scriptElement);
@@ -175,6 +176,19 @@
     }
 
     /**
+     * @param removeTag If true, remove the tag after the script finishes
+     *          loading. This shrinks the DOM, possibly at the expense of
+     *          readability if you are debugging javaScript.
+     *
+     *          Default value is {@code false}, but this may change in a future
+     *          release.
+     */
+    public FromUrl setRemoveTag(boolean removeTag) {
+      this.removeTag = removeTag;
+      return this;
+    }
+
+    /**
      * This call allows you to specify which DOM window object to install the
      * script tag in. To install into the Top level window call
      * 
@@ -243,20 +257,27 @@
    * @param callback callback that runs when the script is loaded and parsed.
    */
   private static native void attachListeners(JavaScriptObject scriptElement,
-      Callback<Void, Exception> callback) /*-{
+      Callback<Void, Exception> callback, boolean removeTag) /*-{
     function clearCallbacks() {
       scriptElement.onerror = scriptElement.onreadystatechange = scriptElement.onload = function() {
       };
+      if (removeTag) {
+        @com.google.gwt.core.client.ScriptInjector::nativeRemove(Lcom/google/gwt/core/client/JavaScriptObject;)(scriptElement);
+      }
     }
     scriptElement.onload = $entry(function() {
       clearCallbacks();
-      callback.@com.google.gwt.core.client.Callback::onSuccess(Ljava/lang/Object;)(null);
+      if (callback) {
+        callback.@com.google.gwt.core.client.Callback::onSuccess(Ljava/lang/Object;)(null);
+      }
     });
-    // or possibly more portable script_tag.addEventListener('error', function(){...}, true); 
+    // or possibly more portable script_tag.addEventListener('error', function(){...}, true);
     scriptElement.onerror = $entry(function() {
       clearCallbacks();
-      var ex = @com.google.gwt.core.client.CodeDownloadException::new(Ljava/lang/String;)("onerror() called.");
-      callback.@com.google.gwt.core.client.Callback::onFailure(Ljava/lang/Object;)(ex)
+      if (callback) {
+        var ex = @com.google.gwt.core.client.CodeDownloadException::new(Ljava/lang/String;)("onerror() called.");
+        callback.@com.google.gwt.core.client.Callback::onFailure(Ljava/lang/Object;)(ex);
+      }
     });
     scriptElement.onreadystatechange = $entry(function() {
       if (scriptElement.readyState == 'complete' || scriptElement.readyState == 'loaded') {
diff --git a/user/src/com/google/gwt/core/client/impl/ScriptTagLoadingStrategy.java b/user/src/com/google/gwt/core/client/impl/ScriptTagLoadingStrategy.java
index 6b2b97e..1e88bf5 100644
--- a/user/src/com/google/gwt/core/client/impl/ScriptTagLoadingStrategy.java
+++ b/user/src/com/google/gwt/core/client/impl/ScriptTagLoadingStrategy.java
@@ -15,7 +15,8 @@
  */
 package com.google.gwt.core.client.impl;
 
-import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Callback;
+import com.google.gwt.core.client.ScriptInjector;
 import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure;
 
 /**
@@ -23,10 +24,8 @@
  * therefore cross site compatible. Note that if this strategy is used, the
  * deferred fragments must be wrapped in a callback called runAsyncCallbackX()
  * where X is the fragment number.
- * 
+ *
  * This is the default strategy for the CrossSiteIframeLinker.
- * 
- * TODO(unnurg): Try to use the ScriptInjector here
  */
 
 public class ScriptTagLoadingStrategy extends LoadingStrategyBase {
@@ -37,96 +36,57 @@
   protected static class ScriptTagDownloadStrategy implements DownloadStrategy {
     @Override
     public void tryDownload(final RequestData request) {
-      int fragment = request.getFragment();
-      JavaScriptObject scriptTag = createScriptTag(request.getUrl());
-      setOnSuccess(fragment, onSuccess(fragment, scriptTag, request));
-      setOnFailure(scriptTag, onFailure(fragment, scriptTag, request));
-      installScriptTag(scriptTag);
+      setAsyncCallback(request.getFragment(), request);
+
+      ScriptInjector.fromUrl(request.getUrl()).setRemoveTag(true).setCallback(
+        new Callback<Void, Exception>() {
+          @Override
+          public void onFailure(Exception reason) {
+            cleanup(request);
+          }
+
+          @Override
+          public void onSuccess(Void result) {
+            cleanup(request);
+          }
+      }).inject();
     }
   }
-  
-  protected static void callOnLoadError(RequestData request) {
-    request.onLoadError(new HttpDownloadFailure(request.getUrl(), 404,
-      "Script Tag Failure - no status available"), true);
+
+  private static void asyncCallback(RequestData request, String code) {
+    boolean firstTimeCalled = clearAsyncCallback(request.getFragment());
+    if (firstTimeCalled) {
+      request.tryInstall(code);
+    }
   }
 
-  private static native boolean clearCallbacksAndRemoveTag(
-      int fragment, JavaScriptObject scriptTag) /*-{
-    if (scriptTag.parentNode == null) {
-      // onSuccess or onFailure must have already been called.
+  private static void cleanup(RequestData request) {
+    boolean neverCalled = clearAsyncCallback(request.getFragment());
+    if (neverCalled) {
+      request.onLoadError(new HttpDownloadFailure(request.getUrl(), 404,
+        "Script Tag Failure - no status available"), true);
+    }
+  }
+
+  /**
+   * Returns true if the callback existed.
+   */
+  private static native boolean clearAsyncCallback(int fragment) /*-{
+    if (!__gwtModuleFunction['runAsyncCallback' + fragment]) {
       return false;
     }
-    var head = document.getElementsByTagName('head').item(0);
-    @com.google.gwt.core.client.impl.ScriptTagLoadingStrategy::clearOnSuccess(I)(fragment);
-    @com.google.gwt.core.client.impl.ScriptTagLoadingStrategy::clearOnFailure(Lcom/google/gwt/core/client/JavaScriptObject;)(
-      scriptTag);
-    head.removeChild(scriptTag);
+    delete __gwtModuleFunction['runAsyncCallback' + fragment];
     return true;
   }-*/;
-  
-  private static native void clearOnFailure(JavaScriptObject scriptTag) /*-{
-    scriptTag.onerror = scriptTag.onload = scriptTag.onreadystatechange = function(){};
-  }-*/;
 
-  private static native void clearOnSuccess(int fragment) /*-{
-    delete __gwtModuleFunction['runAsyncCallback' + fragment];
-  }-*/;
-  
-  private static native JavaScriptObject createScriptTag(String url) /*-{
-    var head = document.getElementsByTagName('head').item(0);
-    var scriptTag = document.createElement('script');
-    scriptTag.src = url;
-    return scriptTag;
-  }-*/;
-
-  private static native void installScriptTag(JavaScriptObject scriptTag) /*-{
-    var head = document.getElementsByTagName('head').item(0);
-    head.appendChild(scriptTag);
-  }-*/;
-
-  private static native JavaScriptObject onFailure(
-      int fragment, JavaScriptObject scriptTag, RequestData request) /*-{
-    return function() {
-      if (@com.google.gwt.core.client.impl.ScriptTagLoadingStrategy::clearCallbacksAndRemoveTag(ILcom/google/gwt/core/client/JavaScriptObject;)(
-        fragment, scriptTag)) {
-        @com.google.gwt.core.client.impl.ScriptTagLoadingStrategy::callOnLoadError(Lcom/google/gwt/core/client/impl/LoadingStrategyBase$RequestData;)(
-          request)
-      }
-    }
-  }-*/;
-  
-  private static native JavaScriptObject onSuccess(int fragment,
-      JavaScriptObject scriptTag, RequestData request) /*-{
-    return function(code, instance) {
-      if (@com.google.gwt.core.client.impl.ScriptTagLoadingStrategy::clearCallbacksAndRemoveTag(ILcom/google/gwt/core/client/JavaScriptObject;)(
-        fragment, scriptTag)) {
-        request.@com.google.gwt.core.client.impl.LoadingStrategyBase.RequestData::tryInstall(Ljava/lang/String;)(
-          code);
-      }
-    }
-  }-*/;
-  
-  private static native void setOnFailure(JavaScriptObject script,
-      JavaScriptObject callback) /*-{
-    script.onerror = function() {
-      callback();
-    }
-    script.onload = function() {
-      callback();
-    }
-    script.onreadystatechange = function () {
-      if (script.readyState == 'loaded' || script.readyState == 'complete') {
-        script.onreadystatechange = function () { }
-        callback();
-      }
-    }
-  }-*/;
-  
-  private static native void setOnSuccess(int fragment, JavaScriptObject callback) /*-{
-    __gwtModuleFunction['runAsyncCallback'+fragment] = callback;
+  private static native void setAsyncCallback(int fragment, RequestData request) /*-{
+    __gwtModuleFunction['runAsyncCallback' + fragment] = $entry(function(code, instance) {
+      @com.google.gwt.core.client.impl.ScriptTagLoadingStrategy::asyncCallback(Lcom/google/gwt/core/client/impl/LoadingStrategyBase$RequestData;Ljava/lang/String;)(
+        request, code);
+    });
   }-*/;
 
   public ScriptTagLoadingStrategy() {
     super(new ScriptTagDownloadStrategy());
-  } 
+  }
 }
diff --git a/user/test/com/google/gwt/core/client/ScriptInjectorTest.java b/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
index 7596688..f245a1c 100644
--- a/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
+++ b/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
@@ -204,7 +204,8 @@
     this.delayTestFinish(TEST_DELAY);
     final String scriptUrl = "script_injector_test4.js";
     assertFalse(nativeTest4Worked());
-    final JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).inject();
+    final JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false).inject();
 
     // We'll check using a callback in another test. This test will poll to see
     // that the script had an effect.
@@ -242,8 +243,8 @@
     delayTestFinish(TEST_DELAY);
     final String scriptUrl = "script_injector_test5.js";
     assertFalse(nativeTest5Worked());
-    JavaScriptObject injectedElement =
-        ScriptInjector.fromUrl(scriptUrl).setCallback(new Callback<Void, Exception>() {
+    JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false)
+        .setCallback(new Callback<Void, Exception>() {
           @Override
           public void onFailure(Exception reason) {
             assertNotNull(reason);
@@ -272,8 +273,8 @@
   public void testInjectUrlTopWindow() {
     final String scriptUrl = "script_injector_test6.js";
     assertFalse(nativeTest6Worked());
-    JavaScriptObject injectedElement =
-        ScriptInjector.fromUrl(scriptUrl).setWindow(ScriptInjector.TOP_WINDOW).inject();
+    JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false)
+        .setWindow(ScriptInjector.TOP_WINDOW).inject();
     // We'll check using a callback in another test. This test will poll to see
     // that the script had an effect.
     Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
@@ -309,8 +310,8 @@
     delayTestFinish(TEST_DELAY);
     final String scriptUrl = "script_injector_test7.js";
     assertFalse(nativeTest7Worked());
-    JavaScriptObject injectedElement =
-        ScriptInjector.fromUrl(scriptUrl).setWindow(ScriptInjector.TOP_WINDOW).setCallback(
+    JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false)
+        .setWindow(ScriptInjector.TOP_WINDOW).setCallback(
             new Callback<Void, Exception>() {
 
               @Override