Sunday, October 20, 2013

Atmosphere and AngularJS

Atmosphere and AngularJS

Atmosphere framework is the The Asynchronous WebSocket/Comet Framework for building asynchronous web applications. What makes it outstanding among similar frameworks is that:
  • Atmosphere is supported by the plethora of existing web frameworks either natively or via plugins.
  • Atmosphere works on all Servlet based servers including Tomcat, JBoss Jetty, Resin, GlassFish, Undertow, WebSphere, WebLogic etc. Not running a Servlet Container? Netty, Play! Framework or Vert.x
  • Atmosphere transparently supports WebSockets, Server Side Events (SSE), Long-Polling, HTTP Streaming (Forever frame) and JSONP.
  • Atmosphere provides nice javascript library that handles WebSocket communication and falls back to long-polling transparently.
What does it all mean to you? Simplicity and maintenance relief. You code your asynchronous web application using one codebase and deploy it on any Servlet container or straight to the cloud. There's no need to think about WebSocket support or hardcode specific vendor implementations.

Sounds too good to be true, doesn't it? Let me show you that it is true, indeed.

The tutorial is based on the excellent example from the Atmosphere examples project and modified to use AngularJS for the front end.

The code and tutorial cover the following workflow:

  1. User opens Chat web page url in the browser (running on localhost)
  2. User enters the name
  3. User sends messages to the chat.
    • User sees his/her own message.
    • If there's nobody else in the chat, User opens another browser tab or window and proceeds to the step 1.
    • Otherwise, both users see the messages being send

While this sounds like a simple and easy thing to do, you should remember that every time it comes to the coding, all simple things exponentially grow in complexity. Let's analyze requirement more thoroughly.

  • There must be a front-end allowing user to submit the name.
  • There must be a way to connect and listen for the incoming messages asynchronously from the front end.
  • The server must asynchronously process incoming chat messages and notify all users in the chat.

As it will become clear after completion of this tutorial, Atmospheres framework makes it incredibly easy to deal with the server side asynchronous processing. You'll find that most of time you are spending building nice front-ends instead of dealing with complexities of asynchronous request processing.

Prerequisites

The source code for this tutorial can be explored at https://github.com/spyboost/atmosphere-chat-angular

This is what you need to have installed

  • Java: http://www.oracle.com/technetwork/java/javase/downloads/index.html
  • Maven: http://maven.apache.org/download.cgi
  • Git (if you want to clone the code repository and run it instead of accomplishing the step-by-step guide)

If you want to build the application step by step click and read step-by-step section. Otherwise, keep reading next section.

Clone and run the code

Clone the code from the repository:

git clone git@github.com:spyboost/atmosphere-chat-angular.git

And the output should be similar to this:

Cloning into 'atmosphere-chat-angular'...
remote: Counting objects: 55, done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 55 (delta 7), reused 50 (delta 7)
Receiving objects: 100% (55/55), 13.72 KiB, done.
Resolving deltas: 100% (7/7), done.

Now run the embedded jetty server

cd atmosphere-chat-angular

And run the embedded jetty maven goal

mvn jetty:run

The console logs should start fill in and when the Jetty is ready to serve requests you should see the logs:

2013-10-19 18:13:00.589:INFO:oejs.ServerConnector:main: Started ServerConnector@6b8dc72c{HTTP/1.1}{0.0.0.0:8080}
[INFO] Started Jetty Server
[INFO] Starting scanner at interval of 1 seconds.

Now open the browser localhost:8080 and play around with the project. The workflow should help explore it better.

Step by step

Open command line and type this command

mvn archetype:generate -DgroupId=org.atmosphere.samples.chat.angular -DartifactId=atmosphere-chat-angular -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

If the command executed successfully, you should see this output in the end:

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

This command generates a new maven web project. You should see this directory tree:

atmosphere-chat-angular
|- src
  |- main
     |+ resources
     |- webapp
       |- WEB-INF
          |- web.xml
       |- index.jsp
|- pom.xml
          

We don't need index.jsp, so let's remove it:

rm atmosphere-chat-angularsrcmainwebappindex.jsp

Let's add all dependencies to our project. Open atmosphere-chat-angularpom.xml and modify it:

atmosphere-chat-angularpom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.atmosphere.samples</groupId>
  <artifactId>atmosphere-chat-angular</artifactId>
  <packaging>war</packaging>
  <name>atmosphere-chat-angular</name>
  <url>http://maven.apache.org</url>
  <version>1.0.0-SNAPSHOT</version>

  <properties>
    <atmosphere-version>[2, )</atmosphere-version>
    <client-version>[2.0.5, )</client-version>
    <logback-version>[1, )</logback-version>
    <jetty-version>9.0.6.v20130930</jetty-version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.atmosphere</groupId>
      <artifactId>atmosphere-runtime</artifactId>
      <version>${atmosphere-version}</version>
    </dependency>
    <dependency>
      <groupId>org.atmosphere.client</groupId>
      <artifactId>javascript</artifactId>
      <version>${client-version}</version>
      <type>war</type>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback-version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>${logback-version}</version>
    </dependency>
      <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-mapper-asl</artifactId>
      <version>[1, )</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
        <source>1.7</source>
        <target>1.7</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>${jetty-version}</version>
      </plugin>
    </plugins>
  </build>
</project>

Unfortunately, the maven-archetype-webapp is not up to date with the Servlet 3.0 specification and generates outdated 2.5 web.xml. Let's change that.

Open atmosphere-chat-angularsrcmainwebappWEB-INFweb.xml and modify it to look like this:

atmosphere-chat-angularsrcmainwebappWEB-INFweb.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
  <servlet>
      <servlet-name>AtmosphereServlet</servlet-name>
      <servlet-class>org.atmosphere.cpr.AtmosphereServlet</servlet-class>
      <load-on-startup>0</load-on-startup>
      <async-supported>true</async-supported>
  </servlet>
  <servlet-mapping>
      <servlet-name>AtmosphereServlet</servlet-name>
      <url-pattern>/chat/*</url-pattern>
      </servlet-mapping>
</web-app>

Code, sweet code

Now we're going to write some java code. Finally! I know you've been eagerly waiting for diving into some real stuff.

We'll learn how Atmosphere registers url handlers and manages connection lifecycle.

The project that we generated with Maven archetype has folders only with static web application files. We have to create folders manually following Maven convention. Create this path: atmosphere-chat-angularsrcmainjava

Now lets' start with java classes. First, let's create the ChatMessage class which will be used to send messages from users:

atmosphere-chat-angularsrcmainjavaChatMessage.java
package org.atmosphere.samples.chat.angular;

import java.util.Date;

public final class ChatMessage{
    private String message;
    private String author;
    private long time;

    public ChatMessage(){
        this("", "");
    }

    public ChatMessage(String author, String message){
        this.author = author;
        this.message = message;
        this.time = new Date().getTime();
    }

    public String getMessage(){
        return message;
    }

    public String getAuthor(){
        return author;
    }

    public void setAuthor(String author){
        this.author = author;
    }

    public void setMessage(String message){
        this.message = message;
    }

    public long getTime(){
        return time;
    }

    public void setTime(long time){
        this.time = time;
    }
}

So now we have ChatMessage class which encapsulates message details. It's very simple but we don't need complexity, do we?

Atmosphere framework uses encoder/decoder pattern to send and receive data. Encoders are used to encode the data and send over the wire. Decoders are used to convert the incoming data into a java class. Pretty simple concept, but we'll see how powerful and easy to configure and use it is. Like everything in Atmosphere framework.

Even though technically encoders and decoders have separate responsibilities, logically it makes sense to encapsulate the behaviour in one implementation as it deals with one class.

Let's create our encoder and decoder implementation for ChatMessage class and call it ChatMessageEncoderDecoder:

atmosphere-chat-angularsrcmainjavaChatMessageEncoderDecoder.java
package org.atmosphere.samples.chat.angular;

import org.atmosphere.config.managed.Decoder;
import org.atmosphere.config.managed.Encoder;
import org.codehaus.jackson.map.ObjectMapper;

import java.io.IOException;

public final class ChatMessageEncoderDecoder implements Encoder<ChatMessage, String>, Decoder<String, ChatMessage>{
    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public ChatMessage decode(final String s){
        try{
            return mapper.readValue(s, ChatMessage.class);
        }catch(IOException e){
            throw new RuntimeException(e);
        }
    }

    @Override
    public String encode(final ChatMessage message){
        try{
            return mapper.writeValueAsString(message);
        }catch(IOException e){
            throw new RuntimeException(e);
        }
    }
}

Let's take a look at the file and understand the details. The first thing that should be noted is how generics improve code readability.

Encoder<ChatMessage, String> means that we're encoding from ChatMessage into String.

Decoder<String, ChatMessage> means that we're decoding from String into ChatMessage.

While it's possible to encode into and decode from any arbitrary type, and string format in particular, our ChatMessageEncoderDecoder implementation delegates all work to the ObjectMapper. ObjectMapper in turn transform our chat message into JSON.

And now the final and the most interesting piece. The request handler that will asynchronously handle all incoming requests and respond with plain vanilla JSON using encoder/decoder

atmosphere-chat-angularsrcmainjavaChat.java
package org.atmosphere.samples.chat.angular;

import org.atmosphere.config.service.Disconnect;
import org.atmosphere.config.service.ManagedService;
import org.atmosphere.config.service.Message;
import org.atmosphere.config.service.Ready;
import org.atmosphere.cpr.AtmosphereResource;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

/**
 * Simple annotated class that demonstrate the power of Atmosphere.
 * This class supports all transports, support message length guarantee,
 * heart beat, message cache thanks to the @ManagedService.
 */
@ManagedService(path = "/chat")
public final class Chat{
    private static final Logger logger = LoggerFactory.getLogger(Chat.class);

    /**
     * Invoked when the connection as been fully established and suspended, e.g ready for receiving messages.
     *
     * @param r the atmosphere resource
     */
    @Ready
    public final void onReady(final AtmosphereResource r){
        logger.info("Browser {} connected.", r.uuid());
    }

    /**
     * Invoked when the client disconnect or when an unexpected closing of the underlying connection happens.
     *
     * @param event the event
     */
    @Disconnect
    public final void onDisconnect(final AtmosphereResourceEvent event){
        if(event.isCancelled())
            logger.info("Browser {} unexpectedly disconnected", event.getResource().uuid());
        else if(event.isClosedByClient())
            logger.info("Browser {} closed the connection", event.getResource().uuid());
    }

    /**
     * Simple annotated class that demonstrate how
     * {@link org.atmosphere.config.managed.Encoder} and {@link org.atmosphere.config.managed.Decoder
     * can be used.
     *
     * @param message an instance of {@link ChatMessage }
     * @return the chat message
     * @throws IOException
     */
    @Message(encoders = {ChatMessageEncoderDecoder.class}, decoders = {ChatMessageEncoderDecoder.class})
    public final ChatMessage onMessage(final ChatMessage message) throws IOException{
        logger.info("{} just send {}", message.getAuthor(), message.getMessage());
        return message;
    }

}

Even briefly looking at the Chat.java reveals how easy it is to set a request handler in Atmosphere.

@ManagedService(path = "/chat") means that Chat listens to the requests on /chat uri. We'll see the details on how to send requests from client side in the Client side code section. But, in essence, it's as easy as just calling atmosphere.subscribe({url: '/chat'}); Atmosphere client side utility will take care of setting appropriate headers and request parameters and initiate WebSocket connection, if the browser supports it, otherwise falling back to long-polling. The code doesn't have to change at all.

@Ready annotated method onReady is called when the connection is fully established and the Chat controller is ready to communicate with the client. In our example, the communication is sending and receiving chat messages.

@Message annotated method onMessage is invoked when client sends chat messages. As we're living in the web world, the client sends data in plain text, though using JSON format. This is where our encoder and decoder ChatMessageEncoderDecoder steps into the scene. @Message annotation accepts 2 arguments: encoders and decoders. In our example we use same class for encoding and decoding messages, but in more complex applications there can be many encoders and decoders which may satisfy any requirement for message formats. For example, we could create Google protocol buffers encoder and decoder and our chat would convert messages to the new format while the code and logic in application would remain unchanged.
The incoming request is being parsed by the decoder and JSON request payload gets converted into a ChatMessage instance and this ChatMessage instance is how the message argument is passed to onMessage(final ChatMessage message) method call. When the server replies, returning from the onMessage method, the encoder is used to encode the method return value into a String and send data back to the client. In our example the method returns ChatMessage too, thus our encoder converts a String into a ChatMessage. But the return value can be of any type, not necessarily the same as the incoming parameter type.

  • Client (web browser) => sends request with JSON encoded string, for example, {"message": "What is the answer to life the universe and everything?", "author": "Deep Thought"}
  • Chat controller invokes onMessage() and finds out that it has to convert JSON string into a ChatMessage instance.
  • {"message": "What is the answer to life the universe and everything?", "author": "Deep Thought"} is passed to ChatMessageEncoderDecoder.decode() which constructs a new ChatMessage
  • ChatMessage message is passed as message parameter variable to the Chat.onMessage() and the method starts processing
  • When Chat.onMessage returns the result, which appears to be a ChatMessage instance too, Atmosphere framework looks at the configured encoders and finds out that ChatMessageEncoderDecoder.encode() has to be called
  • ChatMessageEncoderDecoder.encode(message) => produces a JSON string => JSON string gets transferred back to the client.

Succinctly the request/response workflow can be represented like this

  • Browser => sends JSON => ChatMessageEncoderDecoder.decode(JSON) => produces ChatMessage => Chat.onMessage(ChatMessage) is invoked
  • Chat.onMessage => returns ChatMessage => ChatMessageEncoderDecoder.encode(ChatMessage) => produces JSON => Atmosphere triggers a browser callback with JSON result

Now we're getting to a very interesting moment of discovering how exactly Atmosphere triggers a browser callback with JSON result.

Client side code

Client side code of handing connections and asynchronous request/response processing is very simple with Atmosphere framework. In fact, Atmosphere provides javascript library and the API via "atmosphere" global Window object which gets populated when the library is imported.

Just add it to the header of the html file and you are all set:

<script src="javascript/atmosphere.js"></script>

The example of connecting to the Atmosphere server endpoint and registering a callback to receive messages.

var request = {
    url: '/chat',
    contentType : 'application/json',
    transport : 'websocket',
    reconnectInterval : 5000,
    enableXDR: true,
    timeout : 60000
  };

//onMessage is triggered when Atmosphere server sends an asynchronous message to the browser.
request.onMessage = function(response){
  var responseBody = response.responseBody; //JSON string
  var message = atmosphere.util.parseJSON(responseBody); you have your message object now. Use it.
}

var socket = atmosphereService.subscribe(request);
//socket is used to push messages to the server.
socket.push(atmosphere.util.stringifyJSON({author: 'Deep Thought', message: 'What is the answer to life the universe and everything?'}));
      

Atmosphere allows you to define these callbacks on your request object:

  • onOpen - called when the connection is opened
  • onMessage - called when the server pushes data to the client
  • onClientTimeout - called when the connection times out
  • onReopen - called when the connection is being reopened
  • onClose - called when the connection is closed
  • onError - called when the communication error occurs

That would be almost it and we could jump into coding html and plain javascript but there's one caveat when we want to use AngularJS. As you may know, AngularJS requires you to call $apply() function any time you deal with callbacks initiated outside Angular scope management life cycle. And the Atmosphere callbacks is exactly these types of callbacks. What it means to us is that if we want to update model in the onMessage callback, for example, we have to either wrap the whole onMessage function body in the $apply() or remember to call $scope.$apply(); when we're done with model updates. And this is not only for onMessage callback - the same steps needs to be done in every callback we want to use, be it onOpen, onReopen, etc.

So the example of the Angularized onMessage callback could be something like this:

request.onMessage(function(response){
  $scope.$apply(function(){
    $scope.model.message = atmosphere.util.parseJSON(response.responseBody);
  });
});

While it looks like not a big deal, and essentially it is not, it can be error prone as it's easy to forget to embrace your method calls into $scope.$apply() and then spend precious moments of your life figuring out why model changes are not visible.

For this purpose, your faithful author created a simple javascript wrapper utility https://github.com/spyboost/angular-atmosphere

To make my and your (if you decide that this utility fits your needs) life easier, you can import it in your html like this:

<script src="http://spyboost.github.io/angular-atmosphere/service/angular-atmosphere-service.js"></script>

This is served from github pages and is very fast due to github CDN capabilities.

OK, now I mentioned all font end prerequisites, libraries and design choices. Let's roll up sleeves and start coding front-end!

Take a look at the index.html file below. It is very small. It's only 52 lines of markup. I'm using Twitter Bootstrap 3.0 to make my design not look lame, as it would have been if I did all styling myself.

Create atmosphere-chat-angularsrcmainwebappindex.html with the following content:

atmosphere-chat-angularsrcmainwebappindex.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Atmosphere Chat AngularJS</title>
  <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"/>
  <link rel="stylesheet" href="css/chat.css" />
  <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
  <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
  <script src="javascript/atmosphere.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
  <script src="//spyboost.github.io/angular-atmosphere/service/angular-atmosphere-service.js"></script>
  <script src="javascript/application.js"></script>
</head>
<body data-ng-app="angular.atmosphere.chat" data-ng-controller="ChatController">
<div id="header">
  <h3 data-ng-bind="model.header" data-ng-init="model.header='Atmosphere Chat. Default transport is WebSocket, fallback is long-polling'"></h3>
</div>
<p data-ng-show="!model.connected">Connecting...</p>
<p>{{model.content}}</p>
<div class="container">
  <div class="row">
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title">Chat room</h3>
      </div>
      <div class="panel-body">
        <div class="messages">
          <p data-ng-repeat="message in model.messages">
            <span class="time">{{message.date | date:'shortDate'}} {{message.date|date:'shortTime'}}</span>
            <span class="author">{{message.author}}</span> :
            <span class="text">{{message.text}}</span>
          </p>
        </div>
      </div>
    </div>
  </div>
  <span data-ng-show="!model.name">Enter your name:</span>
  <div data-ng-show="model.logged">
    <span><b>{{model.name}}</b> says:</span>
  </div>
  <form role="form" class="form-horizontal">
    <div class="form-group">
      <div class="col-lg-9">
        <input type="text" class="form-control" id="input" data-ng-disabled="!model.connected" x-webkit-speech/>
      </div>
    </div>
  </form>
</div>
</body>
</html>

This is the atmosphere-chat-angularsrcmainwebappcsschat.css

atmosphere-chat-angularsrcmainwebappcsschat.css
.time {
  color: lightgray;
}

.author {
  font-weight: bold;
}

And finally, the last and definitely not the least but highly awaited part - the front end javascript application code: atmosphere-chat-angularsrcmainwebappjavascriptapplication.js

atmosphere-chat-angularsrcmainwebappjavascriptapplication.js
angular.module('angular.atmosphere.chat', ['angular.atmosphere']);

function ChatController($scope, atmosphereService){
  $scope.model = {
    transport: 'websocket',
    messages: []
  };

  var socket;

  var request = {
    url: '/chat',
    contentType: 'application/json',
    logLevel: 'debug',
    transport: 'websocket',
    trackMessageLength: true,
    reconnectInterval: 5000,
    enableXDR: true,
    timeout: 60000
  };

  request.onOpen = function(response){
    $scope.model.transport = response.transport;
    $scope.model.connected = true;
    $scope.model.content = 'Atmosphere connected using ' + response.transport;
  };

  request.onClientTimeout = function(response){
    $scope.model.content = 'Client closed the connection after a timeout. Reconnecting in ' + request.reconnectInterval;
    $scope.model.connected = false;
    socket.push(atmosphere.util.stringifyJSON({ author: author, message: 'is inactive and closed the connection. Will reconnect in ' + request.reconnectInterval }));
    setTimeout(function(){
      socket = atmosphereService.subscribe(request);
    }, request.reconnectInterval);
  };

  request.onReopen = function(response){
    $scope.model.connected = true;
    $scope.model.content = 'Atmosphere re-connected using ' + response.transport;
  };

  //For demonstration of how you can customize the fallbackTransport using the onTransportFailure function
  request.onTransportFailure = function(errorMsg, request){
    atmosphere.util.info(errorMsg);
    request.fallbackTransport = 'long-polling';
    $scope.model.header = 'Atmosphere Chat. Default transport is WebSocket, fallback is ' + request.fallbackTransport;
  };

  request.onMessage = function(response){
    var responseText = response.responseBody;
    try{
      var message = atmosphere.util.parseJSON(responseText);
      if(!$scope.model.logged && $scope.model.name)
        $scope.model.logged = true;
      else{
        var date = typeof(message.time) === 'string' ? parseInt(message.time) : message.time;
        $scope.model.messages.push({author: message.author, date: new Date(date), text: message.message});
      }
    }catch(e){
      console.error("Error parsing JSON: ", responseText);
      throw e;
    }
  };

  request.onClose = function(response){
    $scope.model.connected = false;
    $scope.model.content = 'Server closed the connection after a timeout';
    socket.push(atmosphere.util.stringifyJSON({ author: $scope.model.name, message: 'disconnecting' }));
  };

  request.onError = function(response){
    $scope.model.content = "Sorry, but there's some problem with your socket or the server is down";
    $scope.model.logged = false;
  };

  request.onReconnect = function(request, response){
    $scope.model.content = 'Connection lost. Trying to reconnect ' + request.reconnectInterval;
    $scope.model.connected = false;
  };

  socket = atmosphereService.subscribe(request);

  var input = $('#input');
  input.keydown(function(event){
    var me = this;
    var msg = $(me).val();
    if(msg && msg.length > 0 && event.keyCode === 13){
      $scope.$apply(function(){
        // First message is always the author's name
        if(!$scope.model.name)
          $scope.model.name = msg;

        socket.push(atmosphere.util.stringifyJSON({author: $scope.model.name, message: msg}));
        $(me).val('');
      });
    }
  });
}

That's the whole beauty of using AngularJS - you just update model and the UI get's updated via their cool bi-directional binding.

I hope the code is clean and easy to understand and you, my dear reader, can easily grasp all details. However, I'll go through some important points to make your life easier.

  • Under normal circumstances, Angular updates model instantaneously while you are typing into the text field. It's not desiserd behaviour in our scenario, that's why I had to rely on my custom input handler to update the model only after the user hits Enter key:
    atmosphere-chat-angularsrcmainwebappjavascriptapplication.js:85
    var input = $('#input');
    input.keydown(function(event){
      var me = this;
      var msg = $(me).val();
      if(msg && msg.length > 0 && event.keyCode === 13){
        $scope.$apply(function(){
          // First message is always the author's name
          if(!$scope.model.name)
            $scope.model.name = msg;
    
          socket.push(atmosphere.util.stringifyJSON({author: $scope.model.name, message: msg}));
          $(me).val('');
        });
      }
    });

    The code checks if the entered value is not empty and if it is and the Enter key was pressed, the code constucts and message object and sends it to the server with socket.push() method call.

  • The rest of the code creates a request object with callbacks.

    I'll briefly focus on the request.onMessage callback. Atmosphere triggers onMessage callback when the server pushes data to the client. Very important moment here is that this push is asynchronous and the client doesn't initiate the specific request to get this response, i.e. the communication is initiated by the server which triggers onMessage callback on the client. The benefits of this approach is that the data can be supplied on demand when it's ready removing network overhead of constantly pinging the server and checking if there are any updates. The client silently sits and waits until the server notifies updates to it. Similarly to when you order your favourite pizza and just sit and wait until a delivery guy rings your bell. You don't call every second to the pizza shop asking if the delivery guy moved forward, do you?
    In the pizza example, ordering the pizza is creating the request object and calling atmosphereService.subscribe(request); The delivery guy ringing your bell is the Atmosphere calling socket.onMessage callback. The time between ordering pizza and opening doors after the bell has rung is the useful time your program without distracting with update status requests.

    I'll leave the exercise for you, my dear reader, to review other callbacks and I hope they're simple and straightforward to understand.

Done

Now you are all set. Done. You can run the project with instructions explained above in mvn jetty:run section.

Local development

Windows

If you are on Windows and would like to modify the code and see the results hot redeployed you'll have to do one trick. There's a bug in Windows that affects Jetty server explained in Troubleshooting Locked Files on Windows on the official Jetty site. If you are curious as I were, you can read the details of the bug, but in short, when you run "mvn jetty:run" command and then want to update, for example, index.html file to see the changes updated without restarting the Jetty server, on Windows you won't be able to do it, because Windows locks the static files that Jetty loads. One of the solutions is to extract webdefault.xml from the Jetty distribution and change to false the value of useFileMappedBuffer parameter.

I did it for you already and you can get the file here http://spyboost.github.io/blog/atmosphere-chat-angular/webdefault.xml

Save the http://spyboost.github.io/blog/atmosphere-chat-angular/webdefault.xml into the project root atmosphere-chat-angular folder.

Now let's slightly modify our atmosphere-chat-angularpom.xml

atmosphere-chat-angularpom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.atmosphere.samples</groupId>
  <artifactId>atmosphere-chat-angular</artifactId>
  <packaging>war</packaging>
  <name>atmosphere-chat-angular</name>
  <url>http://maven.apache.org</url>
  <version>1.0.0-SNAPSHOT</version>

  <properties>
    <atmosphere-version>[2, )</atmosphere-version>
    <client-version>[2.0.5, )</client-version>
    <logback-version>[1, )</logback-version>
    <jetty-version>9.0.6.v20130930</jetty-version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.atmosphere</groupId>
      <artifactId>atmosphere-runtime</artifactId>
      <version>${atmosphere-version}</version>
    </dependency>
    <dependency>
      <groupId>org.atmosphere.client</groupId>
      <artifactId>javascript</artifactId>
      <version>${client-version}</version>
      <type>war</type>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback-version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>${logback-version}</version>
    </dependency>
      <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-mapper-asl</artifactId>
      <version>[1, )</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
        <source>1.7</source>
        <target>1.7</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>${jetty-version}</version>
        <configuration>
          <scanIntervalSeconds>1</scanIntervalSeconds>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <profiles>
    <profile>
      <id>windows</id>
      <activation>
        <os>
          <family>Windows</family>
        </os>
      </activation>
      <build>
        <plugins>
          <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>${jetty-version}</version>
            <configuration>
              <webApp>
                <defaultsDescriptor>webdefault.xml</defaultsDescriptor>
              </webApp>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

As you can see, I just added scanIntervalSeconds parameter and added Windows profile to override the defaultDescriptor

Linux or Unix

For lucky Linux or Unix users, the webdefault.xml trick is not required because these operating systems doesn't lock resources.

However, you still need to add scanIntervalSeconds parameter to your pom.xml (see example above) for Jetty to pick up and hot redeploy your changes made to class files.

Saturday, December 29, 2012

AngularJS with input file directive

How to make +AngularJS work with input file.

Problem: input type file doesn't work with +AngularJS
<input type="file" data-ng-model="param.file" data-ng-change="change($event)"/>

This doesn't work with angular 1.0.3. Unfortunately. The change event is not triggered. Model is not initialized. SNAFU. But with shining +AngularJS  feature called directive it is very easy to fix it and make it work elegantly without much efforts. Let's see.
index.html

<div
   data-ng-app="angularjstutorial.blogspot.com/2012/12/angularjs-with-input-file-directive.html"
   data-ng-controller="MainController">
   <input type="file" data-file="param.file"/>
   <div>param.file: {{param.file}}</div>           
</div>

script.js

module.directive('file', function(){
    return {
        scope: {
            file: '='
        },
        link: function(scope, el, attrs){
            el.bind('change', function(event){
                var files = event.target.files;
                var file = files[0];
                scope.file = file ? file.name : undefined;
                scope.$apply();
            });
        }
    };
});