The postings on this site are the contributor's and don’t necessarily represent IBM’s positions, strategies or opinions.
January 28th, 2009 Alex Moffat Posted in GWT | 13 Comments »
This is a simple tutorial showing how to use GWT to call services provided by website B from code loaded from website A, there are other similar tutorials out there on the internet, this is mine. The example I’m going to build displays a user’s recent tweets in by calling the Twitter REST API and the complete source code for the example is linked at the end of the post.
You can’t use the straightforward GWT RequestBuilder in this situation as it uses XMLHttpRequest which is restricted by the same origin policy. I’m going to work around this restriction using three features of the HTML SCRIPT element.
First, the SCRIPT element will load JavaScript from anywhere. All you need to do it set the src attribute to the location of the script. So, if you have an API that returns JavaScript, generally JSON, then the SCRIPT element can be used to call that API. For example
<script src="http://twitter.com/statuses/user_timeline/alexmoffat.json"/>
will load my tweets. This is how we get the data from the server back to the client.
Second, SCRIPT elements can be created and added to the DOM from JavaScript. So to make a call all we need to do is create a SCRIPT element, set the src attribute to point to the correct url and insert the element into the DOM.
Third, when the JavaScript is returned from the server it is evaluated. However, this is where we need a little help from the server. Merely evaluating some JSON won’t have any effect on the client, it just creates an anonymous JavaScript object that you can’t get at. Further, there’s no easy way to know when the script has loaded so we don’t know when the API call has returned some data.
The solution for this is JSON with padding, JSONP. If instead of returning {”name”: “Alex”} the server returned fcn({”name”:”Alex”} then when the script was evaluated the JavaScript function fcn would be called on the client, and passed the JavaScript data. Provided we define a function called fcn on the client we’ll get notified when the API call returns, and we’ll be passed the data from the server.
Many APIs, including the Twitter REST API, provide a parameter to let you request a JSONP formatted response instead of a plain JSON one, and provide the name of the function that should be used. For Twitter this is “callback” so http://twitter.com/statuses/user_timeline/alexmoffat.json?callback=foo will return a JSONP response with a call to a function called foo that passes it the JSON data.
An actual implementation needs a couple of other pieces, I’ve adapted mine from the example in the GWT Get JSON via HTTP tutorial.
private static native void getJson(int requestId, String twitterUser, TweetsList handler) /*-{
// [0] The name of the callback function that will get called.
var callback = "callback" + requestId;
// [1] Create a script element.
var script = document.createElement("script");
script.setAttribute("src",
"http://twitter.com/statuses/user_timeline/" + twitterUser +
".json?page=1&callback=" + callback);
script.setAttribute("type", "text/javascript");
// [2] Add a callback function on the window object.
window[callback] = function(jsonObj) {
// [3] Call the handleJsonResponseMethod.
handler.@com.lombardi.gwtexample.twitter.client.TweetsList::handleJsonResponse(ILcom/google/gwt/core/client/JsArray;)(requestId, jsonObj);
window[callback + "done"] = true;
};
// [4] Wait up to 5 seconds for the call to return.
setTimeout(function() {
if (!window[callback + "done"]) {
handler.@com.lombardi.gwtexample.twitter.client.TweetsList::handleJsonResponse(ILcom/google/gwt/core/client/JsArray;)(requestId, null);
}
// [5] Cleanup. Remove script and callback elements.
document.body.removeChild(script);
delete window[callback];
delete window[callback + "done"];
}, 5000);
// [6] Attach the script element to the document body.
document.body.appendChild(script);
}-*/;
The getJson method takes three parameters, a request id, the name of the Twitter user who’s tweets we want to retrieve and an instance of the class that has the method that will be used to process the response from Twitter.
Step 0 is to create the name of the callback function that will be passed to the Twitter API. The requestId is used to build the function name so that we can have multiple calls in progress at once if we need to.
In step 1 the script element that will call the API is constructed. The name of the callback function is passed to the API call using the callback parameter. At this point we’ve not yet called the API and the callback function doesn’t yet exist.
Next, in step 2 the actual callback function is created and added to the window object. This is the function that will be executed when the JSONP data is returned by Twitter so it takes a single parameter, the JSON payload. In the body of the function, step 3, the JSON returned from Twitter is passed out of the native method and into the GWT code, I’m using overlay types for this, which I’ll say more about later on. Also as part of the callback function an additional property is added to the window to record the successful completion of the callback.
Step 4 schedules a cleanup function for execution after 5 seconds. The first thing this does is to check the property set by the callback function to see if the callback function has been executed. If it has not then the GWT method is called with a null value to indicate a failure. This is to cope with calls that fail to return in a reasonable amount of time. The rest of the cleanup function, step 5, removes the script from the document (it’s not been added yet, that’s next) and gets rid of the callback function and callback completion property.
Finally, in step 6, the script created in step 1 is added to the document, which will trigger the call to the server.
The handleJsonResponse method looks like
private void handleJsonResponse(int requestId, JsArray tweets) {
if (requestId == lastUsedRequestId) {
content.getElement().setInnerHTML("");
for (int i = 0, m = tweets.length(); i < m; i++) {
displayTweet(tweets.get(i).getText());
}
}
}
I'm using overlay types so that I can deal with the data from Twitter more easily. For instance the getText method of a Tweet object is defined as
public final native String getText() /*-{
return this.text;
}-*/;
which makes it possible for me to write nice Java code to access the data with no overhead or modification to object prototypes.
You can download the source code for the twitter xss example.
March 12th, 2009 at 12:46 am
Thanks for nice explanation. BTW is it possible to POST some data to a remote URL (Not from same origin)?
I think above explanation only works for GET requests.
We are trying to use POST. And do not have any solution yet.
BTW: We are heavily influenced by your use of GWT, and started our product using GWT.
http://blog.collaber.com/2009/02/28/collaber-on-web-early-screenshots/
Thank you for sharing the knowledge.
March 12th, 2009 at 9:33 am
I’m sorry but I don’t know of a way to do cross site POSTs. You’re right that the SCRIPT solution only works for GET requests. If you can proxy the request through a servlet (or other request handling mechanism) on your server then you could do it that way but that’s not a cross site call so it doesn’t really count
Glad you like the stuff we’ve done with GWT.
March 12th, 2009 at 6:17 pm
You can do cross-site posts using the window.name trick to return JSON data. See my post here: http://timepedia.blogspot.com/2008/07/cross-domain-formpanel-submissions-in.html
March 13th, 2009 at 8:33 am
Wow, and thanks. That’s really ingenious, I’ll have to give it a try.
May 26th, 2009 at 8:11 pm
thats great that you are talking about the twitter api,a good example of searching with the twitter api is on twiogle.com because you can search on twitter and google at the same time.
September 5th, 2009 at 4:48 pm
This is a great article. I just wonder if it’s possible to see the raw response from the remote server (in this case Twitter), on failure? Sometimes the handleJsonResponse() method reports that the ‘tweets’ array is null, meaning something went wrong on Twitter’s side I guess. Is there a way to see what error the Twitter servers responded with?
Thanks
September 5th, 2009 at 8:55 pm
Interesting question. The handleJsonResponse is called either when the server responds and the JavaScript it responds with executes or when the timeout fires. Either case could give you null for the tweets array I guess but it’s impossible to tell with the code as it is which case you’ve encountered. I suggest either adding another parameter to handleJsonResponse to indicate whether the window callback function was called or the timeout fired or using a calling a different method on timeout. Anyway, the plan would be to somehow distinguish the timeout from a valid but empty response. Then you could try in the timeout case looking at the contents of the script element to see if you can find any info there. Of course it may just be that twitter is being slow and the timeout value is too short
December 13th, 2009 at 3:31 pm
[...] how to “fix it“, the bad part is make the JavaScript code for that. I’m using the code of a guy that already create a twitter example in gwt. I change the code a little bit, one class to make the [...]
December 13th, 2009 at 3:32 pm
[...] how to “fix it“, the bad part is make the JavaScript code for that. I’m using the code of a guy that already create a twitter example in gwt. I change the code a little bit, one class to make the [...]
December 22nd, 2009 at 6:41 pm
I am a GWT newbie and have not done much of DHTML.
I read this as well as the one that supports post method.
I have a cross-site requirement where I want to only display Reports/charts. No data is required by the client app.
Would a simple iframe serve my purpose?
December 22nd, 2009 at 9:50 pm
Very likely yes. You could do this by setting or changing the src of the iframe. This would work fine.
December 23rd, 2009 at 3:51 pm
Thanks. I was reading GWT 2.0 Docs and found this link on Crosssite with JSONP.
http://code.google.com/webtoolkit/doc/latest/tutorial/Xsite.html
Does that mean it has been taken care by the API?
December 23rd, 2009 at 9:17 pm
It’s not handled transparently by the API. The stuff I wrote in this post is pretty much a version of the tutorial you link to. I use the twitter api because it’s a nice api that’s readily available for use so you don’t have to go to the trouble the tutorial does to set up your own external server.