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