Archive for January, 2009

MapQuest Proxy for Jaxer

Friday, January 16th, 2009

If you’re a MapQuest developer chances are you’re pretty handy with JavaScript on the client. It’s been the sad case that you leave these skills behind when you switch over to server side programming. Well, happy day, things are changing as support for Server Side JavaScript (SSJS) takes off. With SSJS you can break out your JavaScript Ninja skills on the server side. I’ve been looking at the Jaxer SSJS platform and worked up this example to show off some of the possibilities.

With the MapQuest JavaScript API one can get tiled maps on a page with only client JavaScript. As soon as you use other MapQuest Services such geocoding, routing, or search, you need to supply a proxy on the web server. The proxy is need to allow the MapQuest client library to call home while avoiding the browser’s same-domain security policy.

The example I’m presenting here is an implementation of such a proxy for the Jaxer platform. I think a SSJS proxy is an illuminating and useful example. It’s a useful example since you need a proxy for putting MapQuest and Jaxer together. Also, it’s an illuminating example since it is a typical server side task, fetching data from a different source. The source could be files, in database, RSS feeds, or, as in this case, a server in a different domain. MapQuest (MQ) supplies proxies in common languages such PHP, Java, etc., so this example will also allow comparison of the SSJS implementation to implementations in other languages.

The example consists of two HTML pages, one of which is served to the client and one which runs on the Jaxer server and acts as the proxy.

The Client Side

The client page is a hello world type of example for MapQuest – create a tiled map, geocode an address, and place a Point of Interest marker at the geocoded address. The complete source code for this example is available here. Here’s a screenshot of the client page in action:

The text boxes on the right are a debug log provided the MQ client library that shows interaction between the MQ client library and the proxy. Specifically, the box labeled “Request URL” is address where the MapQuest client library is configured to find the proxy. The Request XML box shows the data the client library is sending, which in this case is a request for geocoding per the MQ API (see the MQ XML Interface Reference). The “Response XML” is the XML returned by the MapQuest geocoding service via the proxy.

Here’s the startMap() method that is called when the client page loads:

<script src="http://btilelog.access.mapquest.com/tilelog/transaction?transaction=script&amp;amp;amp;amp;amp;key=your-MQ-key=true&amp;amp;amp;amp;amp;v=5.3.s&amp;amp;amp;amp;amp;ipkg=controls1" type="text/javascript"></script>
<script src="lib/mapquest/mqcommon.js"></script>
<script src="lib/mapquest/mqutils.js"></script>
<script src="lib/mapquest/mqobjects.js"></script>
<script src="lib/mapquest/mqexec.js"></script>
<script>
var g_proxyServerName = 'localhost';
var g_proxyServerPort = '8000';
var g_proxyServerPath = 'jaxer-mapquest/proxy.html'

var g_serverName = 'geocode.dev.mapquest.com';
var g_serverPort = '80';
var g_serverPath = 'mq';

var g_geoExec = new MQExec(g_serverName, g_serverPath, g_serverPort, g_proxyServerName, g_proxyServerPath, g_proxyServerPort);

function startMap(){
var g_mqMap = new MQA.TileMap(document.getElementById('mapWindow'), 2, new MQA.LatLng(40, -95), "map");

var address = new MQAddress();
address.setCity('Gobles');
address.setState('MI');
address.setCountry('USA');

var gaCollection = new MQLocationCollection("MQGeoAddress");
g_geoExec.geocode(address, gaCollection);
var mqAddress = gaCollection.get(0);

var poi = new MQA.Poi(mqAddress.getMQLatLng());
poi.setInfoTitleHTML('Hello World');
poi.setInfoContentHTML('From Gobles, MI');
g_mqMap.addPoi(poi);
}
</script>

The MQExec object interacts with the proxy. The location of the proxy on localhost is specified as well as the details of the proxied MapQuest server (geocode.dev.mapquest.com). When the MQ client library needs to call home for geocoding services (via g_geoExec.geocode()), it will form a GET or POST with the request details and send the request to http://localhost:8000/jaxer-mapquest/proxy.html.

The Server Side

The sever side proxy is implemented in a page named proxy.html. The page receives the post from the MQ client library, extracts the data, forwards it along to the MQ server, and then returns the MQ response back to the client.

The HTML page, proxy.html, is processed by the Jaxer server. We’ll use a slightly different processing model than the typical Jaxer page lifecycle. In the typical lifecycle a page is parsed on the Jaxer server into a DOM model, scripts that are tagged to be ran on the server are invoked, client side proxies are mixed-in for scripts that are tagged to be proxied, and then the page is serialized and sent off to the client. In this case, we use server side scripts to overwrite the the DOM on the server side prior to the serialization for returning the page to the client.

This is a handy Jaxer technique. Since you set the content type and the response content you can pull stunts like serializing arbitrary JavaScript objects and returning them as JSON to the client. In this case, I’m overwriting the HTML page and returning the content and content type from the map quest server (XML). To me, this technique is the more similar to a J2EE Servlet than the typical Jaxer lifecycle, which feels more like a JSP.

The proxy.html page consist primarily of JavaScript that get executes in the server context. The server side script execution is triggered with a onserverload tag:

<body onserverload="BCC.mqproxy.proxify();">

The proxify() method shows the general steps that the proxy executes:

proxify: function(){
parseClientRequest();
makeMQRequest();
replyToClient();
}

In order to overwrite the page’s DOM with arbitrary content, one calls the Jaxer.Response.setContent() method. In this case, we’ll overwrite the page’s entire DOM with the response and content type obtained from the MQ response. The property Jaxer.response is an instance of Jaxer.Response that points to the current response:

var replyToClient = function(){
Jaxer.response.headers['Content-Type'] = mqResponse.headers['Content-Type'];
Jaxer.response.setContents(mqResponse.text);
}

And that’s all it takes to send back arbitrary content from a Jaxer HTML page.

Before we can reply to the client, this page has to parse the client request and call the server at MapQuest. The following function reads the data posted by the client from Jaxer.request (an instance of Jaxer.Request):

var parseClientRequest = function(){
getMqServerUrlParams();
if (Jaxer.request.method == 'POST') {
getPostData();
}
else {
getUrlParms();
}
};

The MQ client sends the target server, port and path as URL parameters and the getMQServerUrlParams() method picks these off. Jaxer makes the parsed URL available as a property of the request object:

var serverParams = {
sname: '',
sport: '',
spath: ''
};

var getMqServerUrlParams = function(){
logger.debug('getMqServerUrlParams');
for (param in serverParams) {
serverParams[param] = Jaxer.request.parsedUrl.queryParts[param];
logger.debug('getMqServerUrlParams: ' + param + ': ' + serverParams[param]);
}
};

The remainder of the request data comes in as either other URL parameters or as POSTed XML. In the case of XML data, the proxy needs to add a couple of XML elements for a client ID and password. Here’s the routine to get the XML from the request, parse it, modify the DOM and serialize back to XML:


// Fetch the XML form data and add credentials
var getPostData = function(){
logger.debug('getPostData');
logger.debug('getPostData: postdata:          ' + Jaxer.request.postData);

var doc = new DOMParser().parseFromString(Jaxer.request.postData, 'text/xml');
if (doc.documentElement.nodeName == "parsererror") {
throw new Error("People we have an issue: XML parse error");
}
var authenticalNodeList = doc.documentElement.getElementsByTagName("Authentication");
if (authenticalNodeList.length > 0) {
authenticatedRequest = true;
var authenticalNode = authenticalNodeList[0];

// add password
var passwordEl = doc.createElement('Password');
var passwordTextEl = doc.createTextNode(mqPassword);
authenticalNode.appendChild(passwordEl).appendChild(passwordTextEl);

// add client ID
var clientIdEl = doc.createElement('ClientId');
var clientIdTextEl = doc.createTextNode(mqClientId);
authenticalNode.appendChild(clientIdEl).appendChild(clientIdTextEl);
}

// back to a string
postData = new XMLSerializer().serializeToString(doc);
logger.debug('getPostData: fixed up postdata: ' + postData);
};

Notice that there is no cross-browser monkey business to get a parser and serializer. On the server side, Jaxer uses Mozilla so you can count the available features. This example also makes use of the Jaxer.Log facility which lets you write to server side logs and specify the level of logging detail.

The last code snippet show how to place a synchronous server side HTTP request with Jaxer.Web.send(). The URL is assembled and a POST or GET is made per the client request:

var makeMQRequest = function(){
mqUrl = 'http://' + serverParams.sname + ':' + serverParams.sport + '/' + serverParams.spath;
sendOptions.extendedResponse = true;
if (Jaxer.request.method == 'POST') {
if (authenticatedRequest == true) {
mqUrl += '/mqserver.dll?e=5';
}
sendOptions.contentType = 'application/x-www-form-urlencoded';
}
else { // GET
var urlQParams = '';
for (p in urlParams) {
urlQParams += p + '=' + urlParams[p];
}
if (urlParams.length > 0) {
mqUrl += '?' + urlQParams;
}
}
// Call home
mqResponse = Jaxer.Web.send(mqUrl, Jaxer.request.method, postData, sendOptions);
};

Wrap up

This example shows how to proxy web content in other domains using Jaxer. The same basic approach can be used to access other server side resources (files, database, etc).

I’m a big fan of scripting languages, and JavaScript is my current favorite. I’ve been using it not only in web browsers but also as domain specific language in a Business Process Modeling application I’m working with. I’m at the peak of my JavaScript skills, so, I’m really happy to be able to break out JavaScript on the server side.