Fix JSONP collisions when using multiple modules on the same page.
Review at http://gwt-code-reviews.appspot.com/130820/show
Issue: 4386
Patch by: jat
Review by: rice
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7469 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/jsonp/client/JsonpRequest.java b/user/src/com/google/gwt/jsonp/client/JsonpRequest.java
index ad8d5d4..c199339 100644
--- a/user/src/com/google/gwt/jsonp/client/JsonpRequest.java
+++ b/user/src/com/google/gwt/jsonp/client/JsonpRequest.java
@@ -32,32 +32,47 @@
public class JsonpRequest<T> {
/**
- * Each request will be assigned a new id.
+ * A global JS variable that holds the next index to use.
*/
- private static int callbackCounter = 0;
+ @SuppressWarnings("unused") // accessed from JSNI
+ private static final String CALLBACKS_COUNTER_NAME = "__gwt_jsonp_counter__";
/**
- * __gwt_jsonp__ is a global object that contains callbacks of pending requests.
+ * A global JS object that contains callbacks of pending requests.
*/
private static final String CALLBACKS_NAME = "__gwt_jsonp__";
- private static final JavaScriptObject CALLBACKS = createCallbacksObject(CALLBACKS_NAME);
+ private static final JavaScriptObject CALLBACKS = getOrCreateCallbacksObject();
/**
- * Creates a global object to store callbacks of pending requests.
- *
- * @param name The name of the global object.
- * @return The created object.
+ * @return the next ID to use, incrementing the global counter.
*/
- private static native JavaScriptObject createCallbacksObject(String name) /*-{
- return $wnd[name] = new Object();
+ private static native int getAndIncrementCallbackCounter() /*-{
+ var name = @com.google.gwt.jsonp.client.JsonpRequest::CALLBACKS_NAME;
+ var ctr = @com.google.gwt.jsonp.client.JsonpRequest::CALLBACKS_COUNTER_NAME;
+ return $wnd[name][ctr]++;
}-*/;
private static Node getHeadElement() {
return Document.get().getElementsByTagName("head").getItem(0);
}
+ /**
+ * @return a global object to store callbacks of pending requests, creating
+ * it if it doesn't exist.
+ */
+ private static native JavaScriptObject getOrCreateCallbacksObject() /*-{
+ var name = @com.google.gwt.jsonp.client.JsonpRequest::CALLBACKS_NAME;
+ if (!$wnd[name]) {
+ $wnd[name] = new Object();
+ $wnd[name]
+ [@com.google.gwt.jsonp.client.JsonpRequest::CALLBACKS_COUNTER_NAME]
+ = 0;
+ }
+ return $wnd[name];
+ }-*/;
+
private static String nextCallbackId() {
- return "I" + (callbackCounter++);
+ return "I" + getAndIncrementCallbackCounter();
}
private final String callbackId;
@@ -120,6 +135,16 @@
return timeout;
}
+ @Override
+ public String toString() {
+ return "JsonpRequest(id=" + callbackId + ")";
+ }
+
+ // @VisibleForTesting
+ String getCallbackId() {
+ return callbackId;
+ }
+
/**
* Sends a request using the JSONP mechanism.
*
diff --git a/user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java b/user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java
index 8022eb7..0d0cc69 100644
--- a/user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java
+++ b/user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java
@@ -16,7 +16,10 @@
package com.google.gwt.jsonp.client;
import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
@@ -25,16 +28,6 @@
public class JsonpRequestTest extends GWTTestCase {
/**
- * The maximum amount of time to wait for a response in milliseconds.
- */
- private static final int RESPONSE_DELAY = 2500;
-
- /**
- * The current Id of the async callback.
- */
- protected static int currentId;
-
- /**
* Checks that an error is received.
*/
private class AssertFailureCallback<T> implements AsyncCallback<T> {
@@ -42,19 +35,19 @@
private int id;
public AssertFailureCallback(String expectedMessage) {
- id = ++currentId;
+ id = currentTestId;
this.expectedMessage = expectedMessage;
}
public void onFailure(Throwable throwable) {
- if (id == currentId) {
+ if (id == currentTestId) {
assertEquals(expectedMessage, throwable.getMessage());
finishTest();
}
}
public void onSuccess(T value) {
- if (id == currentId) {
+ if (id == currentTestId) {
fail();
}
}
@@ -64,24 +57,34 @@
* Checks that the received value is as expected.
*/
private class AssertSuccessCallback<T> implements AsyncCallback<T> {
- private T expectedValue;
- private int id;
+ private final T expectedValue;
+ private final int id;
+ private final Counter counter;
private AssertSuccessCallback(T expectedValue) {
- this.id = ++currentId;
+ this(expectedValue, null);
+ }
+
+ private AssertSuccessCallback(T expectedValue, Counter counter) {
+ this.id = currentTestId;
+ this.counter = counter;
this.expectedValue = expectedValue;
}
public void onFailure(Throwable throwable) {
- if (id == currentId) {
+ if (id == currentTestId) {
fail();
}
}
public void onSuccess(T value) {
- if (id == currentId) {
+ if (id == currentTestId) {
assertEquals(expectedValue, value);
- finishTest();
+ if (counter != null) {
+ counter.count();
+ } else {
+ finishTest();
+ }
}
}
}
@@ -100,10 +103,51 @@
}
}
+ /**
+ * A class that counts results and calls finishTest when the right number
+ * have been received.
+ */
+ private class Counter {
+
+ private int count;
+
+ public Counter(int count) {
+ this.count = count;
+ }
+
+ public void count() {
+ if (--count < 0) {
+ fail("Too many results");
+ }
+ if (count == 0) {
+ finishTest();
+ }
+ }
+ }
+
+ /**
+ * The maximum amount of time to wait for a response in milliseconds.
+ */
+ private static final int RESPONSE_DELAY = 2500;
+
+ /**
+ * The ID of the current test.
+ */
+ protected static int currentTestId;
+
private static String echo(String value) {
return GWT.getModuleBaseURL() + "echo?action=SUCCESS&value=" + value;
}
+ private static String echoDelayed(String value) {
+ return echoDelayed(value, 500);
+ }
+
+ private static String echoDelayed(String value, long delayMs) {
+ return GWT.getModuleBaseURL() + "echo?action=SUCCESS&delay=" + delayMs
+ + "&value=" + value;
+ }
+
private static String echoFailure(String error) {
return GWT.getModuleBaseURL() + "echo?action=FAILURE&error=" + error;
}
@@ -131,6 +175,31 @@
Boolean.TRUE));
}
+
+ @DoNotRunWith(Platform.HtmlUnit)
+ // Fails in devmode with HtmlUnit, JS "null" exception
+ public void testCancel() {
+ delayTestFinish(2000);
+ // setup a server request that will delay for 500ms
+ JsonpRequest<String> req = jsonp.requestString(echoDelayed("A", 500),
+ new AssertFailureCallback<String>("A"));
+ // cancel it before it comes back
+ req.cancel();
+ // wait 1s to make sure we don't get a callback
+ new Timer() {
+ @Override
+ public void run() {
+ finishTest();
+ }
+ }.schedule(1000);
+ }
+
+ public void testDelay() {
+ delayTestFinish(RESPONSE_DELAY);
+ JsonpRequest<String> req = jsonp.requestString(echoDelayed("'A'"),
+ new AssertSuccessCallback<String>("A"));
+ }
+
public void testDouble() {
delayTestFinish(RESPONSE_DELAY);
jsonp.requestDouble(echo("123.456"), new AssertSuccessCallback<Double>(
@@ -144,6 +213,20 @@
new AssertFailureCallback<String>("ERROR"));
}
+ @DoNotRunWith(Platform.HtmlUnit)
+ // Hangs indefinitely in devmode with HtmlUnit
+ public void testIds() {
+ delayTestFinish(RESPONSE_DELAY);
+ JsonpRequest<String> reqA = jsonp.requestString(echo("'A'"),
+ new AssertSuccessCallback<String>("A"));
+ JsonpRequest<String> reqB = jsonp.requestString(echo("'B'"),
+ new AssertSuccessCallback<String>("B"));
+ // WARNING: knows the current structure of IDs
+ int idA = Integer.parseInt(reqA.getCallbackId().substring(1));
+ int idB = Integer.parseInt(reqB.getCallbackId().substring(1));
+ assertEquals("Unexpected ID sequence", idA + 1, idB);
+ }
+
public void testInteger() {
delayTestFinish(RESPONSE_DELAY);
jsonp.requestInteger(echo("123"), new AssertSuccessCallback<Integer>(123));
@@ -180,6 +263,19 @@
jsonp.requestString(echo("null"), new AssertSuccessCallback<String>(null));
}
+ @DoNotRunWith(Platform.HtmlUnit)
+ // Hangs indefinitely in devmode with HtmlUnit
+ public void testOverlapped() {
+ delayTestFinish(RESPONSE_DELAY);
+ Counter counter = new Counter(3);
+ JsonpRequest<String> reqA = jsonp.requestString(echoDelayed("'A'", 750),
+ new AssertSuccessCallback<String>("A", counter));
+ JsonpRequest<String> reqB = jsonp.requestString(echoDelayed("'B'", 500),
+ new AssertSuccessCallback<String>("B", counter));
+ JsonpRequest<String> reqC = jsonp.requestString(echo("'C'"),
+ new AssertSuccessCallback<String>("C", counter));
+ }
+
public void testString() {
delayTestFinish(RESPONSE_DELAY);
jsonp.requestString(echo("'Hello'"), new AssertSuccessCallback<String>(
@@ -202,4 +298,9 @@
jsonp = new JsonpRequestBuilder();
jsonp.setTimeout(RESPONSE_DELAY + 500);
}
+
+ @Override
+ protected void gwtTearDown() throws Exception {
+ ++currentTestId;
+ }
}
diff --git a/user/test/com/google/gwt/jsonp/server/EchoServlet.java b/user/test/com/google/gwt/jsonp/server/EchoServlet.java
index 18461dc..7e8cef2 100644
--- a/user/test/com/google/gwt/jsonp/server/EchoServlet.java
+++ b/user/test/com/google/gwt/jsonp/server/EchoServlet.java
@@ -17,7 +17,6 @@
import java.io.IOException;
-import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -47,9 +46,11 @@
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
- throws ServletException, IOException {
+ throws IOException {
+ res.setContentType("application/javascript");
switch (Action.valueOf(req.getParameter("action"))) {
case SUCCESS: {
+ handleDelay(req);
String callback = req.getParameter("callback");
String value = req.getParameter("value");
if (value == null) {
@@ -61,6 +62,7 @@
}
case FAILURE: {
+ handleDelay(req);
String failureCallback = req.getParameter("failureCallback");
String error = req.getParameter("error");
if (failureCallback != null) {
@@ -80,4 +82,21 @@
// Don't respond anything so that a timeout happens.
}
}
+
+ /**
+ * Handle a delay query parameter if present.
+ *
+ * @param req
+ */
+ private void handleDelay(HttpServletRequest req) {
+ String delayVal = req.getParameter("delay");
+ if (delayVal != null) {
+ int delayMs = Integer.parseInt(delayVal);
+ try {
+ Thread.sleep(delayMs);
+ } catch (InterruptedException e) {
+ log("Interrupted sleep, continuing");
+ }
+ }
+ }
}