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

1 comment:

  1. Hi Ben please see this question http://stackoverflow.com/questions/18741069/how-avoid-to-load-zk-powered-logo

    ReplyDelete