| /* |
| * 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.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. |
| */ |
| void send(final String baseUri) { |
| registerCallbacks(CALLBACKS, canHaveMultipleRequestsForSameId); |
| StringBuffer uri = new StringBuffer(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); |
| } |
| 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) /*-{ |
| delete callbacks[this.@com.google.gwt.jsonp.client.JsonpRequest::callbackId]; |
| }-*/; |
| } |