Customized Server Message

From Documentation
Revision as of 08:19, 25 October 2010 by Tmillsclare (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
DocumentationSmall Talks2010JuneCustomized Server Message
Customized Server Message

Author
Herbert Daume, DIMOCO GmbH, Austria


Herbert profilfoto.jpg
Herbert has been a professional JAVA programmer for several years. At present he is responsible for the application software department of DIMOCO Austria. One of his main tasks is to use sophisticated state-of-the-art technologies to underscore DIMOCO as a competent technology leader. This is the reason why ZK is used for administration and call-center interfaces of DIMOCO services.

Date
June 22, 2010
Version
ZK 3.6.3

Introduction

There are some approaches to customize the server message on long running events. This article shows how to do this with ZK's Server Push technology. The ZK framework is performing Sever Push by periodically client - side polling the server. One has to notice that there is only one communication channel for one client; if this channel is blocked by a long running operation of an event, the polling also has to wait and is processed after the result from the previous event processing has been received on the client. This makes it impossible to inform the client with a customized message, since the message will displayed after the long running operation has been finished. To display the message with Server Push technology while the operation is running, this operation has to be processed asynchronously. The event will start the work in a separate thread on the server, and the event will return immediately to free the channel for Server Push requests. When the thread has finished his work, the result must then also be delivered via Server Push to the client.

Implementation

The following source code shows the implementation of displaying a customized message while at client side while loading a large list of users from the database on the server side.

As already mentioned, the long running operation is processed asynchronously in a separate thread. The following source implements a thread which executes a execute() method.of a generic abstract Job Class. This Job Class has a message(String msg) method, which can be called by the long running operation to alter the displayed message on the client.

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

import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.util.Clients;

/**
 * Thread class for displaying user defined message while ZK is doing a long operation.
 * 
 * @author hdaume
 *
 */
public class ServerBusy extends Thread {
	private static Map<Desktop, Map<Long, String>> map = new HashMap<Desktop, Map<Long,String>>();
	private static long counter;
	
	private ServerBusyJob sbj;
	private Object mutex;
	private boolean isWaiting;
	
	private static Map<Desktop, Long> serverPushCounter = new HashMap<Desktop, Long>();
	
	public abstract static class ServerBusyJob {
		private Desktop dt;
		private long msgID = -1;
		
		public ServerBusyJob(Desktop dt) {
			this.dt = dt;
		}
		
		private Desktop getDesktop() {
			return this.dt;
		}
		
		public abstract void execute() throws Exception;
		
		protected final void message(String msg) throws InterruptedException {
			if ( msg == null && msgID != -1 ) {
				rmMessage(dt, msgID);
				msgID = -1;
			} else if ( msg != null ) {
				if ( msgID != -1 ) 
					rmMessage(dt, msgID);
				
				msgID = addMessage(dt, msg);
			}
		}
	}
	
	private ServerBusy(ServerBusyJob sbj, Object mutex) {
		this.sbj = sbj;
		this.mutex = mutex;
	}

	private static synchronized void increaseServerPush(Desktop dt) {
		if ( ! dt.isServerPushEnabled() )
			dt.enableServerPush(true);

		Long counter = serverPushCounter.get(dt);
		if ( counter == null ) {
			counter = new Long(0);
		}
		
		counter++;
		
		serverPushCounter.put(dt, counter);
	}
	
	private static synchronized void decreaseServerPush(Desktop dt) {
		Long counter = serverPushCounter.get(dt);
		if ( counter != null && counter > 1 ) {
			counter--;
			serverPushCounter.put(dt, counter);
		} else {
			if ( dt.isServerPushEnabled() )
				dt.enableServerPush(false);
			
			serverPushCounter.remove(dt);
		}
	}
	
	private static synchronized long addMessage(Desktop dt, String message) throws InterruptedException {
		counter++;
		
		Map<Long, String> dmsg = map.get(dt);
		if ( dmsg == null ) {
			dmsg = new HashMap<Long, String>();
			map.put(dt, dmsg);
		}
		
		dmsg.put(counter, message);
		
		displayMessages(dt);

		return counter;
	}
	
	private static synchronized void rmMessage(Desktop dt, long id) throws InterruptedException {
		Map<Long, String> dmsg = map.get(dt);
		if ( dmsg != null ) {
			dmsg.remove(id);
			
			if ( dmsg.size() == 0 )
				map.remove(dt);
			
			displayMessages(dt);
		}
	}

	private static synchronized void displayMessages(Desktop dt) throws InterruptedException {
		Map<Long, String> dmsg = map.get(dt);
		
		Executions.activate(dt);
		try {
			if ( dmsg == null ) {
				Clients.showBusy(null, false);
			} else {
				StringBuffer buffer = new StringBuffer();
				
				Iterator<String> it = dmsg.values().iterator();
				while ( it.hasNext() ) {
					buffer.append((buffer.length() > 0 ? "</td></tr><tr><td>" : "") + it.next());
				}
				
				Clients.showBusy("<table><tr><td>" + buffer.toString() + "</td></tr></table>", true);
			}
		} finally {
			Executions.deactivate(dt);
		}
	}
	
	public static void execAsync(ServerBusyJob sbj) {
		Object mutex = new Integer(0);
		
		ServerBusy sb = new ServerBusy(sbj, mutex);
		synchronized(mutex) {
			increaseServerPush(sbj.getDesktop());
			
			sb.isWaiting = true;
			try {
				sb.start();
				try { Executions.wait(mutex); } catch ( Exception e ) {}
			} finally {
				sb.isWaiting = false;
			}
		}
	}
	
	public void run() {
		try {
			try {
				this.sbj.execute();
					
			} finally {
				if ( this.sbj.msgID != -1 )
					rmMessage(this.sbj.getDesktop(), this.sbj.msgID);
			}
				
		} catch ( Exception e ) {
			e.printStackTrace();
		} finally {
			synchronized(mutex) {
				if ( this.isWaiting )
					Executions.notify(this.sbj.getDesktop(), mutex);
			}
			
			decreaseServerPush(this.sbj.getDesktop());
		}
	}
}

The static map is used to store the messages which shall be displayed on the given desktop.; the counter is used to privide a unique identifier for each message. The Server Push functionality is disabled by default, since it produces extra network traffic. To save this traffic, Server Push is automaticly activated when the long running operation starts and autoaticly deactivated when the operation has finished. This is done with the mutex, serverPushCounter and isWaiting variables which are used for synchronizing the originater and the ServerBusy thread. Otherwise, deactivting Server Push twice would result in an Exception.

To finally use this thing, one has to define the listbox in a *.zul file:

<listbox id="userlist" rows="10" fixedLayout="true" style="background:white;" model="@{win.user}" selectedItem="@{selectedUser}" >
	<listhead sizable="false">
		<listheader align="left" width="120px" label="${c:l('label.email')}" />
		<listheader align="left" width="120px" label="${c:l('label.shortname')}" />
		<listheader align="left" width="55px" label="${c:l('label.language')}" />
	</listhead>
	<listitem sclass="row-border item" self="@{each=usr}">
		<listcell><vbox align="start" width="100%"><label value="@{usr.email}" /></vbox></listcell>
		<listcell><vbox align="start" width="100%"><label value="@{usr.shortname}" /></vbox></listcell>
		<listcell><vbox align="center" width="100%"><label value="@{usr.language.key}" /></vbox></listcell>
	</listitem>
</listbox>

As you can see, this is a standard listbox definition, and the data of this listbox is fetched by calling the getUser() method on the Controller:

	public List<UserAccount> getUser() {
		List<UserAccount> list = this.cache;
		
		if ( list == null ) {
			list = new ArrayList<UserAccount>();

			final Listbox lb = (Listbox) getFellow("userlist");
			final Desktop dt = getDesktop();
			final List<UserAccount> resultList = list;
				
			ServerBusy.execAsync(new ServerBusyJob(this.getDesktop()) {
					
				@Override
				public void execute() throws Exception {
					for ( int i = 0; i < UserAccountHelper.userListSize(); i++ ) {
						UserAccount ua = UserAccountHelper.get(i);
							
						this.message("User Loaded: " + ua.getDisplayName());

						resultList.add(ua);
					}
						
					this.message("Transferring Data....");

					Executions.activate(dt);						
					lb.setModel(new ListModelArray(resultList));						
					Executions.deactivate(dt);
				}
			});

			cache = list;
		}
	}

The User list is stored in a cache on the Controller; if the cache is empty, the user list is loaded by creating a new ServerBusyJob() anonymous class and executing the execute() method by the ServerBusy thread. Every time a user is loaded, this user is displayed as a server message. After all users have been loaded, a final message informs the user that the user data is being transferred to the client and the code

Executions.activate(dt);						
lb.setModel(new ListModelArray(resultList));						
Executions.deactivate(dt);

makes this by using Server Push.

Comments



Copyright © DIMOCO GmbH. This article is licensed under GNU Free Documentation License.