ZK 10 Preview: Migrating from Stateful components to Stateless components
Jumper Chen, Director of Products and Technology, Potix Corporation
Sep. TBD, 2022
ZK 10.0.0.FL.20220818-Eval
Introduction
As some ZK folks may be inquisitive about how possible to migrate from an existing ZK former Stateful component application to ZK 10 Stateless component application with a few adjustments.
Today, we are about to share with you the guidance step by step by taking ZK Sandbox Demo as an example. Before we dive into details, there is some information we want to clarify to you that the manner we demonstrate here is different from the previous ZK 10 article (a pure cloud-native application). Instead, we migrate the application to run in a single server instance that could adopt more classic ZK features and more effortless. Moreover, the server memory usage still reduces a lot (from this application, it decreases by more than 30% monitored by VisualVM tool).
Overview
The ZK Sandbox demo is a decade project that started in 2008, and it was composed of lots of ZUL files with a few Composers, and each of the ZUL files was executed by the ZK event handler (namely onClick=”xxx”
) and ZScript (a snippet Java code). Both of the features are not possible to work in the Stateless component application, we will talk about it more later on. Let’s start today’s journey.
Configuration
To enable the ZK 10 Stateless component, we have to specify the following Filter setting in web.xml
1 <filter>
2 <filter-name>DispatcherRichletFilter</filter-name>
3 <filter-class>org.zkoss.zephyr.ui.http.DispatcherRichletFilter</filter-class>
4 <init-param>
5 <param-name>basePackages</param-name>
6 <param-value>org.zkoss.zksandbox.ui</param-value>
7 </init-param>
8 </filter>
9 <filter-mapping>
10 <filter-name>DispatcherRichletFilter</filter-name>
11 <url-pattern>/*</url-pattern>
12 </filter-mapping>
As you can see in lines 5 and 6, we declared the package org.zkoss.zksandbox.ui for scanning the Richlet annotation @RichletMapping to allow to dispatch of the HTTP request as RESTFul API.
Migrate Stateful Composer to Stateless Richlet
The ZK Sandbox demo is composed of two primary Composers, MainLayoutComposer and DemoWindowComposer, to communicate with each other as the major functions. In the Stateless component approach, we are going to migrate the MainLayoutComposer, the outer layout of the demo, to MainLayoutRichlet by using the ZK 10 feature to dispatch the HTTP request to a RESTful way API and lay out the UI components in the Stateless component APIs as follows.
MainLayoutRichlet.java
1 @RichletMapping("/")
2 public class MainLayoutRichlet implements StatelessRichlet {
3 @RichletMapping("")
4 public List index() {
5 // omitted
6 }
7 }
Note: A ZK composer is associated with a component of a ZUL file, so here we need to migrate the code in the ZUL file into Java API first. (Can we use StatelessComposer here instead? Yes, possible, but we choose StatelessRichlet in this case)
ZUL Processing Instructions
These are the ZUL file Processing Instructions declared in a ZUL file.
Before (index.zul)
<?page id="userGuide" title="ZK Sandbox"?>
<?link rel="stylesheet" type="text/css" href="/macros/zksandbox.css.dsp?v=${desktop.webApp.build}"?>
<?script type="text/javascript" src="/macros/zksandbox.js.dsp?v=${desktop.webApp.build}"?>
<?meta name="keywords" content="ZK, Ajax, Mobile, Framework, Ajax framekwork, RIA, Direct RIA, Java, JSP, JSF, Open Source, Comet" ?>
After (MainLayoutRichlet.java)
private void initHeaderInfo(Page page) {
page.setTitle("ZK Sandbox");
PageCtrl pc = (PageCtrl) page;
pc.addBeforeHeadTags("<meta name=\"keywords\" content=\"ZK, Ajax, Mobile, Framework, Ajax framekwork, RIA, Direct RIA, Java, JSP, JSF, Open Source, Comet\">");
// after ZK JS files
pc.addAfterHeadTags("<link rel=\"stylesheet\" type=\"text/css\" href=\"macros/zksandbox.css.dsp?v=" + page.getDesktop().getWebApp().getBuild() + "\">");
pc.addAfterHeadTags("<script type=\"text/javascript\" src=\"macros/zksandbox.js.dsp?v=" + page.getDesktop().getWebApp().getBuild() + "\"></script>");
}
ZUL Component Directive
Before (index.zul)
<?component name="category" extends="button" widgetClass="zksandbox.Category" mold="default"?>
<?component name="categorybar" extends="div" widgetClass="zksandbox.Categorybar"?>
After (MainLayoutRichlet.java)
private final static IButton CATEGORY = IButton.DEFAULT.withWidgetClass("zksandbox.Category");
private final static IDiv<IAnyGroup> CATEGORY_BAR = IDiv.DEFAULT.withWidgetClass("zksandbox.Categorybar");
ZUL UI Component
Before (index.zul)
<style>
.z-html {
display: block;
}
</style>
<style if="${zk.mobile != null}">…</script>
<borderlayout id="main" sclass="${themeSClass}" apply="org.zkoss.zksandbox.MainLayoutComposer">…</borderlayout>
After (MainLayoutRichlet.java)
@RichletMapping("")
public List index() {
initHeaderInfo(((ExecutionCtrl)Executions.getCurrent()).getCurrentPage());
Double number = Executions.getCurrent().getBrowser("mobile");
return Arrays.asList(
IStyle.of(".z-html {display: block;}"),
number != null ? IStyle.ofSrc("~./style/index.css.dsp") : IStyle.DEFAULT,
initMainLayout(),
"www.zkoss.org".equals(Executions.getCurrent().getServerName()) ||
"www.potix.com".equals(Executions.getCurrent().getServerName()) ?
IScript.ofSrc("macros/ga.js") : IScript.ofSrc("")
);
}
As you can see from the highlighted text above, we create a index.css.dsp to put all the style content instead of a pure text value for easing maintenance.
Model and Renderer
Model is a state to store the application data and Renderer is a way to layout the UI according to the application Data. In the Stateless component, by default we don’t have a way to store the Model and Renderer for later use at the server, nevertheless, ZK 10 provides some utility APIs named Controller.
In this case, we employ IListboxController instead of using Model and Renderer directly inside a Listbox component. For example,
Before (MainLayoutComposer.java)
1 public void onMainCreate(Event event) {
2 final Execution exec = Executions.getCurrent();
3 final String id = exec.getParameter("id");
4 Listitem item = null;
5 if (id != null) {
6 try {
7 final LinkedList<DemoItem> list = new LinkedList<DemoItem>();
8 final DemoItem[] items = getItems();
9 for (int i = 0; i < items.length; i++) {
10 if (items[i].getId().equals(id))
11 list.add(items[i]);
12 }
13 if (!list.isEmpty()) {
14 itemList.setModel(new ListModelList<DemoItem>(list, true));
15 itemList.renderAll();
16 item = (Listitem) self.getFellow(id);
17 setSelectedCategory(item);
18 }
19 } catch (ComponentNotFoundException ex) { // ignore
20 }
21 }
After (MainLayoutRichlet.java)
1 private IListboxController initListboxController() {
2 IListboxController<DemoItem, IListitem> itemList = IListboxController.of(
3 IListbox.ofId("itemList")... , // The IListbox instance
4 getSelectedModel(), // The ListModel we need.
5 (DemoItem di, Integer index) -> IListitem.ofId(di.getId()) // The Render to layout the UI elements
6 .withChildren(IListcell.of(di.getLabel(), di.getIcon())
7 .withHeight("30px")));
8 service.setListboxController(itemList);
9 return itemList;
10 }
After initiating the IListboxController, we store the instance in the service instance (line 8), a ZK Desktop scope storage one per browser page, and then we can receive it back when some Action handlers have occurred.
Note: ZK Richlet mechanism is a singleton pattern per application (we could assume it’s like static instance in Java), so it’s not thread-safe in the Java Servlet world. Thankfully, Stateless components are immutable to be run in Richlet safely, yet the stateful data is doubtful here. However, we could store the demo data per thread per ZK Desktop scope to workaround the thread-safe issue here.
Component and Event for AutoWiring
In the Stateless component, we need to replace each ZK event listener with an action handler. For example,
Before (MainLayoutComposer.java)
public void onBookmarkChange$main(BookmarkEvent event) {
String id = event.getBookmark();
if (id.length() > 0) {
final DemoItem[] items = getItems();
for (int i = 0; i < items.length; i++) {
if (items[i].getId().equals(id)) {
_selected = (Button)self.getFellow(items[i].getCateId());
itemList.setModel(getSelectedModel());
itemList.renderAll();
Listitem item = ((Listitem)itemList.getFellow(id));
item.setSelected(true);
itemList.invalidate();
setSelectedCategory(item);
xcontents.setSrc(((DemoItem) item.getValue()).getFile());
item.focus();
return;
}
}
}
}
As you can see above, the onBookmarkChange$main event is registered by ZK GenericForwardComposer automatically that concatenates the onBookmarkChange event with the event target $main (The id with "main" component).
After (MainLayoutRichlet.java)
@Action(from = "#main", type = Events.ON_BOOKMARK_CHANGE)
public void doBookmarkChange$main(UiAgent uiAgent, BookmarkData data) {
String id = data.getBookmark();
if (id.length() > 0) {
final DemoItem[] items = getItems();
for (int i = 0; i < items.length; i++) {
DemoItem demoItem = items[i];
if (demoItem.getId().equals(id)) {
service.setSelectedCategory(demoItem.getCateId());
IListboxController<DemoItem, IListitem> listboxController = service.getListboxController();
listboxController.setModel(getSelectedModel());
listboxController.setSelectedObject(demoItem);
uiAgent.replaceWith(Locator.ofId("itemList"), listboxController.build());
uiAgent.smartUpdate(Locator.ofId(id), new IListitem.Updater().selected(true).focus(true));
setSelectedCategory(demoItem);
uiAgent.replaceChildren(Locator.ofId("xcontents"), Immutables.createComponents(
demoItem.getFile(), null));
return;
}
}
}
}
The action handler is registered by declaring an @Action annotation with the Events.ON_BOOKMARK_CHANGE action type from the #main client widget selector. (This is equivalent to the above Stateful component example) And each ZK event in the ZK Stateless component is to use the term Action with the postfix Data, for example, BookmarkEvent here is BookmarkData, and MouseEvent is MouseData, and so on.
Note: Xconctents is not an Include component here for the Stateless component.
Reuse ZUL to Build Stateless components
As we mentioned earlier, each demo case in the ZK Sandbox demo is a ZUL file with a DemoWindowComposer, and here we need to migrate from the extending GenricForwardComposer declaration to the implementing StatelessComposer declaration and implement the only method in the StatelessComposer called build(BuildContext<IWindow<IAnyGroup>> ctx)
to receive the IComponent tree from the ctx BuildContext passed by the ZUL file processing from the Stateful component to the Stateless component automatically. (Note: not 100% compatible, please see the appendix)
Before (Stateful DemoWindowComposer.java)
public void doAfterCompose(Window comp) throws Exception {
super.doAfterCompose(comp);
comp.setContentSclass("demo-main-cnt");
comp.setSclass("demo-main");
final Div inc = new Div();
Executions.createComponents("/bar.zul", inc, null);
inc.setStyle("float:right");
comp.insertBefore(inc, comp.getFirstChild());
if (view != null) execute();
}
After (Stateless DemoWindowComposer.java)
public IWindow build(BuildContext<IWindow<IAnyGroup>> ctx) {
final String code = ISelectors.<ITextbox>findById(ctx.getOwner(),
"codeView").getValue();
// post a dummy event to execute the code above later.
Component dummy = Locator.of("dummy")
.toComponent((evt, scope) -> execute(code, true));
Events.postEvent(dummy, new Event("onAfterBuild"));
List newChildren = new ArrayList(ctx.getOwner().getChildren());
newChildren.add(0, IDiv.DEFAULT.withStyle("float:right")
.withChildren(Immutables.createComponents("/bar.zul", null)));
return ctx.getOwner().withContentSclass("demo-main-cnt")
.withSclass("demo-main").withChildren(newChildren);
}
Unlike the Stateful DemoWindowComposer, there is no way to bind the component directly inside the Stateless DemoWindowComposer class method field. Instead, we can utilize ISelectors API (Similar to Selectors in the former ZK API) to look up from an IComponent tree to receive the Stateless component data, which is established by the ZUL file. For example, the original Stateful composer was to execute the codeView data of a textbox component inside doAfterCompose method directly, but in the Stateless composer, we have no chance to bind the codeView component to get its data there, so we utilize ISelectors API to get the component data back, and then execute its code later. The "later” timing in the Stateless composer has two different approaches, one is to use an async mechanism (this must enable ZK Server Push feature), and the other is to simulate a ZK classic event post.
Async Mechanism
We can apply the UiAgentAPI.runAsync()
method to create an async function callback that ZK 10 provided.
For example,
UiAgent.getCurrent().runAsync(uiAgent -> {
execute(code, true);
});
But this manner has a drawback that the UI display will get a little delay, it’s depended on a server push to execute another Java thread to wait for the next HTTP request to execute the code.
Simulate ZK Classic Event Post
In the Stateless component API, we can simulate a ZK classic event callback by a Locator API. For example,
Component dummy = Locator.of("dummy")
.toComponent((evt, scope) -> execute(code, true));
Events.postEvent(dummy, new Event("onAfterBuild"));
The Locator API in ZK 10 is to locate a client widget target to do some updates with UiAgent APIs, and it also provides an API to construct a fake Component by its toComponent()
method, the first argument for this method is a callback for providing the mimic ZK classic event post. And the code will execute in the same thread in the next event loop that ZK provided.
Note: In the Stateless Composer, there is no real ZK stateful component that exists.
Communication between Composers
In the Stateful composer, it uses Path.getCompoent()
API to receive the stateful component back, and does the invalidate()
method to reset any changes in the xcontents component and reload with its origin src property that Include component offered. For example,
public void onClick$reloadBtn(Event event) {
demoView.setSelected(true);
Path.getComponent("//userGuide/xcontents").invalidate();
}
In the Stateless composer, we couldn’t either use Path.getComponent()
nor Selectors
API to receive the component back at the server side. Instead, we could leverage ZK Event Queue here to notify the information to the outer Composer or Richlet we use in this case.
In MainLayoutRichlet, we need to subscribe to an Event Queue (by default ZK Desktop scope) for example,
public void service(Page page) throws Exception {
StatelessRichlet.super.service(page);
// init event queue
EventQueues.lookup("mainLayoutRichlet").subscribe((event -> {
UiAgent.getCurrent().replaceChildren(Locator.ofId("xcontents"), Immutables.createComponents(service.getListboxController().getSelectedObject().getFile(), null));
}));
}
As you see above, we override the service()
method which is called once per page visit, and use UiAgent API to update the client state with the data that we store at the server’s service instance. (equivalent to Include component’s invalidate()
)
Here is the post of the event in the DemoWindowComposer, for example,
@Action(from = "#reloadBtn", type = Events.ON_CLICK)
public void onClick$reloadBtn() {
UiAgent.getCurrent().smartUpdate(Locator.ofId("demoView"),
new ITab.Updater().selected(true));
EventQueues.lookup("mainLayoutRichlet").publish(new Event("reloadBtn"));
}
Limitations
- ZScript Function
- If some codes in the ZScript touch the component that could not work in the Stateless component world.
- Event Handler on ZUL File
- Like
onClick=”doSomething”
, this couldn’t transform into the Stateless component automatically, but you can rewrite them in a StatelessComposer instead.
- Like
- Fulfill Mechanism
- Fulfill is a feature in ZK to apply the content on a ZUL file when an event handler occurs. However, this feature needs to be rewritten with pure Java code directly in the Stateless world.
- MVVM Mechanism
- ZK 10 Stateless component is used for replacing the MVC pattern. MVVM mechanism in ZK 10, you can check – ZK Client MVVM.
Download
- The ZK 10 Sandbox demo (Stateless Component) could be downloaded from here.
- The ZK 9 Sandbox demo (Stateful Component) could refer to here.
Appendix
Compatible Table
Function | ZK 9 (Stateful Component) | ZK 10 (Stateless Component) | Solution |
---|---|---|---|
Component | Include |
|
rewrite |
HBox | IHlayout | rewrite | |
VBox | IVlayout | rewrite | |
Splitter | ISplitlayout | rewrite | |
Chart | x | ||
Nodom | x | ||
Jasperreport | x | ||
Captcha | x | ||
HtmlMacroComponent | x | ||
Fragment | x | (MVVM) | |
Apply (Shadow Components) | x | (MVVM) | |
ZHTML Components | ! | not available yet | |
Addons | Keikai 5 | x | |
ZK CKEditor | x | ||
ZK Charts | x | ||
ZK Gmaps | x | ||
ZK Calendar | x | ||
ZK Pivottable | x | ||
APIs | Event Handler (ZScript) on ZUL | x | use StatelessComposer instead |
ZScript | Java Code | rewrite | |
Fulfill Mechanism | Java Code | rewrite | |
Renderer | Template | IXxxController | rewrite | |
Deprecated Components | Applet | x | |
Flash | x | ||
Flashchart | x | ||
Fusionchart | x |
Features Table
ZK 10 (Stateless Component) | default mode (Desktop alive) |
cloud mode (namely cloud-native application) |
Solution |
---|---|---|---|
StatelessRichlet | V | V | |
StatelessComposer (ZUL & ZPR) | V | ||
Action Handler: (Lambda function) | V | ||
ZK Event Queue | V | ! | using other mechanisms, such as Redis, RabbitMQ... |
Model Controller | V | ||
ZK Server Push | V | ! | using other mechanisms |
ZK Websocket Channel | V | ||
Reduce Application Memory | 30+% | totally free | |
Load-balance Support | ! | V | sticky session, session replication, or spring session-data |
Resilient Application | ! | V | session replication or spring session-data |
Dynamically Scaling Application | ! | V | session replication or spring session-data |
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |