В этой статье описывается новая функция в 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 / .