Developing Plug-Ins - ServerPushTemplate, IServerPushCallback and ITopicSubscriber

From iDempiere en
Dptest2.png

This tutorial is brought to you by Jan Thielemann from evenos GmbH (www.evenos-consulting.de). If you have questions, criticism or improvement suggestions, feel free to visit me (Jan.thielemann) or write me an email

Goal of this tutorial

Some times you want to populate changes immediately to other users on the system. A good example is the broadcast feature. It lets you send a message to other users and if they are logged in, they instantly get a popup with your message. In this tutorial i want to show you how you can benefit from the technology behind the boratcast feature by developing a very simple chat dashboard panel.

If you are not familliar with DashboardPanels, you should take a look at the other tutorials mentioned in the prerequisites because i will only cover the technology of sending messages in this tutorial.

Prerequisites

Before you start this tutorial, you should take a look at the follwing tutorials:


The workflow

The Code

This is the final code for our DashboardPanel. Don't be afraid, in the next section i will explain it step by step.

package org.adempiere.webui.dashboard;

import java.util.HashMap;
import java.util.Map;

import org.adempiere.base.Service;
import org.adempiere.webui.apps.AEnv;
import org.adempiere.webui.component.Label;
import org.adempiere.webui.component.Textbox;
import org.adempiere.webui.util.IServerPushCallback;
import org.adempiere.webui.util.ServerPushTemplate;
import org.compiere.util.Env;
import org.idempiere.distributed.IMessageService;
import org.idempiere.distributed.ITopic;
import org.idempiere.distributed.ITopicSubscriber;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Button;
import org.zkoss.zul.Vlayout;

public class DPTest extends DashboardPanel implements EventListener<Event>, ITopicSubscriber<Map<String, String>> {
	private static final long serialVersionUID = 3769375216193585342L;

	static final String ON_OUR_TOPIC = "onOurTopic";
	static final String SOURCE_USER = "SOURCE_USER";
	static final String DESTINATION_USER = "DESTINATION_USER";
	static final String OUR_MSG = "OUR_MSG";

	String current_user = Env.getAD_User_ID(Env.getCtx()) + "";

	Textbox msg = new Textbox();
	Textbox user = new Textbox();
	Label label = new Label("Nothing yet");
	String labeltext;

	IServerPushCallback callback;
	ServerPushTemplate template;

	public DPTest() {
		super();

		this.setSclass("dptest-box");
		this.setHeight("220px");

		Vlayout layout = new Vlayout();
		layout.setParent(this);
		layout.setSclass("broadcaster-layout");
		layout.setSpacing("0px");
		layout.setStyle("height: 100%; width: 100%");

		layout.appendChild(new Label("Message to send:"));
		msg.setParent(layout);

		layout.appendChild(new Label("User:"));
		user.setParent(layout);

		Button btn = new Button("Send");
		btn.addEventListener(Events.ON_CLICK, this);
		layout.appendChild(btn);

		layout.appendChild(new Label("Received Message:"));
		label.setParent(layout);

		IMessageService service = Service.locator().locate(IMessageService.class).getService();
		if (service != null) {
			ITopic<Map<String, String>> intopic = service.getTopic(ON_OUR_TOPIC);
			intopic.subscribe(this);
		}

		this.template = new ServerPushTemplate(AEnv.getDesktop());
		this.callback = new IServerPushCallback() {
			@Override
			public void updateUI() {
				label.setText(labeltext);
			}
		};
	}

	@Override
	public void onEvent(Event event) throws Exception {
		Map<String, String> properties = new HashMap<String, String>();
		properties.put(SOURCE_USER, current_user);
		properties.put(DESTINATION_USER, user.getText());
		properties.put(OUR_MSG, msg.getText());

		IMessageService service = Service.locator().locate(IMessageService.class).getService();
		if (service != null) {
			ITopic<Map<String, String>> itopic = service.getTopic(ON_OUR_TOPIC);
			itopic.publish(properties);
		}
	}

	@Override
	public void onMessage(Map<String, String> message) {
		String dest = message.get(DESTINATION_USER);
		if (dest.equalsIgnoreCase(current_user)) {
			labeltext = "Nachricht von " + message.get(SOURCE_USER) + ": " + message.get(OUR_MSG);
			template.executeAsync(callback);
		}
	}
}

Explanation

Lets start with the first line:

implements EventListener<Event>, ITopicSubscriber<Map<String, String>>

As you can see, we implement the EventListener and the ITopicSubscriber interface. The EventListener is used to for our button. The ITopicSubscriber is used to get notified when there are new messages in the OSGi Framework. Both interfaces will be additionally explained when we come to their methods onEvent() and onMessage().


For this one, theres not much to say if you are familiar with Java and you give something about warnings. ;)

private static final long serialVersionUID = 3769375216193585342L;

The following Strings are used in the OSGi messaging system. The system consists of so called topics. When you want so send something over the messaging system, you chose a topic and send your message to this topic. Everybody who registered themself as a topic subscriber for this topic will receive the message.

We use the ON_OUR_TOPIC constant to identify our own topic in which we will send and receive messages. The messages themselfs will consist out of HashMaps which have a String as the key and a String as the value. Thats why we implemented the ITopicSubscriber as ITopicSubscriber<Map<String, String>>.

The other constants are used as the keys in this HashMap. We use them so store the source and destination user as well as the message in the HashMap and send it to the system.

static final String ON_OUR_TOPIC = "onOurTopic";
static final String SOURCE_USER = "SOURCE_USER";
static final String DESTINATION_USER = "DESTINATION_USER";
static final String OUR_MSG = "OUR_MSG";

This code should also be self-explanatory. We load the current user to use him as the source user when we send a message. The rest is used for our DashboardPanels layout.

String current_user = Env.getAD_User_ID(Env.getCtx()) + "";
Textbox msg = new Textbox();
Textbox user = new Textbox();
Label label = new Label("Nothing yet");
String labeltext;

These two are a little bit harder to understand. What they basically do is: when a message from the OSGi message system arrives, it is possible that the message does not arriv in the ui-thread. If you try to update your ui in the onMessage() method, you will get a NPE. By using the IServerPushCallback in combination wiht the ServerPushTemplate you can make sure that your ui changes happen in the ui-thread and you won't get an error.

IServerPushCallback callback;
ServerPushTemplate template;

So this is basic stuff again. We build our layout consisting out of some labels, textboxes and a button. Also we register our DashboardPanel as the EventHandler for the button. Thats why we implemented the EventListener interface. We could also use a anonymous inner type for the EventListener but let's not interfere in implementation details. :)

this.setSclass("dptest-box");
this.setHeight("220px");
Vlayout layout = new Vlayout();
layout.setParent(this);
layout.setSclass("broadcaster-layout");
layout.setSpacing("0px");
layout.setStyle("height: 100%; width: 100%");
layout.appendChild(new Label("Message to send:"));
msg.setParent(layout);
layout.appendChild(new Label("User:"));
user.setParent(layout);
Button btn = new Button("Send");
btn.addEventListener(Events.ON_CLICK, this);
layout.appendChild(btn);
layout.appendChild(new Label("Received Message:"));
label.setParent(layout);

This one is interesting again. We first locate the messaging service and then we subscribe our DashboardPanel for our topic. From now on, everytime when a message is sent to the ON_OUR_TOPIC topic, our DashboardPanels onMessage() method will be called by the message system.

IMessageService service = Service.locator().locate(IMessageService.class).getService();
if (service != null) {
	ITopic<Map<String, String>> intopic = service.getTopic(ON_OUR_TOPIC);
	intopic.subscribe(this);
}

Here we create the ServerPushTemplate and the IServerPushCallback. This time it's a a anonymous inner type - don't judge me, it has a reason! If you take a look at DashboardPanel.java and the interface IDashboardPanel it implements, you will see that the DashboardPanel already implements a IServerPushCallback. However, if you try to use "this" as the callback, you will get a NPE. So what we do here is, we create a new ServerPushTemplate with the Desktop provided by AEnv and create a new IServerPushCallback. In the callback, we use the String stored in labeltext and set it as the text of our lable. Since label.setText() is a ui-thread method, we must do this in the updateUI() method of a IServerPushCallback because otherwise we will get an error.

this.template = new ServerPushTemplate(AEnv.getDesktop());
this.callback = new IServerPushCallback() {
	@Override
	public void updateUI() {
		label.setText(labeltext);
	}
};

The onEvent() method is called when our button was pressed. We then create a new HashMap and store our source and destination user as well as the message we want to send in it. Then again, we locate the message system and get our topic. Then we use the topics publish() method to send our HashMap to the system. You can basically send whatever you want, just make sure you parameterize all the Maps, ITopics and ITopicSubscriber to fit your needs. For each type of object you want to receive, you need to implement a different ITopicSubscriber and subcribe to a different kind of ITopic.

@Override
public void onEvent(Event event) throws Exception {
	Map<String, String> properties = new HashMap<String, String>();
	properties.put(SOURCE_USER, current_user);
	properties.put(DESTINATION_USER, user.getText());
	properties.put(OUR_MSG, msg.getText());

	IMessageService service = Service.locator().locate(IMessageService.class).getService();
	if (service != null) {
		ITopic<Map<String, String>> itopic = service.getTopic(ON_OUR_TOPIC);
		itopic.publish(properties);
	}
}

The last part of the code is the onMessage() method. This one gets called as soon as there is a new message in the system which fits our ON_OUR_TOPIC topic. As you can see, when the message arrives, we get the value for the destination user. Then we check if our DashboardPanels user is the destination user. If so, we change the labeltext String. You can do this because this String is a instance variable of our DashboardPanel and not a ui component like the lable. If you would try to change the label with label.setText() here, you would get a NPE. So after we created the text we want to display in our label, we call template.executeAsync(callback). This will call the updateUI() method in our IServerPushCallback in a ui-thread and there we can safely set the labels text with label.setText(labeltext);

@Override
public void onMessage(Map<String, String> message) {
	String dest = message.get(DESTINATION_USER);
	if (dest.equalsIgnoreCase(current_user)) {
		labeltext = "Nachricht von " + message.get(SOURCE_USER) + ": " + message.get(OUR_MSG);
		template.executeAsync(callback);
	}
}



Example

Time for an example. Imagine the following situation:

  1. You have two users using this DashboardPanel
  2. Both users log themself in into the system
  3. Both DashboardPanels register themselfs as a ITopicSubscriber for a Topic called "onOurTopic"
  4. One of the user (user A) enters a message, the ad_user_id of the other user (user B) and hits the send button
  5. The onEvent() method of user As DashboardPanel is fired and a new message is published to the message system
  6. The message system calls onMessage() of both DashboardPanels
  7. The DashboardPanel of user A checks that this message is not for him and does nothing
  8. The DashboardPanel of user B checks that the message is for him, creates the labeltext and executes the callback
  9. The IServerPushCallback of user Bs DashboardPanel updates the label. User B can now see the message from user A

Dptest1.png

Cookies help us deliver our services. By using our services, you agree to our use of cookies.