In modern Manufacturing Execution Systems (MES) or high-frequency logistics environments, the default “polling” mechanism often feels sluggish. Users today expect instant feedback: just like a chat app, when a barcode is scanned in the warehouse, the dashboard in the office should blink immediately.

In this post, we will dive into how to solve “Server Push Delay” in iDempiere by moving from standard ZK EventQueues to a robust OSGi EventAdmin approach, achieving true sub-second updates across browser sessions.


The Problem: “It’s too slow!”

We recently built a Resource KPI Board for a production floor. It displays real-time stats (Target vs. Delivered) for each workstation.

  • Operator: Scans a product barcode on an industrial tablet.
  • Manager: Watches the KPI Dashboard on a 65-inch TV screen.

Initially, we used the standard ZK EventQueues.APPLICATION. However, users reported lags of 10-30 seconds or, worse, missed updates.

Why does this happen? ZK’s default mechanism sometimes falls back to “piggyback” polling if the client-side configuration isn’t perfect. Furthermore, EventQueues managed strictly within the web container can struggle when business logic fires from a background process or a different OSGi bundle context.

The Solution: OSGi EventAdmin + Server Push

To fix this, we looked at how iDempiere handles its own “Broadcast Messages.” It utilizes OSGi EventAdmin, the native event bus of the OSGi framework. It sits at a lower, system-wide level, making it the perfect bridge between business logic and the UI.

By coupling OSGi Events with ZK’s enableServerPush, we created a lightning-fast, decoupled update cycle.

The Architecture

  1. Publisher (The Scanner): Fires an OSGi Event (idempiere/mes/update) containing metadata (Order ID, Qty).
  2. Subscriber (The Dashboard): A ZK UI component registers an OSGi EventHandler.
  3. The Bridge: When the OSGi event arrives, the handler uses Executions.schedule() to safely inject the task into the ZK Desktop thread.

Implementation Guide

Core Components

1. MESBroadcastUtil – Event Broadcasting Utility

Java

package tw.ninniku.mrp.util;

import java.util.HashMap;
import java.util.Map;
import org.adempiere.base.event.EventManager;
import org.osgi.service.event.Event;

public class MESBroadcastUtil {
    // Define Event Topic
    public static final String TOPIC_MES_UPDATE = "idempiere/mes/update";
    public static final String PROPERTY_ORDER_ID = "M_Production_ID";
    public static final String PROPERTY_QTY = "Qty";

    /**
     * Send event to the OSGi System Bus
     * sendEvent = Synchronous delivery (Recommended for real-time updates)
     * postEvent = Asynchronous delivery
     */
    public static void publishMESUpdate(int orderId, int qty) {
        Map<String, Object> properties = new HashMap<>();
        properties.put(PROPERTY_ORDER_ID, orderId);
        properties.put(PROPERTY_QTY, qty);
        Event event = new Event(TOPIC_MES_UPDATE, properties);
        EventManager.getInstance().sendEvent(event);
    }
}

2. Model afterSave – Triggering the Event

Trigger the event within the afterSave() method of your Model class:

Java

@Override
protected boolean afterSave(boolean newRecord, boolean success) {
    if (success) {
        // Send real-time update event
        MESBroadcastUtil.publishMESUpdate(get_ID(), 0);
    }
    return super.afterSave(newRecord, success);
}

3. AbstractKanbanForm – Base Class

Java

package tw.ninniku.mrp.webui.form;

import org.adempiere.base.event.EventManager;
import org.adempiere.webui.panel.ADForm;
import org.adempiere.webui.panel.IFormController;
import org.osgi.service.event.EventHandler;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zul.Hlayout;
import tw.ninniku.mrp.util.MESBroadcastUtil;

public abstract class AbstractKanbanForm extends ADForm 
        implements IFormController, EventListener<Event> {

    protected Hlayout mainLayout = new Hlayout();
    private EventHandler subscriber;
    
    /** Subclass must implement: Returns Kanban name for Debugging */
    protected abstract String getKanbanName();
    
    /** Subclass must implement: Refreshes Kanban content */
    protected abstract void refreshKanban();
    
    /**
     * Crucial! Enable Server Push when the page is attached.
     */
    @Override
    public void onPageAttached(org.zkoss.zk.ui.Page newpage, 
                               org.zkoss.zk.ui.Page oldpage) {
        super.onPageAttached(newpage, oldpage);
        if (newpage != null) {
            newpage.getDesktop().enableServerPush(true);
        }
    }
    
    @Override
    protected void initForm() {
        mainLayout.setHflex("1");
        mainLayout.setVflex("1");
        this.appendChild(mainLayout);

        // Render UI immediately
        refreshKanban();

        // Subscribe to real-time update events
        subscribeToEvents();
    }
    
    /**
     * Subscribe to OSGi EventAdmin events
     */
    private void subscribeToEvents() {
        try {
            // Get current Desktop and enable Server Push
            final Desktop myDesktop = Executions.getCurrent().getDesktop();
            myDesktop.enableServerPush(true);

            subscriber = new EventHandler() {
                @Override
                public void handleEvent(final org.osgi.service.event.Event event) {
                    // Check event topic
                    if (MESBroadcastUtil.TOPIC_MES_UPDATE.equals(event.getTopic())) {
                        // Check if Desktop is still alive
                        if (myDesktop.isAlive()) {
                            try {
                                // Use Executions.schedule to bridge to the ZK UI thread
                                Executions.schedule(myDesktop,
                                    new EventListener<Event>() {
                                        public void onEvent(Event evt) {
                                            // UI Safe Zone - UI can be updated here
                                            refreshKanban();
                                        }
                                    }, new Event("updateKanban"));
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            };

            // Register event listener
            EventManager.getInstance().register(
                MESBroadcastUtil.TOPIC_MES_UPDATE, subscriber);

            // Important! Prevent memory leaks - Unregister when component detaches
            this.addEventListener("onDetach", (event) -> {
                EventManager.getInstance().unregister(subscriber);
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public ADForm getForm() {
        return this;
    }
}

Implementation Steps

Step 1: Create the Event Broadcasting Utility Class

Create MESBroadcastUtil.java to define event topics and sending methods.

Step 2: Trigger the Event in the Model

In the Model class that requires real-time updates, override the afterSave() method and call MESBroadcastUtil.publishMESUpdate().

Step 3: Create the Base Form Class

Create AbstractKanbanForm.java including:

  • onPageAttached(): To enable Server Push.
  • subscribeToEvents(): To subscribe to events.
  • onDetach listener: To unregister and prevent memory leaks.

Step 4: Implement the Specific Form

Extend AbstractKanbanForm and implement:

  • getKanbanName(): Return an identification name.
  • refreshKanban(): Logic to redraw the UI.

Java

public class WLabKanban extends AbstractKanbanForm {
    
    @Override
    protected String getKanbanName() {
        return "LabKanban";
    }

    @Override
    protected void refreshKanban() {
        mainLayout.getChildren().clear();
        // Rebuild UI components...
    }
}

Important Considerations

1. Server Push Must be Enabled

Ensure you call desktop.enableServerPush(true); otherwise, the server cannot push updates to the client.

2. UI Updates Must Occur in the ZK Thread

OSGi event handlers run on different threads and cannot update the UI directly. You must use Executions.schedule() to bridge the execution:

Java

Executions.schedule(desktop, 
    new EventListener<Event>() {
        public void onEvent(Event evt) {
            // Update UI here
        }
    }, new Event("eventName"));

3. Prevent Memory Leaks

Always unregister the event listener when the component is detached or removed.

4. Check Desktop Vitality

Before processing an event, verify if the Desktop is still alive using myDesktop.isAlive().


Required Import-Package in MANIFEST.MF

Ensure your OSGi bundle includes the following packages:

MANIFEST.MF

Import-Package: 
 org.adempiere.base.event,
 org.adempiere.webui.component,
 org.adempiere.webui.panel,
 org.osgi.service.event;version="[1.4,2)",
 org.zkoss.zk.ui,
 org.zkoss.zk.ui.event,
 org.zkoss.zul,
 ...

Would you like me to help you refine the code comments further or generate a WordPress-friendly featured image for this tutorial?

Why is this better?

  • Decoupling: Your business logic bundle doesn’t need to depend on the org.adempiere.ui.zk bundle. It just sends a message to the OSGi bus.
  • Performance: OSGi EventAdmin is highly optimized for high-frequency internal messaging.
  • Reliability: Unlike ZK EventQueues which can be scoped to a session, OSGi events are system-wide.
  • Cluster Ready: If you implement a custom IMessageService, this pattern can be extended to broadcast across multiple physical servers.

Conclusion

Real-time feedback creates a “premium” feel for end-users and increases operational efficiency. By leveraging the OSGi infrastructure already built into iDempiere, you can build incredibly responsive dashboards without the overhead of external WebSocket servers.

Have you used OSGi events for UI updates? Let me know your thoughts or performance tips in the comments below!

By Ray Lee (System Analyst)

iDempeire ERP Contributor, 經濟部中小企業處財務管理顧問 李寶瑞

Leave a Reply

Your email address will not be published. Required fields are marked *