Listening Framework v2.0
User Guide
Copyright © 1998,99, 2003 Cristiano Sadun - All rights reserved

This software is licensed under GPL.

Table of contents


  1. Purpose
  2. Creating a client class
  3. Creating a server class
  4. Signals
  5. Handling Exceptions
  6. Threading considerations
  7. Additional listeners
  8. The SignalSource interface


Change History

1. Purpose

This framework allows Java objects to easily embed support to receive or transmit asynchronous signals (aka events) to other objects, in a manner similar to the standard java.awt event processing. This includes normal "passive" objects and thread objects.

For example, let's suppose a thread is executing a heavy duty task which will likely take some time to finish.

An external class ("client") is interested into the outcome of the task. A straightforward way to check for the results is to poll the thread state waiting until it has reached a "task finished" state, with code like this:

// "t" is a reference to the working thread
while (! t.hasFinished()) {
	..output something...
}

This approach has various drawbacks: it implies busy waiting by the client class; the client class must have explicit access and knowledge of the internal state of the thread, or the latter must implement a specific interface (like the hasFinshed() method above), and later updates to the task-thread may require the knowledge of different states, therefore requiring the amendment of the interface and all the client code.

A different approach would be possible if the client could simply "register" as interested in the outcome of the results, and then be directly (and asynchronously) notified by the working thread when the job is done. In the meanwhile, the client class can go on executing his own job (maybe in a different thread, maybe not).

This framework supports exactly this kind of behaviour, allowing "server" classes to expose a registry, to which client classes can register; and establishing a protocol for event notification from server classes to clients. The code can be easily "plugged in" in a fast and efficient way.

See the javadoc documentation for detailed reference.

2. Creating a client class

A client object is a object which is interested in receiving Signals - is "listening" to them. To indicate such a role, any client object shall implement the Listener interface.

This interface declares just one method receive, which will be called when a signal is received.

import com.deltax.util.listener.*;

public StatusPanel extends java.awt.Frame implements Listener {

  ...code..

  
  public void receive(Signal s) {
	 statusLabel.setText("Job finished!");
	 repaint();
  }
  

}

The class above is an imaginary "status window" of an application (extends java.awt.Frame) and declares itself as able to "listen" to signal sources (implementing the interface).
The implementation shown above just changes a GUI label upon any signal receiving. As we will see later, real implementations are likely to discriminate upon the received signal and undertake different actions depending on it. The framework provides already different flavours of signals ("Signal" being the simplest and anonymous one) and ad hoc signals can be created by your application.

See Client Registration for code that registers a listener client to a signal source.

3. Creating a server class

Signals

A server class is a potential generator of asynchronous events (Signals in the framework's denotation). A Signal is much like a java.awt.Event, and is implemented in the java class Signal (which is a very simple, "base" signal) and various subclasses.

ListenerSupport

Besides, a server class mantains a registry of clients interested in receiving signals. This registry is implemented in the class ListenerSupport which will simply be a private member of the server class.

This class is the core class for servers, as much of the functionality is encapsulated therein.

import com.deltax.util.listener.*;

public class HeavyThread extends Thread {

  private ListenerSupport ls = new ListenerSupport();

  public HeavyThread() {	// Class constructor
  	...
  }

  public void run() { // Thread loop
  	...heavy job...
  }

}

The class above is a Thread (that's just for example) and owns a ListenerSupport object referenced by ls.

Bridging registry support

ListenerSupport provides four services: three for registry handling and one for notification. Let's have a look to the first three, the methods addListener, removeListener and isRegistered.

Such methods allow to add a client class to the registry, and our server class will likely provide bridge methods to them:

  ...HeavyThread code as above..

  public void addListener(Listener l) {
  	ls.addListener(l);
  }

  public void removeListener(Listener l) {
  	ls.removeListener(l);
  }

  public boolean isRegistered(Listener l) {
  	return ls.isRegistered(l);
  }

This means that client object will be able to call such methods on HeavyThread, and this will carry on the call to the ListenerSupport, which holds the actual registry.

Client registration

A client can then register by simply invoking the methods like in:

  public StatusPanel() {	// Constructor for StatusPanel, see 2.
  	super("Status Panel");           // Invoke "Frame" constructor
	Thread t = new HeavyThread();    // Create a HeavyThread
  	t.registerListener(this);        // Register itself as listener
  	t.start();                       // Run the thread
  }

In the code above, the example class StatusPanel (which implements Listener, see 2.) creates a new HeavyThread and register itself as a listern to signals generated by such source.

Notifying listeners

Let's now see how the actual notification is performed. In server code (which is HeavyThread, in our example) there is already a ListenerSupport member (named ls in the example); to notify listeners, it is sufficient to create the appropriate Signal object and call the notify() method of ls. For example, to notify that the heavy task is finished, the run method in HeavyThread will look like this:

   public void run() {

     ..do heavy job..
     
     Signal s = new Signal(this);  // Creates a new Signal object
     ls.notify(s);                 // Notify all the listeners
     
   }

The code above will create a Signal object and invoke the notify method, which will notify (which is, invoke the "receive" method) of all the registered listeners.

Version 2.0 of the library introduces a BlockedListenerException exception which is raised when a remove operation is attempted on a lister which doesn't unblock (i.e. is stuck in the receive() method) within a certain timeout. Note that BlockedListenerException is an unchecked exception - so it's up to your client code to decide whether or not handle it explicitly.

4. Signals

The minimum amount of information that must be provided when creating a Signal is the source object which is creating the signal itself: the class Signal encapsulates just this simple signal, which is therefore called anonymous.

Three other Signal classes are built-in in the framework: MsgSignal, TimeStampedSignal and ExceptionSignal. This last one is illustrated in the Handling Exceptions chapter; MsgSignal extends Signal by adding a message; the server code can provide information about the specific kind of signal in the message.
For example, in HeavyThread class... (signal source):

   // Source-specific information
   public static final String TASK_FINISHED = "Completed";
   public static final String TASK_ABORTED = "Aborted";

   public void run() {

     ..do heavy job..

     MsgSignal s;
     if (suceeded) s = new MsgSignal(this, TASK_FINISHED);
     else s = new MsgSignal(this, TASK_ABORTED);
     
     ls.notify(s);                 // Notify all the listeners
   }

and in StatusFrame class... (listener):

  public void receive(Signal s) {

     // Cast to MsgSignal
     MsgSignal ms = (MsgSignal)s;

     // Show the Msg in user interface
     statusLabel.setText("Job "+ms.getMsg());
     repaint();

     // Check the information
     if ( HeavyThread.TASK_ABORTED.equals(ms.getMsg()) ) {
       // Pop up a window asking the user if he wants to retry, etc..
       ....
     }
     repaint();
  }

An alternative method of identifying signal is, of course, to subclass Signal; for example:

   public class HeavyThreadFinished extends Signal {
     public HeavyThreadFinished(Object src) { super(src); }
   }

   public class HeavyThreadAborted extends Signal {
     public HeavyThreadAborted(Object src) { super(src); }
   }

then the client code will look like:

   public void receive(Signal s) {
     if (s instanceof HeavyThreadAborted) ...
     else if (s instanceof HeavyThreadFinished) ...
     else // If signal is unknown, rais a runtime exception
          throw new RuntimeException(
            "An unexpected signal "+s.getClass().getName()+" has been received"
          );
   }

Which tecnique is preferable depends of course on your application design.

TimeStampedSignal simply carries information on the moment in which the event has been generated, represented as a standard long value. Such information can be retrieved by using the getTime() method, as in:

   public synchronized void receive(Signal s) {
   	if (s instanceof TimeStampedSignal) {
   	  java.util.Date d = new Date( (TimeStampedSignal)s.getTime() );
   	  System.out.println("Signal generated on "+d);
   	}
   	else System.out.println("Signal received");
   }
which simply prints receiving information.

5. Handling Exceptions

Sometimes a class wants to adopt a separate processing in case of unexpected conditions. The usual Java exception handling mechanism is not able to explicitly handle asynchronous exceptions (at least at application level), so a separate mechanism is provided in the framework to support this case.

ExceptionListener extends the Listener interface adding a method receiveException, which is used to receive signal of class ExceptionSignal.
A client class can decide to implement this interface. In this case, whenever the signal source notifies an ExceptionSignal, a call to receiveException() will occur, instead of receive().

Note that the signal source class shall not do anything special to obtain this behaviour: just create and notify an ExceptionSignal instead of a Signal. This code fragment provides an example:

   ... HeavyThread code...

   public void run() {

     try {
      ..do heavy job..
      ls.notify(new Signal(this));  // Notify all the listeners of job completion
     } catch (Exception e) {
      ls.notify(new ExceptionSignal(e, this)); // Notify all the listeners of exception
     }

The framework will take care of the proper dispatching.

6. Threading considerations

Synchronization of receive()

(the discussion below holds for both receive() and receiveException())

When implementing receive() in the listener code, it may need to be declared as synchronized, depending on the number of signal sources to which it's registered.

The main issue is that a single signal source will not invoke receive() until the last signal has finished processing; in other words, signalling occurs asynchronously with respect to the client's thread, but before being notified with another signal, the listener can complete the processing of the current signal.

This means that if a listener is registered with a single source, receive() does not need to be synchronized.

If the listener registers itself with more than one source, however, the various sources do not know anything about each other but receive() is still the single entry point for signal notification from all the sources. Therefore, when a listener registers itself with more than one source, receive() has to be declared as synchronized to avoid race conditions (which is, when a signal from a certain source is being processed, other signals are kept out of the method code).

This can be avoided only if the processing code is completely reentrant (this means that any method called in the processing thread is reentrant as well), a condition which is unlikely to hold, but for the simplest listeners.
An example of a completely reentrant listener is the StreamListener class provided by the framework, whose implementation of receive() is just a call to System.out.println. Since this last method is synchronized, and the code does not do anything else, this particular implementation is completely reentrant and therefore does not need to be declared synchronized. It should be obvious that this is due to the very simple form of processing code in such a case.

Threading model for notification

The signal flow is designed in such a way that's asynchronous with both the signal source and the signal listener. This means that:

- Signal source can issue multiple signals without waiting for the listener to process them; - Signal listeners can process signals without blocking the ability of the source to produce new signals;

However, as stated above, processing a signal from a certain source blocks the notification of other signals from the same source.
This means that, if the processing of a signal blocks, or does not terminate, the listener itself will never receive any more signals.

7. Additional Listeners

A number of pre-built Listener objects are provided with the framework. These can be aggregated in classes that do not want to directly implement the Listener interface, or for debug/development purposes.

StreamListener just prints out a description of any received signal to an output stream. By default, this stream is System.out, so that the net effect is to show received signals on the standard output.

BridgeListener is designed to be attached to an existing listener, so that a description of the received signals is sent to a certain Writer besides being processed by the original listener. For example, considering the StatusPanel class (which implements Listener) of the previous examples, the constructor code may look like this:

  public StatusPanel() {	// Constructor for StatusPanel, see 2.
    super("Status Panel");           // Invoke "Frame" constructor
    Thread t = new HeavyThread();    // Create a HeavyThread
    Writer w = new OutputStreamWriter(System.out);   // Create a writer to System.out
    BridgeListener bl = new BridgeListener(w, this); // Bridges to this
    t.registerListener(this);        // Register the bridge as listener
    t.start();                       // Run the thread
  }

With this code, any signal generated by HeavyThread will be forwarded to this after having been written to the Writer.

HistoryListener is simply a specialization of BridgeListener which uses a FileWriter, and allows to pass directly the file name at construction time. This allows to write to a file an history of the signals received by a certain listener.

8. The SignalSource interface

An interface SignalSource is provided. It does not need to be implemented by a signal source: it may be implemented to be able to explicitly recognized signal sources at program level (if needed), and is actually implemented by the two classes BaseSignalSource and BaseSignalSourceThread.

The two classes are "plug&play" implementations of a signal source (which extends Object and Thread, respectively): they embed a ListenerSupport and provide the standard bridging methods to its functionality (see the code in 3), while protecting the notify() methods (which must not be visible outside the signal source itself).

An application class can simply extend these classes, and directly use notify() and notifyException() to produce signals; for example:

  public class HeavyThread extends BaseSignalSourceThread {
  
    public void run() {
      ...do the job...
      
      notify(new Signal(this));
    }
  }

The client code remains identical to 2. As shown, when using BaseSignalSources there's no need of declaring the listner support and the bridging methods.


Copyright © 1998,99 Cristiano Sadun - All rights reserved
Please read the License Agreement before using the software and this manual in any way.