Dev (Back & Front)ARTIGO

Usando o Play! 2.0 com o Twitter Bootstrap, WebSockets, Akka e OpenLayers

Para um de nossos clientes, temos que mostrar um mapa com a posição exata dos veículos e que a atualização seja em tempo real. Então comecei a criar um protótipo usando o framework Play!, na sua última versão liberada (a 2.0), utilizando o Java API. Comecei pelo websocket-chat das amostras do Play! 2.0.

A finalidade do protótipo é mostrar um veículo em movimento no mapa. A localização dele é enviada para o servidor por meio de uma ligação REST (no final, isto será enviado por um aplicativo do Android), e os usuários conectados serão capazes de ver no mapa o veículo se movimentando em tempo real.

Primeiro, vamos ver uma pequena demonstração!

Para tornar as coisas um pouco melhores, decidi integrar o Twitter Boostrap (v2.0.1) usando o LessCss. Para isso, usei as dicas deste artigo (nada difícil aqui).

Depois, integrei o OpenLavers, um framework Javascript usado para visualização de mapas. Usei o exemplo de integração do GoogleMaps e acrescentei algumas camadas KML. Isso é feito em arquivos map.scala.html e maptracker.js. Nada demais até aqui (puro Javascripit e olha que eu nem sou um expert…).

A parte interessante é aquela onde se usa o WebSockets. Do ponto de vista do cliente, é perfeitamente normal:

var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var mapSocket = new WS("@routes.Application.mapsocket().webSocketURL(request)");

mapSocket.onmessage = function(event) {
var data = JSON.parse(event.data);

marker = moveMaker(map, marker, data.longitude, data.latitude);

}

// if errors on websocket
var onalert = function(event) {
$(".alert").removeClass("hide");
}

mapSocket.onerror = onalert;
mapSocket.onclose = onalert;

Quando o cliente recebe um dado JSON pelo websocket, ele move o marcador no mapa. E se um erro ocorre no websocket  (o servidor parar, por exemplo), um quadro de erro é exibido gracas ao Twitter Boostrap:

Na parte do servidor, o WebSocket é criado pelo controlador do aplicativo, e é manipulado pelo Actor Akka MapAnime.java; ele acessa as bibliotecas nativas Akka  para lidar com os eventos do controlador.

public class MapAnime extends UntypedActor {

static ActorRef actor = Akka.system().actorOf(new Props(MapAnime.class));

Map<String, WebSocket.Out<JsonNode>> registrered = new HashMap<String, WebSocket.Out<JsonNode>>();

/**
*
* @param id
* @param in
* @param out
* @throws Exception
*/
public static void register(final String id,
final WebSocket.In<JsonNode> in, final WebSocket.Out<JsonNode> out)
throws Exception {

actor.tell(new RegistrationMessage(id, out));

// For each event received on the socket,
in.onMessage(new Callback<JsonNode>() {
@Override
public void invoke(JsonNode event) {
// nothing to do
}
});

// When the socket is closed.
in.onClose(new Callback0() {
@Override
public void invoke() {
actor.tell(new UnregistrationMessage(id));
}
});
}

public static void moveTo(float longitude, float latitude) {

actor.tell(new MoveMessage(longitude, latitude));

}

@Override
public void onReceive(Object message) throws Exception {

if (message instanceof RegistrationMessage) {

// Received a Join message
RegistrationMessage registration = (RegistrationMessage) message;

Logger.info("Registering " + registration.id + "...");
registrered.put(registration.id, registration.channel);

} else if (message instanceof MoveMessage) {

// Received a Move message
MoveMessage move = (MoveMessage) message;

for (WebSocket.Out<JsonNode> channel : registrered.values()) {

ObjectNode event = Json.newObject();
event.put("longitude", move.longitude);
event.put("latitude", move.latitude);

channel.write(event);
}

} else if (message instanceof UnregistrationMessage) {

// Received a Unregistration message
UnregistrationMessage quit = (UnregistrationMessage) message;

Logger.info("Unregistering " + quit.id + "...");
registrered.remove(quit.id);

} else {
unhandled(message);
}

}

public static class RegistrationMessage {
public String id;
public WebSocket.Out<JsonNode> channel;

public RegistrationMessage(String id, WebSocket.Out<JsonNode> channel) {
super();
this.id = id;
this.channel = channel;
}
}

public static class UnregistrationMessage {
public String id;

public UnregistrationMessage(String id) {
super();
this.id = id;
}
}

public static class MoveMessage {

public float longitude;

public float latitude;

public MoveMessage(float longitude, float latitude) {
this.longitude = longitude;
this.latitude = latitude;
}

}

}

Os métodos « register » e « moveTo » são chamados pelo controlador e enviam mensagem para o sistema Akka. Essas mensagens são processadas pelo método « onReceive ». Assim, quando é recebida uma MoveMessage, cria-se um objeto JSON com a longitude e a latitude e isso é enviado para os clientes por meio dos websockets.

Eu também escrevi rapidamente uma classe de teste que analisa um arquivo de texto e envia solicitações de REST com um novo local para o servidor a cada 100ms.

O projeto está hospedado no GitHub. Ele funciona com o Google Chrome v17 ev11 Firefox.

Para testá-lo:

  • Baixe o Play! 2.0;
  • Clone o Git repo;
  • Inicie o « play run » no diretório do projeto;
  • Conecte o « http://localhost:9000/map «;
  • Em outro terminal, rode o « play test » para enviar solicitações  REST e para fazer o veículo mover.

O problema que precisei resolver foi que a aplicação não é Stateless, porque no Actor eu armazeno um mapa de clientes conectados. Talvez eu tenha que olhar para o Rediz ou algo parecido, qualquer ajuda seria muito bem-vinda.

Então, como conclusão, fui capaz de desenvolver rapidamente um protótipo funcional e eu acho que tentarei usar o Play! 2.0 em vários projetos. 

  • O que é bom:
  1. Altamente produtivo;
  2. Modelos typesafe view baseados no Scala;
  3. Menor integração CSS;
  4. Integração Akka;
  5. Javascript compilado com o Google Closure Compiler;
  6. Não precisa aprender o Scala para o momento.
  • A ser melhorado:
  1. A compilação de tempo do Scala deve ser aumentada, por que no meu computador ele leva até 4s para compilar uma exibição;
  2. Os erros do compilador do Scala são enigmáticos;
  3. Não posso implantar uma demonstração sobre o Heroku porque ele não suporta (ainda?) websockets.

Atualização: pouco depois, achei um artigo do @steve_objectify usando tecnologias similares.

***

Artigo original disponível em: hhttp://www.ekito.fr/portail/playing-2-0-with-twitter-bootstrap-websockets-akka-and-openlayers?lang=en

depois de um logo período como CTO em uma startup francesa, juntou-se à companhia ekito, onde atua como arquiteto em Java e aprendeu bastante sobre metodologias ágeis. Atualmente, na ekito, usa suas habilidades (treinamento técnico e em Agile) para dar assistência a pequenas empresas em todo o mundo. É possível segui-lo no Twitter: http://twitter.com/ndeverge.

Ver perfil