Chapter 16: Remote Events

This chapter looks at the distributed event model that is part of Jini. It looks at how remote event listeners are registered with objects, and how these objects notify their listeners of changes. It also looks at how leases are managed by event sources.

16.1. Event Models

Java has a number of event models, differing in various subtle ways. All of these involve an object generating an event in response to some change of state, either in the object itself (someone has changed a field, say), or in the external environment (a user has moved the mouse). At some earlier stage, a listener (or set of listeners) will have registered interest in this event and will have suitable methods called on them with the event as parameter. The event models all have their origin in the Observer pattern from the Gang of Four book, but this is modified by other pressures, such as Java Beans.

There are low-level input events, which are generated by user actions in controlling an application with a graphical user interface. These events - of type KeyEvent and MouseEvent - are placed in an event queue. These are removed from the queue by a separate thread and dispatched to the relevant objects. In this case, the object that is responsible for generating the event is not responsible for dispatch to listeners, and creation and dispatch of events occurs in different threads.

Input events are a special case, caused by the need to listen to user interactions and always deal with them without loss of response time. Most events are dealt with in a simpler manner: an object maintains its own list of listeners, generates its own events, and dispatches it directly to its listeners. In this category fall all the semantic events generated by the AWT and Swing toolkits, such as ActionEvent, ListSelectionEvent, etc. There is a large range of these event types, and they all call different methods in the listeners, based on the event name. For example, an ActionEvent is used in a listener's actionPerformed() method of an ActionListener. There are naming conventions in this, caused by Java Beans.

Java Beans is also the influence behind PropertyChange events, which get delivered whenever a bean changes a ``bound'' or ``constrained'' property value. These are delivered to PropertyChangeListener's propertyChange() method and to VetoableChangeListener's vetoableChange() method. These are usually used to signal a change in a field of an object, where this change may be of interest to others either for information or for vetoing.

Jini objects may also be interested in changes in other Jini objects, and would like to be listeners for such changes. The networked nature of Jini has led to a particular event model which differs slightly from the other models already in Java. The differences are caused by factors such as

  1. Network delivery is unreliable: messages may be lost. Synchronous methods requiring a reply may not work here

  2. Network delivery is time-dependent: messages may arrive at different times to different listeners. So the state of an object as perceived by a listener at any time may be inconsistent with the state perceived by others. Passing complex object state across the network may be more complex to manage than passing simpler information

  3. A remote listener may have disappeared by the time the event occurs. Listeners have to be allowed to ``time out'', like services do.

  4. Java Beans can require method names and event types that vary. This requires availablity of classes across the network, which is more complex than a single method on a single event type (the original Observer pattern used a single method, for simplicity).

16.2. Remote Events

Unlike the large number of event classes in the AWT and Swing (for example), Jini typically uses events of one type, the RemoteEvent or a small number of subclasses. The class has public methods


package net.jini.core.event;

public class RemoteEvent implements java.io.Serializable {
    public long getID();
    public long getSequenceNumber();
    public java.rmi.MarshalledObject getRegistrationObject();
}
Events in Beans and AWT convey complex object state information. Jini events avoid this, and convey just enough information to allow state information to be found if needed. A remote event is serializable and can be moved around the network to its listeners.

AWT Events such as MouseEvent contain an id field that is set to values such as MOUSE_PRESSED or MOUSE_RELEASED. These are not seen by the AWT programmer because the AWT event dispatch system uses this field to choose appropriate methods such as mousePressed() or mouseReleased(). Jini does not make these assumptions about event dispatch, and just gives you the identifier. Either the source or the listener (or both) will know what this value means. For example, a file classifier that can update its knowledge of MIME types could have message types ADD_TYPE and REMOVE_TYPE to reflect the sort of changes it is going through.

In a synchronous system with no losses both sides of an interaction can keep consistent ideas of state and order of events. In a network system this is not so easy. Jini makes no assumptions about guarantees of delivery, and does not even assume that events are delivered in order. The Jini event mechanism does not specify how events get from producer to listener - it could be by RMI calls, but may be through an unreliable third party. The event source supplies a sequence number that could be used to construct state and ordering information if needed. This generalises things such as time-stamps on mouse events. For example, a message with id of ADD_TYPE and sequence number of 10 could correspond to the state change ``added MIME type text/xml for files with suffix .xml''. Another event with id of REMOVE_TYPE and sequence number of 11 would be taken as a later event even if it arrived earlier. The event source should be able to supply state information upon request, given the sequence number.

An idea borrowed from systems such as the Xt Intrinsics and Motif is for registration of interest by a client in an event to include a piece of data from the client called a handback, which is returned with each event. This can be a reminder of client state at the time of registration. For example, a Jini taxi-driver might register interest in taxi bookings while passing through an area. As part of its registration it could include its current location. Then when it receives a booking event it is told about its old location, and it could check to see if it is still interested in events from that old location. A more novel possibility is that one object can register another object for events. So your stock-broker could register you for events about stock movements, and when you receive an event you also get a reminder about who registered your interest (plus a request for commission...).

16.3. Event Registration

Jini does not say how to register listeners with objects that can generate events. This is unlike other event models in Java that specify methods such as


public void addActionListener(ActionListener listener);
for ActionEvent generators. What Jini does do is to specify a convenience class as a return value from this registration. This convenience class is

package net.jini.core.event;
import net.jini.core.lease.Lease;

public class EventRegistration implements java.io.Serializable {
    public EventRegistration(long eventID, Object source,
                             Lease lease, long seqNum);
    public long getID();
    public Object getSource();
    public Lease getLease();
    public long getSequenceNumber();
}

This return object contains information that may be of value to the object that registered a listener. Each registration will typically only be for a limited amount of time, and this information may be returned in the Lease object. If the event registration was for a particular type, this may be returned in the id field. A sequence number may also be given. The meaning of these may depend on the particular system - in other words, Jini gives you a class that is optional in use, and whose fields are not tightly specified. This gives you the freedom to choose your own meanings to some extent. Note that in Jini 1, the source object was typically this and the programmer would rely on Java substituting a proxy. In Jini 2.0 the proxy will have to be explicitly given. For example:


    new EventRegistration(0L, proxy, null, 0L)

The event model means that as the programmer of a event producer, you have to define (and implement) methods such as


public EventRegistration addRemoteEventListener(RemoteEventListener listener);
There is no standard interface for this.

16.4. Listener List

Each listener for remote events must implement the RemoteEventListener interface


public interface RemoteEventListener
                 extends java.rmi.Remote, java.util.EventListener {
    public void notify(RemoteEvent theEvent)
                throws UnknownEventException,
                       java.rmi.RemoteException;
}
Because it extends Remote it means that the listener will most likely be something like an RMI stub for a remote object, so that calling notify() will result in a call on the remote object, with the event being passed across to it.

In event generators, there are multiple implementations for handling lists of event listeners all the way through the Java core and extensions. This is tedious, re-inventing the same thing. There are basically two cases

  1. Only one listener can be in the list

  2. Any number of listeners can be in the list

16.4.1 Single Listener

The case where there is only one listener allowed can be done by using a single-valued variable, shown in 16.1.

Figure 16.1: A single listener
The simplest case of event registration is

protected RemoteEventListener listener = null;

public EventRegistration addRemoteListener(RemoteEventListener listener)
       throws java.util.TooManyListenersException {
    if (this.listener == null {
        this.listener = listener;
    } else {
        throw new java.util.TooManyListenersException();
    }
    return new EventRegistration(0L, proxy, null, 0L);
}
This is closest to the ordinary Java event registration: no really useful information is returned that wasn't known before. In particular, there is no lease object, so one could probably assume that the lease is being granted ``forever'', as would be the case with non-networked objects.

When an event occurs, the listener can be informed by the event generator calling fireNotify(). In Jini 2.0 the source object will be a proxy:


protected void fireNotify(long eventID,
                          long seqNum) {
    if (listener == null) {
        return;
    }

    RemoteEvent remoteEvent = new RemoteEvent(proxy, eventID, 
                                              seqNum, null);
    listener.notify(remoteEvent);
}       

It is easy to add a "handback" to this: just add another field to the object, and set and return this in the registration and notify methods. Far more complex is the addition of a non-null lease. Firstly, the event source has to decide on a "lease policy", that is, for what periods of time is it going to grant leases. Then it has to implement a time-out mechanism to discard listeners when their leases expire. And finally, it has to handle lease renewal and cancellation requests, possibly using its lease policy again to make decisions. The landlord package would be of use here.

16.4.2 Multiple Listeners

For the case of any number of listeners, the convenience class javax.swing.event.EventListenerList can be used. The object delegates all list handling to the convenience class, as in figure 16.2.

Figure 16.2: Multiple listeners
A version suitable for ordinary events is

import javax.swing.event.EventListenerList;

EventListenerList listenerList = new EventListenerList();

public EventRegistration addRemoteListener(RemoteEventListener l) {
    listenerList.add(RemoteListener.class, l);
    return new EventRegistration(0L, proxy, null, 0L);
}

public void removeRemoteListener(RemoteEventListener l) {
    listenerList.remove(RemoteListener.class, l);
}


// Notify all listeners that have registered interest for
// notification on this event type.  The event instance 
// is lazily created using the parameters passed into 
// the fire method.

protected void fireNotify(long eventID,
                          long seqNum) {
    RemoteEvent remoteEvent = null;

    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();

    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int n = listeners.length - 2; n >= 0; n -= 2) {
        if (listeners[n] == RemoteEventListener.class) {
	    RemoteEventListener listener = 
	                     (RemoteEventListener) listeners[n+1];
            if (remoteEvent == null) {
                remoteEvent = new RemoteEvent(proxy, eventID, 
                                              seqNum, null);
            }
            try {
	        listener.notify(remoteEvent);
            } catch(UnknownEventException e) {
	        e.printStackTrace();
	    } catch(java.rmi.RemoteException e) {
	        e.printStackTrace();
	    }
        }
    }
}       
Then a source object need only call fireNotify() to send the event to all listeners. (You may decide that it is easier to simply use a Vector of listeners!)

It is again straightforward to add handbacks to this. The only tricky point is that each listener can have its own handback, so they will need to be stored in some kind of map (say a HashMap) keyed on the listener. Then before notify() is called for each listener, the handback will need to be retrieved for the listener and a new remote event created with that handback.

16.5. Listener Source

Jini is a networked federation of objects. The ordinary Java event model has all objects in a single address space, so that registration of event listeners and notifying these listeners all takes place using objects in the one space. We have already seen that this is not the case with Jini, and in many cases one is dealing with proxy objects, not the ``real'' ones. This happens just the same with remote events, except that now we often have the direction of proxies reversed. To see what we mean by this, consider what happens if a client wants to monitor any changes in the service. The client will already have a proxy object for the service. It will use this proxy to register itself as a listener. But the service proxy will most likely just hand this listener back off to the service itself (that is what proxies such as RMI proxies do). So we need to get a proxy for the client over to the service.

Consider the file classification problems of earlier. There a file classifier had a ``hard-coded' set of filename extensions built in. However, it may be possible to extend these, if applications come along that know how to define (and maybe handle) such extensions. In this case an application would locate the file classification server, and using an exported method from the file classification interface would add the new MIME type and file extension. This is no departure from any standard Java or earlier Jini stuff. It only affects the implementation level of the file classifier, changing it from a static list of filename extensions to a more dynamic one.

What it does affect is the poor application that has been blocked (probably sleeping) on an unknown filename extension. When the classifier installs a new type, it can send an event saying so. The blocked application can then try again to see if the extension is now known. If so, it uses it, if not it blocks again. Note that we don't bother with the actual state change, since it is just as easy to make another query knowing that the state has changed. More complex situations may require more information to be maintained. In order to get to this situation, the application must have registered its interest in events, and the event producer must be able to find the listener.

How this gets resolved is for the client to find the service in the same way as we have already discussed. It ends up with a proxy object in the client's address space. One of the methods on the proxy will be to add an event listener, which will be called by the client. For simplicity, assume that it is the client which is being added. The proxy will then call the "real" object's add listener method, back on its server side. But in doing this, we have made a remote call across the network, and the client, which was local to the call on the proxy, is now remote to the "real" object! So what the "real" object is getting is a proxy to the client! Then, when it makes notification calls, the client's proxy can make a remote call back to the client itself. These proxies are shown in figure 16.3

Figure 16.3: Proxies for services and listeners

16.6. File Classifier with Events

Let's make this more concrete by looking at a new file classifier that can have its set of mappings dynamically updated. In the last chapter we also considered such a situation, but from the point of view of leasing such additions. In this chapter we ignore leasing issues and just concentrate on generating events as the mappings change.

The first interface required is MutableFileClassifier, known to all objects. This adds methods to add and remove types, and also to register listeners for events. The event types are labelled by two constants. The listener model is simple, and does not include handbacks or leases. The sequence identifier must be increasing, so we just add one on each event generation, although we don't really need it here: it is easy for a listener to just make MIME type queries again.



package common;

import java.io.Serializable;

/**
 * MutableFileClassifier.java
 */

import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.EventRegistration;

public interface MutableFileClassifier extends FileClassifier {

    static final public long ADD_TYPE = 1;
    static final public long REMOVE_TYPE = 2;

    /*
     * Add the MIME type for the given suffix.
     * The suffix does not contain '.' e.g. "gif".
     * Overrides any previous MIME type for that suffix
     */
    public void addType(String suffix, MIMEType type)
	throws java.rmi.RemoteException;

    /*
     * Delete the MIME type for the given suffix.
     * The suffix does not contain '.' e.g. "gif".
     * Does nothing if the suffix is not known
     */
    public void removeType(String suffix)
	throws java.rmi.RemoteException;

    public EventRegistration addRemoteListener(RemoteEventListener listener)
	throws java.rmi.RemoteException;


} // MutableFileClasssifier

The RemoteFileClassifier just changes its package and inheritance for this


package mutable;

import common.MutableFileClassifier;
import java.rmi.Remote;

/**
 * RemoteFileClassifier.java
 */

public interface RemoteFileClassifier extends MutableFileClassifier, Remote {
        
} // RemoteFileClasssifier

The implementation changes from a static list of if...then statements to a dynamic map keyed on file suffixes. It manages the event listener list for multiple listeners in the simple way discussed earlier. It generates events whenever a new suffix/type is added or successfully removed

There are however, several subtleties related to proxies. When a listener registers by addListener(), an EventRegistration is returned. This contains the service object (or rather, its proxy). Similarly, when notify() is called on the listener it is passed a RemoteEvent and this also contains the service (or rather, its proxy). With the "old" version of RMI all of the work to do with proxies was looked after by the Java runtime, But with the Jeri model, handling of proxies must be made explicit. This means that the implementation object must know its proxy in order to prepare EventRegistration and RemoteEvent objects.

In all of the servers we have seen so far, the server creates the service and then goes on to create its proxy. This means that the service normally does not know its proxy. Two alternative mechanisms to overcome this are for the service to implement a method such as setProxy() or for the service to create its own proxy and make it available to the server by a method such as getProxy(). Jini from version 2.0 has an interface ProxyAccessor which supports the second method


interface ProxyAccessor {
    public Object getProxy();
}

The implementation needs to be passed enough information (e.g. a configuration) in its constructor to create a proxy. The methods addType() and removeType() manipulate the map of MIME types and also call firNotify() to generate events:



package mutable;

import java.rmi.server.UnicastRemoteObject;
import java.rmi.MarshalledObject;
import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.RemoteEvent;
import net.jini.core.event.EventRegistration;
import java.rmi.RemoteException;
import java.rmi.Remote;
import net.jini.core.event.UnknownEventException ;

import javax.swing.event.EventListenerList;

import net.jini.export.*;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.BasicILFactory;

import net.jini.export.ProxyAccessor;

import net.jini.config.*;

import common.MIMEType;
import common.MutableFileClassifier;
import java.util.Map;
import java.util.HashMap;

/**
 * FileClassifierImpl.java
 */

public class FileClassifierImpl implements RemoteFileClassifier, ProxyAccessor {

    /**
     * Map of String extensions to MIME types
     */
    protected Map map = new HashMap();

    /**
     * Listeners for change events
     */
    protected EventListenerList listenerList = new EventListenerList();

    protected long seqNum = 0L;

    protected Remote proxy;

    public MIMEType getMIMEType(String fileName) 
	throws java.rmi.RemoteException {
	System.out.println("Called with " + fileName);

	MIMEType type;
	String fileExtension;
	int dotIndex = fileName.lastIndexOf('.');

	if (dotIndex == -1 || dotIndex + 1 == fileName.length()) {
	    // can't find suitable suffix
	    return null;
	}

	fileExtension= fileName.substring(dotIndex + 1);
	type = (MIMEType) map.get(fileExtension);
	return type; 

    }

    public void addType(String suffix, MIMEType type)
	throws java.rmi.RemoteException {
	System.out.println("type added");
	map.put(suffix, type);
	fireNotify(ADD_TYPE);
    }

    public void removeType(String suffix)
	throws java.rmi.RemoteException {
	System.out.println("Type removed");
	if (map.remove(suffix) != null) {
	    fireNotify(REMOVE_TYPE);
	}
    }

    public EventRegistration addRemoteListener(RemoteEventListener listener)
	throws java.rmi.RemoteException {
	listenerList.add(RemoteEventListener.class, listener);

	return new EventRegistration(0, 
				     proxy, 
				     null, /* Lease is null for simplicity only.
					      It should be e.g. a LandlordLease
					   */
				     0);
    }

    // Notify all listeners that have registered interest for
    // notification on this event type.  The event instance 
    // is lazily created using the parameters passed into 
    // the fire method.

    protected void fireNotify(long eventID) {
	RemoteEvent remoteEvent = null;
	
	// Guaranteed to return a non-null array
	Object[] listeners = listenerList.getListenerList();
	
	// Process the listeners last to first, notifying
	// those that are interested in this event
	for (int i = listeners.length - 2; i >= 0; i -= 2) {
	    if (listeners[i] == RemoteEventListener.class) {
		RemoteEventListener listener = (RemoteEventListener) listeners[i+1];
		if (remoteEvent == null) {
		    remoteEvent = new RemoteEvent(proxy, eventID, 
						  seqNum++, null);
		}
		try {
		    listener.notify(remoteEvent);
		} catch(UnknownEventException e) {
		    e.printStackTrace();
		} catch(RemoteException e) {
		    // Remove this listener from the list due to failure
		    listenerList.remove(RemoteEventListener.class, listener);
		    System.out.println("notification failed, listener removed");
		}
	    }
	}
    }    

    // Implementation for ProxyAccessor
    public Object getProxy() {
        return proxy;
    }

    public FileClassifierImpl()  throws java.rmi.RemoteException {
	// empty constructor for proxy generation
    }

    public FileClassifierImpl(String[] configArgs)  throws java.rmi.RemoteException {
	// load a predefined set of MIME type mappings
	map.put("gif", new MIMEType("image", "gif"));
	map.put("jpeg", new MIMEType("image", "jpeg"));
	map.put("mpg", new MIMEType("video", "mpeg"));
	map.put("txt", new MIMEType("text", "plain"));
	map.put("html", new MIMEType("text", "html"));

	try {
	    // get the configuration (by default a FileConfiguration) 
	    Configuration config = ConfigurationProvider.getInstance(configArgs); 
	    
	    // and use this to construct an exporter
	    Exporter exporter = (Exporter) config.getEntry( "FileClassifierServer", 
							    "exporter", 
							    Exporter.class); 
	    // export an object of this class
	    proxy = exporter.export(this);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}
    }
} // FileClassifierImpl

The server changes by passing in configuration information to the implementation's constructor and then getting the proxy from in it in order to register the service.


package mutable;

import net.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import net.jini.discovery.LookupDiscovery;
import net.jini.core.lookup.ServiceRegistrar;
import java.rmi.RemoteException;
import net.jini.lookup.ServiceIDListener;
import net.jini.lease.LeaseRenewalManager;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.discovery.DiscoveryEvent;
import net.jini.discovery.DiscoveryListener;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;

import net.jini.config.*; 
import net.jini.export.*; 

/**
 * FileClassifierServer.java
 */

public class FileClassifierServer 
    implements ServiceIDListener {

    // explicit proxy for Jini 2.0
    protected Remote proxy;
    protected FileClassifierImpl impl;
    private static String CONFIG_FILE = "jeri/file_classifier_server.config";
    
    public static void main(String argv[]) {
	FileClassifierServer server = new FileClassifierServer();

        // stay around forever
	Object keepAlive = new Object();
	synchronized(keepAlive) {
	    try {
		keepAlive.wait();
	    } catch(InterruptedException e) {
		// do nothing
	    }
	}
    }

    public FileClassifierServer() {
	String[] configArgs = new String[] {CONFIG_FILE};

	try {
	    impl = new FileClassifierImpl(configArgs);


	} catch(Exception e) {
            System.err.println("New impl: " + e.toString());
            System.exit(1);
	}

	proxy = (Remote) impl.getProxy();

	// install suitable security manager
	System.setSecurityManager(new RMISecurityManager());

	JoinManager joinMgr = null;
	try {
	    LookupDiscoveryManager mgr = 
		new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
					   null,  // unicast locators
					   null); // DiscoveryListener
	    joinMgr = new JoinManager(proxy, // service proxy
				      null,  // attr sets
				      this,  // ServiceIDListener
				      mgr,   // DiscoveryManager
				      new LeaseRenewalManager());
	} catch(Exception e) {
	    e.printStackTrace();
	    System.exit(1);
	}
    }

    public void serviceIDNotify(ServiceID serviceID) {
	// called as a ServiceIDListener
	// Should save the id to permanent storage
	System.out.println("got service ID " + serviceID.toString());
    }
    
} // FileClassifierServer

The client must have an object which implements RemoteEventListener.



package client;

import common.MutableFileClassifier;
import common.MIMEType;

import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;

import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.RemoteEvent;
import java.rmi.*;
import java.rmi.server.ExportException;

import net.jini.export.Exporter; 
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.tcp.TcpServerEndpoint;

/**
 * TestFileClassifierEvent.java
 */

public class TestFileClassifierEvent implements DiscoveryListener, RemoteEventListener {

    public static void main(String argv[]) {
	TestFileClassifierEvent client = new TestFileClassifierEvent();

        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(100000L);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public TestFileClassifierEvent() {
	System.setSecurityManager(new RMISecurityManager());

	LookupDiscovery discover = null;
        try {
            discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        discover.addDiscoveryListener(this);

    }
    
    public void discovered(DiscoveryEvent evt) {

        ServiceRegistrar[] registrars = evt.getRegistrars();
	Class [] classes = new Class[] {MutableFileClassifier.class};
	MutableFileClassifier classifier = null;
	ServiceTemplate template = new ServiceTemplate(null, classes, 
						       null);
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Lookup service found");
            ServiceRegistrar registrar = registrars[n];
	    try {
		classifier = (MutableFileClassifier) registrar.lookup(template);
	    } catch(java.rmi.RemoteException e) {
		e.printStackTrace();
		continue;
	    }
	    if (classifier == null) {
		System.out.println("Classifier null");
		continue;
	    }

	    // Add ourselves as an event listener
	    Exporter exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
						  new BasicILFactory());

	    // export an object of this class
	    RemoteEventListener proxy = null;
	    try {
		proxy =  (RemoteEventListener) exporter.export(this); 
	    } catch (ExportException e) {
		e.printStackTrace();
		continue;
	    }

	    try {
		classifier.addRemoteListener(proxy);
	    } catch (RemoteException e) {
		e.printStackTrace();
		continue;
	    }
	    
	    // Add some types to the service to generate events
	    try {
		classifier.addType("ps", new MIMEType("text", "postscript"));
		classifier.removeType("ps");
	    } catch(java.rmi.RemoteException e) {
		System.err.println(e.toString());
		continue;
	    }
	}
    }

    public void discarded(DiscoveryEvent evt) {
	// empty
    }

    public void notify(RemoteEvent evt) {
	System.out.println("Event of type " + evt.getID());
    }
} // TestFileClassifier

16.7. Leasing Event Listeners

The implementation given above creates a null object for a lease. This is not correct, and should be a non-null object. However, conceptually there is nothing new in this that we have not already covered in earlier chapters. See for example, Chapter 15 "leased changes to a service" for how to add a landlord lease.

16.8. Monitoring Changes in Services

Services will start and stop. When they start they will inform the lookup services, and sometime after they stop they will be removed from the lookup services. But there are a lot of times when other services or clients will want to know when services start or are removed. For example: the editor that wants to know if a disk service has started so that it can save its file; the graphics display program that wants to know when printer services start up; the user interface for a camera that wants to track changes in disk and printer services so that it can update the ``Save'' and ``Print'' buttons.

A service registrar acts as a generator of events of type ServiceEvent which subclass from RemoteEvent. These events are generated in response to changes of state of services which match (or fail to match) a template pattern for services. This event type has three categories from the ServiceEvent.getTransition() method:

  1. TRANSITION_NOMATCH_MATCH: a service has changed state so that whereas it previously did not match the template, now it does. In particular, if it didn't exist before now it does. This transition type can be used to spot new services starting. This transition can also be used to spot changes in the attributes of an existing registered service which are wanted: for example, an off-line printer can change attributes to being on-line, which now makes it a useful service

  2. TRANSITION_MATCH_NOMATCH: a service has changed state so that whereas it previously did match the template, now it doesn't. This can be used to detect when services are removed from a lookup service. This transition can also be used to spot changes in the attributes of an existing registered service which are notwanted: for example, an on-line printer can change attributes to being off-line

  3. TRANSITION_MATCH_MATCH: a service has changed state, but it matched both before and after. This typically happens when an Entry value changes, and is used to monitor changes of state such as a printer running out of paper, or a piece of hardware signalling that it is due for maintenance work

A client that wants to monitor changes of services on a lookup service must first create a template for the types of service it is interested in. A client that want to monitor all changes could prepare a template such as


ServiceTemplate templ = new ServiceTemplate(null, null, null); // or
ServiceTemplate templ = new ServiceTemplate(null, new Class[] {}, new Entry[] {}); // or
ServiceTemplate templ = new ServiceTemplate(null, new Class[] {Object.class}, null);
It then sets up a transition mask as a bit-wise OR of the three service transitions, and then calls notify() on the ServiceRegistrar object. Note that this method expects to receive a proxy object (this was implicit in Jini 1 but needs to be made explicit in Jini 2.0) A program to monitor all changes is


/**
 * RegistrarObserver.java
 */

package observer;

import net.jini.core.event.RemoteEventListener;
import net.jini.core.event.RemoteEvent;
import net.jini.core.lookup.ServiceEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.lookup.ServiceID;
import net.jini.core.event.EventRegistration;
import net.jini.lease.LeaseRenewalManager;
import net.jini.core.lookup.ServiceMatches;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import net.jini.core.entry.Entry;
import net.jini.core.event.UnknownEventException;
import net.jini.config.*; 
import net.jini.export.*; 
import java.rmi.Remote;

public class RegistrarObserver implements RemoteEventListener {
    
    protected static LeaseRenewalManager leaseManager = new LeaseRenewalManager();
    protected ServiceRegistrar registrar;

    protected final int transitions = ServiceRegistrar.TRANSITION_MATCH_NOMATCH |
                                  ServiceRegistrar.TRANSITION_NOMATCH_MATCH |
                                  ServiceRegistrar.TRANSITION_MATCH_MATCH;

    public RegistrarObserver() throws RemoteException {
    }

    public RegistrarObserver(Configuration config,
			     ServiceRegistrar registrar) throws RemoteException {
	RemoteEventListener proxy;

	this.registrar = registrar;

	Exporter exporter = null;
	try {
	    exporter = (Exporter) config.getEntry( "JeriExportDemo", 
					"exporter", 
					Exporter.class); 
	} catch(ConfigurationException e) {
	    e.printStackTrace();
	    return;
	}
	// export an object of this class
	proxy = (RemoteEventListener) exporter.export(this);

	ServiceTemplate templ = new ServiceTemplate(null, null, null);
	EventRegistration reg = null;
	try {
	    reg = registrar.notify(templ,
			     transitions,
			     proxy,
			     null,
			     Lease.ANY);
	    System.out.println("notifed id " + reg.getID());
	} catch(RemoteException e) {
	    e.printStackTrace();
	}
	leaseManager.renewUntil(reg.getLease(), Lease.FOREVER, null);
    }

    public void notify(RemoteEvent evt)
	throws RemoteException, UnknownEventException {
	try {
	    ServiceEvent sevt = (ServiceEvent) evt;
	    int transition = sevt.getTransition();
	    System.out.println("transition " + transition);
	    switch (transition) {
	    case ServiceRegistrar.TRANSITION_NOMATCH_MATCH:
		System.out.println("nomatch -> match");
		break;
	    case ServiceRegistrar.TRANSITION_MATCH_MATCH:
		System.out.println("match -> match");
		break;
	    case ServiceRegistrar.TRANSITION_MATCH_NOMATCH:
		System.out.println("match -> nomatch");
		break;
	    }
	    System.out.println(sevt.toString());
	    if (sevt.getServiceItem() == null) {
		System.out.println("now null");
	    } else {
		Object service = sevt.getServiceItem().service;
		System.out.println("Service is " + service.toString());
	    }
	} catch(Exception e) {
	    e.printStackTrace();
	}

    }
    
} // RegistrarObserver

A suitable driver for this is


package client;

import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.lookup.ServiceMatches;
import net.jini.config.*; 
import java.util.Vector;
import observer.RegistrarObserver;

/**
 * ReggieMonitor.java
 */

public class ReggieMonitor implements DiscoveryListener {

    private Vector observers = new Vector();
    private Configuration config;

    public static void main(String argv[]) {
	new ReggieMonitor(argv);

        // stay around long enough to receive replies
        try {
            Thread.currentThread().sleep(100000L);
        } catch(java.lang.InterruptedException e) {
            // do nothing
        }
    }

    public ReggieMonitor(String[] argv) {
	String[] configArgs = new String[] {argv[0]};

	try {
	    // get the configuration (by default a FileConfiguration) 
	    config = ConfigurationProvider.getInstance(configArgs); 
	} catch(Exception e) {
	    System.err.println(e.toString());
	    e.printStackTrace();
	    System.exit(1);
	}	

	System.setSecurityManager(new RMISecurityManager());

	LookupDiscovery discover = null;
        try {
            discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
        } catch(Exception e) {
            System.err.println(e.toString());
            System.exit(1);
        }

        discover.addDiscoveryListener(this);

    }
    
    public void discovered(DiscoveryEvent evt) {

        ServiceRegistrar[] registrars = evt.getRegistrars();
 
        for (int n = 0; n < registrars.length; n++) {
	    System.out.println("Service lookup found");
            ServiceRegistrar registrar = registrars[n];
	    if (registrar == null) {
		System.out.println("registrar null");
		continue;
	    }
	    try {
		System.out.println("Lookup service at " +
			       registrar.getLocator().getHost());
	    } catch(RemoteException e) {
		System.out.println("Lookup service infor unavailable");
	    }

	    try {
		observers.add(new RegistrarObserver(config, registrar));
	    } catch(RemoteException e) {
		System.out.println("adding observer failed");
	    }

	    ServiceTemplate templ = new ServiceTemplate(null, new Class[] {Object.class}, null);
	    ServiceMatches matches = null;
	    try {
		matches = registrar.lookup(templ, 10);
	    } catch(RemoteException e) {
		System.out.println("lookup failed");
	    }

	    for (int m = 0; m < matches.items.length; m++) {
		if (matches.items[m] != null && matches.items[m].service != null) {
		    System.out.println("Reg knows about " + matches.items[m].service.toString() +
				   " with id " + matches.items[m].serviceID);
		}
	    }

	}
    }

    public void discarded(DiscoveryEvent evt) {
	// remove observer
    }
} // ReggieMonitor

16.9. Summary

This chapter has looked at how the remote event differs from the other event models in Java, and looked at how to create and use them.

16.10. Copyright

If you found this chapter of value, the full book "Foundations of Jini 2 Programming" is available from APress or Amazon .

This file is Copyright (©) 1999, 2000, 2001, 2003, 2004, 2005 by Jan Newmarch (http://jan.netcomp.monash.edu.au) [email protected].

Creative Commons License This work is licensed under a Creative Commons License, the replacement for the earlier Open Content License.