Cherry pick r10407 into GWT 2.4 release branch


git-svn-id: https://google-web-toolkit.googlecode.com/svn/releases/2.4@10409 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
new file mode 100644
index 0000000..4c08242
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/ScriptInjector.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2011 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.core.client;
+
+/*
+ * Design note: This class intentionally does not use the GWT DOM wrappers so
+ * that this code can pull in as few dependencies as possible and live in the
+ * Core module.
+ */
+
+/**
+ * Dynamically create a script tag and attach it to the DOM.
+ * 
+ * Usage with script as local string:
+ * <p>
+ * 
+ * <pre>
+ *   String scriptBody = "var foo = ...";
+ *   ScriptInjector.fromString(scriptBody).inject();
+ * </pre>
+ * <p>
+ * Usage with script loaded as URL:
+ * <p>
+ * 
+ * <pre>
+ *   ScriptInjector.fromUrl("http://example.com/foo.js").setCallback(
+ *     new Callback<Void, Exception>() {
+ *        public void onFailure(Exception reason) {
+ *          Window.alert("Script load failed.");
+ *        }
+ *        public void onSuccess(Void result) {
+ *          Window.alert("Script load success.");
+ *        }
+ *     }).inject();
+ * </pre>
+ * 
+ * 
+ */
+public class ScriptInjector {
+
+  /**
+   * Builder for directly injecting a script body into the DOM.
+   */
+  public static class FromString {
+    private boolean removeTag = true;
+    private final String scriptBody;
+    private JavaScriptObject window;
+
+    /**
+     * @param scriptBody The script text to install into the document.
+     */
+    public FromString(String scriptBody) {
+      this.scriptBody = scriptBody;
+    }
+
+    /**
+     * Injects a script into the DOM. The JavaScript is evaluated and will be
+     * available immediately when this call returns.
+     * 
+     * By default, the script is installed in the same window that the GWT code
+     * is installed in.
+     * 
+     * @return the script element created for the injection. Note that it may be
+     *         removed from the DOM.
+     */
+    public JavaScriptObject inject() {
+      JavaScriptObject wnd = (window == null) ? nativeDefaultWindow() : window;
+      assert wnd != null;
+      JavaScriptObject doc = nativeGetDocument(wnd);
+      assert doc != null;
+      JavaScriptObject scriptElement = nativeMakeScriptElement(doc);
+      assert scriptElement != null;
+      nativeSetText(scriptElement, scriptBody);
+      nativeAttachToHead(doc, scriptElement);
+      if (removeTag) {
+        nativeRemove(scriptElement);
+      }
+      return scriptElement;
+    }
+
+    /**
+     * @param removeTag If true, remove the tag immediately after injecting the
+     *          source. This shrinks the DOM, possibly at the expense of
+     *          readability if you are debugging javaScript.
+     * 
+     *          Default value is {@code true}.
+     */
+    public FromString setRemoveTag(boolean removeTag) {
+      this.removeTag = removeTag;
+      return this;
+    }
+
+    /**
+     * @param window Specify which window to use to install the script. If not
+     *          specified, the top current window GWT is loaded in is used.
+     */
+    public FromString setWindow(JavaScriptObject window) {
+      this.window = window;
+      return this;
+    }
+  }
+
+  /**
+   * Build an injection call for adding a script by URL.
+   */
+  public static class FromUrl {
+    private Callback<Void, Exception> callback;
+    private final String scriptUrl;
+    private JavaScriptObject window;
+
+    private FromUrl(String scriptUrl) {
+      this.scriptUrl = scriptUrl;
+    }
+
+    /**
+     * Injects an external JavaScript reference into the document and optionally
+     * calls a callback when it finishes loading.
+     * 
+     * @return the script element created for the injection.
+     */
+    public JavaScriptObject inject() {
+      JavaScriptObject wnd = (window == null) ? nativeDefaultWindow() : window;
+      assert wnd != null;
+      JavaScriptObject doc = nativeGetDocument(wnd);
+      assert doc != null;
+      JavaScriptObject scriptElement = nativeMakeScriptElement(doc);
+      assert scriptElement != null;
+      if (callback != null) {
+        attachListeners(scriptElement, callback);
+      }
+      nativeSetSrc(scriptElement, scriptUrl);
+      nativeAttachToHead(doc, scriptElement);
+      return scriptElement;
+    }
+
+    /**
+     * Specify a callback to be invoked when the script is loaded or loading
+     * encounters an error.
+     * <p>
+     * <b>Warning:</b> This class <b>does not</b> control whether or not a URL
+     * has already been injected into the document. The client of this class has
+     * the responsibility of keeping score of the injected JavaScript files.
+     * <p>
+     * <b>Known bugs:</b>  This class uses the script tag's <code>onerror()
+     * </code> callback to attempt to invoke onFailure() if the 
+     * browser detects a load failure.  This is not reliable on all browsers 
+     * (Doesn't work on IE or Safari 3 or less).
+     * <p>
+     * On Safari version 3 and prior, the onSuccess() callback may be invoked
+     * even when the load of a page fails.  
+     * <p>
+     * To support failure notification on IE and older browsers, you should 
+     * check some side effect of the script (such as a defined function)
+     * to see if loading the script worked and include timeout logic.
+     * 
+     * @param callback callback that gets invoked asynchronously.
+     */
+    public FromUrl setCallback(Callback<Void, Exception> callback) {
+      this.callback = callback;
+      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
+     * 
+     * <code>
+     *   builder.setWindow(ScriptInjector.TOP_WINDOW);
+     * </code>
+     * 
+     * @param window Specifies which window to install in.
+     */
+    public FromUrl setWindow(JavaScriptObject window) {
+      this.window = window;
+      return this;
+    }
+  }
+
+  /**
+   * Returns the top level window object. Use this to inject a script so that
+   * global variable references are available under <code>$wnd</code> in JSNI
+   * access.
+   * <p>
+   * Note that if your GWT app is loaded from a different domain than the top
+   * window, you may not be able to add a script element to the top window.
+   */
+  public static final JavaScriptObject TOP_WINDOW = nativeTopWindow();
+
+  /**
+   * Build an injection call for directly setting the script text in the DOM.
+   * 
+   * @param scriptBody the script text to be injected and immediately executed.
+   */
+  public static FromString fromString(String scriptBody) {
+    return new FromString(scriptBody);
+  }
+
+  /**
+   * Build an injection call for adding a script by URL.
+   * 
+   * @param scriptUrl URL of the JavaScript to be injected.
+   */
+  public static FromUrl fromUrl(String scriptUrl) {
+    return new FromUrl(scriptUrl);
+  }
+
+  /**
+   * Attaches event handlers to a script DOM element that will run just once a
+   * callback when it gets successfully loaded.
+   * <p>
+   * <b>IE Notes:</b> Internet Explorer calls {@code onreadystatechanged}
+   * several times while varying the {@code readyState} property: in theory,
+   * {@code "complete"} means the content is loaded, parsed and ready to be
+   * used, but in practice, {@code "complete"} happens when the JS file was
+   * already cached, and {@code "loaded"} happens when it was transferred over
+   * the network. Other browsers just call the {@code onload} event handler. To
+   * ensure the callback will be called at most once, we clear out both event
+   * handlers when the callback runs for the first time. More info at the <a
+   * href="http://www.phpied.com/javascript-include-ready-onload/">phpied.com
+   * blog</a>.
+   * <p>
+   * In IE, do not trust the "order" of {@code readyState} values. For instance,
+   * in IE 8 running in Vista, if the JS file is cached, only {@code "complete"}
+   * will happen, but if the file has to be downloaded, {@code "loaded"} can
+   * fire in parallel with {@code "loading"}.
+   * 
+   * 
+   * @param scriptElement element to which the event handlers will be attached
+   * @param callback callback that runs when the script is loaded and parsed.
+   */
+  private static native void attachListeners(JavaScriptObject scriptElement,
+      Callback<Void, Exception> callback) /*-{
+    function clearCallbacks() {
+      scriptElement.onerror = scriptElement.onreadystatechange = scriptElement.onload = function() {
+      };
+    }
+    scriptElement.onload = $entry(function() {
+      clearCallbacks();
+      callback.@com.google.gwt.core.client.Callback::onSuccess(Ljava/lang/Object;)(null);
+    });
+    // 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)
+    });
+    scriptElement.onreadystatechange = $entry(function() {
+      if (scriptElement.readyState == 'complete' || scriptElement.readyState == 'loaded') {
+        scriptElement.onload();
+      }
+    });
+  }-*/;
+
+  private static native void nativeAttachToHead(JavaScriptObject doc, JavaScriptObject scriptElement) /*-{
+    doc.getElementsByTagName("head")[0].appendChild(scriptElement);
+  }-*/;
+
+  private static native JavaScriptObject nativeDefaultWindow() /*-{
+    return window;
+  }-*/;
+
+  private static native JavaScriptObject nativeGetDocument(JavaScriptObject wnd) /*-{
+    return wnd.document;
+  }-*/;
+
+  private static native JavaScriptObject nativeMakeScriptElement(JavaScriptObject doc) /*-{
+    var element = doc.createElement("script");
+    element.type = "text/javascript";
+    return element;
+  }-*/;
+
+  private static native void nativeRemove(JavaScriptObject scriptElement) /*-{
+    var p = scriptElement.parentNode;
+    p.removeChild(scriptElement);
+  }-*/;
+
+  private static native void nativeSetSrc(JavaScriptObject element, String url) /*-{
+    element.src = url;
+  }-*/;
+
+  private static native void nativeSetText(JavaScriptObject element, String scriptBody) /*-{
+    element.text = scriptBody;
+  }-*/;
+
+  private static native JavaScriptObject nativeTopWindow() /*-{
+    return $wnd;
+  }-*/;
+
+  /**
+   * Utility class - do not instantiate
+   */
+  private ScriptInjector() {
+  }
+}
diff --git a/user/test/com/google/gwt/core/CoreSuite.java b/user/test/com/google/gwt/core/CoreSuite.java
index 72bdc4b..293900f 100644
--- a/user/test/com/google/gwt/core/CoreSuite.java
+++ b/user/test/com/google/gwt/core/CoreSuite.java
@@ -21,6 +21,7 @@
 import com.google.gwt.core.client.JsArrayMixedTest;
 import com.google.gwt.core.client.JsArrayTest;
 import com.google.gwt.core.client.SchedulerTest;
+import com.google.gwt.core.client.ScriptInjectorTest;
 import com.google.gwt.core.client.impl.AsyncFragmentLoaderTest;
 import com.google.gwt.core.client.impl.SchedulerImplTest;
 import com.google.gwt.core.client.impl.StackTraceCreatorTest;
@@ -47,6 +48,7 @@
     suite.addTestSuite(JsArrayMixedTest.class);
     suite.addTestSuite(SchedulerImplTest.class);
     suite.addTestSuite(SchedulerTest.class);
+    suite.addTestSuite(ScriptInjectorTest.class);
     suite.addTestSuite(StackTraceCreatorTest.class);
     suite.addTestSuite(StrictModeTest.class);
     suite.addTestSuite(XhrLoadingStrategyTest.class);
diff --git a/user/test/com/google/gwt/core/client/ScriptInjectorTest.java b/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
new file mode 100644
index 0000000..e86ee4b
--- /dev/null
+++ b/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright 2011 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.core.client;
+
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
+import com.google.gwt.core.client.ScriptInjector.FromString;
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Tests for {@link ScriptInjector}
+ */
+public class ScriptInjectorTest extends GWTTestCase {
+  private static boolean browserChecked = false;
+  private static final int CHECK_DELAY = 100;
+
+  private static boolean isIE = false;
+  private static final int TEST_DELAY = 10000;
+
+  /**
+   * Check if the browser is IE6,7,8,9.
+   * 
+   * @return <code>true</code> if the browser is IE6, IE7, IE8, IE9
+   *         <code>false</code> any other browser
+   */
+  static boolean isIE() {
+    if (!browserChecked) {
+      isIE = isIEImpl();
+      browserChecked = true;
+    }
+    return isIE;
+  }
+
+  private static native boolean isIEImpl() /*-{
+    var ua = navigator.userAgent.toLowerCase();
+    if (ua.indexOf("msie") != -1) {
+      return true;
+    }
+    return false;
+  }-*/;
+
+  private native boolean isSafari3OrBefore() /*-{
+    return @com.google.gwt.dom.client.DOMImplWebkit::isWebkit525OrBefore()();
+  }-*/;
+  
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.core.Core";
+  }
+
+  /**
+   * Install a script in the same window as GWT.
+   */
+  public void testInjectDirectThisWindow() {
+    delayTestFinish(TEST_DELAY);
+    String scriptBody = "__ti1_var__ = 1;";
+    assertFalse(nativeTest1Worked());
+    new FromString(scriptBody).inject();
+    boolean worked = nativeTest1Worked();
+    JavaScriptObject scriptElement = findScriptTextInThisWindow(scriptBody);
+    if (!isIE()) {
+      cleanupThisWindow("__ti1_var__", scriptElement);
+      assertFalse("cleanup failed", nativeTest1Worked());
+    }
+    assertTrue("__ti1_var not set in this window", worked);
+    assertNull("script element 1 not removed by injection", scriptElement);
+    finishTest();
+  }
+
+  /**
+   * Install a script in the top window.
+   */
+  public void testInjectDirectTopWindow() {
+    String scriptBody = "__ti2_var__ = 2;";
+    assertFalse(nativeTest2Worked());
+    ScriptInjector.fromString(scriptBody).setWindow(ScriptInjector.TOP_WINDOW).inject();
+    boolean worked = nativeTest2Worked();
+    JavaScriptObject scriptElement = findScriptTextInTopWindow(scriptBody);
+    if (!isIE()) {
+      cleanupTopWindow("__ti2_var__", scriptElement);
+      assertTrue("__ti2_var not set in top window", worked);
+    }
+    assertNull("script element 2 not removed by injection", scriptElement);
+  }
+
+  /**
+   * Install a script in the same window as GWT, turn off the tag removal.
+   */
+  public void testInjectDirectWithoutRemoveTag() {
+    assertFalse(nativeTest3Worked());
+    String scriptBody = "__ti3_var__ = 3;";
+    new FromString(scriptBody).setRemoveTag(false).inject();
+    boolean worked = nativeTest3Worked();
+    JavaScriptObject scriptElement = findScriptTextInThisWindow(scriptBody);
+    if (!isIE()) {
+      cleanupThisWindow("__ti3_var__", scriptElement);
+      assertFalse("cleanup failed", nativeTest3Worked());
+    }
+    assertTrue(worked);
+    assertNotNull("script element 3 should have been left in DOM", scriptElement);
+  }
+
+  /**
+   * Inject an absolute URL on this window.
+   */
+  public void testInjectUrlAbsolute() {
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "http://www.google.com/jsapi?key=GWTUNITEST";
+    assertFalse(nativeInjectUrlAbsoluteWorked());
+    ScriptInjector.fromUrl(scriptUrl).setCallback(new Callback<Void, Exception>() {
+
+      @Override
+      public void onFailure(Exception reason) {
+        assertNotNull(reason);
+        fail("Injection failed: " + reason.toString());
+      }
+
+      @Override
+      public void onSuccess(Void result) {
+        assertTrue(nativeInjectUrlAbsoluteWorked());
+        finishTest();
+      }
+
+    }).inject();
+  }
+
+  /**
+   * Inject an absolute URL on the top level window.
+   */
+  public void testInjectUrlAbsoluteTop() {
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "http://www.google.com/jsapi?key=GWTUNITEST_ABSOLUTE";
+    assertFalse(nativeAbsoluteTopUrlIsLoaded());
+    ScriptInjector.fromUrl(scriptUrl).setWindow(ScriptInjector.TOP_WINDOW).setCallback(
+        new Callback<Void, Exception>() {
+
+          @Override
+          public void onFailure(Exception reason) {
+            assertNotNull(reason);
+            fail("Injection failed: " + reason.toString());
+          }
+
+          @Override
+          public void onSuccess(Void result) {
+            assertTrue(nativeAbsoluteTopUrlIsLoaded());
+            finishTest();
+          }
+        }).inject();
+  }
+
+  /**
+   * This script injection should fail and fire the onFailure callback.
+   * 
+   * Note, the onerror mechanism used to trigger the failure event is a modern browser
+   * feature.
+   * 
+   * On IE, the script.onerror tag has been documented, but busted for <a
+   * href=
+   * "http://stackoverflow.com/questions/2027849/how-to-trigger-script-onerror-in-internet-explorer/2032014#2032014"
+   * >aeons</a>.
+   * 
+   */
+  public void testInjectUrlFail() {
+    if (isIE() || isSafari3OrBefore()) {
+      return;
+    }
+    
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "uNkNoWn_sCrIpT_404.js";
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl).setCallback(new Callback<Void, Exception>() {
+
+          @Override
+          public void onFailure(Exception reason) {
+            assertNotNull(reason);
+            finishTest();
+          }
+
+          @Override
+          public void onSuccess(Void result) {
+            fail("Injection unexpectedly succeeded.");
+          }
+        }).inject();
+    assertNotNull(injectedElement);
+  }
+
+  /**
+   * Install a script in the same window as GWT by URL
+   */
+  public void testInjectUrlThisWindow() {
+    this.delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "script_injector_test4.js";
+    assertFalse(nativeTest4Worked());
+    final JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).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() {
+      int numLoops = 0;
+
+      @Override
+      public boolean execute() {
+        numLoops++;
+        boolean worked = nativeTest4Worked();
+        if (!worked && (numLoops * CHECK_DELAY < TEST_DELAY)) {
+          return true;
+        }
+        JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
+        if (!isIE()) {
+          cleanupThisWindow("__ti4_var__", scriptElement);
+          assertFalse("cleanup failed", nativeTest4Worked());
+        }
+        assertTrue("__ti4_var not set in this window", worked);
+        assertNotNull("script element 4 not found", scriptElement);
+        assertEquals(injectedElement, scriptElement);
+        finishTest();
+
+        // never reached
+        return false;
+      }
+    }, CHECK_DELAY);
+    assertNotNull(injectedElement);
+  }
+
+  /**
+   * Install a script in the same window as GWT by URL
+   */
+  public void testInjectUrlThisWindowCallback() {
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "script_injector_test5.js";
+    assertFalse(nativeTest5Worked());
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl).setCallback(new Callback<Void, Exception>() {
+          @Override
+          public void onFailure(Exception reason) {
+            assertNotNull(reason);
+            fail("Injection failed: " + reason.toString());
+          }
+
+          @Override
+          public void onSuccess(Void result) {
+            boolean worked = nativeTest5Worked();
+            JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
+            if (!isIE()) {
+              cleanupThisWindow("__ti5_var__", scriptElement);
+              assertFalse("cleanup failed", nativeTest5Worked());
+            }
+            assertTrue("__ti5_var not set in this window", worked);
+            assertNotNull("script element 5 not found", scriptElement);
+            finishTest();
+          }
+        }).inject();
+    assertNotNull(injectedElement);
+  }
+
+  /**
+   * Install a script in the top window by URL
+   */
+  public void testInjectUrlTopWindow() {
+    final String scriptUrl = "script_injector_test6.js";
+    assertFalse(nativeTest6Worked());
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl).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() {
+      int numLoops = 0;
+
+      @Override
+      public boolean execute() {
+        numLoops++;
+
+        boolean worked = nativeTest6Worked();
+        if (!worked && (numLoops * CHECK_DELAY < TEST_DELAY)) {
+          return true;
+        }
+        JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
+        if (!isIE()) {
+          cleanupTopWindow("__ti6_var__", scriptElement);
+          assertFalse("cleanup failed", nativeTest6Worked());
+        }
+        assertTrue("__ti6_var not set in top window", worked);
+        assertNotNull("script element 6 not found", scriptElement);
+        finishTest();
+        // never reached
+        return false;
+      }
+    }, CHECK_DELAY);
+    assertNotNull(injectedElement);
+  }
+
+  /**
+   * Install a script in the top window by URL
+   */
+  public void testInjectUrlTopWindowCallback() {
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "script_injector_test7.js";
+    assertFalse(nativeTest7Worked());
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl).setWindow(ScriptInjector.TOP_WINDOW).setCallback(
+            new Callback<Void, Exception>() {
+
+              @Override
+              public void onFailure(Exception reason) {
+                assertNotNull(reason);
+                fail("Injection failed: " + reason.toString());
+              }
+
+              @Override
+              public void onSuccess(Void result) {
+                boolean worked = nativeTest7Worked();
+                JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
+                if (!isIE()) {
+                  cleanupTopWindow("__ti7_var__", scriptElement);
+                  assertFalse("cleanup failed", nativeTest7Worked());
+                }
+                assertTrue("__ti7_var not set in top window", worked);
+                assertNotNull("script element 7 not found", scriptElement);
+                finishTest();
+              }
+            }).inject();
+    assertNotNull(injectedElement);
+  }
+
+  private void cleanupThisWindow(String property, JavaScriptObject scriptElement) {
+    cleanupWindow(nativeThisWindow(), property, scriptElement);
+  }
+
+  private void cleanupTopWindow(String property, JavaScriptObject scriptElement) {
+    cleanupWindow(nativeTopWindow(), property, scriptElement);
+  }
+
+  private native void cleanupWindow(JavaScriptObject wnd, String property,
+      JavaScriptObject scriptElement) /*-{
+    delete wnd[property];
+    if (scriptElement) {
+      scriptElement.parentNode.removeChild(scriptElement);
+    }
+  }-*/;
+
+  private JavaScriptObject findScriptTextInThisWindow(String text) {
+    return nativeFindScriptText(nativeThisWindow(), text);
+  }
+
+  private JavaScriptObject findScriptTextInTopWindow(String text) {
+    return nativeFindScriptText(nativeTopWindow(), text);
+  }
+
+  private JavaScriptObject findScriptUrlInThisWindow(String url) {
+    return nativeFindScriptUrl(nativeThisWindow(), url);
+  }
+
+  private JavaScriptObject findScriptUrlInTopWindow(String url) {
+    return nativeFindScriptUrl(nativeTopWindow(), url);
+  }
+
+  private native boolean nativeAbsoluteTopUrlIsLoaded() /*-{
+    return !!$wnd.google && !!$wnd.google.load;
+  }-*/;
+
+  private native JavaScriptObject nativeFindScriptText(JavaScriptObject wnd, String text) /*-{
+    var scripts = wnd.document.getElementsByTagName("script");
+    for ( var i = 0; i < scripts.length; ++i) {
+      if (scripts[i].text.match("^" + text)) {
+        return scripts[i];
+      }
+    }
+    return null;
+  }-*/;
+
+  /**
+   * Won't work for all urls, uses a regular expression match
+   */
+  private native JavaScriptObject nativeFindScriptUrl(JavaScriptObject wnd, String url) /*-{
+    var scripts = wnd.document.getElementsByTagName("script");
+    for ( var i = 0; i < scripts.length; ++i) {
+      if (scripts[i].src.match(url)) {
+        return scripts[i];
+      }
+    }
+    return null;
+  }-*/;
+
+  private native boolean nativeInjectUrlAbsoluteWorked() /*-{
+    return !!window.google && !!window.google.load;
+  }-*/;
+
+  private native boolean nativeTest1Worked() /*-{
+    return !!window["__ti1_var__"] && window["__ti1_var__"] == 1;
+  }-*/;
+
+  private native boolean nativeTest2Worked() /*-{
+    return !!$wnd["__ti2_var__"] && $wnd["__ti2_var__"] == 2;
+  }-*/;
+
+  private native boolean nativeTest3Worked() /*-{
+    return !!window["__ti3_var__"] && window["__ti3_var__"] == 3;
+  }-*/;
+
+  private native boolean nativeTest4Worked() /*-{
+    return !!window["__ti4_var__"] && window["__ti4_var__"] == 4;
+  }-*/;
+
+  private native boolean nativeTest5Worked() /*-{
+    return !!window["__ti5_var__"] && window["__ti5_var__"] == 5;
+  }-*/;
+
+  private native boolean nativeTest6Worked() /*-{
+    return !!$wnd["__ti6_var__"] && $wnd["__ti6_var__"] == 6;
+  }-*/;
+
+  private native boolean nativeTest7Worked() /*-{
+    return !!$wnd["__ti7_var__"] && $wnd["__ti7_var__"] == 7;
+  }-*/;
+
+  private native JavaScriptObject nativeThisWindow() /*-{
+    return window;
+  }-*/;
+
+  private native JavaScriptObject nativeTopWindow() /*-{
+    return $wnd;
+  }-*/;
+}
diff --git a/user/test/com/google/gwt/core/public/script_injector_test4.js b/user/test/com/google/gwt/core/public/script_injector_test4.js
new file mode 100644
index 0000000..52d39ab
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test4.js
@@ -0,0 +1 @@
+__ti4_var__ = 4;
\ No newline at end of file
diff --git a/user/test/com/google/gwt/core/public/script_injector_test5.js b/user/test/com/google/gwt/core/public/script_injector_test5.js
new file mode 100644
index 0000000..ca1d38a
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test5.js
@@ -0,0 +1 @@
+__ti5_var__ = 5;
\ No newline at end of file
diff --git a/user/test/com/google/gwt/core/public/script_injector_test6.js b/user/test/com/google/gwt/core/public/script_injector_test6.js
new file mode 100644
index 0000000..ea1948d
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test6.js
@@ -0,0 +1 @@
+__ti6_var__ = 6;
\ No newline at end of file
diff --git a/user/test/com/google/gwt/core/public/script_injector_test7.js b/user/test/com/google/gwt/core/public/script_injector_test7.js
new file mode 100644
index 0000000..76d55ff
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test7.js
@@ -0,0 +1 @@
+__ti7_var__ = 7;
\ No newline at end of file