REST Web Services in the Cloud: Part 1

The Client Side: JavaScript, HTML/CSS and nginx

In the first post of this two part series, I looked at how to implement a cloud-based REST web service using JSON, Jersey/JDBC and the CloudBees Java PaaS platform.  In this second blog, I’d like to look at a simple AJAX-style browser client using a combination of JavaScript and HTML/CSS, served from a local nginx web/proxy server. As a reminder, here’s what the basic web service looks like when accessed directly from the browser:

CloudBees Java PaaS platform

Let’s start with the HTML page that I am using: it’s very simple, all the presentation elements (such as they are) are handled by the CSS stylesheet and all the work of retrieving the JSON-formatted data and turning it into simple HTML is handled by a piece of JavaScript that is called as soon as the browser loads the page:

<!DOCTYPE html>
<html>
  <head>
    <title>Countries</title>
    <link rel="stylesheet" type="text/css" href=/resources/stats.css />
    <script src="/resources/get-countries.js"></script>
    <script language="JavaScript" type="application/javascript">
      window.onload = getCountries();
    </script>
  </head>
  <body>
    <span id="stats"></span>
  </body>
</html>

The stylesheet is basic: I’ve re-used a simple scheme that just makes HTML tables a bit cleaner and more modern than the usual browser defaults:

#stats { font-family:Trebuchet MS, Arial, Helvetica, sans-serif; width:100%; border-collapse:collapse; }
#stats h3 { font-size:1.4em; border:none; margin-bottom:3px; }
#stats td, #stats th { font-size:1.2em; border:1px solid #696969; padding:3px 7px 2px 7px; }
#stats th { text-align:left; color:#fff; background-color:#808080; }
#stats tr.alt td { color:#000; background-color:#DCDCDC; }

In a later post, I’ll look at more interesting things you can do with JavaScript frameworks like JQuery and Dojo, but for now I hope this helps to illustrate what’s going on - I also like this clear separation between the roles of HTML, CSS and JavaScript. The JavaScript code in particular is closely based on the examples in David Flanagan’s outstanding book JavaScript - The Definitive Guide, which is one of the most highly-used books on my shelf.  BTW, all the code in this example is available on GitHub in case you want to try it out for yourself: please do - and, in so doing, you can get a CloudBees account for free!

Here’s the getCountries() function that gets called from the window.onload event in the HTML page. The basic idea is that the function makes an XMLHttpRequest (XHR) call to /mark/countries (two variants: one for IE and one for Chrome/Firefox/Safari/Opera, etc.) and this retrieves the raw JSON data from the web service and calls getCountriesJSON(), which does the work of constructing the final HTML page that is inserted into the page <body> using document.getElementById(“stats”) and innerHTML - recall that the top-level HTML doc simply has <body><span id=”stats”></span></body>.

function getCountries()
{
  var url = "/mark/countries";
  // AJAX code for Mozilla, Safari, Opera etc.
  if (window.XMLHttpRequest) {
    xmlhttp = new XMLHttpRequest();
    xmlhttp.open("GET", url);
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
        var countriesDoc = getCountriesJSON(xmlhttp.responseText);
        var elt = document.getElementById("stats");
        elt.innerHTML = countriesDoc;
      }
    };
    xmlhttp.send(null);
  }
  // AJAX code for IE
  else if (window.ActiveXObject)  {
    xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
    if (xmlhttp) {
      xmlhttp.open("GET", url);
      xmlhttp.onreadystatechange = function() {
        if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
          var countriesDoc = getCountriesJSON(xmlhttp.responseText);
          var elt = document.getElementById("stats");
          elt.innerHTML = countriesDoc;
        } 
      };
      xmlhttp.send(null);
    }
  }
}

Here’s the code for getCountriesJSON(): the XMLHttpRequest.ResponseText object that is passed as a parameter contains the raw JSON.  This gets loaded into a JS variable (countriesJSON) via an eval() call and then it’s a simple matter of iterating through the array to create the table rows:

function getCountriesJSON(response)
{
  var countriesJSON = eval('(' + response + ')');
  var countries = countriesJSON.countries;
  var countriesPage = "";
  countriesPage += "<table id=\"stats\">";
  countriesPage += "<th>Id</th><th>Country</th><th>Capital</th>";
  for (var j=0; j < countries.length; j++) {
    if (j%2 == 0) countriesPage += "<tr>";
    else countriesPage += "<tr class=\"alt\">";
    countriesPage += "<td>" + countries[j].id + "</td>"
                  + "<td>" + countries[j].country + "</td>"
                  + "<td>" + countries[j].capital + "</td>"
                  + "</tr>";
  }
  countriesPage += "</table>";
  return countriesPage;
}

Using nginx as a Web/Proxy Server

For now, I’m simply using a local nginx server to do double duty:

  1. As a Web Server hosting my HTML pages and CSS/JS resources

  2. As a Reverse Proxy Server for my cloud web services

I’ll look at various options for how you would handle this in production scenarios in a later post, as it’s an interesting subject that deserves its own discussion. For now, just note that the reverse proxy is important; otherwise I wouldn’t be able to make that XMLHttpRequest call, due to the security rules all browsers enforce on cross-domain scripting.  With a single web/proxy server serving the HTML pages, CSS/JS resources and back-end services, this simply isn’t an issue; otherwise I would have to use JSONP to make the XHR call.  More on that later.

Here’s the (very minimal) nginx configuration I use for testing:

worker_processes  1;
error_log  logs/error.log;
pid        logs/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
   default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  logs/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       8888;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location /mark {
                proxy_pass      http://jersey.mqprichard.cloudbees.net/mark;
        }
        location ~* \.(?:ico|css|js|gif|jpe?g|png|bmp|html) {
                root /Users/markprichard/www;
                expires max;
                add_header Pragma public;
                add_header Cache-Control "public, must-revalidate, proxy-revalidate";
        }
    }
}

The main things to note are: 

  1. I’m using ~markprichard/www as the docroot, with the JS/CSS resources in ~markprichard/www/resources. 

  2. The URL pattern /mark is proxied to  http://jersey.mqprichard.cloudbees.net/mark, which takes care of the XHR call to the cloud web service.  As far as the browser is concerned, this is going to the same host:port as the original request, which avoids the cross-domain scripting problem.

Here is what the final result looks like:

cloud web service

Cheers,

Mark Prichard, Senior Director of Product Management
CloudBees
www.cloudbees.com

Mark Prichard is Java PaaS Evangelist for CloudBees. He came to CloudBees after 13 years at BEA Systems and Oracle, where he was Product Manager for the WebLogic Platform. A graduate of St John’s College, Cambridge and the Cambridge University Computer Laboratory, Mark works for CloudBees in Los Altos, CA.  Follow Mark on Twitter and via his blog Clouds, Bees and Blogs.

CloudBees and Eclipse: The Video Series

You may also enjoy this video tour of CloudBees and Eclipse… 

Comments

public class HelloCountries extends HttpServlet - this can be a simple pojo and extends HttpServlet is not required.

I have tried this using ClickStart - Tomcat 7 and jax-rs-2.0. the build fails due to the following: [ERROR] /scratch/jenkins/workspace/sizingtool/src/main/java/com/xxxx/xxxx.java:[7,19] package javax.ws.rs does not exist. I have add these as part of my build path in Eclipse. Do I need to do anything specific to add these to my CloudBees deployment?

Add new comment