HTML5 Zone is brought to you in partnership with:

I am a developer specialized in Java Web technologies. Julien has posted 1 posts at DZone. You can read more from them at their website. View Full User Profile

WebSocket with WebMotion and AngularJS

12.24.2012
| 9382 views |
  • submit to reddit

This article describes a new feature in WebMotion: the WebSockets management. We will use the AngularJS Javascript framework for the client side.

To illustrate this feature, we will create a dashboard managing tasks with their states (todo, in progress or done). This dashboard should be shared between several users and refreshs itself automatically.

Reminder, WebMotion is a Java web framework. It uses a mapping file to describe the link between the server and the client. It is based on the JEE API with Servlet 3.

AngularJS is a MVC Javascript framework. It allows to add directives in your HTML document to get your pages dynamic. One of the features is that it updates automatically the model between your controler and your HTML page.

For more details, you can visit the websites http://www.webmotion-framework.org and http://angularjs.org.

A demo of the final dashboard sample is available here : http://www.webmotion-framework.org/dashboard/. and the source code is available to the follow address : http://svn.debux.org/webmotion-ext/dashboard/.

Create the project

Maven is used as buid manager for the example. WebMotion offers an archetype to initial the project. To use it, you just have to enter the following command :

$ mvn archetype:generate \
    -DarchetypeGroupId=org.debux.webmotion \
    -DarchetypeArtifactId=webmotion-archetype \
    -DarchetypeVersion=2.3.3 \
    -DgroupId=org.debux.webmotion \
    -DartifactId=dashboard \
    -Dpackage=org.debux.webmotion.dashboard \
    -Dversion=1.0-SNAPSHOT \
    -DusesExtras=N

For AngularJS side, you just have to include the script in your pages and declare an application. The example is composed with one page.

<html ng-app="DashboardApp">
    <head>
        <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
    </head>
</html>

Model part

Concerning the data (the tasks and their states) save, the server will store them in memory list.

public class Task {
    protected String id;
    protected String name;
    protected String status;

    public Task(String name) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.status = "todoTasks";
    }
}

The server will return to the navigator the tasks as three lists, one for each status.

public class SortedTasks {
    List<Task> todoTasks = new ArrayList<Task>();
    List<Task> progressTasks = new ArrayList<Task>();
    List<Task> doneTasks = new ArrayList<Task>();
}

public static SortedTasks getSortedTasks(List<Task> tasks) {
    SortedTasks sortedTasks = new SortedTasks();

    for (Task task : tasks) {
        String status = task.getStatus();

        if ("todoTasks".equals(status)) {
            sortedTasks.todoTasks.add(task);

        } else if ("progressTasks".equals(status)) {
            sortedTasks.progressTasks.add(task);

        } else if ("doneTasks".equals(status)) {
            sortedTasks.doneTasks.add(task);
        }
    }
    return sortedTasks;
}

The last step is to initialize the model by a listener call at server startup.

public class StartupListenner implements WebMotionServerListener {

    public void onStart(Mapping mapping, ServerContext serverContext) {
        List<Task> tasks = Arrays.asList(
            new Task("Task 0"),
            new Task("Task 1"),
            new Task("Task 10"),
            new Task("Task 11")
        );
        serverContext.setAttribute("tasks", new ArrayList<Task>(tasks));
    }

    public void onStop(ServerContext serverContext) { // do nothing }

}

To enable this listener, we have to not forget the listener declaration in the WebMotion mapping file. This file is in the src/main/resources folder. The file is structured by some sections. The section "[config]" allows you to configure your WebMotion application.

[config]
server.listener.class=org.debux.webmotion.dashboard.StartupListenner

View part

The current application will be a single page web application. This page will displays the created tasks in the dashboard. An AngularJS controller is linked to the HTML body to manipulate the DOM corresponding with the model thanks to AngularJS.

<body ng-controller="MainCtrl">...</body>

Concerning Javascript part, the controller is a function. It is the controller which receive the events et manipulate the model.

function MainCtrl($scope) {
}

The page will be divided in two areas. The first one will be the creation form, with an input field. The addTask function will be called while the creation form is submitted.

<form ng-submit="addTask()">
    <input type="text" ng-model="taskName" size="30" required placeholder="add new task here">
    <input >
</form>

The second area displays all the created tasks, sorted by their status. The model is got from the controller and is browsed with the "ng-repeat" directive to create a div tag for each task. In the case of deleting or moving a task, a method is called on the controller, i.e. delTask and updateTask.

<div >
    <div >
        <h1>Todo</h1>
        <div >
            <button >×</button>
            {{task.name}}
        </div>
    </div>
    <div >
        <h1>In progress</h1>
        <div >
            <button >×</button>
            {{task.name}}
        </div>
    </div>
    <div >
        <h1>Done</h1>
        <div >
            <button >×</button>
            {{task.name}}
        </div>
    </div>
</div>

A AngularJS directive allows to add a behaviour on the DOM. The directives drag-event and drop-event are specefics directives for the project. It allows to handle easily the HTML5 drag n'drop, adding the necessary events on the elements and the callback on the current controller.

angular.module('components', [])
    .directive('dragEvent', ['$parse', function($parse) {
        return function(scope, element, attrs) {
            element.bind("dragstart", function (evt) {
                var id = element.attr("id");
                evt.dataTransfer.setData("drag-id", id);

                var fn = $parse(attrs.dragEvent);
                fn(scope, {$element : element});
            });
            element.attr("draggable", true);
        }
    }])
    .directive('dropEvent', ['$parse', function($parse) {
        return function(scope, element, attrs) {
            element.bind("dragover dragenter", function (evt) {
                evt.stopPropagation();
                evt.preventDefault();

                return false;
            });

            element.bind("drop", function (evt) {
                var id = evt.dataTransfer.getData("drag-id");
                var elementTransfer = angular.element(document.getElementById(id));
                element.append(elementTransfer);

                evt.stopPropagation();
                evt.preventDefault();

                var fn = $parse(attrs.dropEvent);
                fn(scope, {$element : elementTransfer, $to : element});
            });
        }
    }]);

angular.module('DashboardApp', ['components']);

It remains to add the style on the tasks thanks to Twitter Boostrap :

<link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css">
<style>
    html, body, .row, .span4 {
        height: 100%;
    }
    .task {
        width: 110px;
        height: 100px;
        float: left;
        background: #ffff66;
        padding: 10px;
        margin: 10px;
        border-radius: 3px;
    }
</style>

Endly, the URL to access the page, is added in the section "action" in WebMotion. An action in the mapping file is composed by the HTTP method, the path and the action to execute. In the following rule, the action is to return the index.html page to the user.

[actions]
GET       /                           view:index.html

Controller part

To manage the WebSocket in the server, we use JSON message send feature provided by WebMotion. This protocol allows to call easly from the client a method in the WebSocket. The JSON object contains the method name and the parameters.

To use it, you must create, like a classical action in WebMotion, a controller which returns the render to the client on method call. The renderer allows you to return pages, data, redirections, ... to navigator. In this cas the render is type of RenderWebSocket.

public class TasksManager extends WebMotionController {

    private static final Logger log = LoggerFactory.getLogger(TasksManager.class);

    public Render createWebsocket() {
        TasksManagerWebSocket socket = new TasksManagerWebSocket();
        return new RenderWebSocket(socket);
    }

    public class TasksManagerWebSocket extends WebMotionWebSocketJson {
    }
}

Don't forget to declare the mapping. The action is declared with the classe name and the method to call :

[actions]
GET       /tasksManager               TasksManager.createWebsocket

By default, the method return (by the socket) is passed only to the caller connection which sent the JSON object. In this exemple, we would to broadcast all the connections to inform the modification. To do that, you must store all the connections in the context and redefine the method which send the result thus :

@Override
public void onOpen() {
    // Store all connections
    ServerContext serverContext = getServerContext();
    List<TasksManagerWebSocket> connections = (List<TasksManagerWebSocket>) serverContext.getAttribute("connections");
    if (connections == null) {
        connections = new ArrayList<TasksManagerWebSocket>();
        serverContext.setAttribute("connections", connections);
    }
    connections.add(this);
}

@Override
public void onClose() {
    ServerContext serverContext = getServerContext();
    List<TasksManagerWebSocket> connections = (List<TasksManagerWebSocket>) serverContext.getAttribute("connections");
    connections.remove(this);
}

@Override
public void sendObjectMessage(String methodName, Object message) {
    ServerContext serverContext = getServerContext();
    List<TasksManagerWebSocket> connections = (List<TasksManagerWebSocket>) serverContext.getAttribute("connections");
    for (TasksManagerWebSocket socket : connections) {
        socket.superSendObjectMessage(methodName, message);
    }
}

public void superSendObjectMessage(String methodName, Object message) {
    super.sendObjectMessage(methodName, message);
}

Then, we add the methods that manage the tasks in the socket. Each method return all the tasks to all user to refresh the dashboard :

public SortedTasks getTasks() {
    ServerContext serverContext = getServerContext();
    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    SortedTasks sortedTasks = Task.getSortedTasks(tasks);
    return sortedTasks;
}

public SortedTasks addTask(String name) {
    ServerContext serverContext = getServerContext();

    Task task = new Task(name);
    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    tasks.add(task);

    return getTasks();
}

public SortedTasks updateTask(final String id, String newStatus) {
    ServerContext serverContext = getServerContext();

    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    Task task = (Task) CollectionUtils.find(tasks, new Predicate() {
        public boolean evaluate(Object object) {
            Task other = (Task) object;
            return other.getId().equals(id);
        }
    });

    task.setStatus(newStatus);
    tasks.remove(task);
    tasks.add(task);

    return getTasks();
}

public SortedTasks delTask(final String id) {
    ServerContext serverContext = getServerContext();

    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    CollectionUtils.filter(tasks, new Predicate() {
        public boolean evaluate(Object object) {
            Task other = (Task) object;
            return !other.getId().equals(id);
        }
    });

    return getTasks();
}

The factory helps to create a utility to pass the call to the websocket. To manage the websockets in AngularJS, you can use a generic factory :

angular.module('components', [])
    .factory('WebSocket', function() {
        return {
            connect : function(url) {
                var self = this;
                this.connection = new WebSocket(url);

                this.connection.onopen = function() {
                    if (this.onopen) {
                        self.onopen();
                    }
                }
                this.connection.onclose = function() {
                    if (this.onclose) {
                        self.onclose();
                    }
                }
                this.connection.onerror = function (error) {
                    if (this.onerror) {
                        self.onerror(error);
                    }
                }
                this.connection.onmessage = function(event) {
                    if (this.onmessage) {
                        self.onmessage(event);
                    }
                }
            },

            send : function(message) {
                this.connection.send(message);
            },

            close : function() {
                this.connection.onclose = function () {};
                this.connection.close()
            }
        }
    });

Otherwise, you can use a specific factory for your call. The method in the factory is created with the JSON object to return to the server for each call. A callback on the receipt of the message allows to refresh the data on side the client.

angular.module('components', [])
    .factory('TasksManager', function() {
        var url = "ws://localhost:8080/Dashboard/tasksManager";
        return {
            init : function() {
                var self = this;
                this.connection = new WebSocket(url);

                this.connection.onopen = function() {
                    console.log("connected");
                    self.getTasks();
                }
                this.connection.onclose = function() {
                    console.log("onclose");
                }
                this.connection.onerror = function (error) {
                    console.log(error);
                }
                this.connection.onmessage = function(event) {
                    console.log("refresh");
                    var data = angular.fromJson(event.data);
                    self.refresh(data.result);
                }
            },

            getTasks : function() {
                this.sendMessage({
                    method : "getTasks",
                    params : {}
                });
            },

            addTask : function(name) {
                this.sendMessage({
                    method : "addTask",
                    params : {
                        name : name
                    }
                });
            },

            updateTask : function(id, status) {
                this.sendMessage({
                    method : "updateTask",
                    params : {
                        id : id,
                        newStatus : status
                    }
                });
            },

            delTask : function(id) {
                this.sendMessage({
                    method : "delTask",
                    params : {
                        id : id
                    }
                });
            },

            sendMessage : function(event) {
                this.connection.send(JSON.stringify(event));
            }
        }
    });

Finally, simply inject the factory in the controller and plug the events from the view.

function MainCtrl($scope, TasksManager) {

    $scope.addTask = function() {
        TasksManager.addTask($scope.taskName);
        $scope.taskName = "";
    }

    $scope.updateTask = function(element, status) {
        var id = element.attr("id");
        TasksManager.updateTask(id, status);
    }

    $scope.delTask = function(task) {
        TasksManager.delTask(task.id);
    }

    TasksManager.refresh = function(tasks) {
        $scope.tasks = tasks;
        $scope.$digest();
    }

    TasksManager.init();
}

Run the application

You can now run the application with Jetty entering the following command line :

$ mvn jetty:run

It is possible to deploy the application in dedicated server Jetty, Tomcat or Glassfish.

You can visualize the result in your favorites navigators to contact the refresh effect to enter the following address http://localhost:8080/Dashboard/.

Published at DZone with permission of its author, Julien Ruchaud.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)