Java Enterprise in a Nutshell (In a Nutshell (OReilly))

9.13. Event Notification

In addition to finding, browsing, searching, and altering objects stored in a naming or directory service, the JNDI API also supports the concept of event notification . This involves registering listeners for various types of events that may occur in a naming/directory server. This functionality is provided through the javax.naming.event package.

Essentially, JNDI event notification is similar to other event models in the Java platform: you obtain a reference to an event source and then register an event listener with the source. When the specified events fire in the event source, notifications are made by invoking callback methods on the event listeners.

9.13.1. Event Sources

Event sources in JNDI are represented by special subclasses of Context and DirContext: EventContext and EventdirContext, both from the javax.naming.event package. These interfaces provide the additional functionality required to register event listeners with a naming/directory service. Acquiring a reference to an event source is simply a matter of casting a returned Context or DirContext. For example, if we wanted to get an event source for the "person" branch of a directory service:

Context ctx = new InitialContext(props).lookup("ou=person"); EventDirContext evCtx = (EventDirContext)ctx;

In order to take advantage of event notification, the JNDI provider (and the underlying naming or directory service) needs to support event notification, and its Context implementations need to implement the EventContext or EventdirContext interfaces. If the provider doesn't support event notification, the cast in the second line of the previous example fails with a ClassCastException. There is a more fault-tolerant way to check for event notification support:

Context ctx = new InitialContext(props).lookup("ou=People"); if (ctx instanceof EventDirContext) { // Register listeners with directory event source ... } else if (ctx instanceof EventContext) { // Register listeners with naming event source ... } else { // No event notification support System.out.println("Sorry, provider doesn't support event notification"); }

The LDAP provider supplied with JDK 1.3 and later does support event notification, so the remainder of our examples will assume that we're accessing LDAP services.

9.13.2. Writing Event Listeners

Once you have an event source from your naming/directory service, you can register listeners for various events. Event listeners in JNDI are simply classes that implement the NamingListener interface. This interface acts as the base interface for the other types of listeners supported by JNDI, which we'll discuss next. It also supports handling errors that occur in the naming service. If you register a listener with the naming service and the service then encounters some error that will prevent it from notifying you of events, it makes you aware of the error by calling the namingExceptionThrown( ) callback on the NamingListener interface.

Two types of events can occur in a naming or directory service. The first involves changes relative to the namespace of the service (i.e., when objects are added or removed from a naming service or an object's name in the service is changed). The second type of event involves changes relative to the actual objects stored in the naming service (i.e., when an object's binding in the naming service has been changed or its attributes (in a directory service) have been altered).

There are two subclasses of NamingListener provided in JNDI to match these two types of events: NamespaceChangeListener and ObjectChangeListener. To be notified of changes in the namespace, create an implementation of the NamespaceChangeListener interface and register it with an event source. There are three event notification callbacks on this interface that you can implement. The objectAdded( ) method is called when a new object is added to the namespace, objectRemoved( ) is called when any object is removed, and objectRenamed( ) is called when an object's binding in the namespace is changed. A very simple NamespaceChangeListener implementation is shown in Example 9-12. This listener simply logs events to standard output as they occur.

Example 9-12. Listener for namespace changes

import javax.naming.*; import javax.naming.event.*; public class NamespaceChangeLogger implements NamespaceChangeListener { // Default constructor public NamespaceChangeLogger( ) {} // Callback for object addition events public void objectAdded(NamingEvent ev) { Binding b = ev.getNewBinding( ); System.out.println("--> ADD: Object of type " + b.getClassName( ) + " added at binding \"" + b.toString( ) + "\""); } // Callback for object removal events public void objectRemoved(NamingEvent ev) { Binding b = ev.getOldBinding( ); System.out.println("--> REMOVE: Object of type " + b.getClassName( ) + " removed from binding \"" + b.toString( ) + "\""); } // Callback for object addition events public void objectRenamed(NamingEvent ev) { Binding bNew = ev.getNewBinding( ); Binding bOld = ev.getOldBinding( ); System.out.println("--> RENAME: Object of type " + bNew.getClassName( ) + " renamed from binding \"" + bOld.toString( ) + "\" to binding \"" + bNew.toString( ) + "\""); } // Callback for errors in the naming service public void namingExceptionThrown(NamingExceptionEvent ev) { System.out.println( "--> ERROR: An error occurred in the naming service:"); ev.getException( ).printStackTrace( ); } }

Each event notification callback takes a single NamingEvent argument. NamingEvent is a subclass of the generic java.util.EventObject class that is the base class for events in the AWT event model and the JavaBeans event model. A NamingEvent, as the name implies, contains information describing an event that occurred in the naming service. The getOldBinding( ) and getNewBinding( ) methods return Binding objects that indicate the state of the affected object's binding before and after the event. In the case of add events, the old binding is null; in the case of removal events, the new binding is null. The NamingEvent interface also allows you to access the EventContext that fired the event. In our example listener, we simply log the old and new binding when we are notified of add, remove, or change events.

In the case of errors encountered by the naming service, the namingContext-Thrown( ) callback will be called on our listener and given a NamingExceptionEvent. This is another subclass of EventObject; NamingExceptionEvent wraps a NamingException that has been thrown within the naming service. In our example, when a naming error occurs, we simply log it to standard output and print out the stack trace for the NamingException enclosed in the NamingExceptionEvent.

Changes to objects in the naming service are tracked using an implementation of the ObjectChangeListener interface. Creating an ObjectChangeListener is similar to the NamespaceChangeListener: create a subclass that includes concrete implementations of the notification callbacks defined in the interface. For ObjectChangeListener, there is only one event callback to implement: objectChanged( ). You also need to implement namingExceptionThrown( ) to handle any errors. Example 9-13 shows an example of an ObjectChangeListener implementation.

Example 9-13. Listener for object change events

import javax.naming.*; import javax.naming.event.*; public class ObjectChangeLogger implements ObjectChangeListener { // Default constructor public ObjectChangeLogger( ) {} // Callback for object change events public void objectChanged(NamingEvent ev) { Binding bNew = ev.getNewBinding( ); Binding bOld = ev.getOldBinding( ); System.out.println("--> CHANGE: Object of type " + bNew.getClassName( ) + " changed, previous binding = \"" + bOld.toString( ) + "\" post-change binding = \"" + bNew.toString( ) + "\""); } // Callback for errors in the naming service public void namingExceptionThrown(NamingExceptionEvent ev) { System.out.println( "--> ERROR: An error occurred in the naming service:"); ev.getException( ).printStackTrace( ); } }

Of course, you could also create a single listener that receives both namespace change events and object change events by simply implementing both listener interfaces with a single class.

9.13.3. Registering Event Listeners

Event listeners are registered with an event source by specifying a target (the subset of objects in the naming service for which you want notifications), along with the listener that should receive the notifications. This is done using the addNamingListener( ) methods available on the EventContext and Eventdir-Context interfaces.

The target objects for a listener can be specified in a few different ways. To define a subset of objects based strictly on their bindings and parent-child relationships in the naming service, you can use the addNamingListener( ) methods on the EventContext interface. With these registration methods, the target of a listener is specified by providing the name of an object (in the form of either a string or a Name object) and a scope. The name is simply a valid binding within the naming service being usedin an LDAP directory it is an X.500-style name such as "ou=people,cn=Smith, John". The scope is indicated by one of three static constants defined on the EventContext interface, with the following meanings:

OBJECT_SCOPE

Limit the target to just the named object or context.

ONELEVEL_SCOPE

Limit the target to the children of the named context, excluding the context itself. The name must refer to a context when this scope is used.

SUBTREE_SCOPE

Extend the target to include the entire subtree of the naming hierarchy, starting at (and including) the named object or context.

So, for example, if we wanted to listen for object-change events on the entry for "John Smith" in our LDAP directory, we would register one of our ObjectChangeLogger listeners like so:

EventContext ctx = (EventContext)(new InitialContext( )); ObjectChangeLogger ocLogger = new ObjectChangeLogger( ); ctx.addNamingListener("ou=people,cn=Smith, John", EventContext.OBJECT_SCOPE, ocLogger;

If we wanted to be notified for any changes in the namespace in the entire context starting with "John Smith", we might do this:

NamespaceChangeLogger ncLogger = new NamespaceChangeLogger( ); ctx.addNamingListener("ou=people,cn=Smith, John", EventContext.SUBTREE_SCOPE, ncLogger);

It's also possible to use more elaborate filters to specify the target of your listener, using the EvenTDirContext interface and its overloaded versions of addNamingListener( ). In these versions, specify the name of an object or context as before, but instead of a simple scope value, specify a search filter that defines the subset of the naming service that you want to target. Specifying your notification target this way is very similar to performing a search on a directory service using the DirContext.search( ) method. The search filter takes the form of a filter string, an optional set of filter arguments (in the form of an Object array), and a set of search controls (in the form of a SearchControls object). So if we wanted to listen for object change events on any objects in the "people" branch with a surname of "Jim", we could do the following:

EventDirContext dirCtx = (EventDirContext)(new InitialContext( )); ObjectChangeLogger ocLogger = new ObjectChangeLogger( ); SearchControls sctl = new SearchControls( ); sctl.setSearchScope(SearchControl.SUBTREE_SCOPE); dirCtx.addNamingListener("ou=people", "(sn=Jim)", sctl, ocLogger);

Notice that in this case, we specify the scope of the target using the setSearchScope( ) method on the SearchControls that we provide with the filter.

9.13.3.1. A listen command

Returning to our NamingShell example, we can use the event notification features of JNDI to add a listen command to our shell. We can use this command to register listeners for events in the naming service. First, we'll create a single listener that can be registered for both namespace events and object events, by implementing both the NamespaceChangeListener and ObjectChangeListener interfaces. This listener, AnyChangeLogger, is just a merging of the two listeners we created in the previous section and is shown in Example 9-14.

Example 9-14. Listener for any naming events

import javax.naming.*; import javax.naming.event.*; public class AnyChangeLogger implements NamespaceChangeListener, ObjectChangeListener { // Default constructor public AnyChangeLogger( ) {} // Callback for object addition events public void objectAdded(NamingEvent ev) { Binding b = ev.getNewBinding( ); System.out.println("--> ADD: Object of type " + b.getClassName( ) + " added at binding \"" + b.toString( ) + "\""); } // Callback for object removal events public void objectRemoved(NamingEvent ev) { Binding b = ev.getOldBinding( ); System.out.println("--> REMOVE: Object of type " + b.getClassName( ) + " removed from binding \"" + b.toString( ) + "\""); } // Callback for object rename events public void objectRenamed(NamingEvent ev) { Binding bNew = ev.getNewBinding( ); Binding bOld = ev.getOldBinding( ); System.out.println("--> RENAME: Object of type " + bNew.getClassName( ) + " renamed from binding \"" + bOld.toString( ) + "\" to binding \"" + bNew.toString( ) + "\""); } // Callback for object change events public void objectChanged(NamingEvent ev) { Binding bNew = ev.getNewBinding( ); Binding bOld = ev.getOldBinding( ); System.out.println("--> CHANGE: Object of type " + bNew.getClassName( ) + " changed, previous binding = \"" + bOld.toString( ) + "\" post-change binding = \"" + bNew.toString( ) + "\""); } // Callback for errors in the naming service public void namingExceptionThrown(NamingExceptionEvent ev) { System.out.println( "--> ERROR: An error occurred in the naming service:"); ev.getException( ).printStackTrace( ); } }

Our listen command takes one required argument, the name of the target object or context, and an optional search filter argument that can further define the target of the listener. The command class is shown in Example 9-15.

Example 9-15. The listen command

import java.util.Vector; import javax.naming.*; import javax.naming.event.*; import javax.naming.directory.*; class listen implements Command { public void execute(Context c, Vector v) throws CommandException { if (NamingShell.getCurrentContext( ) == null) throw new CommandException(new Exception( ), "No current context"); else if (v.isEmpty( )) throw new CommandException(new Exception( ), "No target specified"); String name = (String)v.firstElement( ); String filter = null; if (v.size( ) > 1) { filter = (String)v.elementAt(1); } try { // Cast context to an event context EventContext evCtx = (EventContext)c; // Create our listener NamingListener listener = new AnyChangeLogger( ); // If no filter specified, just register a listener using EventContext if (filter == null) { evCtx.addNamingListener(name, EventContext.OBJECT_SCOPE, listener); } // If we have a filter, use the EventDirContext to specify the target else { EventDirContext evDirCtx = (EventDirContext)c; evDirCtx.addNamingListener(name, filter, null, listener); } System.out.println("Registered listener for " + name + (filter != null ? (" and filter " + filter) : "")); } catch (ClassCastException cce) { cce.printStackTrace( ); throw new CommandException(cce, "The current context does not support event notification."); } catch (NamingException e) { throw new CommandException(e, "The search for " + filter + " failed"); } } public void help( ) { System.out.println("Usage: listen name [filter]"); } }

The execute( ) method checks the incoming command arguments to see what kind of target is being specified. If just a single argument is given, it's assumed to be the name of the object or context target, and we use the EventContext interface to register one of our listeners. If a second argument is provided, it's assumed to be a search filter string, and we use the EventdirContext interface to register the listener using both the name and filter to specify the target. If we are unable to cast the current Context to an EventContext or EventdirContext, then we assume that the naming service doesn't support event notification, and report that to the user by throwing a CommandException.

With this command, we can use the NamingShell to register listeners in our naming service. For example, if we're connected to an LDAP-based person directory and want to listen for any events associated with a person with a UID of jsmith:

> java com.oreilly.jent.jndi.NamingShell Created initial context using jndi.properties /% cd ou=People Current context now ou=People ou=People% listen uid=jsmith Registered listener for uid=jsmith ou=People%

Категории