Skip to main content

More on Web Sockets and HTML5 in Glassfish

Posted by spericas on October 14, 2010 at 10:52 AM PDT

 In my last blog entry, I described VideoSharing which is an application that uses Web sockets to remote UI events and enable participants to control HTML5 video players remotely. Today, I'd like to share with you a similar type of collaboration application, but this time one that uses other HTML5 features: namely, 2D canvases and client SQL databases. The name of this application is BoardMirror.

 Rather than sharing a video object, like in VideoSharing, BoardMirror shares a canvas object and lets participants draw figures on it. For the sake of simplicity, only two figures can be drawn on a canvas: rectangles and circles. These figures are drawn in a random location inside the canvas and using a random color. Here is a screenshot of the application:

The mirror or collaboration part of the application is the result of copying these figures in all the other canvases that are connected to the server. And, of course, we use Web sockets for this purpose. A Web socket is nothing more than a socket connection that can be established between a (modern) Web browser and a Web container. It provides a low-latency, bi-directional communication parallel to the HTTP channel. It is Ajax/Comet on steroids.

In addition, the BoardMirror application has the ability to store your work of art in a local SQL database in the browser. This is done using one of the new APIs in HTML5. In fact, HTML5 has two options for storing data on the client side: Web Storage and Web SQL Databases. The former is implemented by most browsers and can store name-value pairs; the latter, as suggested by its name, gives you full SQL storage but it's only implement in some browsers.

Let me start describing the canvas and 2D APIs first. As in the VideoSharing application, we use a JSF facelet to define our main page:

    <html ...>
      ...
      <h:body>
          <h5:canvas width="600" height="400"/>
          <h:outputScript library="js" name="json2.js" target="head"/>
          <h:outputScript library="js" name="app.js" target="head"/>
          <br/>
          <input type="button" value="Rectangle" onclick="APP.drawRectangle()"/>
          <input type="button" value="Circle" onclick="APP.drawCircle()"/>
          <input type="button" value="Clear" onclick="APP.clearCanvas(true)"/>
          <input type="button" value="Load" onclick="APP.loadCanvas()"/>
          <input type="button" value="Save" onclick="APP.saveCanvas()"/>
      </h:body>
   <html>

where the h5:canvas element is defined in the same way as h5:video in the VideoSharing blog. What follows is a JavaScript method that draws a rectangle, sends the rectangle to be drawn remotely and stores the rectangle in a local data structure so that it can saved in a local database, if so requested by the user.

    drawRectangle: function() {
        // Get access to canvas and draw rectangle
        var canvas = this.getCanvas();
        var ctx = canvas.getContext('2d');
        ctx.fillStyle = this.random.color();
        var width = this.random.number(40, 100);
        var height = this.random.number(40, 100);
        var x = this.random.number(0, canvas.width - width);
        var y = this.random.number(0, canvas.height - height);
        ctx.fillRect(x, y, width, height);

        // Mirror rectangle via web sockets and store in array
        var shape = {type: "rectangle", x: x, y: y, width: width,
            height: height, color: ctx.fillStyle};
        this.network.send(JSON.stringify(shape));
        this.shapes[this.shapes.length] = shape;
    },

The code that draws the rectangle should be self explanatory, yet another 2D API! The last three lines of code are there to: create a shape object, send the shape over the network and store the shape in an array, respectively. The network object is initialized in the same way as in the VideoSharing application; only it's onmessage handler is different:

    websocket.onmessage = function (evt) {
        var shape = JSON.parse(evt.data);
        if (shape.type == "clear") {
            APP.clearCanvas(false);
        } else {
            APP.mirrorShape(shape, false);
        }
    };

Since a canvas can be remotely cleared, there's a special clear shape type that triggers such a remote event. For all the other shapes (real shapes, that is), the mirrorShape() method is called. This method simply draws the shape in the local canvas (without resending the shape over the network again!).

As stated above, this application also supports storing your canvas in a local database. This is done by storing all the shapes in JSON format. The database logic is implemented in the object returned by the database function shown next.

    var database = function (db) {
      return {
        error: function (err) {
            alert(err.message);
        },
        initialize: function() {
            if (window.openDatabase) {
                db = openDatabase('shapes', '1.0', 'All Shapes', 64 * 1024);
                db.transaction(
                    function (tx) {
                        tx.executeSql('CREATE TABLE shapes (id UNIQUE, shape STRING)')
                    });
                this.load();
            } else {
                alert("Your browser does not support SQL databases");
            }
        },
        load: function() {
            db.transaction(function (tx) {
                tx.executeSql('SELECT * FROM shapes',
                [],
                function (tx, results) {
                    for (var i = 0; i < results.rows.length; i++) {
                        var shape = JSON.parse(results.rows.item(i).shape);
                        APP.shapes[i] = shape;
                    }
                    APP.refreshCanvas();
                },
                this.error)
            } );
        },
        save: function() {
            db.transaction(function (tx) {
                tx.executeSql("DELETE FROM shapes");
                for (var i = 0; i < APP.shapes.length; i++) {
                    tx.executeSql('INSERT INTO shapes (id, shape) VALUES (?, ?)',
                    [i, JSON.stringify(APP.shapes[i])], this.error);
                }
                alert("Canvas has been saved into a local database");
            });
        }
      }
    };

In the initialize() method, the SQL database is opened and a table is created to store the shapes. If the table exists, an error will be ignored since no error handler is provided in the call to tx.executeSql(). The load() method runs a select query without any parameters (notice the "[]") and provides a results handler that parses and stores the shape in the array. The save() method clears the table and then inserts the shape one by one into the database. Note how parameters are passed to fill in the value placeholders denoted by "?" in the SQL string. This should be familiar to anyone that has written JDBC code before.

One interesting aspect of the SQL API is that, following a pattern that is common in Javascript, all operations are asynchronous. This is why functions are passed to methods like db.transaction() and tx.executeSql(). However, the order in which SQL statements are executed follows the execution order of the calls to tx.executeSql(). That is, statements are always queued and run in order —and this is why methods like save() above work.

The Web sockets code in Glassfish that supports this application is identical to that in the VideoSharing application. For more information, the reader is referred to the VideoSharing screencast or to the attached source code bundle. Please note that Web sockets are not enabled by default in Glassfish. To enable them you must execute the following single-line command on your domain:

asadmin set configs.config.server-config.network-config.protocols.protocol.http-listener-1.http.
websockets-support-enabled=true
AttachmentSize
boardmirror.jpg132 KB
BoardMirror.zip27.12 KB
Related Topics >>