Web Apps and so forth..

Tuesday, July 07, 2009

ZK on OSGi - Dynamic & Asynchronous Richlets

After some days struggling I finally managed to integrate ZK, which allow you to easily build rich webapps, with the OSGi framework. This combination allow to dynamically register .zul files or Richlets and most interestingly, to update UI components asynchonously from OSGi bundles or events.
We will see in this post how to bundelize zk libs, how to configure and start zk engines, how to register zul files or richlets and finally how to update a richlet through a java method accessible in OSGi.

Bundelization of ZK jars

The first step is to make all zk jars suitable for OSGi environment by editing their manifest file. Thanks to the bnd tool, this tasks is pretty straightforward. I've written an ant script to do the job. Just put in a directory each zk jar you want to bundelize, the bnd jar, an optional bnd file (for setting manifest properties like Bundle-Version), the following ant script and launch "ant" in a console.

<project name="ZK OSGi Multi Bundles" default="all" basedir=".">

<property name="bundle.version" value="3.6.2"/>
<property name="bundle.base" value="org.zkoss.zk.osgi"/>

<target name="bnd">
<taskdef resource="aQute/bnd/ant/taskdef.properties"
classpath="bnd/bnd-0.0.337.jar"/>
<bndwrap
jars="bsh.jar,commons-collections.jar,commons-fileupload.jar,commons-io.jar,
commons-logging.jar,jasperreports.jar,zcommon.jar,zcommons-el.jar,zk.jar,
zhtml.jar,zkex.jar,zkmax.jar, zkplus.jar,zml.jar,zul.jar,zweb.jar"
classpath=""
output="."/>
<!--output="./output/${bundle.base}.*-${bundle.version}.jar"-->
</target>

<target name="deploy">
<move todir="output">
<fileset dir=".">
<include name="*.bar"/>
</fileset>
<mapper type="glob" from="*.bar" to="${bundle.base}.*-${bundle.version}.jar"/>
</move>
</target>

<target name="all" depends="bnd, deploy"/>

</project>
A maven pom would certainly be more handy as it will download the jars and manage bundle name / version easily but the first solution satisfied me so i didn't bother make a second one.

Configure and launch ZK engine

The Zk architecture is constituted by two servlets, named zkLoader and auEngine in the docs. Integrate Zk on OSGi is as simple as register servlets with an osgi http service. However these servlets require specific URI mapping which can't be set with a basic implementation of the http service. Hopefully the Pax Web implementation extends the http service with such functionalities.

Here follow the code to configure and register Zk engine servlets with Pax Web.

//Get Pax Web http service
ServiceReference ref = context.getServiceReference("org.ops4j.pax.web.service.WebContainer");
if(ref == null) return;
http = (WebContainer)context.getService(ref);
if(http == null) return;

//Configure zkLoader servlet
DHtmlLayoutServlet zkLoader = new DHtmlLayoutServlet(); //Class of the zkLoader servlet
Hashtable<String, String> loader_initparam = new Hashtable<String, String>(); //set init parameters
loader_initparam.put("servlet-name", "zkLoader");
loader_initparam.put("update-uri", "/zkau"); //URI mapped to auEngine
String loader_mapping[] = {"*.zul", "*.zhtml"}; //mapping of UI files

//Configure auEngine servlet
DHtmlUpdateServlet auEngine = new DHtmlUpdateServlet();
Hashtable<String, String> engine_initparam = new Hashtable<String, String>();
engine_initparam.put("servlet-name", "auEngine");
String engine_mapping[] = {"/zkau/*"}; //same URI as the parameter "update-uri" of zkLoader

//Get the http context (zk servlets should be registered with the same http context)
HttpContext ctx = http.createDefaultHttpContext();

//Register zk session listener
http.registerEventListener(new HttpSessionListener(), ctx);

//Register zk servlets
http.registerServlet(zkLoader, loader_mapping, loader_initparam, ctx);
http.registerServlet(auEngine, engine_mapping, engine_initparam, ctx);

Note: Launching ZK servlets from those newly ZK bundles run flawlessly but cause http requests to abort :


This is due to a natural incompatibility between the ZK config file loader (which load metainfo files inside zk jars) and the OSGi ClassLoaders.

I've tried to wrap a unique ZK bundle and merge metainfo config files but this attempt was unsuccessful. The only workaround I found is to include zk jars in the bundle which register servlets. Anyway ZK api bundles are still useful for other bundles implementing Richlets.

Hello World zul

A .zul or .zhtml file can be registered using the registerRessource() method from the http service.
Create a directory "rsc" in your project and add a hello.zul file :

<window title="OMG a ZK Window !" border="normal" width="210px">
Hello from OSGi .zul ressource !
</window>


Then register it :

http.registerResources("/", "/rsc", ctx);
Don't forget to include this resource and zk jars (unfortunately) in the manifest. With bnd it can looks like :
Include-Resource: lib/bsh.jar=./lib/zk/bsh.jar, \
...
lib/zweb.jar=./lib/zk/zweb.jar, \
rsc/hello.zul = ./rsc/hello.zul

Bundle-ClassPath: .,\
lib/bsh.jar, \
...
lib/zweb.jar
It works !


This method to register zul files over OSGi don't seems to be more dynamic than with a classical web container. Even if it is possible to re-register the resource folder, a better solution to achieve dynamism would be to use Richlets.

Hello World richlet

Add a new class extending GenericRichlet in your project :
public class HelloRichlet extends GenericRichlet {

@Override
public void service(Page page) {
page.setTitle("Richlet from OSGi");

Window window = new Window("OMG a ZK Window !", "normal", false);

label = new Label("Hello from OSGi !!");
label.setParent(window);

window.setWidth("230px");
window.setPage(page);
}
}
Then you can register it using the zk api :
//Get the configuration
Configuration config = WebManager.getWebManager(zkLoader.getServletContext()).getWebApp().getConfiguration();

//Register Richlet
config.addRichlet("hello", HelloRichlet.class.getName(), null);
config.addRichletMapping("hello", "/hello");
In order to tell Zk to parse this configuration when the user requests the richlet, add "/*" in the URI mapping of zkLoader :
String loader_mapping[] = {"*.zul", "*.zhtml", "/*"};  //mapping of UI files
Now you can access the richlet in http://localhost:8080/hello.



Asynchronous UI modification from OSGi

Now that we're able to register Richlets on OSGi, we are going to modify them at runtime without using ZK event listeners. Of course it is still possible to use them, but the idea is to update components asynchronously from java methods. There are few things to do :
  • Activate the server push functionality (Comet)
  • Create a waiting thread which will update the component
  • Start this thread when the richlet is requested
  • Add the update method in the richlet which will notify the thread

To enable Comet in Ajax compliant browsers add the following line when configuring Zk.
//Enable server push with the zk Comet implementation
Devices.setServerPushClass("ajax", CometServerPush.class);
Here is an example of the thread class, updating a label :
/**
* Thread waiting for an update. It has to be launched in a zk richlet.
*/
public class LabelAsynchronousUpdate extends Thread {

private Label label;
private Desktop desktop;
private ArrayBlockingQueue<String> text; //used to notify the thread and give the argument

public LabelAsynchronousUpdate(Label label) {
this.label = label;
this.desktop = label.getDesktop();
this.text = new ArrayBlockingQueue<String>(1);
}

/**
* Call to update the label.
* The thread will be notified through the BloquingQueue.
*/
public void updateLabel(String text) {
try {
this.text.put(text); //Add the text object in the blocking queue
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

public void run() {
while (true) {
if (!desktop.isServerPushEnabled())
desktop.enableServerPush(true);

try {
//Wait for text message to appear in the queue
String message = text.take(); //take and remove from queue

Executions.activate(desktop); //active the desktop for server-push
try {
label.setValue(message); //update
} finally {
Executions.deactivate(desktop);
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
Finally update the hello richlet :
public class HelloRichlet extends GenericRichlet {

private Label label;
private LabelAsynchronousUpdate labelupdate;

@Override
public void service(Page page) {
page.setTitle("Richlet from OSGi");

//Enable Comet
page.getDesktop().enableServerPush(true);

Window window = new Window("OMG a ZK Window !", "normal", false);

label = new Label("Hello from OSGi !!");
label.setParent(window);

window.setWidth("230px");
window.setPage(page);

//Create and launch an updating thread. It needs to be launched from the richlet
//in order to let zk hold the connection
this.labelupdate = new LabelAsynchronousUpdate(label);
this.labelupdate.start();
}

/**
* Change the label asynchronously.
*/
public void changeLabel(String message) {
if (labelupdate != null)
this.labelupdate.updateLabel(message);
}
}
In order to test you have to get the instance of the richlet from the Configuration object, and call the changeLabel() method.
Thread.sleep(10000);  //test 10 sec after bundle start

HelloRichlet hr = (HelloRichlet) config.getRichlet("hello");
if (hr != null)
hr.changeLabel("OMFG ! This label is updatable asynchronously from OSGi !");



Conclusion

Using ZK on OSGi allow to use the benefits of a service oriented architecture. It is easier to handle dynamism and easier to achieve modularity. For example we can imagine bundles exporting richlets as services and another bundle registering them dynamically.
With the asynchronous modification it becomes easier to automatically represent a service through the web. If for instance we have some "light" or "temperature sensor" services, corresponding to real objects in a house, it would then be possible to handle asynchronous notification of state change and build a solid web interface.

Download full source code here.

2 comments:

  1. Hi Mathieu,
    first of all thanks for the blog.
    I tried it and it works. My only problem arises when I have two bundles embedding the ZK framewrok. If I say that both the zkLoader registered in the activators must intercept all *.zul requests then there are several problems of class and page loading. I would like to force each loader on a particular path (i.e. /app1/*.zul) but it doesn't work. I have also tried with (/app1/*) but it seems that the ZK engine is not able to find pages. Did you have any suggestions? Thanks in advance
    Giovanni

    ReplyDelete
  2. I would also say that running two zkLoader can lead to a lot of troubles (I haven't tried). You must find a way to register only one zkLoader, which can manage several applications, then share applications between bundles. However I don't know what constraints you are running through so I can't give you more that my humble opinion.

    ReplyDelete