July 1, 2012
filed in the early afternoon by dr_who in: hacking

the other day i tried to come up with a minimalistic Ajax setup — minimalistic in the sense of minimal coding overhead. my ingredients of choice were jquery and JavaScript on the client side and python on the server side. the goal was to use both GET and POST REST calls for obtaining the dynamic data and utilize JSON as the data format.

the python server code makes use of python’s BaseHTTPServer.HTTPServer and BaseHTTPServer.BaseHTTPRequestHandler classes:

AjaxServer extends BaseHTTPServer.HTTPServer and supplies Handler which in turn extends BaseHTTPServer.BaseHTTPRequestHandler. the Handler class also implements the GET and POST REST interfaces.

the server supports

  • text/html GETs on file names from the document root, retrieving index.html for GETs on directories, and
  • application/json GETs on
  • /ajax/time returning the current time
  • /ajax/processcount returning the number of currently running processes
  • application/json POSTs on any URL returning the length of the posted content

the client is a simple HTML5 page that uses jquery’s ajax() support to invoke the GET REST and POST REST calls on our python server.

client code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>AJAX sandbox</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="not found document">
    <meta name="author" content="hud@zurich.ibm.com">
    <link href="css/bootstrap.css" rel="stylesheet">
    <style>
      body {
      padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
      }
    </style> 

    <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
    <!--[if lt IE 9]>
        <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->
  </head>
  <body id="home">
    <div>OK, let's go!</div>

    <div>current time: <span id="ajax-datetime">waiting for time update...</span></div>

    <div>current process count: <span id="ajax-processcount">waiting for process count update...</span></div>

    <div><input id="typehere" type="text" value="type here" /></div>
    <div id="ajax-processedinput">waiting for result</div>

    <!-- JavaScript at the bottom to speed up page loading -->
    <script src="/js/jquery-1.7.2.min.js"></script>
    <script src="/js/bootstrap.min.js"></script>
    <script src="/js/ajax-datetime.js"></script>
    <script src="/js/ajax-processcount.js"></script>
    <script src="/js/ajax-processinput.js"></script>
  </body>
</html>

the HTML5 code itself doesn’t really do anything. the magic happens in the ajax-datetime.js, ajax-processcount.js, ajax-processinput.js javascripts:

ajax-datetime.js calls the /ajax/time GET REST method every 3 seconds and sets the content of the #ajax-datetime div element to the return value of the GET REST call:

$(document).ready(function() { 
    function updateDateTime() {
        $.ajax({
            cache: false,
            url: "/ajax/time",
            type: "GET",
            contentType: "application/json"
        }).done(function(json) {
            $("#ajax-datetime").html(json.time);
        });
    }

    setInterval(updateDateTime, (3 * 1000));
});

pretty much the same procedure is used for ajax-processcount.js:

$(document).ready(function() { 

    function updateProcessCount() {
        $.ajax({
            cache: false,
            url: "/ajax/processcount",
            type: "GET",
            contentType: "application/json"
        }).done(function(json) {
            $("#ajax-processcount").html(json.processcount);
        }).error(function(json) {
            $("#ajax-processcount").html("connection to server lost...");
        });
    }

    setInterval(updateProcessCount, (5 * 1000));
});

the only difference is that ajax-processcount.js invokes the /ajax/processcount GET REST method and does so every 5 seconds — demonstrating the asynchronous nature of AJAX rather nicely.

while the previous two javascripts used GET to retrieve information from our python server, the next, ajax-processinput.js attaches itself to the #typehere text input field and POSTs the content of the field to /ajax/processinput, returning the result of the POST REST call via the #ajax-processedinput div element:

$(document).ready(function() { 

    function processInput() {
        $.ajax({
            cache: false,
            url: "/ajax/processinput",
            type: "POST",
            contentType: "application/json",
            dataType: "json",
            data: JSON.stringify({ "input": $("#typehere").val() })
        }).done(function(json) {
            $("#ajax-processedinput").html(json.processed);
        }).error(function(json) {
            $("#ajax-processedinput").html("connection to server lost...");
        });
    }

    $("#typehere").change(function () {
        processInput();
    });
});

unfortunately, even though i tell ajax(...) that i’m sending JSON — the contentType: "application/json" line — jquery will not convert the data i’m sending into JSON and i need to explicitly turn it into JSON myself:

...
data: JSON.stringify({ "input": $("#typehere").val() })
...

server code

the server code consists of two modules: ajaxserver and handler.

handler contains the Handler class which extends BaseHTTPServer.BaseHTTPRequestHandler and implements the GET and POST REST calls explained earlier:

#!/usr/bin/env python2.6
# -*- encoding: utf-8 -*- 

'''
handler.Handler is the HTTP handler for ajaxserver
'''

import BaseHTTPServer
import logging as log
import os
import subprocess

try:
    import simplejson as json
except ImportError:
    import json
    
from datetime import datetime
    
class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
        
    def logRequest(s):
        log.debug("%s: %s/%s" %(s.client_address, s.command, s.path))
        
    def do_GET(self):
        self.logRequest()
        
        path = self.path
        
        if 'Content-type' in self.headers:
            contentType = self.headers['Content-type']
        else:
            contentType = 'text/html'
            
        log.debug("ajaxserver.GET: Path %s" % path)
        log.debug("ajaxserver.GET: Content-type %s" % contentType)
            
        if contentType.startswith('text/html'):
            (respCode, respContent, respContentType) = self.do_GET_HTML()
        elif contentType.startswith('application/json'):
            (respCode, respContent, respContentType) = self.do_GET_JSON()
                
        self.send_response(respCode, "thank you")
        self.send_header('Content-type', respContentType)
        self.send_header('Content-length', len(respContent))
        self.end_headers()
                
        self.wfile.write(respContent)
                
    def do_POST(self):
        self.logRequest()
                
        path = self.path
        contentType = self.headers['Content-type']
                
        log.debug("ajaxserver.POST: Path %s" % path)
        log.debug("ajaxserver.POST: Content-type %s" % contentType)
                
        if not contentType.startswith('application/json'):
            self.send_response(404, "oh, shiny!")
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({ "error": "only accepting application/json" }))
            return
                
        contentLength = int(self.headers['Content-length'])
        content = self.rfile.read(contentLength)
                
        log.debug("ajaxserver.POST: Content-length %d" % contentLength)
        log.debug("ajaxserver.POST: Content: %s" % content)
                
        data = json.loads(content)
        if 'input' not in data:
            log.debug("ajaxserver.POST: missing input parameter: %s" % content)
            self.send_response(404, "oh, shiny!")
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({ "error": "missing input parameter" }))
            return
                
        self.send_response(200, "oh, shiny!")
        self.send_header('Content-type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({ "processed": len(data['input'])}))
                
        return
                
    def do_GET_HTML(self):
                    
        docRoot = self.server.docRoot
        docPath = "%s%s" % (docRoot, self.path)
        errorPath = "%s/not-found.html" % docRoot
                    
        if self.path.startswith('../'):
            log.debug('ajaxserver.GET(HTML): "%s" trying to escape from the sandbox' % self.path)
            docFile = open(errorPath, 'r')
            code = 404
        elif docPath.endswith('/') and os.path.exists("%sindex.html" % docPath):
            log.debug('ajaxserver.GET(HTML): serving %sindex.html for %s' % (docPath, self.path))
            docFile = open("%sindex.html" % docPath, 'r')
            code = 200
        elif os.path.exists(docPath):
            log.debug('ajaxserver.GET(HTML): serving %s for %s' % (docPath, self.path))
            docFile = open(docPath, 'r')
            code = 200
        else:
            log.debug('ajaxserver.GET(HTML): no document found for %s' % self.path)
            docFile = open(errorPath, 'r')
            code = 404
            
        doc = docFile.read()
        docFile.close()
                        
        return (code, doc, 'text/html')
                    
    def utilProcessCount(self):
        psCmd = subprocess.Popen(["ps", "ax"], stdout=subprocess.PIPE)
        wcCmd = subprocess.Popen(["wc", "-l"], stdin=psCmd.stdout, stdout=subprocess.PIPE)
        count = wcCmd.communicate()[0]
                        
        return json.dumps({ 'processcount': int(count) - 1 })
                    
    def do_GET_JSON(self):
                        
        if self.path.startswith('/ajax/time'):
            return (200, '{ "time": "%s" }' % datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC'), 'application/json')
                        
        if self.path.startswith('/ajax/processcount'):
            return (200, self.utilProcessCount(), 'application/json')
                        
        return (200, '{ "resp": "bark!" }', 'application/json')

ajaxserver contains the AjaxServer class which extends the BaseHTTPServer.HTTPServer class and is the driver of my simple python web server. it also contains the setup code to instantiate the server and start it:

#!/usr/bin/env python2.6
# -*- encoding: utf-8 -*- 

'''
ajaxServer is a simple proof-of-concept web server for serving AJAX pages and their contents.
'''

import BaseHTTPServer
import logging as log
import logging.handlers
import os
import re
import subprocess
import sys
import urllib2

import handler

from datetime import date

class AjaxServer(BaseHTTPServer.HTTPServer):

    def __init__(self, serverAddress, requestHandlerClass, docRoot):
        BaseHTTPServer.HTTPServer.__init__(self, serverAddress, requestHandlerClass)
        self.docRoot = docRoot

# from python module doc
def run(server_class=BaseHTTPServer.HTTPServer,
        handler_class=BaseHTTPServer.BaseHTTPRequestHandler,
        root='root',
        ip='', port=8000):

    server_address = (ip, port)

    httpd = server_class(server_address, handler_class, root)
    log.info("ajaxserver: waiting for requests")
    httpd.serve_forever()

if __name__ == '__main__':

    log.basicConfig(level = log.DEBUG,
    format = '%(asctime)s %(levelname)-8s %(message)s',
    datefmt = '%a, %d %b %Y %H:%M:%S')

    ip = '127.0.0.1'
    port = 8000
    docRoot = '../root'

try:

    # start the server
    log.info("ajaxserver: starting HTTP server on %s:%d" %(ip, port))
    run(server_class=AjaxServer, handler_class=handler.Handler, ip=ip, port=port, root=docRoot)
    log.info("ajaxserver: terminating")

except KeyboardInterrupt, k:
    print "\r>>terminated by user, good bye!>>"
    log.info("aborted by user, terminating")

running it

start the ajax server:

> ./ajaxserver 
Sun, 01 Jul 2012 16:35:12 INFO     ajaxserver: starting HTTP server on 127.0.0.1:8000
Sun, 01 Jul 2012 16:35:12 INFO     ajaxserver: waiting for requests

then point your browser to http://localhost:8000/ — in your terminal running ajaxserver you’ll see a bunch of log lines showing you the requests triggered by loading the index.html page.

the browser will initially show just the static text of index.html

— after a couple of seconds the current date and time will be displayed and continuously updated, followed by the current process count of your machine. when you enter text into the text input field and tab out, the length of your input will be shown next to it.

all three activities are happening independently from one another, the page itself is never reloaded (as you can verify in the ajaxserver log output).

all content posted on these pages is an expression of my own mind. my employer is welcome to share these opinions but then again he might not want to.

4 comments »

  1. For “ajax-processedinput.js”, you missed a $ as the first character.

    After all, this is a wonderful demo. Thanks a lot.

    comment by Alex — October 21, 2013 @ 18:13

  2. thx! also, the formatting was completely borked. fixed that as well.

    comment by Dr. Dirk Husemann — October 21, 2013 @ 20:19

  3. C:\essai_ajax>python ajaxserver.py Tue, 05 Nov 2013 17:09:22 INFO ajaxserver: starting HTTP server on 127.0.0.1 :8000 Traceback (most recent call last): File “ajaxserver.py”, line 53, in run(server_class=AjaxServer, handler_class=handler.Handler, ip=ip, port=port , root=docRoot) NameError: name ‘run’ is not defined

    I don’t know why … Thank you to explain me

    comment by tb — November 5, 2013 @ 16:10

  4. there was an indentation error in my code example, the def run() function block should have been “de-dented” one level — i’ve fixed it.

    comment by dr_who — January 17, 2014 @ 17:50

RSS feed for comments on this post. TrackBack URI

Leave a comment