Showing posts with label HTML5. Show all posts
Showing posts with label HTML5. Show all posts

Friday, December 4, 2015

Thursday, April 3, 2014

ZK, AngularJS and Pure HTML Template


Introduction

As we already know ZK used to generate all html content within 'redraw' function of a ZK Widget, this is a little bit (?) hard to maintain and customize dom structure and style of component.

This article describe how to create ZK Component with AngularJS Directive so you can use a pure HTML template to generate dom elements of ZK Component and has some advantages:
1. Easier to maintain.
2. Easier to customize.

And also some drawbacks (part of them has workaround):
1. Need to load html template via several ajax call.
  Workaround: You can load all templates at once,
       for more information please refer to
       AngularJS: load all templates in one file or AngularJS Merge Template
2. Will blink while rerender, you can use a fake dom to avoid blinking but this may affect the client side performance.
3. Has issue of dynamically insert and compile complex dom elements so need to rerender if dynamically add children.

Note:

1. This is just a POC done in a short time, probably not well organized and has some unknown issues.
2. I am not familiar with AngularJS so this POC may contains bad practice.

Pre-request

AngularJS Create Components

ZK Component Development Tutorial: Getting Started

ZK CDT: Fire Event to Server to Create a SelectableTextNote

Result

Online demo
http://screencast.com/t/nlLsNdbV8A

Description:

The concept is simple.

In AngularJS, a directive which support pure html template is a function that returns an Object Literal, and registered with a specific name (see Creating Custom Directives).
Both of them -- the name and the function -- can be declared at any places.
e.g. inside the Javascript of ZK Widget.

So we can specify Angular Tag (ngTag) and Angular Template Url (ngTemplateUrl) to each ZK Component and do below:

In ZK Component:
* Server Side:
** Make sure that each templateUrl maps to different tag.
* Client Side:
** Output ngTag in redraw function.
** Store component to zk.ngcomponents in redraw function.
** Register Angular Directive with ngTag.

In Angular Directive
* Use ngTemplateUrl as templateUrl in directive.
* Store $scope, $compile and controller to $element in controller function of directive.
* Bind ZK Widget with Angular Objects above in link function of directive.

Then we can use AngularJS Directive with pure HTML Template then control ZK Widget from AngularJS Directive and vice versa.

See

NgComp.java#fixTemplateMapping
NgComp.js#redraw
tabs.js#registerTemplate
zacomponents.js#addComponent
zacomponents.js#storeNgObjects
zacomponents.js#directive
zacomponents.js#bindng_

for more information.

Program Introduction

Testing zul page
index.zul

Testing Composer
TestComposer.java

AngularJS Library
angular.min.js

Bootstrap3 CSS Library
bootstrap.min.css

Utils for AngularJS ZK Widget
zacomponents.js

Basic APIs of AngularJS ZK Widget
NgComp.js

The Folder Contains All Default Templates
templates

Basic Java Class for AngularJS ZK Component
NgComp.java

Others:
Please refer to the full project below

Download

Full project at github

Demo Flash (click View Raw to download)

Monday, February 3, 2014

AngularJS Wireup Backend


Simple Note

Official sample with personal comments.

To run this sample you will need need to setup a server since this sample rely on ajax that need to be run under http protocol.

Personally I do not like this sample, mixing too many things and not described well.

Pre-request

AngularJS Controller


Testing Files

Run test.html to test

https://github.com/benbai123/HTML_CSS_Javascript_practice/tree/master/Frameworks/Angularjs/Basic/Basic_003__Wireup_Backend

JS Libs

https://github.com/benbai123/HTML_CSS_Javascript_practice/tree/master/Frameworks/Angularjs/lib

Reference

http://angularjs.org/

Sunday, October 6, 2013

WebSocketServerPush vs ZKComet Performance Testing


Simple Note

Testing Environment

Server:

Tomcat 7.0.42

ZK Version:

6.5.2

WebSocket ServerPush Implementation:

ZK ServerPush with WebSocket - Rework
http://ben-bai.blogspot.tw/2013/10/zk-serverpush-with-websocket-rework.html

Testing Steps

1. Start Tomcat

2. Open visualVM

3. Open 5 testing pages

There are 2 cases for WebSocket ServerPush, push to desktop separately (index.zul) and push to all desktop at once (index_per_desktop.zul), 1 case for ZK Comet (push to desktop separately - index.zul)

4. Wait 2 minutes

5. Take snapshot

Testing Result

(Click snapshot to view large image)

Executing Result:

ZK Comet - index.zul



WebSocket ServerPush - index.zul



WebSocket ServerPush - index_per_desktop.zul



JVisualVM

ZK Comet - index.zul



WebSocket ServerPush - index.zul



WebSocket ServerPush - index_per_desktop.zul



Observation

Execution Speed

WebSocket ServerPush is two times faster than ZK Comet

Memory Consumption

Highest: WebSocket ServerPush (215/187 MB for per-desktop/push-to-all) is two times higher than ZK Comet (100MB), resonable since WebSocket is faster, do more work and use more memory at the same time.

Lowest: Almost the same (20 ~ 23 MB), most of the memory can be GCed.

CPU Usage

WebSocket AU (2%~8%/1%~6% for per-desktop/push-to-all after page loaded) is better than ZK Comet (1%~24% after page loaded).

Conclution

WebSocket do double work with 1/4 ~ 1/3 CPU Usage and double Memory Consumption.

Download

Testing projects at github
https://github.com/benbai123/ZK_Practice/tree/master/General/Performance_Testing/WebSocketServerPush_vs_ZKComet

Saturday, October 5, 2013

ZK ServerPush with WebSocket - Rework


Introduction

This article describe how to use WebSocket to do ServerPush in ZK MVVM, modified from previous article ZK: Server Push with WebSocket (http://ben-bai.blogspot.tw/2013/07/zk-server-push-with-websocket.html) with the desktop based implementation from ZK AURequest with WebSocket (http://ben-bai.blogspot.tw/2013/09/zk-aurequest-with-websocket.html).

It integrates WebSocket in ZK for ServerPush in more general way and has better performance (compared with previous component based version).

Environment: Tomcat 7.0.42, ZK 6.5.2

NOTE: This is just a POC with some customized components.

Pre-request

ZK AURequest with WebSocket
http://ben-bai.blogspot.tw/2013/09/zk-aurequest-with-websocket.html

ZK Create Helper Tag to Make Programmer Happier
http://ben-bai.blogspot.tw/2013/09/zkcreatehelpertag-to.html

ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html

Result

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

Program

There are lots of source files, will only post code for some of them and post link for others.

index.zul

Entry page

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/WebContent/index.zul

push_to_all.zul

Push value to all desktop (i.e., Application scope) with button click.

<zk>
    <!-- Tested with ZK 6.5.2 -->
    <window apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.TestPushToAllVM')">
        Counter:
        <!-- intbox, listen to positive integer in the begining -->
        <intbox readonly="true" />
            <contextBinding field="value" context="@load(vm.counter)" />
        listen to: <label value="@load(vm.counter)" />
        <div />
        inverse Counter:
        <!-- intbox, listen to negative integer in the begining -->
        <intbox id="inverseCounter" readonly="true" />
            <contextBinding field="value" context="@load(vm.inverseCounter)" />
        listen to: <label value="@load(vm.inverseCounter)" />
        <div />
        <!-- update value of positive/negative integer -->
        <button id="updateCounterBtn" label="updaet counter and negative counter"
            onClick="@command('updateCounter')" />
        <!-- switch listening context of the two intboxess above -->
        <button label="switch context" id="switchBtn"
            onClick="@command('switchCounter')" />
    </window>
</zk>


test.TestPushToAllVM.java

VM for push_to_all.zul

package test;

import impl.serverpush.ServerPushUtil;

import java.util.concurrent.atomic.AtomicInteger;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;

/** Tested with ZK 6.5.2
 * 
 * @author benbai123
 *
 */
public class TestPushToAllVM {
    /** context listened by intboxes */
    private String _counter = "positive";
    private String _inverseCounter = "negative";
    /** desktop of this VM */
    private Desktop targetDesktop;

    /** counter used to update positive/negative integer */
    private static AtomicInteger _cntCounter = new AtomicInteger(0);

    // getters
    public String getCounter () {
        return _counter;
    }
    public String getInverseCounter () {
        return _inverseCounter;
    }
    /**
     * update value to context 'positive' and 'negative' via
     * WebSocket
     * 
     * All components that listen to these context will be updated
     */
    @Command
    public void updateCounter () {
        int val = _cntCounter.incrementAndGet();
        push(val, "positive", false);
        push(-1*val, "negative", false);
    }
    /** switch listening components of 'positive' and 'negative' context
     * current desktop only
     * 
     */
    @Command
    @NotifyChange({"counter", "inverseCounter"})
    public void switchCounter () {
        int val = _cntCounter.get();
        // push to opposite (for self desktop) before switch context
        push(val, "negative", true);
        push(-1*val, "positive", true);
        String tmp = _counter;
        _counter = _inverseCounter;
        _inverseCounter = tmp;
        
    }

    /** push value
     * 
     * @param val
     * @param context
     */
    private void push (Object val, String context, boolean desktopOnly) {
        try {
            if (targetDesktop == null) {
                targetDesktop = Executions.getCurrent().getDesktop();
            }
            if (desktopOnly) {
                // push to current desktop only
                ServerPushUtil.pushVlaue(val, context, targetDesktop);
            } else {
                // push to all desktop
                ServerPushUtil.pushVlaue(val, context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


automatic_push.zul

Update value with java thread, Desktop scope.

<zk>
    <!-- Tested with ZK 6.5.2 -->
    <window apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.TestAutoPushVM')">
        Self:
        <!-- intbox that will be updated -->
        <intbox readonly="true" />
            <contextBinding field="value" context="@load(vm.task)" />
        <!-- start update -->
        <button label="start" onClick="@command('start')" />
        <!-- stop update -->
        <button label="stop" onClick="@command('stop')" />
        <!-- textbox that bind rows/cols to integer -->
        <textbox />
            <contextBinding id="cbd" field="@load(vm.prop)" context="@load(vm.task)" />
        <button label="switch field (rows/cols)" onClick="@command('switchRowCol')" />
    </window>
</zk>


test.TestAutoPushVM.java

VM for automatic_push.zul

package test;

import impl.serverpush.ServerPushUtil;

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;

/** Tested with ZK 6.5.2
 * 
 * @author benbai123
 *
 */
public class TestAutoPushVM {

    /** Counter used to update integer */
    private AtomicInteger _cnt = new AtomicInteger(0);
    /** ServerPush task timer */
    private Timer timer;
    /** Desktop of this VM */
    private Desktop targetDesktop;
    /** Binded field of textbox */
    private String _prop = "rows";

    // getters
    public String getTask () {
        return "timerTask";
    }
    public String getProp () {
        return _prop;
    }
    /**
     * start server push with WebSocket for
     * specific context "timerTask"
     * 
     * current desktop only
     */
    @Command
    public void start () {
        if (timer == null) {
            // update once immediately when first time start
            if (_cnt.get() == 0) {
                push(_cnt.incrementAndGet(), "timerTask");
            }
            timer = new Timer();
            timer.schedule(getTimerTask(), 1000, 1000);
        }
    }
    /**
     * stop server push with WebSocket for
     * specific context "timerTask"
     * 
     * current desktop only
     */
    @Command
    public void stop () {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }
    @Command
    @NotifyChange("prop")
    public void switchRowCol () {
        if ("rows".equals(_prop)) {
            _prop = "cols";
        } else {
            _prop = "rows";
        }
    }
    // task to be scheduled to update context "timerTask" every second
    private TimerTask getTimerTask () {
        return new TimerTask() {
            public void run () {
                push(_cnt.incrementAndGet(), "timerTask");
            }
        };
    }
    /** push value to specific context
     * 
     * @param val value to push
     * @param context context to push
     */
    private void push (Object val, String context) {
        try {
            if (targetDesktop == null) {
                targetDesktop = Executions.getCurrent().getDesktop();
            }
            // push to current desktop only
            ServerPushUtil.pushVlaue(val, context, targetDesktop);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


components.IWebSocketEnhancedComponent.java

Copied from AURequestWithWebSocket, completely the same.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/IWebSocketEnhancedComponent.java

components.Intbox.java

Copied from AURequestWithWebSocket, change _useWebSocketAU default to true and override smartUpdate instead of setter.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/Intbox.java

components.Textbox.java

Created for this POC, override smartUpdate instead of override setters separately.
Almost the same with Intbox (actually completely the same after serialVersionUID

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/components/Textbox.java

components.serverpush.IWebSocketServerPushEnhancedComponent.java

Define a "WebSocket Enhanced Component -- ServerPush type" for ServerPush pattern

package components.serverpush;

import impl.serverpush.Binding;
import components.IWebSocketEnhancedComponent;

/** Tested with ZK 6.5.2<br>
 * 
 * Define a "WebSocket Enhanced Component -- ServerPush type"
 * for ServerPush pattern
 * 
 * @author benbai123
 *
 */
public interface IWebSocketServerPushEnhancedComponent extends IWebSocketEnhancedComponent {
    /** Add Binding for specific field/context pair
     * 
     * The added Binding will be stored to a Binding List of a Component
     * 
     * @param field field to bind
     * @param context context to bind with field
     * @return the Added Binding
     */
    public Binding addSocketContextBinding (String field, String context);
    /** Remove Binding for specific field/context pair
     * 
     * Remove it from Binding List of a Component
     * 
     * @param field field of field/context pair to remove
     * @param context context of field/context pair to remove
     */
    public void removeSocketContextBinding (String field, String context);
}


components.serverpush.Intbox.java

Created for this POC, implements IWebSocketServerPushEnhancedComponent to support WebSocket ServerPush Almost the same with Textbox (completely the same after serialVersionUID)

package components.serverpush;

import impl.serverpush.Binding;
import impl.serverpush.ServerPushUtil;

/** Tested with ZK 6.5.2<br>
 * 
 * Created for this POC, implements IWebSocketServerPushEnhancedComponent to
 * support WebSocket ServerPush
 * 
 * Almost the same with Textbox (completely the same after serialVersionUID)
 * 
 * @author benbai123
 *
 */
public class Intbox extends components.Intbox implements IWebSocketServerPushEnhancedComponent {

    private static final long serialVersionUID = -3277498174057967067L;

    @Override
    public Binding addSocketContextBinding(String field, String context) {
        return ServerPushUtil.addContextBinding(this, field, context);
    }

    @Override
    public void removeSocketContextBinding(String field, String context) {
        ServerPushUtil.removeContextBinding(this, field, context);
    }

}


components.serverpush.Textbox.java

Created for this POC, implements IWebSocketServerPushEnhancedComponent to support WebSocket ServerPush Almost the same with Intbox (completely the same after serialVersionUID)

package components.serverpush;

import impl.serverpush.Binding;
import impl.serverpush.ServerPushUtil;

/** Tested with ZK 6.5.2<br>
 * 
 * Created for this POC, implements IWebSocketServerPushEnhancedComponent to
 * support WebSocket ServerPush
 * 
 * Almost the same with Intbox (completely the same after serialVersionUID)
 * 
 * @author benbai123
 *
 */
public class Textbox extends components.Textbox implements IWebSocketServerPushEnhancedComponent {

    private static final long serialVersionUID = 2354643632056197764L;

    @Override
    public Binding addSocketContextBinding(String field, String context) {
        return ServerPushUtil.addContextBinding(this, field, context);
    }

    @Override
    public void removeSocketContextBinding(String field, String context) {
        ServerPushUtil.removeContextBinding(this, field, context);
    }

}


components.helper.ContextBinding.java

Helper component to help a component to bind field with ServerPush context

package components.helper;

import impl.serverpush.Binding;

import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Div;

import components.serverpush.IWebSocketServerPushEnhancedComponent;

/** Tested with ZK 6.5.2
 * 
 * Helper component to help a component to bind field with ServerPush context
 * 
 * @author benbai123
 *
 */
public class ContextBinding extends Div {

    private static final long serialVersionUID = -7156149643515776677L;
    /** field to bind with context (required) */
    private String _field = "";
    /** context to bind with field (required) */
    private String _context = "";
    /** specified id of target component (optional) */
    private String _target;
    /** used to store current binding (relatively old) for changing field/context */
    private Binding _oldBinding = null;
    /** found target */
    private IWebSocketServerPushEnhancedComponent _foundTarget;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public ContextBinding () {
        // update target url while created
        addEventListener(Events.ON_CREATE, new EventListener () {
            public void onEvent (Event event) {
                updateTargetBinding();
            }
        });
        // do not output any html
        setWidgetOverride ("redraw", "function (out) {}");
    }
    // setters
    public void setTarget (String target) {
        _target = target;
    }
    public void setField (String field) {
        if (field == null) {
            field = "";
        }
        if (!field.equals(_field)) {
            _field = field;
            updateTargetBinding();
        }
    }
    public void setContext (String context) {
        if (context == null) {
            context = "";
        }
        if (!context.equals(_context)) {
            _context = context;
            updateTargetBinding();
        }
    }

    /** Update Binding of target Component when field/context is changed
     * 
     */
    private void updateTargetBinding () {
        if (!_field.isEmpty() && !_context.isEmpty()) {
            _foundTarget = findTarget();
            if (_foundTarget != null) {
                // remove old binding if exists
                if (_oldBinding != null) {
                    if (_field.equals(_oldBinding.getField())
                            && _context.equals(_oldBinding.getContext())) {
                        // already binded, do nothing
                        return;
                    }
                    _foundTarget.removeSocketContextBinding(_oldBinding.getField(), _oldBinding.getContext());
                }
                // add new binding and store it to _oldBinding
                _oldBinding = _foundTarget.addSocketContextBinding(_field, _context);
            }
        }
    }
    /* package */ IWebSocketServerPushEnhancedComponent getFoundTarget () {
        return _foundTarget;
    }
    /** Try to find target to update
     * The order to try: <br>
     * 1. Try to find fellow under the same space owner with specified "target" attribute.<br>
     * 2. Try to find whether previous sibling is IWebSocketServerPushEnhancedComponent.<br>
     * 3. Try to find whether previous sibling is ContextBinding and already found a target.<br>
     * 4. Try to find whether parent is IWebSocketServerPushEnhancedComponent.<br>
     * 
     * This way it can work without target attribute in most cases.
     * @return IWebSocketServerPushEnhancedComponent if any
     */
    /* package */ IWebSocketServerPushEnhancedComponent findTarget () {
        Component comp;
        Component previous = getPreviousSibling();
        // Try to find fellow under the same space owner with specified "target" attribute.
        if (_target != null && !_target.isEmpty()) {
            comp = getSpaceOwner().getFellowIfAny(_target);
            if (comp != null
                && (comp instanceof IWebSocketServerPushEnhancedComponent)) {
                return (IWebSocketServerPushEnhancedComponent) comp;
            }
        }
        // Try to find whether previous sibling is IWebSocketServerPushEnhancedComponent.
        if (previous instanceof IWebSocketServerPushEnhancedComponent) {
            return (IWebSocketServerPushEnhancedComponent) previous;
        }
        // Try to find whether previous sibling is ContextBinding and already found a target.
        if (previous instanceof ContextBinding) {
            IWebSocketServerPushEnhancedComponent previousTarget = ((ContextBinding) previous).getFoundTarget();
            if (previousTarget != null) {
                return previousTarget;
            }
        }
        // Try to find whether parent is IWebSocketServerPushEnhancedComponent.
        if (getParent() instanceof IWebSocketServerPushEnhancedComponent) {
            return (IWebSocketServerPushEnhancedComponent)getParent();
        }
        return null;
    }
}


impl.DesktopUtils.java

Modified from AURequestWithWebSocket, almost the same, add synchronized for ServerPush.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/DesktopUtils.java

impl.TestWebSocketServlet.java

Modified from AURequestWithWebSocket, almost the same, store channel at desktop so can send response with desktop itself.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/TestWebSocketServlet.java

impl.EventListener.java

Define an Event listener used to process event from Not used in this POC.

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/EventListener.java

impl.RequestFromWebSocket.java

Copied from AURequestWithWebSocket, completely the same excepts this fragment. Not used in this POC

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/src/impl/RequestFromWebSocket.java

impl.serverpush.Binding.java

Binding that bind a field of a component to specific context for WebSocket ServerPush.

package impl.serverpush;

import java.io.Serializable;

/** Binding that bind a field of a component to specific context for WebSocket ServerPush
 * 
 * @author benbai123
 *
 */
public class Binding implements Serializable {
    private static final long serialVersionUID = -1591783705902661276L;
    /** field to bind of component */
    private String _field;
    /** context to bind with field */
    private String _context;
    // Constructor
    public Binding (String field, String context) {
        if (field == null) {
            field = "";
        }
        if (context == null) {
            context = "";
        }
        _field = field;
        _context = context;
    }
    // getters, setters
    public void setField (String field) {
        if (field == null) {
            field = "";
        }
        _field = field;
    }
    public String getField () {
        return _field;
    }
    public void setContext (String context) {
        if (context == null) {
            context = "";
        }
        _context = context;
    }
    public String getContext () {
        return _context;
    }
    // super
    public int hashCode () {
        return (_field + _context).hashCode() * 31;
    }
    public boolean equals (Object obj) {
        if (obj != null
                && (obj instanceof Binding)) {
            Binding b2 = (Binding) obj; 
            return (_field.equals(b2.getField())
                    && _context.equals(b2.getContext()));
        }
        return false;
    }
}


impl.serverpush.ServerPushUtil.java

Utilities for ServerPush with WebSocket, concept is similar to ServerPushWithWebSocket -- Component Oriented Version, rewritten it to Desktop Oriented Version.

package impl.serverpush;

import impl.TestWebSocketServlet;

import java.beans.Statement;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;

/** Tested with ZK 6.5.2<br>
 * 
 * Utilities for ServerPush with WebSocket, concept is similar to
 * ServerPushWithWebSocket -- Component Oriented Version, rewritten it to
 * Desktop Oriented Version.
 * 
 * @author benbai123
 *
 */
@SuppressWarnings("unchecked")
public class ServerPushUtil {
    /** keep all binded Desktop */
    private static List<Desktop> bindedDesktops = new ArrayList<Desktop>();
    /** Lock for push to all desktops */
    private static Integer LOCK_FOR_PUSH_TO_ALL = 0;
    /** Lock for access bindedDesktops */
    private static Integer LOCK_FOR_ACCESS_BINDED_DESKTOPS = 0;
    /** attribute kay for binding map */
    private static final String BINDING_MAP = "BINDING_MAP_FOR_WEBSOCKET_SERVERPUSH";

    /** Add a Binding to a Component
     * 
     * @param comp Component to bind member field with update context
     * @param field field of component to bind
     * @param context context to bind with field
     * @return Binding the added Binding object
     */
    public static Binding addContextBinding (Component comp, String field, String context) {
        Binding binding = null;
        synchronized (comp.getDesktop()) {
            // get binding list of specified component
            List<Binding> bindingList = getBindingList(comp);
            // create a binding object with specified field/context
            binding = new Binding(field, context);
            // add binding into binding list if not exists
            if (!bindingList.contains(binding)) {
                bindingList.add(binding);
            }
        }
        // return created binding
        return binding;
    }
    /** Remove a Binding from a Component
     * 
     * @param comp Component to remove Binding
     * @param field field of the field-context pair to remove
     * @param context context of the field-context pair to remove
     */
    public static void removeContextBinding (Component comp, String field, String context) {
        synchronized (comp.getDesktop()) {
            Map<Component, List<Binding>> bindingMap = getBindingMap(comp.getDesktop());
            // get binding list of specified component
            List<Binding> bindingList = bindingMap.get(comp);
            if (bindingList != null) {
                // create a binding object with specified field/context
                Binding target = new Binding (field, context);
                // remove binding if exists
                for (Binding b : bindingList) {
                    if (b.equals(target)) {
                        bindingList.remove(b);
                        break;
                    }
                }
            }
        }
    }

    /** Push a value to a context for all desktops 
     * 
     * @param value value to push
     * @param context context to push to
     * @throws Exception whatever
     */
    public static void pushVlaue (Object value, String context) throws Exception {
        synchronized (LOCK_FOR_PUSH_TO_ALL) {
            // used to store dead desktop
            List<Desktop> deadDesktops = new ArrayList<Desktop>();
            // make a copy to reduce lock
            List<Desktop> desktopToPush = new ArrayList<Desktop>();
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                desktopToPush.addAll(bindedDesktops);
            }
            // for each binded desktop
            for (Desktop bindedDesktop : desktopToPush) {
                if (bindedDesktop.isAlive()) {
                    // push value to context if alive
                    execPush(value, context, bindedDesktop);
                } else {
                    // store it to deadDesktops otherwise
                    deadDesktops.add(bindedDesktop);
                }
            }
            // remove all dead desktops
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                bindedDesktops.removeAll(deadDesktops);
            }
            desktopToPush.removeAll(deadDesktops);
            for (Desktop dt : desktopToPush) {
                // send response via WebSocket
                TestWebSocketServlet.sendResponse(dt);
            }
        }
    }
    /** Push a value to a context for specified desktop
     * 
     * @param value value to push
     * @param context context to push to
     * @param desktop desktop to apply this push
     * @throws Exception whatever
     */
    public static void pushVlaue (Object value, String context, Desktop desktop) throws Exception {
        if (desktop.isAlive()) {
            // push value to context for specified desktop if alive
            execPush(value, context, desktop);
        } else {
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                // remove specified desktop from bindedDesktops otherwise
                bindedDesktops.remove(desktop);
            }
        }
        // send response via WebSocket
        TestWebSocketServlet.sendResponse(desktop);
    }
    /** Apply "push value to context" for specified desktop
     * 
     * @param value value to push
     * @param context context to push to
     * @param desktop desktop to apply
     * @throws Exception whatever
     */
    public static void execPush (Object value, String context, Desktop desktop) throws Exception {
        // execute push if WebSocket of desktop is ready
        if (TestWebSocketServlet.isWebSocketReady(desktop)) {
            synchronized (desktop) {
                // get binding map
                // (Map<String, List> where String is component ID and List is Binding objects)
                Map<Component, List<Binding>> bindingMap = (Map<Component, List<Binding>>)desktop.getAttribute(BINDING_MAP);
                if (bindingMap != null) {
                    for (Map.Entry<Component, List<Binding>> e : bindingMap.entrySet()) {
                        // get component by ID
                        Component comp = e.getKey();
                        // get binding list
                        List<Binding> bindings = e.getValue();
                        // for each binding
                        for (Binding binding : bindings) {
                            // push value to component if
                            // context of binding is equal to specified context
                            if (binding.getContext().equals(context)) {
                                // get field from binding
                                String field = binding.getField();
                                // build setter name
                                String method = "set" + field.substring(0, 1).toUpperCase() + field.substring(1);
                                // call setter to set value to component
                                Statement stat = new Statement(comp, method, new Object[]{value});
                                stat.execute();
                            }
                        }
                    }
                }
            }
        }
    }
    /** Get Binding list of a Component
     * 
     * @param comp Component to get Binding list
     * @return Binding list for specified Component
     */
    private static List<Binding> getBindingList (Component comp) {
        // get binding map
        Map<Component, List<Binding>> bindingMap = getBindingMap(comp.getDesktop());
        // try to get binding list
        List<Binding> bindingList = bindingMap.get(comp);
        // create and add binding list if not exists
        if (bindingList == null) {
            bindingList = new ArrayList<Binding>();
            bindingMap.put(comp, bindingList);
        }
        // return (created) binding list
        return bindingList;
    }
    /** Get Binding map of a Desktop
     * 
     * @param dt specified Desktop
     * @return Map<Component, List<Binding>> Binding map of specified Desktop
     */
    private static Map<Component, List<Binding>> getBindingMap (Desktop dt) {
        // try to get binding map
        Map<Component, List<Binding>> bindingMap = (Map<Component, List<Binding>>)dt.getAttribute(BINDING_MAP);
        // create and add binding map if not exists
        if (bindingMap == null) {
            bindingMap = new Hashtable<Component, List<Binding>>();
            dt.setAttribute(BINDING_MAP, bindingMap);
            synchronized (LOCK_FOR_ACCESS_BINDED_DESKTOPS) {
                bindedDesktops.add(dt);
            }
        }
        // return (created) binding map
        return bindingMap;
    }
}


zk.xml

Modified from AURequestWithWebSocket, add config for lang-addon.xml, also change the way to get path for WebSocket (line 61~)

https://github.com/benbai123/ZK_Practice/blob/master/Integrate/WebSocket/ServerPushWithWebSocket/WebContent/WEB-INF/zk.xml

lang-addon.xml

Define components used in this POC.

<!-- Tested with ZK 6.5.2
    Define components
-->

<language-addon>
    <addon-name>param</addon-name>
    <language-name>xul/html</language-name>

    <!-- It specifies what language addons this addon
        depends on. If specified, this addon will be
        parsed after all the specified addons are parsed -->
    <depends>zul</depends>

    <!-- define param -->
    <component>
        <component-name>contextBinding</component-name>
        <extends>div</extends>
        <component-class>components.helper.ContextBinding</component-class>
    </component>
    <component>
        <component-name>intbox</component-name>
        <extends>intbox</extends>
        <component-class>components.serverpush.Intbox</component-class>
    </component>
    <component>
        <component-name>textbox</component-name>
        <extends>textbox</extends>
        <component-class>components.serverpush.Textbox</component-class>
    </component>
</language-addon>


Download

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

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

Sunday, September 8, 2013

WebSocketAU vs ZKAU Performance Testing


Simple Note

Testing Steps

1. Start Tomcat

2. Open visualVM

3. Open 5 testing pages (index.zul)

4. Wait 2 minutes

5. Take snapshot

Testing Result

Result of WebSocket AU



Result of ZKAU



Conclutions

Memory Consumption: Almost the same

CPU Usage: WebSocket AU (~15% then 1% ~ 3%) is better than ZK AU (~33% than 2% ~ 13%)

Download

Testing project at github
https://github.com/benbai123/ZK_Practice/tree/master/General/Performance_Testing/WebSocketAU_vs_ZKAU

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