Friday, April 11, 2014

Liferay CometD Ajax Push / Liferay Reverse Ajax

Introduction:

Ajax Push is a mechanism to push data from server. Generally in the web application when the client request for the server then we will get the dynamic data or updated data from server to web client/browser.

But sometimes we need a mechanism it automatically push data from server to client this is called server push mechanism. Server push is technique to push data from server to client. In this scenarios client will not send request to server but server will automatically push the data to client when it get updated.

To make it work server push technique we will use one protocol called Bayeux protocol.

Bayeux:

Bayeux is a protocol for transporting asynchronous messages (primarily over HTTP), with low latency between a web server and a web client. The messages are routed via named channels and can be delivered

We can use protocol for server to client, client to server, client to client (via the server)
The primary purpose of Bayeux is to support responsive bidirectional interactions between web clients, for example using AJAX, and the web server.

Here we will produce data on some channels and client will subscribe channels. When client subscribe channeled as soon as data published on channels will aquatically reached the client who are subscribed channels.


CometD

CometD is a scalable HTTP-based event routing bus that uses a Ajax Push technology pattern known as Comet

CometD is a Dojo Foundation project to provide implementations of the Bayeux protocol in JavaScript, java, Perl, python and other languages. Other organizations (eg. Sun, IBM and BEA) also have implementations of Bayeux that are not strictly part of the CometD project, but where possible they are linked to from this site.


Environment:

Liferay 6.2 +Tomcat 7.x+MySQL 5.1

Note:

The code will work for portal 6.2 version you can try for 6.1 too.

Download Liferay CometD Ajax Push Portlet from following location
You can find source and war file


Portlet Screen-1:


Procedure for deploy portlet and Use:

You can use war file and directly place in your portal deploy folder and test or you can also use source to deploy portlet.

Once portlet is deployed successfully you can see the portlet in sample category name as Liferay Commet D Reverse Ajax.

As soon as you place the portlet in page you can see updated stock prices for every few seconds and it will be updated automatically.

You can also open portlet in another browser there also you can see price changes automatically.

Note:

Before use this portlet please Read entire Article and Use it

Implementation:

We will use Bayeux java implementation for produce data on channels.

Once data produced on channels we will use CometD java script implementation to get the data to client automatically. We have Dojo and JQuery java script implementation for CometD.


Using ComedD Ajax Push in Liferay Portlet
  1. Configure CometD servlet in portlet web.xml
  2. Implement Data Producer to produce data
  3. Implement Service to publish data on Channels Using Bayeux
  4. Use CometD java script in client side to subscribe channel and show data.

Concept:

We will take an example of stock prices updates so that we will send stock price changes to client when changes in price changes in stocks.

We will use Java Scheduler to produce data for every few second from server side and as soon as data produced we will use CometD service to publish data on channels.

In the client side we will use CometD JQuery to subscribe the channels and show the stock price changes.

Configure CometD servlet in portlet web.xml

We need configure CometD servlet to initiate all the process. We have different servlets one of the servlet AnnotationCometdServlet and need to pass CometD service as init parameter.

The following is code snippet

<servlet>
        <servlet-name>cometd</servlet-name>
        <servlet-class>org.cometd.annotation.AnnotationCometdServlet</servlet-class>
        <init-param>
            <param-name>transports</param-name>
            <param-value>org.cometd.websocket.server.WebSocketTransport</param-value>
        </init-param>
        <init-param>
            <param-name>services</param-name>
            <param-value>com.meera.commetd.StockPriceService</param-value>
        </init-param>
        <init-param>
            <param-name>maxLazyTimeout</param-name>
            <param-value>2000</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>cometd</servlet-name>
        <url-pattern>/cometd/*</url-pattern>
    </servlet-mapping>


Implement Data Producer to produce data

We already we need some source need to generate data so we will use some java class to produce data.

We will randomly generate some price for stock quotes and we have used Scheduler Executer to run this task for every few seconds. We have used StockPriceEmitter java class


Implement Service to publish data on Channels Using Bayeux

We need service that responsible for publish data on channels with the help of Bayeux protocol. We need create channel and then need to publish

The following example syntax to create channel and publish

Create channel


bayeuxServer.createChannelIfAbsent(channelName, new ConfigurableServerChannel.Initializer()
            {
                public void configureChannel(ConfigurableServerChannel channel)
                {
                    channel.setPersistent(true);
                    channel.setLazy(true);
                }
            });



Publish data on channel


ServerChannel channel = bayeuxServer.getChannel(channelName);
            channel.publish(sender, data);


Use CometD java script in client side to subscribe channel and show data

In the client side will use CometD java script implementation to subscribe channel and show the data.

We need configure CometD servlet URL and need to add two leisters to make hand shake with server.

The following is syntax


var cometURL = location.protocol + "//" + location.host + config.contextPath + "/cometd";
        cometd.configure({
            url: cometURL,
            logLevel: 'debug'
        });

        cometd.addListener('/meta/handshake', _metaHandshake);
        cometd.addListener('/meta/connect', _metaConnect);
        cometd.handshake();


Subscribe Channel Syntax


cometd.subscribe('/stock/*', function(message)
                    {
var data = message.data;

});


Note:

We need add CometD java script implementation source in page and we will use cometd is java script object available to call methods.

We have implement application related java script in application.js

The following are all required java script source files


<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/cometd-namespace.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/cometd-json.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/AckExtension.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/TransportRegistry.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/Transport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/RequestTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/WebSocketTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/CallbackPollingTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/LongPollingTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/Utils.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/Cometd.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/jquery.cometd.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/application.js"></script>


Complete Source code for Implementation

The following is web.xml file


<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
 http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
       <display-name>LiferayCommetDRevserseAjax-portlet</display-name>
        <servlet>
        <servlet-name>cometd</servlet-name>
        <servlet-class>org.cometd.annotation.AnnotationCometdServlet</servlet-class>
        <init-param>
            <param-name>transports</param-name>
            <param-value>org.cometd.websocket.server.WebSocketTransport</param-value>
        </init-param>
        <init-param>
            <param-name>services</param-name>
            <param-value>com.meera.commetd.StockPriceService</param-value>
        </init-param>
        <init-param>
            <param-name>maxLazyTimeout</param-name>
            <param-value>2000</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>cometd</servlet-name>
        <url-pattern>/cometd/*</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>cross-origin</filter-name>
        <filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>cross-origin</filter-name>
        <url-pattern>/cometd/*</url-pattern>
    </filter-mapping>
       <jsp-config>
              <taglib>
                     <taglib-uri>http://java.sun.com/portlet_2_0</taglib-uri>
                     <taglib-location>
                           /WEB-INF/tld/liferay-portlet.tld
                     </taglib-location>
              </taglib>
              <taglib>
                     <taglib-uri>http://liferay.com/tld/aui</taglib-uri>
                     <taglib-location>/WEB-INF/tld/aui.tld</taglib-location>
              </taglib>
       </jsp-config>
</web-app>


The following is StockPriceServiceJava Class


import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import org.cometd.annotation.Service;
import org.cometd.annotation.Session;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
@Service
public class StockPriceService implements StockPriceEmitter.Listener
{
    @Inject
    private BayeuxServer bayeuxServer;
    @Session
    private LocalSession sender;

    public void onUpdates(List<StockPriceEmitter.Update> updates)
    {
        for (StockPriceEmitter.Update update : updates)
        {
            // Create the channel name using the stock symbol
            String channelName = "/stock/" + update.getSymbol().toLowerCase(Locale.ENGLISH);

            // Initialize the channel, making it persistent and lazy
            bayeuxServer.createChannelIfAbsent(channelName, new ConfigurableServerChannel.Initializer()
            {
                public void configureChannel(ConfigurableServerChannel channel)
                {
                    channel.setPersistent(true);
                    channel.setLazy(true);
                }
            });

            // Convert the Update business object to a CometD-friendly format
            Map<String, Object> data = new HashMap<String, Object>(4);
            data.put("symbol", update.getSymbol());
            data.put("oldValue", update.getOldValue());
            data.put("newValue", update.getNewValue());
              System.out.println("===========================");
            // Publish to all subscribers
            ServerChannel channel = bayeuxServer.getChannel(channelName);
            channel.publish(sender, data);
        }
    }
}



The following is StockPriceEmitter java class


import java.util.ArrayList;
import java.util.Arrays;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; 
public class StockPriceEmitter implements Runnable {
       private final ScheduledExecutorService scheduler = Executors
                     .newSingleThreadScheduledExecutor();
       private final List<String> symbols = new ArrayList<String>();
       private final Map<String, Float> values = new HashMap<String, Float>();
       private final List<Listener> listeners = new CopyOnWriteArrayList<Listener>();

       public StockPriceEmitter() {
              symbols.addAll(Arrays.asList("ORCL", "MSFT", "GOOG", "YHOO", "FB"));
              values.put("ORCL", 29.94f);
              values.put("MSFT", 27.10f);
              values.put("GOOG", 655.37f);
              values.put("YHOO", 17.82f);
              values.put("FB", 21.33f);
       }
       public List<Listener> getListeners() {
              return listeners;
       }
       public void start() {
              run();
       }
       public void stop() {
              scheduler.shutdownNow();
       }
       public void run() {
              Random random = new Random();

              List<Update> updates = new ArrayList<Update>();

              // Randomly choose how many stocks to update
              int howMany = random.nextInt(symbols.size()) + 1;
              for (int i = 0; i < howMany; ++i) {
                     // Randomly choose which one to update
                     int which = random.nextInt(symbols.size());
                     String symbol = symbols.get(which);
                     float oldValue = values.get(symbol);

                     // Randomly choose how much to update
                     boolean sign = random.nextBoolean();
                     float howMuch = random.nextFloat();
                     float newValue = oldValue + (sign ? howMuch : -howMuch);

                     // Store the new value
                     values.put(symbol, newValue);

                     updates.add(new Update(symbol, oldValue, newValue));
              }

              // Notify the listeners
              for (Listener listener : listeners) {
                     listener.onUpdates(updates);
              }

              // Randomly choose how long for the next update
              // We use a max delay of 1 second to simulate a high rate of updates
              long howLong = random.nextInt(1000);
              scheduler.schedule(this, howLong, TimeUnit.MILLISECONDS);
       }

       public static class Update {
              private final String symbol;
              private final float oldValue;
              private final float newValue;

              public Update(String symbol, float oldValue, float newValue) {
                     this.symbol = symbol;
                     this.oldValue = oldValue;
                     this.newValue = newValue;
              }

              public String getSymbol() {
                     return symbol;
              }

              public float getOldValue() {
                     return oldValue;
              }

              public float getNewValue() {
                     return newValue;
              }
       }

       public interface Listener extends EventListener {
              void onUpdates(List<Update> updates);
       }
}


The following is Liferay Portlet Class

import java.io.IOException;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.PortletException;
import com.liferay.util.bridges.mvc.MVCPortlet;
public class LiferayCommetDReverseAjax extends MVCPortlet {
     private StockPriceEmitter emitter;
    public void startStockUpdates(
    ActionRequest actionRequest, ActionResponse actionResponse)
                     throws IOException, PortletException {
    emitter = new StockPriceEmitter();
 StockPriceService service(StockPriceService)getPortletContext().
getAttribute(StockPriceService.class.getName());
  // Register the service as a listener of the emitter
  emitter.getListeners().add(service);
                      // Start the emitter
    emitter.start();
              }
public void stopStockUpdates(
ActionRequest actionRequest, ActionResponse actionResponse)
throws IOException, PortletException {
//emitter = new StockPriceEmitter();
emitter.stop();
              }
}


The following is application.js code


(function($)
{
    var cometd = $.cometd;
//alert(cometd);
    $(document).ready(function()
    {
        function _connectionEstablished()
        {
            $('#body').append('<div>CometD Connection Established</div>');
        }

        function _connectionBroken()
        {
            $('#body').append('<div>CometD Connection Broken</div>');
        }

        function _connectionClosed()
        {
            $('#body').append('<div>CometD Connection Closed</div>');
        }

        // Function that manages the connection status with the Bayeux server
        var _connected = false;
        function _metaConnect(message)
        {
            if (cometd.isDisconnected())
            {
                _connected = false;
                _connectionClosed();
                return;
            }

            var wasConnected = _connected;
            _connected = message.successful === true;
            if (!wasConnected && _connected)
            {
                _connectionEstablished();
            }
            else if (wasConnected && !_connected)
            {
                _connectionBroken();
            }
        }

        // Function invoked when first contacting the server and
        // when the server has lost the state of this client
        function _metaHandshake(handshake)
        {
            if (handshake.successful === true)
            {
                cometd.batch(function()
                {
                    cometd.subscribe('/stock/*', function(message)
                    {
                   
                    var data = message.data;
                        var symbol = data.symbol;
                        var value = data.newValue;
                       // alert(symbol);
                        var id = 'stock_'+ symbol;
                        var symbolDiv=document.getElementById(id);
                        if (!symbolDiv)
                        {
                        symbolDiv = document.createElement('div');
                        symbolDiv.id =id;
                        document.getElementById('stocks').appendChild(symbolDiv);
                        }
                        symbolDiv.innerHTML = '<span class="symbol">' + symbol + ': <b>' + value + '</b></span>';
                    });
                    // Publish on a service channel since the message is for the server only
                    //cometd.publish('/stock/*', { name: 'World' });
                });
            }
        }

        // Disconnect when the page unloads
        $(window).unload(function()
        {
            cometd.disconnect(true);
        });

        var cometURL = location.protocol + "//" + location.host + config.contextPath + "/cometd";
        cometd.configure({
            url: cometURL,
            logLevel: 'debug'
        });

        cometd.addListener('/meta/handshake', _metaHandshake);
        cometd.addListener('/meta/connect', _metaConnect);
        cometd.handshake();
    });
})(jQuery);


The following is portlet JSP page.


<%@ include file="init.jsp"%>
 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/cometd-namespace.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/cometd-json.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/AckExtension.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/TransportRegistry.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/Transport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/RequestTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/WebSocketTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/CallbackPollingTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/LongPollingTransport.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/Utils.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/Cometd.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/jquery.cometd.js"></script>
           <script type="text/javascript" src="<%=renderRequest.getContextPath() %>/js/comet/application.js"></script>
<%--
    The reason to use a JSP is that it is very easy to obtain server-side configuration
    information (such as the contextPath) and pass it to the JavaScript environment on the client.
    --%>
<style>
#stocks .symbol b{
color:red;
}
#controler-table td{
padding-left:30px;
}
</style>
<script type="text/javascript">
        var config = {
            contextPath: '<%=renderRequest.getContextPath()%>'
        };
 </script>
<portlet:actionURL  var="startStockUpdates" name="startStockUpdates">
</portlet:actionURL>
<portlet:actionURL  var="stopStockUpdates" name="stopStockUpdates">
</portlet:actionURL>
<h2>Liferay CometD Ajax Push Portlet</h2>
<br/>
<table id="controler-table">
       <tr>
              <td><a href="<%=startStockUpdates.toString()%>">Start</a></td>
              <td><a href="<%=stopStockUpdates.toString()%>">Stop</a></td>
       </tr>
</table>
<br/>
<div id="stocks"></div>


Author

Popular Posts

Recent Posts

Recent Posts Widget