The attached patch for trunk adds JSONP support, for use with cross-site data retrieval, such as GData APIs.
-- John A. Tamplin Software Engineer (GWT), Google --~--~---------~--~----~------------~-------~--~----~ http://groups.google.com/group/Google-Web-Toolkit-Contributors -~----------~----~----~----~------~----~------~--~---
Index: user/src/com/google/gwt/jsonp/Jsonp.gwt.xml =================================================================== --- user/src/com/google/gwt/jsonp/Jsonp.gwt.xml (revision 0) +++ user/src/com/google/gwt/jsonp/Jsonp.gwt.xml (revision 0) @@ -0,0 +1,18 @@ +<!-- --> +<!-- 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 --> +<!-- 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. License for the specific language governing permissions and --> +<!-- limitations under the License. --> + +<module> + <inherits name='com.google.gwt.user.User'/> + <source path="client"/> +</module> Index: user/src/com/google/gwt/jsonp/client/JsonpRequest.java =================================================================== --- user/src/com/google/gwt/jsonp/client/JsonpRequest.java (revision 0) +++ user/src/com/google/gwt/jsonp/client/JsonpRequest.java (revision 0) @@ -0,0 +1,215 @@ +/* + * 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.dom.client.Document; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.ScriptElement; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DeferredCommand; +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 cancelled. + * + * @param <T> the type of the response object. + */ +public class JsonpRequest<T> { + + /** + * Each request will be assigned a new id. + */ + private static int callbackCounter = 0; + + private static native Node getDocumentElement() /*-{ + return $doc.documentElement; + }-*/; + + private static String nextCallbackId() { + return "__jsonp" + (callbackCounter++) + "__"; + } + + private final String callbackId; + + private final int timeout; + + private final AsyncCallback<T> callback; + + /** + * Whether the result is expected to be an integer or not + */ + @SuppressWarnings("unused") // used by JSNI + private final boolean expectInteger; + + private final String callbackParam; + + private final String failureCallbackParam; + + /** + * 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 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; + } + + /** + * Cancels a pending request. + */ + public void cancel() { + timer.cancel(); + unload(); + } + + public AsyncCallback<T> getCallback() { + return callback; + } + + public int getTimeout() { + return timeout; + } + + /** + * Sends a request using the JSONP mechanism. + * + * @param baseUri To be sent to the server. + */ + void send(final String baseUri) { + registerCallbacks(); + StringBuffer uri = new StringBuffer(baseUri); + uri.append(baseUri.contains("?") ? "&" : "?"); + uri.append(callbackParam).append("=").append(callbackId).append( + ".onSuccess"); + if (failureCallbackParam != null) { + uri.append("&"); + uri.append(failureCallbackParam).append("=").append(callbackId).append( + ".onFailure"); + } + ScriptElement script = Document.get().createScriptElement(); + script.setType("text/javascript"); + script.setId(callbackId); + script.setSrc(uri.toString()); + getDocumentElement().getFirstChild().appendChild(script); + timer = new Timer() { + @Override + public void run() { + onFailure(new TimeoutException("Timeout while calling " + baseUri)); + } + }; + timer.schedule(timeout); + } + + @SuppressWarnings("unused") // used by JSNI + 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(); + } + } + + @SuppressWarnings("unused") // used by JSNI + 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. + */ + private native void registerCallbacks() /*-{ + var self = this; + var callback = new Object(); + $wnd[th...@com.google.gwt.jsonp.client.jsonprequest::callbackId] = callback; + callback.onSuccess = function(data) { + // Box primitive types + if (typeof data == 'boolean') { + data = @java.lang.Boolean::new(Z)(data); + } else if (typeof data == 'number') { + if (se...@com.google.gwt.jsonp.client.jsonprequest::expectInteger) { + data = @java.lang.Integer::new(I)(data); + } else { + data = @java.lang.Double::new(D)(data); + } + } + se...@com.google.gwt.jsonp.client.jsonprequest::onSuccess(Ljava/lang/Object;)(data); + }; + if (th...@com.google.gwt.jsonp.client.jsonprequest::failureCallbackParam) { + callback.onFailure = function(message) { + se...@com.google.gwt.jsonp.client.jsonprequest::onFailure(Ljava/lang/String;)(message); + }; + } + }-*/; + + /** + * Cleans everything once the response has been received: deletes the script + * tag, 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. + */ + DeferredCommand.addCommand(new Command() { + public void execute() { + unregisterCallbacks(); + Node script = Document.get().getElementById(callbackId); + getDocumentElement().getFirstChild().removeChild(script); + } + }); + } + + private native void unregisterCallbacks() /*-{ + delete $wnd[th...@com.google.gwt.jsonp.client.jsonprequest::callbackId]; + }-*/; +} Property changes on: user/src/com/google/gwt/jsonp/client/JsonpRequest.java ___________________________________________________________________ Name: svn:mime-type + text/x-java Name: svn:eol-style + native Index: user/src/com/google/gwt/jsonp/client/TimeoutException.java =================================================================== --- user/src/com/google/gwt/jsonp/client/TimeoutException.java (revision 0) +++ user/src/com/google/gwt/jsonp/client/TimeoutException.java (revision 0) @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * Exception sent when a JSONP calls expires the timeout. + */ +public class TimeoutException extends Exception { + + public TimeoutException() { + } + + public TimeoutException(String s) { + super(s); + } +} Property changes on: user/src/com/google/gwt/jsonp/client/TimeoutException.java ___________________________________________________________________ Name: svn:mime-type + text/x-java Name: svn:eol-style + native Index: user/src/com/google/gwt/jsonp/client/JsonpRequestBuilder.java =================================================================== --- user/src/com/google/gwt/jsonp/client/JsonpRequestBuilder.java (revision 0) +++ user/src/com/google/gwt/jsonp/client/JsonpRequestBuilder.java (revision 0) @@ -0,0 +1,201 @@ +/* + * 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.user.client.rpc.AsyncCallback; + + +/** + * Class to send cross domain requests to an http server. The server will receive a request + * including a callback url parameter, which should be used to return the response as following: + * + * <pre><callback>(<json>);</pre> + * + * where <callback> is the url parameter (see {...@link #setCallbackParam(String)}), and + * <json> is the response to the request in json format. + * + * This will result on the client to call the corresponding {...@link AsyncCallback#onSuccess(Object)} + * method. + * + * <p> + * If needed, errors can be handled by a separate callback: + * + * <pre><failureCallback>(<error>);</pre> + * + * where <error> is a string containing an error message. This will result on the client to + * call the corresponding {...@link AsyncCallback#onFailure(Throwable)} method. See + * {...@link #setFailureCallbackParam(String)}. + * + * <p> + * Example using <a href="http://code.google.com/apis/gdata/json.html#Request">JSON Google Calendar + * GData API</a>: + * + * <pre> + * String url = "http://www.google.com/calendar/feeds/developer-calen...@google.com/public/full" + + * "?alt=json-in-script"; + * JsonpRequestBuilder jsonp = new JsonpRequestBuilder(); + * jsonp.requestObject(url, + * new AsyncCallback<Feed>() { + * public void onFailure(Throwable throwable) { + * Log.severe("Error: " + throwable); + * } + * + * public void onSuccess(Feed feed) { + * JsArray<Entry> entries = feed.getEntries(); + * for (int i = 0; i < entries.length(); i++) { + * Entry entry = entries.get(i); + * Log.info(entry.getTitle() + + * " (" + entry.getWhere() + "): " + + * entry.getStartTime() + " -> " + + * entry.getEndTime()); + * } + * } + * }); + * </pre> + * + * This example uses these overlay types: + * + * <pre> + * class Entry extends JavaScriptObject { + * protected Entry() {} + * + * public final native String getTitle() /*-{ + * return this.title.$t; + * }-*/; + * + * public final native String getWhere() /*-{ + * return this.gd$where[0].valueString; + * }-*/; + * + * public final native String getStartTime() /*-{ + * return this.gd$when ? this.gd$when[0].startTime : null; + * }-*/; + * + * public final native String getEndTime() /*-{ + * return this.gd$when ? this.gd$when[0].endTime : null; + * }-*/; + * } + * + * class Feed extends JavaScriptObject { + * protected Feed() {} + * + * public final native JsArray<Entry> getEntries() /*-{ + * return this.feed.entry; + * }-*/; + * } + * </pre> + * + * </p> + */ +public class JsonpRequestBuilder { + private int timeout = 10000; + private String callbackParam = "callback"; + private String failureCallbackParam = null; + + /** + * @return the name of the callback url parameter to send to the server. The default value is + * "callback". + */ + public String getCallbackParam() { + return callbackParam; + } + + /** + * @return the name of the failure callback url parameter to send to the server. The default is + * null. + */ + public String getFailureCallbackParam() { + return failureCallbackParam; + } + + /** + * @return the expected timeout (ms) for this request. + */ + public int getTimeout() { + return timeout; + } + + public JsonpRequest<Boolean> requestBoolean(String url, AsyncCallback<Boolean> callback) { + return send(url, callback, false); + } + + public JsonpRequest<Double> requestDouble(String url, AsyncCallback<Double> callback) { + return send(url, callback, false); + } + + public JsonpRequest<Integer> requestInteger(String url, AsyncCallback<Integer> callback) { + return send(url, callback, true); + } + + /** + * Sends a JSONP request and expects a JavaScript object as a result. The caller can either use + * {...@link com.google.gwt.json.client.JSONObject} to parse it, or use a JavaScript overlay class. + */ + public <T extends JavaScriptObject> JsonpRequest<T> requestObject(String url, + AsyncCallback<T> callback) { + return send(url, callback, false); + } + + public JsonpRequest<String> requestString(String url, AsyncCallback<String> callback) { + return send(url, callback, false); + } + + /** + * Sends a JSONP request and does not expect any results. + */ + public void send(String url) { + send(url, null, false); + } + + /** + * Sends a JSONP request, does not expect any result, but still allows to be notified when the + * request has been executed on the server. + */ + public JsonpRequest<Void> send(String url, AsyncCallback<Void> callback) { + return send(url, callback, false); + } + + /** + * @param callbackParam The name of the callback url parameter to send to the server. The default + * value is "callback". + */ + public void setCallbackParam(String callbackParam) { + this.callbackParam = callbackParam; + } + + /** + * @param failureCallbackParam The name of the failure callback url parameter to send to the + * server. The default is null. + */ + public void setFailureCallbackParam(String failureCallbackParam) { + this.failureCallbackParam = failureCallbackParam; + } + + /** + * @param timeout The expected timeout (ms) for this request. The default is 10s. + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + private <T> JsonpRequest<T> send(String url, AsyncCallback<T> callback, boolean expectInteger) { + JsonpRequest<T> request = new JsonpRequest<T>(callback, timeout, expectInteger, callbackParam, + failureCallbackParam); + request.send(url); + return request; + } +} Property changes on: user/src/com/google/gwt/jsonp/client/JsonpRequestBuilder.java ___________________________________________________________________ Name: svn:mime-type + text/x-java Name: svn:eol-style + native Index: user/test/com/google/gwt/jsonp/server/EchoServlet.java =================================================================== --- user/test/com/google/gwt/jsonp/server/EchoServlet.java (revision 0) +++ user/test/com/google/gwt/jsonp/server/EchoServlet.java (revision 0) @@ -0,0 +1,80 @@ +/* + * 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.server; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * A servlet that returns a given value, following the JSONP protocol. + * Expected url parameters: + * + * <ul> + * <li>action: one of the following values: + * <ul> + * <li>TIMEOUT: don't respond anything to simulate a timeout + * <li>SUCCESS: return a JSON value + * <li>FAILURE: return an error + * </ul> + * <li>value: the JSON value to return if action == "SUCCESS" + * <li>error: the error message to return if action == "FAILURE" + * </ul> + */ +public class EchoServlet extends HttpServlet { + + private enum Action { + SUCCESS, + FAILURE, + TIMEOUT + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException { + switch (Action.valueOf(req.getParameter("action"))) { + case SUCCESS: { + String callback = req.getParameter("callback"); + String value = req.getParameter("value"); + if (value == null) { + value = ""; + } + res.getWriter().println(callback + "(" + value + ");"); + break; + } + + case FAILURE: { + String failureCallback = req.getParameter("failureCallback"); + String error = req.getParameter("error"); + if (failureCallback != null) { + res.getWriter().println(failureCallback + "('" + error + "');"); + } else { + // If no failure callback is defined, send the error through the + // success callback. + String callback = req.getParameter("callback"); + res.getWriter().println(callback + "('" + error + "');"); + } + break; + } + + case TIMEOUT: + // Don't respond anything so that a timeout happens. + } + } +} Property changes on: user/test/com/google/gwt/jsonp/server/EchoServlet.java ___________________________________________________________________ Name: svn:mime-type + text/x-java Name: svn:eol-style + native Index: user/test/com/google/gwt/jsonp/JsonpTest.gwt.xml =================================================================== --- user/test/com/google/gwt/jsonp/JsonpTest.gwt.xml (revision 0) +++ user/test/com/google/gwt/jsonp/JsonpTest.gwt.xml (revision 0) @@ -0,0 +1,22 @@ +<!-- --> +<!-- 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 --> +<!-- 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. License for the specific language governing permissions and --> +<!-- limitations under the License. --> + +<module> + <!-- Inherit the JUnit support --> + <inherits name='com.google.gwt.junit.JUnit'/> + <inherits name = 'com.google.gwt.jsonp.Jsonp'/> + <!-- Include client-side source for the test cases --> + <source path="client"/> + <servlet path='/echo' class='com.google.gwt.jsonp.server.EchoServlet'/> +</module> Index: user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java =================================================================== --- user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java (revision 0) +++ user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java (revision 0) @@ -0,0 +1,182 @@ +/* + * 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.GWT; +import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.user.client.rpc.AsyncCallback; + +/** + * Tests for {...@link JsonpRequest}. + */ +public class JsonpRequestTest extends GWTTestCase { + + /** + * Checks that an error is received. + */ + private class AssertFailureCallback<T> implements AsyncCallback<T> { + private String expectedMessage; + + public AssertFailureCallback(String expectedMessage) { + this.expectedMessage = expectedMessage; + } + + public void onFailure(Throwable throwable) { + assertEquals(expectedMessage, throwable.getMessage()); + finishTest(); + } + + public void onSuccess(T value) { + fail(); + } + } + + /** + * Checks that the received value is as expected. + */ + private class AssertSuccessCallback<T> implements AsyncCallback<T> { + private T expectedValue; + + private AssertSuccessCallback(T expectedValue) { + this.expectedValue = expectedValue; + } + + public void onFailure(Throwable throwable) { + fail(); + } + + public void onSuccess(T value) { + assertEquals(expectedValue, value); + finishTest(); + } + } + + /** + * Checks that a timeout happens. + */ + private class AssertTimeoutException<T> implements AsyncCallback<T> { + public void onFailure(Throwable throwable) { + assertTrue(throwable instanceof TimeoutException); + finishTest(); + } + + public void onSuccess(T value) { + fail(); + } + } + + private static String echo(String value) { + return GWT.getModuleBaseURL() + "echo?action=SUCCESS&value=" + value; + } + + private static String echoFailure(String error) { + return GWT.getModuleBaseURL() + "echo?action=FAILURE&error=" + error; + } + + private static String echoTimeout() { + return GWT.getModuleBaseURL() + "echo?action=TIMEOUT"; + } + + private JsonpRequestBuilder jsonp; + + @Override + public String getModuleName() { + return "com.google.gwt.jsonp.JsonpTest"; + } + + public void testBooleanFalse() { + jsonp.requestBoolean(echo("false"), new AssertSuccessCallback<Boolean>( + Boolean.FALSE)); + delayTestFinish(500); + } + + public void testBooleanTrue() { + jsonp.requestBoolean(echo("true"), new AssertSuccessCallback<Boolean>( + Boolean.TRUE)); + delayTestFinish(500); + } + + public void testDouble() { + jsonp.requestDouble(echo("123.456"), new AssertSuccessCallback<Double>( + 123.456)); + delayTestFinish(500); + } + + public void testFailureCallback() { + jsonp.setFailureCallbackParam("failureCallback"); + jsonp.requestString(echoFailure("ERROR"), + new AssertFailureCallback<String>("ERROR")); + delayTestFinish(500); + } + + public void testInteger() { + jsonp.requestInteger(echo("123"), new AssertSuccessCallback<Integer>(123)); + delayTestFinish(500); + } + + /** + * Tests that if no failure callback is defined, the servlet receives well + * only a 'callback' parameter, and sends back the error to it. + */ + public void testNoFailureCallback() { + jsonp.setFailureCallbackParam(null); + jsonp.requestString(echoFailure("ERROR"), + new AssertSuccessCallback<String>("ERROR")); + delayTestFinish(500); + } + + public void testNullBoolean() { + jsonp.requestBoolean(echo("null"), new AssertSuccessCallback<Boolean>(null)); + delayTestFinish(500); + } + + public void testNullDouble() { + jsonp.requestDouble(echo("null"), new AssertSuccessCallback<Double>(null)); + delayTestFinish(500); + } + + public void testNullInteger() { + jsonp.requestInteger(echo("null"), new AssertSuccessCallback<Integer>(null)); + delayTestFinish(500); + } + + public void testNullString() { + jsonp.requestString(echo("null"), new AssertSuccessCallback<String>(null)); + delayTestFinish(500); + } + + public void testString() { + jsonp.requestString(echo("'Hello'"), new AssertSuccessCallback<String>( + "Hello")); + delayTestFinish(500); + } + + public void testTimeout() { + jsonp.requestString(echoTimeout(), new AssertTimeoutException<String>()); + delayTestFinish(2000); + } + + public void testVoid() { + jsonp.send(echo(null), new AssertSuccessCallback<Void>(null)); + delayTestFinish(500); + } + + @Override + protected void gwtSetUp() throws Exception { + jsonp = new JsonpRequestBuilder(); + jsonp.setTimeout(1000); + } +} Property changes on: user/test/com/google/gwt/jsonp/client/JsonpRequestTest.java ___________________________________________________________________ Name: svn:mime-type + text/x-java Name: svn:eol-style + native