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.
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
Observer
pattern used
a single method, for simplicity).
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...).
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.
This 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.
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
The case where there is only one listener allowed can be done by using a single-valued variable, shown in 14.1.
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, this, 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()
:
protected void fireNotify(long eventID,
long seqNum) {
if (listener == null) {
return;
}
RemoteEvent remoteEvent = new RemoteEvent(this, 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.
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 14.2.
import javax.swing.event.EventListenerList;
EventListenerList listenerList = new EventListenerList();
public EventRegistration addRemoteListener(RemoteEventListener l) {
listenerList.add(RemoteListener.class, l);
return new EventRegistration(0L, this, 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(this, 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.
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 14.3
Let's make this more concrete by looking at a new file classifier
that can have its set of mappings dynamically updated. The first
to be modified is the interface 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 removeMIMEType(String suffix, MIMEType type)
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.
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 net.jini.core.event.UnknownEventException ;
import javax.swing.event.EventListenerList;
import common.MIMEType;
import common.MutableFileClassifier;
import java.util.Map;
import java.util.HashMap;
/**
* FileClassifierImpl.java
*/
public class FileClassifierImpl extends UnicastRemoteObject
implements RemoteFileClassifier {
/**
* 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;
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 {
map.put(suffix, type);
fireNotify(ADD_TYPE);
}
public void removeMIMEType(String suffix, MIMEType type)
throws java.rmi.RemoteException {
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, this, null, 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(this, eventID,
seqNum++, null);
}
try {
listener.notify(remoteEvent);
} catch(UnknownEventException e) {
e.printStackTrace();
} catch(RemoteException e) {
e.printStackTrace();
}
}
}
}
public FileClassifierImpl() 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"));
}
} // FileClassifierImpl
The proxy changes its inheritance, and as a result has more methods to implement which it just delegates to its server object.
package mutable;
import common.MutableFileClassifier;
import common.MIMEType;
import java.io.Serializable;
import java.io.IOException;
import java.rmi.Naming;
import net.jini.core.event.EventRegistration;
import net.jini.core.event.RemoteEventListener;
/**
* FileClassifierProxy
*/
public class FileClassifierProxy implements MutableFileClassifier, Serializable {
RemoteFileClassifier server = null;
public FileClassifierProxy(FileClassifierImpl serv) {
this.server = serv;
if (serv==null) System.err.println("server is null");
}
public MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException {
return server.getMIMEType(fileName);
}
public void addType(String suffix, MIMEType type)
throws java.rmi.RemoteException {
server.addType(suffix, type);
}
public void removeMIMEType(String suffix, MIMEType type)
throws java.rmi.RemoteException {
server.removeMIMEType(suffix, type);
}
public EventRegistration addRemoteListener(RemoteEventListener listener)
throws java.rmi.RemoteException {
return server.addRemoteListener(listener);
}
} // FileClassifierProxy
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:
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
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
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.
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 com.sun.jini.lease.LeaseRenewalManager; // Jini 1.0
import net.jini.lease.LeaseRenewalManager; // Jini 1.1
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;
public class RegistrarObserver extends UnicastRemoteObject 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(ServiceRegistrar registrar) throws RemoteException {
this.registrar = registrar;
ServiceTemplate templ = new ServiceTemplate(null, null, null);
EventRegistration reg = null;
try {
// eventCatcher = new MyEventListener();
reg = registrar.notify(templ,
transitions,
this,
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 java.util.Vector;
import observer.RegistrarObserver;
/**
* ReggieMonitor.java
*/
public class ReggieMonitor implements DiscoveryListener {
protected Vector observers = new Vector();
public static void main(String argv[]) {
new ReggieMonitor();
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(100000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public ReggieMonitor() {
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(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
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.
If you found this chapter of value, the full book is available from APress or Amazon . There is a review of the book at Java Zone