Pages

Wednesday, November 02, 2011

Atmosphere with jquery tutorial to stream data to web browser

When I started to learn Atmosphere to do async server push to client I was overwhelmed with the available tutorials on the net! The blog at 6312 is the best but it had too much details. Not much patience to read, I just needed a really quick start and not a really long page with lot of details. So, Here is a short tutorial to the people of my kinds who just want to learn and implement server push fast and dirty. For more details and explanation see the blog I mentioned before.

I am using atmosphere 0.7.2, tomcat 6, and JDK 6.

Step 1: Download the sample jquery-pubsub-0.7.2.war from here . Extract the war to a known directory. Copy the jars from the WEB-INF/lib to your webapp lib which needs the atmosphere support. Make sure that your application lib contains only one version of the jar. You do not want to find yourself troubleshooting problems due to multiple version of same jar in the classpath of your webcontainer.

Step 2: Create a folder META-INF in the same level as WEB-INF in your application war. Create context.xml from the content shown below and put the same file in both META-INF and WEB-INF.

<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Loader delegate="true"/>
</Context>

Step 3: Put the below snippet in web.xml between web-app xml tag. Make sure to note the “/account/number” and “/atm/*” path provided. We will use this path “/atm/account/number” to subscribe from the server as shown later.


<servlet>
<servlet-name>AtmosphereServlet</servlet-name>
<description>AtmosphereServlet</description>
<servlet-class>org.atmosphere.cpr.AtmosphereServlet</servlet-class>
<init-param>
<param-name>grizzly.application.path</param-name>
<param-value>/account/number</param-value>
</init-param>
<init-param>
<param-name>org.atmosphere.useWebSocket</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>org.atmosphere.useNative</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>org.atmosphere.cpr.broadcastFilterClasses</param-name>
<param-value>org.atmosphere.client.FormParamFilter,org.atmosphere.client.JavascriptClientFilter
</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>AtmosphereServlet</servlet-name>
<url-pattern>/atm/*</url-pattern>
</servlet-mapping>

Step 4: I would assume you know jquery. Create atm.js with the below code and add it to your collection of javascripts in war.


var topicSubs = 'all'
var urlLocation = 'http://localhost:8080/mywebapp/atm/account/';

var onData = function(data){
$('body').append("Message Received: " + data);
}
var appSubscriber = function() {
var callbackAdded = false;

function subscribe() {
function callback(response) {
if (response.transport != 'polling' && response.state != 'connected' && response.state != 'closed') {
if (response.status == 200) {
var data = response.responseBody;
if (data.length > 0 && data.search('-->')==-1) {
onData(data);
}
}
}
}


$.atmosphere.subscribe(urlLocation + topicSubs, !callbackAdded ? callback : null,$.atmosphere.request = { transport: 'websocket' });
callbackAdded = true;
}

subscribe();
}

Step 5: In your html page subscribe using the below shown code. Make sure to include the above script atm.js in the page. Here, I am overriding the variables declared in the atm.js above with required values. See the path in urlLocation, http”//localhost:8080/mywebapp” is your application URL, “/atm” is for atmosphere servlet URL pattern, “/account” is for jersey as you will see in server side code in sext step, and “/8832221” which is added to the URL by atm.js code is used by the server code to push data for that account number.

<script type="text/javascript">
$(document).ready(function(){
topicSubs = '8832221';
urlLocation = 'http://localhost:8080/mywebapp/atm/account/';
onData = function(data){
$('#content').append("Message Received: " + data + "</br>");
};
appSubscriber();
});

</script>

Step 6: Now that we have the html UI ready, we can write the server side code. Note that the html page will get updates only those that are pushed using the below push method after the html page has loaded and subscribed using the JS above. If you need old updates, then you have to cache when the subscriber is not available for a account and if you need to make sure not some random person can access the data from others account number, then an auth token can be embedded in the subscription string with account number that can be validated. You can code these requirements in the EventsLogger class that has the callback hooks from the atmosphere when different events occur. Some of you may not get updates if you are using different applications talking to each other due to cross domain issue which is solved by adding a http header in the below shown code. If you look at the path annotation it maps the “/account/{topic}”, where the topic is the account number from the html page that is sent to subscribe. Push method also prints all the broadcasters available which are equal to the subscribers from the html and have the id as the name.


import org.atmosphere.cpr.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import com.sun.jersey.spi.resource.Singleton;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;

import org.atmosphere.annotation.Broadcast;
import org.atmosphere.annotation.Resume;
import org.atmosphere.annotation.Suspend;
import org.atmosphere.cpr.AtmosphereHandler;
import org.atmosphere.jersey.Broadcastable;
import org.atmosphere.jersey.JerseyBroadcaster;
import org.atmosphere.jersey.SuspendResponse;


@Path("/account/{topic}")
@Produces("text/html;charset=ISO-8859-1")
public class OurAtmosphereHandler{
private @PathParam("topic")
Broadcaster topic;

@GET
public SuspendResponse<String> subscribe(@Context HttpServletResponse httpResponse) {
httpResponse.addHeader("Access-Control-Allow-Origin","*");
return new SuspendResponse.SuspendResponseBuilder<String>()
.broadcaster(topic)
.outputComments(true)
.addListener(new EventsLogger())
.build();

}

public static void push(String message, String topic){

Collection<Broadcaster> broadcasters = BroadcasterFactory.getDefault().lookupAll();

for(Broadcaster b : broadcasters){
System.out.println(b.toString());
}

System.out.println("Request to push- Message: " + message + ", Topic: " + topic);
Broadcaster b = null;
if(null != (b = BroadcasterFactory.getDefault().lookup(JerseyBroadcaster.class,topic))){
System.out.println("Request to push- Message: " + message + ", Topic: " + topic);
b.broadcast(message + "\n");
}
}

private class EventsLogger implements AtmosphereResourceEventListener {

@Override
public void onSuspend(AtmosphereResourceEvent<HttpServletRequest, HttpServletResponse> event) {
event.getResource().getResponse().addHeader("Access-Control-Allow-Origin","*");
System.out.println("onSuspend(): " + event.getResource().getRequest().getRemoteAddr() + " : " + event.getResource().getRequest().getRemoteHost());
}

@Override
public void onResume(AtmosphereResourceEvent<HttpServletRequest, HttpServletResponse> event) {
event.getResource().getResponse().addHeader("Access-Control-Allow-Origin","*");
System.out.println("onResume(): " + event.getResource().getRequest().getRemoteAddr() + " : " + event.getResource().getRequest().getRemoteHost());
}

@Override
public void onDisconnect(AtmosphereResourceEvent<HttpServletRequest, HttpServletResponse> event) {
System.out.println("onDisconnect(): " + event.getResource().getRequest().getRemoteAddr() + " : " + event.getResource().getRequest().getRemoteHost());
}

@Override
public void onBroadcast(AtmosphereResourceEvent<HttpServletRequest, HttpServletResponse> event) {
event.getResource().getResponse().addHeader("Access-Control-Allow-Origin","*");
System.out.println("onBroadcast(): " + event.getMessage());
}

@Override
public void onThrowable(AtmosphereResourceEvent<HttpServletRequest, HttpServletResponse> event) {
System.out.println("onThrowable(): " + event);
}
}

}

So, have fun using streaming technology using atmosphere. I would suggest using chrome as it supports websockets and is most reliable. You can use polling if working with other browsers. Just change the transport in the atm.js with “polling”. Atmosphere is an awesome technology and I would suggest to visit the blog 6312 that I mentioned in the beginning for more details and many different options and customization.