blob: 4b410d4f416d054f9f6932f0e7936a2b82fc6dc4 [file] [log] [blame]
/*
* Copyright 2009 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.jsonp.client;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.ScriptElement;
import com.google.gwt.safehtml.shared.annotations.IsTrustedResourceUri;
import com.google.gwt.safehtml.shared.annotations.SuppressIsTrustedResourceUriCastCheck;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
* A JSONP request that is waiting for a response. The request can be canceled.
*
* @param <T> the type of the response object.
*/
public class JsonpRequest<T> {
/**
* A global JS variable that holds the next index to use.
*/
private static final String CALLBACKS_COUNTER_NAME = "__gwt_jsonp_counter__";
/**
* A global JS object that contains callbacks of pending requests.
*/
private static final String CALLBACKS_NAME = "__gwt_jsonp__";
private static final JavaScriptObject CALLBACKS = getOrCreateCallbacksObject();
/**
* Prefix appended to all id's that are determined by the callbacks counter.
*/
private static final String INCREMENTAL_ID_PREFIX = "P";
/**
* Prefix appended to all id's that are passed in by the user. The "P"
* suffix must stay in sync with ExternalTextResourceGenerator.java
*/
private static final String PREDETERMINED_ID_PREFIX = "P";
/**
* Returns the next ID to use, incrementing the global counter.
*/
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);
}
/**
* Returns 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 getPredeterminedId(String suffix) {
return PREDETERMINED_ID_PREFIX + suffix;
}
private static String nextCallbackId() {
return INCREMENTAL_ID_PREFIX + getAndIncrementCallbackCounter();
}
private final String callbackId;
private final int timeout;
private final AsyncCallback<T> callback;
/**
* Whether the result is expected to be an integer or not.
*/
private final boolean expectInteger;
private final String callbackParam;
private final String failureCallbackParam;
private final boolean canHaveMultipleRequestsForSameId;
/**
* Timer which keeps track of timeouts.
*/
private Timer timer;
/**
* Create a new JSONP request.
*
* @param callback The callback instance to notify when the response comes
* back
* @param timeout Time in ms after which a {@link TimeoutException} will be
* thrown
* @param expectInteger Should be true if T is {@link Integer}, false
* otherwise
* @param callbackParam Name of the url param of the callback function name
* @param failureCallbackParam Name of the url param containing the
* failure callback function name, or null for no failure callback
*/
JsonpRequest(AsyncCallback<T> callback, int timeout, boolean expectInteger,
String callbackParam, String failureCallbackParam) {
callbackId = nextCallbackId();
this.callback = callback;
this.timeout = timeout;
this.expectInteger = expectInteger;
this.callbackParam = callbackParam;
this.failureCallbackParam = failureCallbackParam;
this.canHaveMultipleRequestsForSameId = false;
}
/**
* Create a new JSONP request with a hardcoded id. This could be used to
* manually control which resources are considered duplicates (by giving them
* identical ids). Could also be used if the callback name needs to be
* completely user controlled (since the id is part of the callback name).
*
* @param callback The callback instance to notify when the response comes
* back
* @param timeout Time in ms after which a {@link TimeoutException} will be
* thrown
* @param expectInteger Should be true if T is {@link Integer}, false
* otherwise
* @param callbackParam Name of the url param of the callback function name
* @param failureCallbackParam Name of the url param containing the
* failure callback function name, or null for no failure callback
* @param id unique id for the resource that is being fetched
*/
JsonpRequest(AsyncCallback<T> callback, int timeout, boolean expectInteger,
String callbackParam, String failureCallbackParam, String id) {
callbackId = getPredeterminedId(id);
this.callback = callback;
this.timeout = timeout;
this.expectInteger = expectInteger;
this.callbackParam = callbackParam;
this.failureCallbackParam = failureCallbackParam;
this.canHaveMultipleRequestsForSameId = true;
}
/**
* Cancels a pending request. Note that if you are using preset ID's, this
* will not work, since there is no way of knowing if there are other
* requests pending (or have already returned) for the same data.
*/
public void cancel() {
timer.cancel();
unload();
}
public AsyncCallback<T> getCallback() {
return callback;
}
public int getTimeout() {
return timeout;
}
@Override
public String toString() {
return "JsonpRequest(id=" + callbackId + ")";
}
// @VisibleForTesting
String getCallbackId() {
return callbackId;
}
/**
* Sends a request using the JSONP mechanism.
*
* @param baseUri To be sent to the server.
*/
@SuppressIsTrustedResourceUriCastCheck
void send(@IsTrustedResourceUri final String baseUri) {
registerCallbacks(CALLBACKS, canHaveMultipleRequestsForSameId);
StringBuilder uri = new StringBuilder(baseUri);
uri.append(baseUri.contains("?") ? "&" : "?");
String prefix = CALLBACKS_NAME + "." + callbackId;
uri.append(callbackParam).append("=").append(prefix).append(
".onSuccess");
if (failureCallbackParam != null) {
uri.append("&");
uri.append(failureCallbackParam).append("=").append(prefix).append(
".onFailure");
}
ScriptElement script = Document.get().createScriptElement();
script.setType("text/javascript");
script.setId(callbackId);
script.setSrc(uri.toString());
timer = new Timer() {
@Override
public void run() {
onFailure(new TimeoutException("Timeout while calling " + baseUri));
}
};
timer.schedule(timeout);
getHeadElement().appendChild(script);
}
private void onFailure(String message) {
onFailure(new Exception(message));
}
private void onFailure(Throwable ex) {
timer.cancel();
try {
if (callback != null) {
callback.onFailure(ex);
}
} finally {
unload();
}
}
private void onSuccess(T data) {
timer.cancel();
try {
if (callback != null) {
callback.onSuccess(data);
}
} finally {
unload();
}
}
/**
* Registers the callback methods that will be called when the JSONP response
* comes back. 2 callbacks are created, one to return the value, and one to
* notify a failure.
*
* @param callbacks the global JS object which stores callbacks
*/
private native void registerCallbacks(JavaScriptObject callbacks, boolean canHaveMultipleRequestsForId) /*-{
var self = this;
var callback = new Object();
callback.onSuccess = $entry(function(data) {
// Box primitive types
if (typeof data == 'boolean') {
data = @java.lang.Boolean::new(Z)(data);
} else if (typeof data == 'number') {
if (self.@com.google.gwt.jsonp.client.JsonpRequest::expectInteger) {
data = @java.lang.Integer::new(I)(data);
} else {
data = @java.lang.Double::new(D)(data);
}
}
self.@com.google.gwt.jsonp.client.JsonpRequest::onSuccess(Ljava/lang/Object;)(data);
});
if (this.@com.google.gwt.jsonp.client.JsonpRequest::failureCallbackParam) {
callback.onFailure = $entry(function(message) {
self.@com.google.gwt.jsonp.client.JsonpRequest::onFailure(Ljava/lang/String;)(message);
});
}
if (canHaveMultipleRequestsForId) {
// In this case, we keep a wrapper, with a list of callbacks. Since the
// response for the request is the same each time, we call all of the
// callbacks as soon as any response comes back.
var callbackWrapper =
callbacks[this.@com.google.gwt.jsonp.client.JsonpRequest::callbackId];
if (!callbackWrapper) {
callbackWrapper = new Object();
callbackWrapper.callbackList = new Array();
callbackWrapper.onSuccess = function(data) {
while (callbackWrapper.callbackList.length > 0) {
callbackWrapper.callbackList.shift().onSuccess(data);
}
}
callbackWrapper.onFailure = function(data) {
while (callbackWrapper.callbackList.length > 0) {
callbackWrapper.callbackList.shift().onFailure(data);
}
}
callbacks[this.@com.google.gwt.jsonp.client.JsonpRequest::callbackId] =
callbackWrapper;
}
callbackWrapper.callbackList.push(callback);
} else {
// In this simple case, just associate the callback directly with the
// particular id in the callbacks object
callbacks[this.@com.google.gwt.jsonp.client.JsonpRequest::callbackId] = callback;
}
}-*/;
/**
* Cleans everything once the response has been received: deletes the script
* tag and unregisters the callback.
*/
private void unload() {
/*
* Some browsers (IE7) require the script tag to be deleted outside the
* scope of the script itself. Therefore, we need to defer the delete
* statement after the callback execution.
*/
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute() {
if (!canHaveMultipleRequestsForSameId) {
// If there can me multiple requests for a particular ID, then we
// don't want to unregister the callback since there may be pending
// requests that have not yet come back and we don't want them to
// have an undefined callback function.
unregisterCallbacks(CALLBACKS, callbackId);
}
Node script = Document.get().getElementById(callbackId);
if (script != null) {
// The script may have already been deleted
getHeadElement().removeChild(script);
}
}
});
}
private native void unregisterCallbacks(JavaScriptObject callbacks, String callbackId) /*-{
callbacks[callbackId].onSuccess = callbacks[callbackId].onFailure = function() {};
}-*/;
}