Google Code offered in: English - Español - 日本語 - 한국어 - Português - Pусский - 中文(简体) - 中文(繁體)
At this point, you've modified the initial implementation of the StockWatcher application, which simulated stock data in the client-side code. The current implementation now retrieves JSON-formatted data from your local server.
In this session, you'll make a call to a remote server instead. To do so you will have to work around SOP (Same Origin Policy) constraints.
This tutorial builds on the GWT concepts and the StockWatcher application created in the Getting Started tutorial. If you have not completed the Getting Started tutorial and are familiar with basic GWT concepts, you can import the StockWatcher project as coded to this point.
File menu, select the Import... menu option.Next button.Finish button.
If you are using ant, edit the gwt.sdk property in StockWatcher/build.xml to point to where you unzipped GWT.
In order to actually run this tutorial, you will need either access to a server other than where StockWatcher is running, one that can run a PHP script, or be able to run a Python script on your machine.
As you modify the current implementation of StockWatcher to access data on a remote server, there are two issues to address:
The Same Origin Policy (SOP) is a browser security measure that restricts client-side JavaScript code from interacting with resources not originating from the same domain name, protocol and port. The browser considers two pages to have the same origin only if these three values are the same. For example, if the StockWatcher application is running in a web page on http://abc.com:80 it cannot interact with stock data loaded from a different domain, http://xyz.com. It can't even load stock data from the same domain if the port is different, for example, http://abc.com:81.
The idea behind SOP is that, for security reasons, the browser should not trust content loaded from arbitrary websites. A malicious web page could inject code that steals data or otherwise compromises security.
As such, for accessing JSON-formatted stock data from another domain or port, the current implementation will not work. The web browser will block the HTTP call to retrieve the JSON.
For a more detailed description of SOP and its effect on GWT, read What is the Same Origin Policy, and how does it affect GWT?
There are two options for working around SOP security:
The first option is to play by the rules of SOP and create a proxy on your local server. You can then make HTTP calls to your local server and have it go fetch the data from the remote server. This works because the code running on your web server is not subject to SOP restrictions. Only the client-side code is.
Specifically for the StockWatcher application, you could implement this strategy by writing server-side code to download (and maybe cache) the JSON-encoded stock quotes from a remote server. You could then use any mechanism we want for retrieving the data from the local server: GWT RPC or direct HTTP using RequestBuilder.
One downside to this approach is that it requires additional server-side code. Another is that the extra HTTP call increases the latency of remote calls and adds to the workload on our web server.
Another option is to dynamically load JavaScript into a <script> tag. Client-side JavaScript can manipulate <script> tags, just like any other element in the HTML Document Object Model (DOM). Client-side code can set the src attribute of a <script> tag to automatically download and execute new JavaScript into the page.
This strategy is not subject to SOP restrictions. So you can effectively use it to load JavaScript (and therefore JSON) from remote servers.
This is the strategy you'll use to get the JSON-formatted stock data from a remote server.
Dynamically loading the JavaScript into a <script> tag solves the SOP issue but introduces another. When you use this method to load JavaScript, although the browser retrieves the code asynchronously, it doesn't notify you when it's finished. Instead, it simply executes the new JavaScript. However, by definition, JSON cannot contain executable code. Put the two together and you'll realize that you can't load plain JSON data using a <script> tag.
To resolve the callback issue, you can specify the name of a callback function as an input argument of the call itself. The web server will then wrap the JSON response in a call to that function. This technique is called JSON with Padding (JSONP). When the browser finishes downloading the new contents of the <script> tag, the callback function executes.
callback125([{"symbol":"DDD","price":10.610339195026,"change":0.053085447454327}]);
Google Data APIs support this technique.
For StockWatcher, the additional requirement in the client-side code is that you include the name of the JavaScript function you're using as a callback in the HTTP request .
Now that you understand the SOP issues surrounding cross-site requests, compare this implementation to the implementation for getting JSON data from a local server. You'll have to change some of the existing implementation but you'll be able to reuse some components as well. Most of the work will be in writing the new method, getJSON, which makes the call to the remote server.
| Task | Same-Site Implementation | Cross-Site Implementation |
|---|---|---|
| Making the call | HTTP with Request Builder | Embed a script whose src attribute is the URL of the JSON data with the name of the callback function appended. |
| Server-side code | Returns JSON string | Returns a Javascript callback function with the JSON string |
| Handling the response | Use JavaScript eval() function to turn JSON string into JavaScript object | Already a JavaScript object; cast it as a StockData array |
| Data objects | Create an overlay type: StockData | Reuse the overlay type |
| Handle Errors | Create a Label widget to display error messages | Reuse the Label widget |
In this tutorial, you have two options for setting up the stock data so that StockWatcher encounters SOP restrictions.
If you have access to a web server, then you can use the following PHP script to return the JSONP.
stockPrices.php
<?php
header('Content-Type: text/javascript');
header('Cache-Control: no-cache');
header('Pragma: no-cache');
define("MAX_PRICE", 100.0); // $100.00
define("MAX_PRICE_CHANGE", 0.02); // +/- 2%
$callback = trim($_GET['callback']);
echo $callback;
echo '([';
$q = trim($_GET['q']);
if ($q) {
$symbols = explode(' ', $q);
for ($i=0; $i<count($symbols); $i++) {
$price = lcg_value() * MAX_PRICE;
$change = $price * MAX_PRICE_CHANGE * (lcg_value() * 2.0 - 1.0);
echo '{';
echo "\"symbol\":\"$symbols[$i]\",";
echo "\"price\":$price,";
echo "\"change\":$change";
echo '}';
if ($i < (count($symbols) - 1)) {
echo ',';
}
}
}
echo ']);';
?>
http://[www.myStockServerDomain.com]/stockPrices.php?q=ABC[{"symbol":"ABC","price":81.284083,"change":-0.007986}]http://[www.myStockServerDomain.com]/stockPrices.php?q=ABC&callback=callback125callback125([{"symbol":"ABC","price":53.554212,"change":0.584011}]);
If you do not have access to a remote server, but have Python installed on your local machine, you can simulate a remote server. If you make an HTTP request to a different port, you will hit the same SOP restrictions as if you were trying to access a different domain.
Use the following script to serve data from a different port on your local machine. For each stock symbol the Python script generates random price and change values in JSON format. Notice in the BaseHTTPServer.HTTPServer constructor that it will be running on port 8000. Also notice that the script supports the callback query string parameter.
quoteServer.py
#!/usr/bin/env python2.4
#
# Copyright 2007 Google Inc. All Rights Reserved.
import BaseHTTPServer
import SimpleHTTPServer
import urllib
import random
MAX_PRICE = 100.0
MAX_PRICE_CHANGE = 0.02
class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def do_GET(self):
form = {}
if self.path.find('?') > -1:
queryStr = self.path.split('?')[1]
form = dict([queryParam.split('=') for queryParam in queryStr.split('&')])
body = '['
if 'q' in form:
quotes = []
for symbol in urllib.unquote_plus(form['q']).split(' '):
price = random.random() * MAX_PRICE
change = price * MAX_PRICE_CHANGE * (random.random() * 2.0 - 1.0)
quotes.append(('{"symbol":"%s","price":%f,"change":%f}'
% (symbol, price, change)))
body += ','.join(quotes)
body += ']'
if 'callback' in form:
body = ('%s(%s);' % (form['callback'], body))
self.send_response(200)
self.send_header('Content-Type', 'text/javascript')
self.send_header('Content-Length', len(body))
self.send_header('Expires', '-1')
self.send_header('Cache-Control', 'no-cache')
self.send_header('Pragma', 'no-cache')
self.end_headers()
self.wfile.write(body)
self.wfile.flush()
self.connection.shutdown(1)
bhs = BaseHTTPServer.HTTPServer(('', 8000), MyHandler)
bhs.serve_forever()
python quoteServer.pyhttp://localhost:8000/?q=ABC[{"symbol":"ABC","price":81.284083,"change":-0.007986}]http://localhost:8000/?q=ABC&callback=callback125callback125([{"symbol":"ABC","price":53.554212,"change":0.584011}]);
Now that you've verified that the server is returning stock data either as a JSON string or as JSONP, you can update StockWatcher to request and then handle the JSONP. The RequestBuilder code is replaced by a call to the getJson method. The first parameter is an ID number that uniquely identifies each HTTP request.
Update the query URL so that it includes both the stock codes your are querying and the name of the callback function. Each callback function will have a unique ID number.
This is the only difference that results in the implementation depending on whether the data is being served from a different domain or a different port.
private static final String JSON_URL = GWT.getModuleBaseURL() + "stockPrices?q=";
private static final String JSON_URL = "http://localhost:8000/?q=";
private static final String JSON_URL = "http://www.myStockServerDomain.com/stockPrices.php?q=";
private Label errorMsgLabel = new Label();
private int jsonRequestId = 0;In the same-site implementation, the JSON URL was appended with the stock codes and then a HTTP GET request sent to the server. RequestBuilder was used to construct the HTTP request.
In the cross-site implementation, the JSON URL is wrapped in the callback method. The RequestBuilder code is replaced by a JSNI function, getJSON(int, String, StockWatcher). The first parameter is an ID number that uniquely identifies each HTTP request.
The getJSON function creates a JavaScript script which makes the call to the server.
/**
* Generate random stock prices.
*/
private void refreshWatchList() {
if (stocks.size() == 0) {
return;
}
String url = JSON_URL;
// Append watch list stock symbols to query URL.
Iterator<String> iter = stocks.iterator();
while (iter.hasNext()) {
url += iter.next();
if (iter.hasNext()) {
url += "+";
}
}
// Append the name of the callback function to the JSON URL.
url = URL.encode(url) + "&callback=";
// Send request to server by replacing RequestBuilder code with a call to a JSNI method.
getJson(jsonRequestId++, url, this);
}
// Send request to server and catch any errors.
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
try {
Request request = builder.sendRequest(null, new RequestCallback() {
public void onError(Request request, Throwable exception) {
displayError("Couldn't retrieve JSON");
}
public void onResponseReceived(Request request, Response response) {
if (200 == response.getStatusCode()) {
updateTable(asArrayOfStockData(response.getText()));
} else {
displayError("Couldn't retrieve JSON (" + response.getStatusText()
+ ")");
}
}
});
} catch (RequestException e) {
displayError("Couldn't retrieve JSON");
}
What follows is the most important change in this implementation. This is where you work around the SOP restrictions by using the src attribute of the <script> tag to make the call to the remote server.
This JSNI method (getJSON) creates a dynamically-loaded <script> element. The src attribute is the URL of the JSON data with the name of a callback function appended. When the script executes, it fetches the padded JSON; the JSON data is passed as an argument of the callback function. When the callback function executes, it calls the Java handleJsonResponse method and passes it the JSON data as a JavaScript object.
Not only does this implementation include some embedded handwritten JavaScript (JSNI) but it uses a bridge method, a technique for calling back into the Java source code during development. (Remember, when you compile StockWatcher, all the client-side Java code is compiled into JavaScript.)
/**
* Make call to remote server.
*/
public native static void getJson(int requestId, String url,
StockWatcher handler) /*-{
var callback = "callback" + requestId;
// [1] Create a script element.
var script = document.createElement("script");
script.setAttribute("src", url+callback);
script.setAttribute("type", "text/javascript");
// [2] Define the callback function on the window object.
window[callback] = function(jsonObj) {
// [3]
handler.@com.google.gwt.sample.stockwatcher.client.StockWatcher::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(jsonObj);
window[callback + "done"] = true;
}
// [4] JSON download has 1-second timeout.
setTimeout(function() {
if (!window[callback + "done"]) {
handler.@com.google.gwt.sample.stockwatcher.client.StockWatcher::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(null);
}
// [5] Cleanup. Remove script and callback elements.
document.body.removeChild(script);
delete window[callback];
delete window[callback + "done"];
}, 1000);
// [6] Attach the script element to the document body.
document.body.appendChild(script);
}-*/;This implementation generates callback function names sequentially in case of multiple pending requests. In particular, notice the syntax used to call the handleJsonResponse(JavaScriptObject) method:
handler.@com.google.gwt.sample.stockwatcher.client.StockWatcher::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(jsonObj);
You can see that once the JSON object is downloaded, the callback in the JSNI method is really just delegating to the Java method handleJsonResponse.
Calling Java methods from JavaScript is somewhat similar to calling Java methods from C code in JNI. In particular, JSNI borrows the JNI mangled method signature approach to distinguish among overloaded methods. JavaScript calls into Java methods are of the following form:
[instance-expr.]@class-name::method-name(param-signature)(arguments)
| Component | Description | Example |
|---|---|---|
| [instance-expr.] | Must be present when calling an instance method and must be absent when calling a static method | handler. (StockWatcher object instance) |
| @class-name | The fully-qualified name of the StockWatcher class | @com.google.gwt.sample.stockwatcher.client.StockWatcher |
| ::method-name | The name of the method we're calling | ::handleJsonResponse |
| (param-signature) | The handleJsonResponse method signature, defined with the JNI syntax | (Lcom/google/gwt/core/client/JavaScriptObject;) |
| (arguments) | The jsonObj containing the JSON data | (jsonObj) |
For more information on manipulating Java objects from within the JavaScript implementation of a JSNI method, see the Developer's Guide, Accessing Java Methods and Fields from JavaScript.
At this point most of your work is done. The one difference is that the return value is already a JavaScript object, not a JSON string. Thus, in the asArrayOfStockData method you no longer have to use the JavaScript eval() function to convert it.
If you receive a response from the server, then call the updateTable method to populate the Price and Change fields. You will still use the overlay type (StockData) and the JsArray (asArrayOfStockData) that you wrote in the same-site implementation.
If a response does not come back from the server, you display a message. You can use the same displayError method and Label widget you created in the same-site implementation.
/**
* Handle the response to the request for stock data from a remote server.
*/
public void handleJsonResponse(JavaScriptObject jso) {
if (jso == null) {
displayError("Couldn't retrieve JSON");
return;
}
updateTable(asArrayOfStockData (jso));
}
import com.google.gwt.core.client.JavaScriptObject;
In this implementation the response is a JavaScript object, not a string. Your next step is to modify the asArrayOfStockData method. Rather than convert the JSON string into a JavaScript array, it needs to cast the JavaScript object returned from the JSNI method as an array of StockData.
/**
* Convert the string of JSON into JavaScript object.
*/
private final native JsArray<StockData> asArrayOfStockData(String json) /*-{
return eval(json);
}-*/;
/**
* Cast JavaScriptObject as JsArray of StockData.
*/
private final native JsArray<StockData> asArrayOfStockData(JavaScriptObject jso) /*-{
return jso;
}-*/;
Whether you chose to serve the JSON-formatted stock data from a different domain or a different port, the new StockWatcher implementation should work around any SOP access restrictions and be able to retrieve the stock data.
python quoteServer.pyBefore you implement mashups of your own, remember that downloading cross-site JSON is powerful, but can also be a security risk. Make sure the servers you interact with are absolutely trustworthy, because they will have the ability to execute arbitrary JavaScript code within your application. Take a minute to read Security for GWT Applications, which describes the potential threats to GWT applications and how you can combat them.