В этой статье описывается новая функция в WebMotion: управление WebSockets. Мы будем использовать Javascript-фреймворк AngularJS для клиентской части.
Чтобы проиллюстрировать эту функцию, мы создадим панель управления задачами с их состояниями (todo, выполняется или завершено). Эта панель должна быть доступна нескольким пользователям и автоматически обновляется.
Напомним, WebMotion — это веб-фреймворк Java. Он использует файл сопоставления для описания связи между сервером и клиентом. Он основан на JEE API с сервлетом 3.
AngularJS — это инфраструктура MVC Javascript. Это позволяет добавлять директивы в ваш HTML-документ, чтобы ваши страницы были динамичными. Одной из особенностей является то, что он автоматически обновляет модель между вашим контроллером и вашей HTML-страницей.
Для получения более подробной информации, вы можете посетить веб-сайты http://www.webmotion-framework.org и http://angularjs.org .
Демонстрация окончательного образца панели инструментов доступна здесь: http://www.webmotion-framework.org/dashboard/ . и исходный код доступен по следующему адресу: http://svn.debux.org/webmotion-ext/dashboard/ .
Создать проект
Maven используется в качестве менеджера Buid для примера. WebMotion предлагает архетип для первоначального проекта. Чтобы использовать его, вам просто нужно ввести следующую команду:
$ 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
Что касается AngularJS, вам просто нужно включить скрипт на своих страницах и объявить приложение. Пример состоит из одной страницы.
<html ng-app="DashboardApp"> <head> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script> </head> </html>
Модельная часть
Что касается сохранения данных (задач и их состояний), сервер сохранит их в списке памяти.
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"; } }
Сервер вернет навигатору задачи в виде трех списков, по одному для каждого состояния.
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; }
Последний шаг — инициализация модели с помощью вызова слушателя при запуске сервера.
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 } }
Чтобы включить этот прослушиватель, мы не должны забывать объявление прослушивателя в файле отображения WebMotion. Этот файл находится в src/main/resources
папке. Файл структурирован по нескольким разделам. Раздел «[config]» позволяет настроить приложение WebMotion.
[config] server.listener.class=org.debux.webmotion.dashboard.StartupListenner
Посмотреть часть
Текущее приложение будет одностраничным веб-приложением. На этой странице будут отображаться созданные задачи на панели инструментов. Контроллер AngularJS связан с телом HTML для управления DOM, соответствующим модели, благодаря AngularJS.
<body ng-controller="MainCtrl">...</body>
Что касается части Javascript, контроллер является функцией. Это контроллер, который получает события и манипулирует моделью.
function MainCtrl($scope) { }
Страница будет разделена на две части. Первым будет форма создания с полем ввода. addTask
Функция будет вызываться в то время как форма создания представляется.
<form ng-submit="addTask()"> <input type="text" ng-model="taskName" size="30" required placeholder="add new task here"> <input > </form>
Во второй области отображаются все созданные задачи, отсортированные по их статусу. Модель получается из контроллера и просматривается с помощью директивы «ng-repeat» для создания тега div для каждой задачи. В случае удаления или перемещения задачи на контроллере вызывается метод, т.е. delTask и 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>
Директива AngularJS позволяет добавить поведение в DOM. Директивы drag-event и drop-event являются специальными директивами для проекта. Это позволяет легко обрабатывать перетаскивание HTML5, добавляя необходимые события на элементы и обратный вызов на текущем контроллере.
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']);
Осталось добавить стиль в задачи благодаря 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>
В конце концов, URL для доступа к странице добавляется в разделе «Действие» в WebMotion. Действие в файле сопоставления состоит из метода HTTP, пути и действия, которое нужно выполнить. В следующем правиле действие заключается в том, чтобы вернуть страницу index.html пользователю.
[actions] GET / view:index.html
Часть контроллера
Для управления WebSocket на сервере мы используем функцию отправки сообщений JSON, предоставляемую WebMotion. Этот протокол позволяет легко вызывать с клиента метод в WebSocket. Объект JSON содержит имя метода и параметры.
Чтобы использовать его, вы должны создать, как классическое действие в WebMotion, контроллер, который возвращает визуализацию клиенту при вызове метода. Рендерер позволяет вам возвращать страницы, данные, перенаправления, … в навигатор. В этом случае рендеринг является типом 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 { } }
Не забудьте объявить сопоставление. Действие объявляется с именем classe и вызываемым методом:
[actions] GET /tasksManager TasksManager.createWebsocket
По умолчанию возвращаемый метод (через сокет) передается только соединению вызывающей стороны, которое отправило объект JSON. В этом примере мы будем транслировать все соединения, чтобы сообщить модификацию. Для этого вы должны сохранить все соединения в контексте и переопределить метод, который отправляет результат следующим образом:
@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); }
Затем мы добавляем методы, которые управляют задачами в сокете. Каждый метод возвращает все задачи всем пользователям, чтобы обновить панель управления:
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(); }
Фабрика помогает создать утилиту для передачи вызова на веб-сокет. Для управления веб-сокетами в AngularJS вы можете использовать общую фабрику:
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() } } });
В противном случае вы можете использовать конкретную фабрику для вашего звонка. Метод в фабрике создается с объектом JSON для возврата на сервер для каждого вызова. Обратный вызов при получении сообщения позволяет обновить данные на стороне клиента.
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)); } } });
Наконец, просто введите фабрику в контроллер и подключите события из представления.
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(); }
Запустите приложение
Теперь вы можете запустить приложение с Jetty, введя следующую командную строку:
$ mvn jetty:run
Развернуть приложение можно на выделенном сервере Jetty, Tomcat или Glassfish.
Вы можете визуализировать результат в избранных навигаторах, чтобы связаться с эффектом обновления, чтобы ввести следующий адрес http: // localhost: 8080 / Dashboard / .