Showing posts with label Tomcat. Show all posts
Showing posts with label Tomcat. Show all posts

Saturday, September 7, 2013

ZK AURequest with WebSocket


Introduction

This article describe how to use WebSocket to process Request/Response in ZK.

NOTE: This is just a POC with customized ZK timer and intbox

Environment: Tomcat 7.0.42, ZK 6.5.2

Result

View demo on line
http://screencast.com/t/J6czUwBia0CI

Pre-request

Simple WebSocket Test with Tomcat
http://ben-bai.blogspot.tw/2013/07/simple-websocket-test-with-tomcat.html

ZK: Override Widget in zk.xml
http://ben-bai.blogspot.tw/2013/07/zk-override-widget-in-zkxml.html

Group Connections with WebSocket
http://ben-bai.blogspot.tw/2013/07/group-connections-with-websocket.html

Program

index.zul

Use customized timer and intbox in it.

<zk>
    <!-- Tested with ZK 6.5.2 -->
    <div apply="test.TestComposer">
        <!-- use custom timer and intbox -->
        <timer id="timer"
            repeats="true" running="true" delay="1000"
            use="components.Timer"
            useWebSocketAU="true" />
        <timer id="timer2"
            repeats="true" running="true" delay="500"
            use="components.Timer"
            useWebSocketAU="true" />
        <intbox id="ibx" value="0"
            use="components.Intbox"
            useWebSocketAU="true" />
        <intbox id="ibx2" value="0"
            use="components.Intbox"
            useWebSocketAU="true" />
    </div>
</zk>


TestComposer.java

Composer for test page, register EventListener for WebSocket to component in it.

package test;


import impl.EventListener;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.select.SelectorComposer;
import org.zkoss.zk.ui.select.annotation.Wire;

import components.IWebSocketEnhancedComponent;
import components.Intbox;
import components.Timer;

/** Tested with ZK 6.5.2
 * 
 * @author benbai123
 *
 */
public class TestComposer extends SelectorComposer<Component> {

    private static final long serialVersionUID = -5014610291543614202L;
    // custom timer and intbox
    @Wire
    Timer timer;
    @Wire
    Timer timer2;
    @Wire
    Intbox ibx;
    @Wire
    Intbox ibx2;

    public void doAfterCompose (Component comp) throws Exception {
        super.doAfterCompose(comp);
        registerEventListenerForWebSocketEnhancedTimer();
    }

    /** Register EventListener for WebSocket,
     * Events.postEvent cannot work with WebSocket (without HttpRequest),
     * so cannot use @Listen in Composer
     * 
     */
    protected void registerEventListenerForWebSocketEnhancedTimer () {
        // cast to IWebSocketEnhancedComponent
        IWebSocketEnhancedComponent enhancedTimer = (IWebSocketEnhancedComponent)timer;
        // register event listener for onTimer event of timer
        enhancedTimer.registerListenerForWebSocketEvent("onTimer",
            new EventListener () {
                private static final long serialVersionUID = -8920291597084200994L;

                public void onEvent (Event event) {
                    // increase value of intbox
                    ibx.setValue(ibx.getValue() + 1);
                }
            }
        );
        enhancedTimer = (IWebSocketEnhancedComponent)timer2;
        // register event listener for onTimer event of timer2
        enhancedTimer.registerListenerForWebSocketEvent("onTimer",
            new EventListener () {
                private static final long serialVersionUID = -5721055473084949736L;

                public void onEvent (Event event) {
                    // increase value of intbox
                    ibx2.setValue(ibx2.getValue() - 1);
                }
            }
        );
    }
}


IWebSocketEnhancedComponent.java

Define a "WebSocket Enhanced Component"

package components;

import impl.EventListener;
import impl.RequestFromWebSocket;

/**
 * Define a "WebSocket Enhanced Component"
 * for request-response pattern
 * 
 * @author benbai123
 *
 */
public interface IWebSocketEnhancedComponent {
    /**
     * Whether use WebSocket to process AU request
     */
    public void setUseWebSocketAU (boolean useWebSocketAU) ;
    /**
     * Help desktop to register itself into session
     * since we need it in WebSocket and we will not
     * use custom desktop for this POC
     */
    public void helpDesktopToRegister () ;
    /**
     * Process event sent from client
     * @see TestWebSocketServlet.TestMessageInbound#onTextMessage(java.nio.CharBuffer)
     */
    public void serviceWebSocket (RequestFromWebSocket request) ;
    /**
     * Update status to client
     */
    public void addUpdateProp (String prop, String value) ;
    /**
     * Register Event Listener at Component itself since
     * Events.postEvent cannot work with WebSocket without 
     * an active Execution, need to register listener manually in
     * Composer
     * @param listener EventListener used to process event from WebSocket request
     */
    public void registerListenerForWebSocketEvent (String evtnm, EventListener listener) ;
}


Timer.java

Implement IWebSocketEnhancedComponent as needed, process custom EventListener with onTimer Event.

package components;

import impl.DesktopUtils;
import impl.EventListener;
import impl.RequestFromWebSocket;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.zkoss.zk.ui.event.Event;

public class Timer extends org.zkoss.zul.Timer implements IWebSocketEnhancedComponent {

    private static final long serialVersionUID = 1006786038089103071L;
    private boolean _useWebSocketAU;
    // hold event listener, put it here since
    // we will not use customized AbstractComponent in this POC
    private Map<String, List<EventListener>> _listenerForWebSocket = new HashMap<String, List<EventListener>>();;

    @Override
    public void setUseWebSocketAU (boolean useWebSocketAU) {
        if (_useWebSocketAU != useWebSocketAU) {
            _useWebSocketAU = useWebSocketAU;
            smartUpdate("useWebSocketAU", useWebSocketAU);
        }
    }

    @Override
    public void helpDesktopToRegister () {
        if (getDesktop() != null) {
            // register desktop
            DesktopUtils.register(getDesktop());
        }
    }

    // called by TestWebSocketServlet.TestMessageInbound#onTextMessage(java.nio.CharBuffer)
    @Override
    public void serviceWebSocket(RequestFromWebSocket request) {
        String command = request.getCommand();
        if ("onTimer".equals(command)) {
            for (EventListener l : _listenerForWebSocket.get("onTimer")) {
                l.onEvent(new Event("onTimer", this, null));
            }
        }
    }

    @Override
    public void addUpdateProp (String prop, String value) {
        // ignore, not used in this POC
    }

    // called by TestComposer
    /**
     * register event listener with specific event name
     */
    @Override
    public void registerListenerForWebSocketEvent (String evtnm, EventListener listener) {
        // try to get listener list with respect to specified event name
        List<EventListener> listeners = _listenerForWebSocket.get(evtnm);
        if (listeners == null) {
            // create new if list is not exists
            listeners = new ArrayList<EventListener>();
            _listenerForWebSocket.put(evtnm, listeners);
        }
        // add event listener into listener list
        listeners.add(listener);
    }

    // render useWebSocketAU as needed
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
        throws java.io.IOException {
        super.renderProperties(renderer);
        if (_useWebSocketAU) {
            helpDesktopToRegister();
            // render useWebSocketAU to client side
            render(renderer, "useWebSocketAU", _useWebSocketAU);
        }
    }
}


Intbox.java

Implement IWebSocketEnhancedComponent as needed, override setValue to update status to client by WebSocket if needed.

package components;

import impl.DesktopUtils;
import impl.EventListener;
import impl.RequestFromWebSocket;

public class Intbox extends org.zkoss.zul.Intbox implements IWebSocketEnhancedComponent {

    private static final long serialVersionUID = -6488494817604420277L;
    private boolean _useWebSocketAU;

    @Override
    public void setUseWebSocketAU (boolean useWebSocketAU) {
        if (_useWebSocketAU != useWebSocketAU) {
            _useWebSocketAU = useWebSocketAU;
            smartUpdate("useWebSocketAU", useWebSocketAU);
        }
    }

    @Override
    public void helpDesktopToRegister () {
        if (getDesktop() != null) {
            // register desktop
            DesktopUtils.register(getDesktop());
        }
    }

    @Override
    public void serviceWebSocket(RequestFromWebSocket request) {
        /* ignore, not used in this POC */
    }

    @Override
    public void addUpdateProp (String prop, String value) {
        // add prop/value to update
        DesktopUtils.updateComponentProp(this, prop, value);
    }

    @Override
    public void registerListenerForWebSocketEvent (String evtnm, EventListener listener) {
        /* ignore, not used in this POC */
    }

    // render useWebSocketAU as needed
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
        throws java.io.IOException {
        super.renderProperties(renderer);
        if (_useWebSocketAU) {
            helpDesktopToRegister();
            // render useWebSocketAU to client side
            render(renderer, "useWebSocketAU", _useWebSocketAU);
        }
    }
    public void setValue (int value) {
        if (_useWebSocketAU) { // use WebSocket to process AU request?
            // set value without smartUpdate
            super.setValueDirectly(value);
            // update client status via WebSocket
            addUpdateProp("value", value+"");
        } else {
            // original function
            super.setValue(value);
        }
    }
}


EventListener.java

Used to process event in this POC.

package impl;

import java.io.Serializable;

import org.zkoss.zk.ui.event.Event;

/**
 * Define an Event listener used to process event from
 * WebSocket request
 * 
 * @author benbai123
 *
 */
public interface EventListener extends Serializable {
    /** Called when the registered event is triggered
     * 
     * @param event
     * @see TestComposer#registerEventListenerForWebSocketEnhancedTimer()
     */
    public void onEvent (Event event) ;
}


RequestFromWebSocket.java

Used to create a request object for WebSocket request message.

package impl;

import java.util.Map;

import org.zkoss.zk.ui.Component;

public class RequestFromWebSocket {
    /** Event name */
    private String _command;
    /** Target component */
    private Component _comp;
    /** Data map */
    private Map<Object, Object> _data;
    // Constructor
    public RequestFromWebSocket (String command, Component comp, Map<Object, Object> data) {
        _command = command;
        _comp = comp;
        _data = data;
    }
    // getters
    public String getCommand () {
        return _command;
    }
    public Component getComponent () {
        return _comp;
    }
    public Map<Object, Object> getData () {
        return _data;
    }
}


DesktopUtils.java

Utility of desktop, used to register desktop, store status to update, build response.

package impl;

import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.http.HttpSession;

import org.zkoss.json.JSONObject;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Sessions;

public class DesktopUtils {
    public static final String ATTRIBUTES_TO_UPDATE = "ATTRIBUTES_TO_UPDATE";
    public static final String REGISTERED_DESKTOPS = "REGISTERED_DESKTOPS";
    public static final String ALREADY_REGISTERED = "ALREADY_REGISTERED";

    public static void updateComponentProp (Component comp, String prop, String val) {
        String id = comp.getUuid();

        getPropMap(comp.getDesktop(), id).put(prop, val);
    }
    /** Register desktop so we can try to find it when
     * WebSocket receive AU request from client side
     */
    public static void register (Desktop desktop) {
        if (desktop.getAttribute(ALREADY_REGISTERED) == null) {
            getRegisteredDesktops(null).put(desktop.getId(), desktop);
            desktop.setAttribute(ALREADY_REGISTERED, ALREADY_REGISTERED);
        }
    }
    /** Get registered desktop
     * 
     * @param sess HttpSession
     * @param id desktop ID
     * @return Desktop
     */
    public static Desktop getRegisteredDesktop (HttpSession sess, String id) {
        return getRegisteredDesktops(sess).get(id);
    }
    /** Remove registered desktop
     * 
     * @param sess HttpSession
     * @param id desktop ID
     */
    public static void removeRegisteredDesktop (HttpSession sess, String id) {
        getRegisteredDesktops(sess).remove(id);
    }
    /** Get Map for registered desktops
     * 
     * @param sess HttpSession
     * @return Map<String, Desktop>
     */
    @SuppressWarnings("unchecked")
    private static Map<String, Desktop> getRegisteredDesktops (HttpSession sess) {
        Map<String, Desktop> registeredDesktops = null;
        if (sess == null) {
            // try to find session if not specified
            sess = (HttpSession)Sessions.getCurrent().getNativeSession();
        }
        synchronized (sess) {
            // try to find Map for registered desktops
            registeredDesktops = (Map<String, Desktop>)sess.getAttribute(REGISTERED_DESKTOPS);
            if (registeredDesktops == null) {
                // create Map for registered desktops and store it
                // into session if not exists
                registeredDesktops = new Hashtable<String, Desktop>();
                sess.setAttribute(REGISTERED_DESKTOPS, registeredDesktops);
            }
        }
        return registeredDesktops;
    }
    /** Build AU response of a desktop
     * 
     * @param desktop the desktop to build AU response
     * @return String response content
     */
    public static String buildResponse (Desktop desktop) {
        Map<String, Map<String, String>> compMap = getAttributesToUpdate(desktop);
        // response JSON object
        JSONObject resp = new JSONObject();
        // for each component data
        for (Entry<String, Map<String, String>> entry : compMap.entrySet()) {
            Map<String, String> propMap = entry.getValue();
            // prop/value JSON object
            JSONObject propAndVal = new JSONObject();
            // for each prop/value pair
            for (Entry<String, String> propEntry : propMap.entrySet()) {
                propAndVal.put(propEntry.getKey(), propEntry.getValue());
            }
            resp.put(entry.getKey(), propAndVal);
        }
        // clear data
        getAttributesToUpdate(desktop).clear();
        return resp.toJSONString();
    }
    /** Get Map that contains properties to update of a component
     * 
     * @param desktop specific desktop
     * @param id component id
     * @return
     */
    private static Map<String, String> getPropMap (Desktop desktop, String id) {
        // try to get propMap
        Map<String, Map<String, String>> compMap = getAttributesToUpdate(desktop);
        Map<String, String> propMap;
        propMap = compMap.get(id);
        if (propMap == null) {
            // create propMap if not exists
            propMap = new Hashtable<String, String>();
            compMap.put(id, propMap);
        }
        return propMap;
    }

    /** Get Map that contains components Map to update of a desktop
     * 
     * structure: componentMap<key, propertyValueMap<prop, value>>
     * 
     * @param desktop
     * @return
     */
    @SuppressWarnings("unchecked")
    private static Map<String, Map<String, String>> getAttributesToUpdate (Desktop desktop) {
        // try to get map
        Map<String, Map<String, String>> attributesToUpdate = 
            (Map<String, Map<String, String>>)desktop.getAttribute(ATTRIBUTES_TO_UPDATE);
        if (attributesToUpdate == null) {
            // create if not exists
            attributesToUpdate = new Hashtable<String, Map<String, String>>();
            desktop.setAttribute(ATTRIBUTES_TO_UPDATE, attributesToUpdate);
        }
        return attributesToUpdate;
    }
}


TestWebSocketServlet.java

Servlet for WebSocket, process request (msg) response in it.

package impl;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;
import org.zkoss.json.JSONArray;
import org.zkoss.json.JSONObject;
import org.zkoss.json.JSONValue;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;

import components.IWebSocketEnhancedComponent;

/**
 * Tested with Tomcat 7.0.42 and ZK 6.5.2
 * @author benbai123
 *
 */
public class TestWebSocketServlet extends WebSocketServlet {

    private static final long serialVersionUID = -7663708549630020769L;

    /**
     * For create connection only, each connection will
     * handle it self as needed
     */
    @Override
    protected StreamInbound createWebSocketInbound(String subProtocol,
            HttpServletRequest request) {
        // request uri, format is desktopId.wsreq
        String uri = request.getRequestURI();

        // get desktopId from uri
        String desktopId = uri.substring(uri.lastIndexOf("/")+1, uri.length()).replace(".wsreq", "");

        // create MessageInbound with desktop ID and session
        return new TestMessageInbound(desktopId, request.getSession());
    }
    private final class TestMessageInbound extends MessageInbound {
        // hold desktop id and session
        private String _desktopId;
        private HttpSession _session;
        // constructor
        public TestMessageInbound (String desktopId, HttpSession session) {
            _desktopId = desktopId;
            _session = session;
        }
        @Override
        protected void onOpen(WsOutbound outbound) {
            /* ignore */
        }
        // remove registered desktop
        @Override
        protected void onClose(int status) {
            DesktopUtils.removeRegisteredDesktop(_session, _desktopId);
        }
        // ignore binary message
        @Override
        protected void onBinaryMessage(ByteBuffer message) throws IOException {
            /* ignore */
        }
        /** Entry point, you can think it is similar to
         * service method (doGet/doPost) in common Servlet
         * 
         */
        @SuppressWarnings("unchecked")
        @Override
        protected void onTextMessage(CharBuffer message) throws IOException {
            // pass request to components
            // request from client: {
            //             dtid: DESKTOP_ID,
            //            [
            //                {"uuid": COMPONENT_ID_1, "evtnm": EVENT_NAME_1, "data": DATA_1},
            //                {"uuid": COMPONENT_ID_2, "evtnm": EVENT_NAME_2, "data": DATA_2},
            //                ...
            //            ]
            //        }

            // parse to JSON object
            JSONObject jsObj = (JSONObject)JSONValue.parse(message.toString());
//            System.out.println("desktop: " + jsObj.get("dtid"));
            // enable the line below to see (merged) request from Client
//            System.out.println("requests: " + jsObj.get("requests"));

            // get desktop
            Desktop desktop = DesktopUtils.getRegisteredDesktop(_session, (String)jsObj.get("dtid"));
            JSONArray requests = (JSONArray)JSONValue.parse((String)jsObj.get("requests"));
            // for each request in requests
            for (int i = 0; i < requests.size(); i++) {
                // get request
                JSONObject reqObj = (JSONObject)requests.get(i);
                // find component
                Component target = desktop.getComponentByUuidIfAny((String)reqObj.get("uuid"));
                // get data
                Map<Object, Object> data = (Map<Object, Object>)reqObj.get("data");
                // create request object
                RequestFromWebSocket req = new RequestFromWebSocket((String)reqObj.get("evtnm"), target, data);
                // pass request object to service method of component
                ((IWebSocketEnhancedComponent)target).serviceWebSocket(req);
            }
            // build and send response after service
            sendResponse(this, desktop);
        }
    }
    /** Send message via WebSocket to specific desktop
     * 
     * @param connection connection to use
     * @param desktop used to build response
     */
    public static void sendResponse (TestMessageInbound connection, Desktop desktop) {
        // build response
        String response = DesktopUtils.buildResponse(desktop);
        try {
            // send response
            connection.getWsOutbound().writeTextMessage(CharBuffer.wrap(response));
        } catch (IOException ignore) {
            /* ignore */
        }
    }
}


zk.xml

Override javascript functions to support WebSocket.

<zk>
    <device-config>
        <device-type>ajax</device-type>
        <embed><![CDATA[
            <script type="text/javascript">
                zk.afterLoad("zul", function () {
                    var _wgt = {};

                    zk.override(zk.Widget.prototype, _wgt, {
                        // setter for set context of WebSocket
                        setUseWebSocketAU: function (v) {
                            if (v != this._useWebSocketAU)
                                this._useWebSocketAU = v;
                        },
                        bind_: function (dt, skipper, after) {
                            // call original function
                            _wgt.bind_.apply(this, arguments);
                            // initiate WebSocket after bind_
                            if (this._useWebSocketAU)
                                this.helpDesktopToInitWebSocket();
                        },
                        // init WebSocket
                        helpDesktopToInitWebSocket: function () {
                            var desktop = this.desktop;
                            // if didn't init
                            if (desktop && !desktop.TestWebSocket) {
                                // override desktop to support WebSocket
                                overrideDesktop(desktop);
                                desktop.TestWebSocket.connect();
                            }
                        }
                    });
                });
                // Override Timer widget and desktop since
                // we do not want to override zAu / jq.xhr for this POC
                zk.afterLoad("zul.utl", function () {
                    var _tmWgt = {};

                    zk.override(zul.utl.Timer.prototype, _tmWgt, {
                        // onTimer
                        _tmfn: function () {
                            if (!this._repeats) this._running = false;
                            // whether _useWebSocketAU?
                            if (this._useWebSocketAU
                                && this.desktop.webSocketReady) {
                                // build and send request via WebSocket
                                var req = {uuid: this.uuid,
                                            evtnm: 'onTimer',
                                            data: {}
                                        };
                                this.desktop.sendRequestToWebSocket(req);
                            } else // call original function
                                this.fire('onTimer', null, {ignorable: true});
                        }
                    });
                });
                function overrideDesktop (desktop) {
                    desktop.TestWebSocket = {
                        socket: null,
                        connect: (function() {
                            // .wsreq for servlet mapping defined in web.xml
                            var path = window.location.host + window.location.pathname,
                                host = 'ws://' + path + desktop.id + '.wsreq';
                            if ('WebSocket' in window) {
                                this.socket = new WebSocket(host);
                            } else if ('MozWebSocket' in window) {
                                this.socket = new MozWebSocket(host);
                            } else {
                                alert('Error: WebSocket is not supported by this browser.');
                                return;
                            }
                            // process message from server
                            this.socket.onmessage = function (msg) {
                                desktop.doWebSocketMessage_(msg);
                            };
                            // store ready state for components to check
                            this.socket.onopen = function () {
                                desktop.webSocketReady = true;
                            };
                        }),
                        disconnect: function () {
                            // close and clear
                            this.socket.close();
                            this.socket = null;
                            desktop.TestWebSocket = null;
                        },
                        // send message to server
                        sendRequestToWebSocket: (function(msg) {
                            this.socket.send(msg);
                        })
                    };
                    desktop.doWebSocketMessage_ = function (msg) {
                        // parse response (msg.data)
                        // pattern: {
                        //            componentId: {prop: val, prop2: val2, ...},
                        //            componentId2: {prop: val, prop2: val2, ...}, ...
                        //        }
                        // Enable the line below to see (merged) response from server
                        // zk.log(msg.data);
                        var resp = jq.evalJSON(msg.data),
                            props, // properties ({key: value, ...}) to update
                            val,
                            setter,
                            wgt;
                        // for each component (ID)
                        for (var key in resp) {
                            // get widget by ID
                            wgt = zk.Widget.$('#'+key);
                            // get properties to update by ID
                            props = resp[key];
                            // for each property
                            for (var prop in props) {
                                // get value by property name
                                val = props[prop];
                                // build setter by property name (xyz -> setXyz)
                                setter = 'set' + prop.charAt(0).toUpperCase() + prop.slice(1);
                                // call setter to set value to widget
                                wgt[setter](val);
                            }
                        }
                    };
                    // API for widget to send request to server
                    desktop.sendRequestToWebSocket = function (req) {
                        if (!desktop.eventArrayForWebSocket)
                            desktop.eventArrayForWebSocket = [];
                        // push req into an array
                        desktop.eventArrayForWebSocket.push(req);
                        // send multiple req with single request
                        if (!desktop.sendRequestTimerForWebSocket) {
                            desktop.sendRequestTimerForWebSocket = setTimeout(function () {
                                var jsonToSend = jq.toJSON({
                                    dtid: desktop.id,
                                    requests: jq.toJSON(desktop.eventArrayForWebSocket)
                                });
                                desktop.TestWebSocket.sendRequestToWebSocket(jsonToSend);
                                desktop.eventArrayForWebSocket = desktop.sendRequestTimerForWebSocket = null;
                            }, 50);
                        }
                    };
                }
            </script>
        ]]></embed>
    </device-config>
</zk>


web.xml

Define servlet for WebSocket.

<?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">

    <display-name>ReplaceAuRequestWithWebSocket</display-name>
    <servlet>
        <servlet-name>testWebSocketServlet</servlet-name>
        <servlet-class>impl.TestWebSocketServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>testWebSocketServlet</servlet-name>
        <url-pattern>*.wsreq</url-pattern>
    </servlet-mapping>
</web-app>


Reference

ZK Source
http://github.com/zkoss/zk/tree/master/zul/src/

Download

Full project at github
https://github.com/benbai123/ZK_Practice/tree/master/Integrate/WebSocket/AURequestWithWebSocket

Demo Flash
https://github.com/benbai123/ZK_Practice/blob/master/demo_src/swf/Integrate/WebSocket/AURequestWithWebSocket.swf

Sunday, July 14, 2013

Group Connections with WebSocket


Introduction

This article describe how to group connections of WebSocket with server side grouping information.

Environment: Tomcat 7.0.42

Pre-request

Simple WebSocket Test with Tomcat
http://ben-bai.blogspot.tw/2013/07/simple-websocket-test-with-tomcat.html

Result

View demo on line
http://screencast.com/t/uc9VPj2k

Program

index.jsp

Create and trigger WebSocket connection with specified group.

<%@ page language="java"
    contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!-- EL is required -->
<%@ page isELIgnored ="false" %>
<html>
    <head>
        <meta http-equiv="Content-Type" 
            content="text/html; charset=UTF-8"/>
        <title>practice one</title>
        <script type="text/javascript">
            var TestWebSocket = {
                socket: null,
                group: null,
                connect: (function() {
                    // store the specified group
                    TestWebSocket.group = '${GROUP}';
                    // create WebSocket with the specified group
                    var loc = window.location,
                        host = ('ws://' + loc.host + loc.pathname).replace('index.jsp', '') + TestWebSocket.group + '.wsreq';
                    if ('WebSocket' in window) {
                        TestWebSocket.socket = new WebSocket(host);
                    } else if ('MozWebSocket' in window) {
                        TestWebSocket.socket = new MozWebSocket(host);
                    } else {
                        alert('Error: WebSocket is not supported by this browser.');
                        return;
                    }
                    // process message from server
                    TestWebSocket.socket.onmessage = function (message) {
                        document.getElementById('content').innerHTML = message.data;
                    };
                }),
                // send specified group to server
                trigger: (function() {
                    TestWebSocket.socket.send(TestWebSocket.group);
                })
            };

            TestWebSocket.connect();
        </script>
    </head>
    <body>
        Group: ${GROUP}
        <div id="content"></div>
        <button id="btn" onclick="TestWebSocket.trigger();">trigger</button>
    </body>
</html>


TestFilter.java

Used to specify group, update it to client side.

package test;

import java.io.IOException;
import java.util.Random;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebFilter(filterName="TestFilter", urlPatterns={"/index.jsp"})
public class TestFilter implements Filter {
    private static final String[] _groups = {"GroupOne", "GroupTwo", "GroupThree"};
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
             FilterChain chain) throws IOException, ServletException {
        Random r = new Random();
        // put the value "GroupOne"/"GroupTwo"/"GroupThree" with
        // name "GROUP" into request scope
        ((HttpServletRequest)request).setAttribute("GROUP", _groups[r.nextInt(3)]);
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}
}


TestWebSocketServlet.java

Create server side connections with specified group.

package test;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.catalina.websocket.WsOutbound;

/**
 * Tested with Tomcat 7.0.42
 * @author benbai123
 *
 */
public class TestWebSocketServlet extends WebSocketServlet {

    private static final long serialVersionUID = -7663708549630020769L;

    // for message that will be sent to client
    private final AtomicInteger _cntForGroupOne = new AtomicInteger(0);
    private final AtomicInteger _cntForGroupTwo = new AtomicInteger(0);
    private final AtomicInteger _cntForGroupThree = new AtomicInteger(0);
    // hold each connection in this Set
    private final Set<TestMessageInbound> connections =
        new CopyOnWriteArraySet<TestMessageInbound>();
    /**
     * For create connection only, each connection will
     * handle it self as needed
     */
    @Override
    protected StreamInbound createWebSocketInbound(String subProtocol,
            HttpServletRequest request) {
        String uri = request.getRequestURI();
        String group = uri.substring(uri.lastIndexOf("/")+1, uri.length()).replace(".wsreq", "");

        // Create connection with specified group
        return new TestMessageInbound(group);
    }
    private final class TestMessageInbound extends MessageInbound {
        private String _group;
        public TestMessageInbound (String group) {
            _group = group;
        }
        // add self instance into connections Set while opened
        @Override
        protected void onOpen(WsOutbound outbound) {
            connections.add(this);
        }
        // remove self instance from connections Set whild closed
        @Override
        protected void onClose(int status) {
            connections.remove(this);
        }
        // ignore binary message since we just want to process text messages
        @Override
        protected void onBinaryMessage(ByteBuffer message) throws IOException {
            // ignore
        }
        // send a message to each connection in connections Set
        // while receive a text message (specific group here)
        @Override
        protected void onTextMessage(CharBuffer message) throws IOException {
            send(message.toString());
        }
        public String getGroup () {
            return _group;
        }
    }
    // send message to specific group
    public void send (String group) {
        String msg = "Current count for " + group + ": " + getCounterByGroup(group);;
        for (TestMessageInbound connection : connections) {
            try {
                if (group.equals(connection.getGroup())) {
                    connection.getWsOutbound().writeTextMessage(CharBuffer.wrap(msg));
                }
            } catch (IOException ignore) {
                /* ignore */
            }
        }
    }
    // get message for specific group
    private int getCounterByGroup (String group) {
        return "GroupOne".equals(group)? _cntForGroupOne.incrementAndGet() :
            "GroupTwo".equals(group)? _cntForGroupTwo.incrementAndGet() : _cntForGroupThree.incrementAndGet();
    }
}


web.xml

For servlet mapping

<?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">
    <display-name>TomcatWebSocketTest</display-name>
    <servlet>
        <servlet-name>testWebSocketServlet</servlet-name>
        <servlet-class>test.TestWebSocketServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>testWebSocketServlet</servlet-name>
        <url-pattern>*.wsreq</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>


Download

Full project at github
https://github.com/benbai123/JSP_Servlet_Practice/tree/master/Practice/WebServer/WebSocket/GroupConnectionsWithWebSocket

Demo Flash
https://github.com/benbai123/JSP_Servlet_Practice/blob/master/demo_src/Server/Tomcat/GroupConnectionsWithWebSocket.swf

Sunday, January 22, 2012

Integrate Tomcat with Apache HTTP Server: Forward JSP/Servlet Content to Tomcat

Usually we want serve static content or php by Apache, only serve JSP/Servlet content by Tomcat.
We can do this by several steps, described as follows:

Steps:

Step 1.
Get Apache HTTP Server:
go to http://httpd.apache.org/,
click from a mirror then download (I choose 2.2.21) and install


The document root of Apache HTTP Server is
APACHE_HOME/htdocs
(APACHE_HOME = [Install path]/Apache2.2)
You can change it by modify  APACHE_HOME /conf/httpd.conf

Create an icon named favicon.ico,
put it into APACHE_HOME/htdocs
You may need the png2ico to help this


Step 2.
Get Tomcat:
go to http://tomcat.apache.org/
click Tomcat 7.0 then download and install
(I just extract it)
then set TOMCAT_HOME Environment Variable

Step 3.
Copy htdocs/index.html and rename it to index.jsp,
put it at APACHE_HOME/htdocs/JEE/Test
(or put Test under TOMCAT_HOME/webapps if you skip Step 4.4)

currently it will show all html if you visit http://localhost/Test/index.jsp

Step 4.
Connect Apache HTTP Server and tomcat as follows:

Step 4.1.
Get mod_jk from http://www.apache.org/dist/tomcat/tomcat-connectors/jk/binaries/windows/
I choose tomcat-connectors-1.2.32-windows-i386-httpd-2.2.x.zip
Extract it and find mod_jk.so in it
Put mod_jk.so into  APACHE_HOME /modules

Step 4.2.
Create workers.properties,
Put it into APACHE_HOME /conf
Edit it as below

# Define worker 'worker1'
worker.list=worker1

# Set properties for worker 'worker1' (ajp13)
worker.worker1.type=ajp13
worker.worker1.host=localhost
worker.worker1.port=8009

worker.worker1.cachesize=10
worker.worker1.cache_timeout=600
worker.worker1.socket_keepalive=1
worker.worker1.recycle_timeout=300

For more information of workers.properties, please refer to
http://tomcat.apache.org/connectors-doc/reference/workers.html

Step 4.3.
Create folder C:/var/log/httpd
Edit APACHE_HOME /conf/http.conf, append

# Load mod_jk module
LoadModule    jk_module  modules/mod_jk.so
# Where to find workers.properties
JkWorkersFile conf/workers.properties
# Where to put jk shared memory
JkShmFile     /var/log/httpd/mod_jk.shm
# Where to put jk logs
JkLogFile     /var/log/httpd/mod_jk.log
# Set the jk log level [debug/error/info]
JkLogLevel    info
# Select the timestamp log format
JkLogStampFormat "[%a %b %d %H:%M:%S %Y] "
# Send servlet for context /Test to worker named worker1
JkMount  /Test/* worker1

This line JkMount /Test/* worker1 denotes forward the context path match /Test/* to Tomcat,
you can add multiple JkMount to define multiple path

Step 4.4.
This step should be skipped if you put Test under TOMCAT_HOME/webapps at Step 3
Edit TOMCAT_HOME/conf/server.xml

Change

<Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

to

<Host name="localhost"  appBase="APACHE_HOME\htdocs\JEE"
            unpackWARs="true" autoDeploy="true">

The Apache HTTP Server is connected to Tomcat now,
the only thing left to do is restart Apache HTTP Server and start Tomcat

Step 5.
Restart Apache HTTP Server:

Click the icon at System Tray


then click Apache2.2 -> Restart

Step 6.
Start Tomcat as a service,

At this point, make sure you have set the environment variables correctly,
for example:



Open cmd.exe as Administrator (Right click on cmd.exe -> Run as Administrator)
cd to TOMCAT_HOME/bin
Execute

service.bat install

NET START Tomcat7


Now you can visit http://localhost/Test/index.jsp and see your JSP page displayed correctly.

Download:
The resources can be downloaded from github
https://github.com/benbai123/JSP_Servlet_Practice/tree/master/Environment

References:
http://tomcat.apache.org/connectors-doc/webserver_howto/apache.html
http://tomcat.apache.org/connectors-doc/reference/workers.html
http://tomcat.apache.org/tomcat-4.1-doc/config/host.html
http://www.devside.net/guides/windows/tomcat