Wednesday, July 25, 2012

REST Web Services in the Cloud: Part 2

The Client Side: JavaScript, HTML/CSS and nginx

In the first post of this 2-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:






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 - 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 which 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";



  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);
  }
  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, but for now just note that the reverse proxy is important, as 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; however, 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:


Cheers, Mark









1 comment:

  1. đồng tâm
    game mu
    cho thuê phòng trọ
    cho thuê phòng trọ
    nhac san cuc manh
    tư vấn pháp luật qua điện thoại
    văn phòng luật
    số điện thoại tư vấn luật
    dịch vụ thành lập doanh nghiệp
    http://we-cooking.com/
    chém giócủa Nhạc thành, nàng cũng không dám tiếp tục tỉ thí với hắn nữa.

    - Tùy tiện đi, ngươi muốn tới đâu thì chúng ta tới đó, chúng ta đi chơi nửa năm rồi trở về học viện, ngươi thấy thế nào?

    Nhạc Thành cất tiếng hỏi.

    - Ở trong Quỷ Uyên khắp nơi đều hỗn loạn, ngươi cần biết rằng có nhiều nơi ngay cả ta cũng không dám tiến vào.

    Thanh âm Yêu Huyên truyền tới tai của Nhạc Thành:

    - Hiện tại ta đã tiến hóa, cũng nên tìm lão gia hỏa kia tính sổ, phỏng chừng một tháng sau ta sẽ trở về.

    - Cái gì, ngươi muốn tự mình đi báo thù, hay là để ta đi cùng với ngươi?

    Thanh âm của Nhạc Thành truyền tới tai của Yêu Huyên. Nhạc Thành cũng biết lần trước Yêu Huyên tới Song Tử đảo là tại vì bị một lão nhân cường giả đánh bị thương ở trong Quỷ Uyên.

    - Người mà ta muốn đối phó thực lực rất mạnh, chỉ sợ lúc đó ngươi chạy cũng không kịp.

    ReplyDelete