Created

Aug 05, 2013

Observation in JCR

Posted by Ugo Cei

Observation is a JCR mechanism that allows code to listen for events generated during repository operations and react to them in various ways. Two types of observation may be supported by a JCR repository: Asynchronous Observation and Journaled Observation. The former allows an application to react to events as they occur in real time, the latter to query the repository for a log of past events. Not all implementations of JCR are required to support one or both types of observation, but an application can query whether a repository supports using code like the following:

if (repository.getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED).equals("true"))

or

if (repository.getDescriptor(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED).equals("true"))

The six type of events that can be detected are as follows:

  • Node added
  • Node moved
  • Node removed
  • Property added
  • Property removed
  • Property changed

A seventh event type, called "Persist", may also appear in certain circumstances related to Journaled Observation.

Notice that, even if observation is supported, your application code is not guaranteed to be able to be notified of all events and, even if it could, reacting to a large number of events might be impractical from a performance standpoint. As the JCR 2.0 specification says:

The scope of event reporting is implementation-dependent. An implementation should make a best-effort attempt to report all events, but may exclude events if reporting them would be impractical given implementation or resource limitations. For example, on an import, move or remove of a subgraph containing a large number of items, an implementation may choose to report only events associated with the root node of the affected graph and not those for every subitem in the structure.


Registering an event listener

An event listener is a class that implements the javax.jcr.observation.EvenListener interface. This interface declares one method that must be implemented by derived classes:

void onEvent(EventIterator it);

Notice that the argument to the onEvent method is an EventIterator (a subclass of RangeIterator that adds an extra Event nextEvent() method for convenience). This is because events are dispatched in bundles of related events, rather than as single events. A bundle corresponds to a single atomic change to a persistent workspace and contains only events caused by that change.

Listeners apply per workspace, not repository-wide; they only receive events for the workspace in which they are registered.

The implementation must then be registered with the ObservationManager by calling its addEventListener method, whose signature is as follows:

void addEventListener(EventListener listener,
                      int eventTypes,
                      java.lang.String absPath,
                      boolean isDeep,
                      java.lang.String[] uuid,
                      java.lang.String[] nodeTypeName,
                      boolean noLocal)


Let's have a look at the arguments for this method.

EventListener listener

This is an instance of a class implementing the EventListener interface, as explained above.

int eventTypes

This represents the set of events our listener is interested in, as a bitwise OR of the integer values of the constants defined in the Event interface for the various event types, which are:

Event.NODE_ADDED
Event.NODE_MOVED
Event.NODE_REMOVED
Event.PERSIST
Event.PROPERTY_ADDED
Event.PROPERTY_CHANGED
Event.PROPERTY_REMOVED


java.lang.String absPath

Only events associated with the node whose path is absPath (or its subgraph if isDeep is true) will trigger a notification.

boolean isDeep

If true, include descendants of the node pointed to by absPath in those being listened for events.

java.lang.String[] uuid

If not null, only listen for events relative to the nodes identified by the UUIDs contained in the array. You can pass null if you don't want to filter by node id, but be careful that passing an empty array will filter out all events.

java.lang.String[] nodeTypeName

If not null, only listen for events relative to the nodes whose type names are contained in the array. You can pass null if you don't want to filter by node type, but be careful that passing an empty array will filter out all events.

boolean noLocal

If true, ignore events generated by the session through which the listener was registered. Useful for avoiding infinite loops of event listeners triggering events themselves.

Typically, an event listener will be implemented as an OSGi component that registers itself when activated:

protected void activate(ComponentContext context) throws Exception {
    session = repository.loginAdministrative(null);
    observationManager = session.getWorkspace().getObservationManager();
    
    observationManager.addEventListener(this, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/", true, null,
            null, true);
    LOGGER.info("********added JCR event listener");
}


Unregistering the event listener

The component should take care of unregistering itself when deactivated, in order to avoid memory leaks:

protected void deactivate(ComponentContext componentContext) {
    try {
        if (observationManager != null) {
            observationManager.removeEventListener(this);
            LOGGER.info("********removed JCR event listener");
        }
    }
    catch (RepositoryException re) {
        LOGGER.error("********error removing the JCR event listener", re);
    }
    finally {
        if (session != null) {
            session.logout();
            session = null;
        }
    }
}


Handling the event

The following sample illustrates an event handler that reacts to events of type PROPERTY_ADDED and PROPERTY_CHANGED on all nodes, as per the addEventListener call above and appends an exclamation mark to the end of the value of "jcr:title" properties. In this case, it is imperative that the value of the noLocal parameter to addEventListener be false, otherwise we would be triggering another property change event and would keep adding "!" to the title forever.

public void onEvent(EventIterator it) {
    while (it.hasNext()) {
        Event event = it.nextEvent();
        try {
            LOGGER.info("********new property event: {}", event.getPath());
            Property changedProperty = session.getProperty(event.getPath());
            if (changedProperty.getName().equalsIgnoreCase("jcr:title")
                    && !changedProperty.getString().endsWith("!")) {
                changedProperty.setValue(changedProperty.getString() + "!");
                session.save();
            }
        }
        catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }        
}


References