Template Examples - Locker"
Robertwenzel (talk | contribs) |
Robertwenzel (talk | contribs) m (→Implementation) |
||
Line 243: | Line 243: | ||
This implementation is only a dummy implementation and doesn't intend to be complete or stable, the goal of this article is not to explain how to do resource locking but to build a UI reacting to asynchronous events. So I'll only cover the ZK specific parts. If you are interested in RxJava2 please refer to the official documentation. | This implementation is only a dummy implementation and doesn't intend to be complete or stable, the goal of this article is not to explain how to do resource locking but to build a UI reacting to asynchronous events. So I'll only cover the ZK specific parts. If you are interested in RxJava2 please refer to the official documentation. | ||
− | Internally | + | Internally a BehaviorSubject ('''LINK ME''') is used for each observed Lockable - which ensures each subscriber receives the latest and subsequent LockEvents. |
<source lang="java" high="2,5,6"> | <source lang="java" high="2,5,6"> |
Revision as of 07:57, 2 January 2018
Robert Wenzel, Engineer, Potix Corporation
January XX, 2018
ZK 8.5
Introduction
What we want
Our goal is to avoid multiple users from editing the same resource simultaneously we need the following functionalities.
- lock / unlock a resource
- e.g. obtain and release exclusive access to edit a specific objects properties
- observe a resource for owner changes (LockEvents)
- it should be possible to observe the same resource with multiple concurrent users
- react to LockEvents and update the UI
- immediately indicate availability/ownership/unavailability of a resource to each observing user
First we need a way to indicate our intention that we want to edit a resource and obtain a lock and vice versa return the lock (unlock) when we are done editing. This is only useful if other users are aware of that condition so they don't attempt concurrent editing (and potentially losing data or producing inconsistent merged results) - means we need to observe the current lock status and finally react in the UI. e.g. by changing labels, styles, disabling/hiding/removing/adding appropriate controls.
Simple example
Before going into details here a small example illustrating what we are trying to achieve.
Here 2 users in 2 separate browsers are seeing (observing) the same resource - initially not locked by anyone. As soon as User-1 locks the resource it becomes editable for him. At the same the lock button disappears for the User-2 - replaced by a message showing the lock status and owner. When unlocking the resource it becomes available for both users again. Then User-2 repeats the cycle. As simple as that.
How does it work
The example above is implemented in simpleLockable.zul and SimpleLockableViewModel.java shouldn't contain too many surprises - Which is the intention of this article -> to show a simple way to achieve that.
simpleLockable.zul
<zk>
<style>
.lockIndicator { padding: 12px; display: inline-block; border-radius: 15px; }
.lockIndicator .z-label { color: white; font-weight: bold; }
.lockIndicator.OWNED { background-color: LimeGreen; }
.lockIndicator.AVAILABLE { background-color: LightSteelBlue; }
.lockIndicator.UNAVAILABLE { background-color: Crimson; }
</style>
<div viewModel="@id('vm') @init('zk.example.template.locker.SimpleLockableViewModel')">
I am '${vm.username}'.
<separator/>
This is
<div sclass="@load(('lockIndicator ' += vm.lockable.status))">
<choose>
<when test="@load(vm.lockable.status eq 'OWNED')">
<textbox value="@bind(vm.lockable.resource.value)"/>
</when>
<otherwise>
<label value="@load(vm.lockable.resource.value)"/>
</otherwise>
</choose>
</div>
<apply template="@load(vm.lockable.status)">
<template name="OWNED">
I am the owner. No one except me can edit until I
<button iconSclass="z-icon-unlock" label="unlock" onClick="@command('unlock')"/> it.
...
</template>
<template name="AVAILABLE">
No one has currently locked it. I can
<button iconSclass="z-icon-lock" label="lock" onClick="@command('lock')"/> it.
...
</template>
<template name="UNAVAILABLE">
currently locked by '${vm.lockable.owner}'.
...
</template>
</apply>
</div>
</zk>
- Line 14: dynamic property sclass changing by lock status
- Lines 16,19: render the resource editable for status OWNED (using the shadow elements <choose>/<when>/<othewise> LINK ME)
- Lines 25,27,33,39: apply (LINK ME) dynamic templates to add controls/information for all 3 different lock states
In the zul file above all the render decisions are based on the EL vm.lockable.status
for now it's sufficient to know that this status property will "somehow" contain and update the current lock status (synchronized for all users).
SimpleLockableViewModel
Also the view model doesn't contain a lot of logic:
public class SimpleLockableViewModel {
private static final AtomicInteger userCounter = new AtomicInteger(0);
private final String username = "User-" + userCounter.incrementAndGet();
private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
private static SimpleResource sharedResource = new SimpleResource("the Resource Value");
@Init
public void init(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
desktop.enableServerPush(true);
lockTracker.observe(new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
@Override
public void onLockEvent(LockEvent event) {
super.onLockEvent(event);
BindUtils.postNotifyChange(null, null, sharedResource, "value");
}
});
}
@Command
public void lock() { lockTracker.lock(); }
@Command
public void unlock() { lockTracker.unlock(); }
public Lockable<SimpleResource> getLockable() { return lockTracker.getLockable(); }
public String getUsername() { return username; }
}
Worth mentioning is line 10 that we enable server-push (LINK ME) as a prerequisite to allow for asynchronous UI updates.
Besides the usual @Command-bindings and getters, the only things remaining are the UiLockTracker
(line 5) and Lockable/MvvmLockable
(line 11) classes.
SimpleResource
LINK ME is a trivial Java-bean with a single property: value - Since Lockable/UiLocktracker are generic this can be an arbitrary type.
Lockable/MvvmLockable
In order to "calculate" the lock status the UiLockable needs a resource, the current user (self) and the lock owner information. Owner changes are indicated with a LockEvent whenever onLockEvent() is called.
public class Lockable<T> {
private final String self;
private final T resource;
private String owner;
public Lockable(String self, T resource) {
this.self = self;
this.resource = resource;
}
protected void onLockEvent(LockEvent event) {
this.owner = event.getOwner();
}
public String getSelf() { return self; }
public T getResource() { return resource; }
public String getOwner() { return owner; }
public LockStatus getStatus() {
return owner == null ? LockStatus.AVAILABLE :
self.equals(owner) ? LockStatus.OWNED : LockStatus.UNAVAILABLE;
}
protected Object getResourceKey() {
... generates a unique key for the resource
}
}
By overriding the onLockEvent method UI updates can be implemented (especially when using MVC).
When using MVVM the changing properties can be notified automatically as in the implementation below.
public class MvvmLockable<T> extends Lockable<T> {
...
@Override
public void onLockEvent(LockEvent event) {
super.onLockEvent(event);
BindUtils.postNotifyChange(null, null, this, "owner");
BindUtils.postNotifyChange(null, null, this, "status");
}
...
Additional notifications of vm properties can be implemented by overriding the onLockEvent as needed.
new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
@Override
public void onLockEvent(LockEvent event) {
super.onLockEvent(event);
BindUtils.postNotifyChange(null, null, sharedResource, "value");
}
}
Notifying the sharedResource.value property will make sure the rendered resource value is also updated in all observing browsers after a lock event. This especially allows conditional notification for different requirements in your UI.
So far we haven't seen how those LockEvents are triggered and distributed among the observing clients. Let's get into the details.
UiLockTracker
The UiLockTracker
is surely the core class of this example - providing a convenient way to observe LockEvents on a Lockable, which we have already seen in the code above following the simple life cycle:
- observe - start observing a resource by providing a Lockable implementation to receive LockEvents
- lock - request a lock on a Lockable
- unlock - release the lock
- reset - stop observing the lockable
Between 1 and 4 it is possible handle LockEvents for that resource.
Usage
private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
...
lockTracker.observe(new MvvmLockable<SimpleResource>(getUsername(), sharedResource) {
@Override
public void onLockEvent(LockEvent event) {
super.onLockEvent(event);
BindUtils.postNotifyChange(null, null, sharedResource, "value");
}
}
line 1: Create a UiLockTracker instance (the parameters are explained in the #Implementation paragraph) line 4: Observe the sharedResource with the current user line 6: Override onLockEvent to handle LockEvents
It provides the methods lockTracker.lock/unlock to request/release a lock - when succeeding those will emit the related LockEvent
s asynchronously resulting in calls to onLockEvent for each observing user.
Since we already use the MvvmLockable class the UI will update automatically by calling @load-bindings on component properties or shadow elements to re-apply templates.
Once the resource doesn't need to be observed any more a call to lockTracker.reset() performs the cleanup.
Implementation
This implementation is only a dummy implementation and doesn't intend to be complete or stable, the goal of this article is not to explain how to do resource locking but to build a UI reacting to asynchronous events. So I'll only cover the ZK specific parts. If you are interested in RxJava2 please refer to the official documentation.
Internally a BehaviorSubject (LINK ME) is used for each observed Lockable - which ensures each subscriber receives the latest and subsequent LockEvents.
private void subscribeToLockEvents(Lockable<T> lockable) {
lockEventSubscription = LockService.observeResource(lockable.getResourceKey())
.doOnNext(event -> System.out.println("locked: " + event.owner + " on " + event.getResourceKey()))
.doOnNext(event -> toggleAliveCheck(event, lockable))
.compose(ZkObservable.activated())
.subscribe(lockable::onLockEvent, this::resetOnError);
}
line 2: create/join the resource subject for the resource key
line 5,6: call onLockEvent inside an activated execution to allow UI updates
The composable ZkObservable.activated() ObservableTransformer is already explained in my previous article on DZone (https://dzone.com/articles/deal-with-hot-observables-in-a-web-ui) - it activates/deactivates the current desktop for modification allowing asynchronous UI updates via server push.
Observing -> locking -> unlocking -> resetting is a straight forward implementation as long as things are under control.
There are certain occasions when things go wrong:
- user forgets to release the lock (while the session times out)
- user closes/refreshed the browser tab
- the network disconnects
- the browser process crashes or terminates
For such cases UiLockTracker implements automatic cleanup mechanisms to ensure a resource becomes available again and observers are removed from the subjects to avoid memory leaks:
DesktopCleanup
1. A DesktopCleanup (LINK ME) listener is dealing with the common scenario that a user closes/refreshes the browser tab or the session times out due to inactivity
private void addDesktopCleanup() {
removeDesktopCleanup();
desktopCleanup = dt -> this.reset();
desktop.addListener(desktopCleanup);
}
private void removeDesktopCleanup() {
if(desktopCleanup != null) {
desktop.removeListener(desktopCleanup);
this.desktopCleanup = null;
}
}
line 3: calls reset() automatically when the desktop is destroyed.
This is enough for users NOT owning a lock since it doesn't really impact other users even if they keep observing until their session times out. However for users holding a lock a second mechanism is in place.
Alive-check
2. An alive-check to actively check whether the connection to a user owning a lock is still responding during the time they own the lock.
The implementation uses a second Observable to poll the client in configurable intervals by activating/deactivating the desktop for an instant of time. Desktop-activation triggers a client-side round trip without any UI changes just making sure the client is still responding.
The interval (10sec) and timeout (5sec) are configured when creating the lockTracker instance.
private final UiLockTracker<SimpleResource> lockTracker = new UiLockTracker<>(10, 5);
private void startAliveCheck() {
System.out.println("alive check: started, " + lockable);
aliveCheckSubscription = Observable.interval(aliveCheckInterval, TimeUnit.SECONDS)
.compose(ZkObservable.activated())
.timeout(aliveCheckTimeout + aliveCheckInterval, TimeUnit.SECONDS)
.subscribe(
aliveCount -> {
System.out.println("alive check: " + (aliveCount + 1) * aliveCheckInterval + "sec, " + lockable);
},
err -> {
System.err.println("Unlocking unresponsive lock owner: " + lockable + " - " + err);
unlock();
});
}
private void stopAliveCheck() {
if(aliveCheckSubscription != null) {
System.out.println("alive check: stopped, " + lockable);
aliveCheckSubscription.dispose();
aliveCheckSubscription = null;
}
}
line 3: start an interval to perform alive check with interval() (LINK ME)
line 5: timeout-operator (LINK ME) triggers an exception when activation takes too long
When an error occurs (e.g. when the timeout is exceeded or desktop activation fails) unlock()
is called to make the resource available again.
LockService
This simplified dummy back-end implementation is based on RxJava. It keeps a map of BehaviorSubjects (one for each resourceKey). Each subject will emit LockEvents to inform all subscribers of the current lock state, subscribers decide how to handle the events accordingly.
This implementation doesn't perform any sanity checks, simply the last call to lock/unlock will determine the next event. It is not the goal of this example to implement transaction safety. Key is the UI has to reflect every decision the back-end makes. Whatever the reason (e.g. higher privileges), it has to react in some way (reflecting the latest state), following the "single source of truth" (LINK ME) principle.
Another UI implementation might decide to keep an item editable (even after losing the lock) it will then have to deal with possible data conflicts later, your back-end will hopefully inform you about merge conflicts (Optimistic vs Pessimistic locking).
A more complex example
Using the same LockTracker and Lockable objects you can build a more complex UI such as the inventory example.
inventory.zul
<?import zk.example.template.locker.lockservice.LockStatus?>
<?style src="/locker/template/lockableEditor.css"?>
<?component name="lockableEditor" templateURI="/locker/template/lockableEditor.zul"?>
<?component name="inventoryItemEdit" templateURI="/locker/template/inventoryItemEdit.zul"?>
<?component name="inventoryItemView" templateURI="/locker/template/inventoryItemView.zul"?>
<zk>
<div viewModel="@id('vm') @init('zk.example.template.locker.InventoryViewModel')">
You are user: ${vm.username}
<grid model="@init(vm.inventory)">
<template name="model">
<row>
<label value="@init(each.id)"/>
<label value="@init(each.name)"/>
<label value="@init(each.value)"/>
<label value="@init(each.quantity)"/>
<button label="view" iconSclass="z-icon-eye" onClick="@command('view', item=each)"/>
<button label="edit" iconSclass="z-icon-edit" onClick="@command('edit', item=each)"/>
</row>
</template>
</grid>
<if test="@load(!empty vm.currentItem)">
<lockableEditor lockable="@load(vm.currentItem)" editCommand="edit" saveCommand="save" cancelCommand="cancel">
<template name="content">
<apply template="@load(lockable.status)" item="@init(lockable.resource)">
<template name="AVAILABLE" src="/locker/template/inventoryItemView.zul"/>
<template name="UNAVAILABLE" src="/locker/template/inventoryItemView.zul"/>
<template name="OWNED" src="/locker/template/inventoryItemEdit.zul"/>
</apply>
</template>
</lockableEditor>
</if>
</div>
</zk>
InventoryViewModel
public class InventoryViewModel {
private static final AtomicInteger userCounter = new AtomicInteger(0);
private final String username = "user-" + userCounter.incrementAndGet();
private ListModelList<InventoryItem> inventory;
private final UiLockTracker<InventoryItem> lockTracker = new UiLockTracker<>(10, 5);
@Init
public void init(@ContextParam(ContextType.DESKTOP) Desktop desktop) {
desktop.enableServerPush(true);
inventory = new ListModelList<>(InventoryService.getInventory());
}
@Command
public void view(@BindingParam("item") InventoryItem item) {
lockTracker.reset();
lockTracker.observe(new MvvmLockable(username, item));
BindUtils.postNotifyChange(null, null, this, "currentItem");
}
@Command
public void edit(@BindingParam("item") InventoryItem item) {
view(item);
lockTracker.lock();
}
@Command
public void save(@BindingParam("item") InventoryItem item) {
lockTracker.unlock();
inventory.notifyChange(item);
}
@Command
public void cancel(@BindingParam("item") InventoryItem item) {
lockTracker.unlock();
}
public ListModelList<InventoryItem> getInventory() { return inventory; }
public Lockable<InventoryItem> getCurrentItem() { return lockTracker.getLockable(); }
public String getUsername() { return username; }
}
Summary
Example Sources
The code examples are available on github in the zk-template-examples repository
- zul files: https://github.com/zkoss-demo/zk-template-examples/tree/master/src/main/webapp/locker
- java classes: https://github.com/zkoss-demo/zk-template-examples/tree/master/src/main/java/zk/example/template/locker
Running the Example
Clone the repo
git clone git@github.com:zkoss-demo/zk-template-examples.git
The example war file can be built using the gradle-wrapper (on windows simply omit the prefix './'):
./gradlew war
Execute using gretty:
./gradlew appRun
Execute using jetty-runner (faster startup):
./gradlew startJettyRunner
Then access the example http://localhost:8080/zk-template-examples/locker/
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |