Developer Documentation Center

Events and Listeners

Updated: October 16, 2020

When you need to integrate some features of an external application into the Nuxeo Platform, or want the Platform to push data into an external application, using the Nuxeo event system is usually a good solution.

The system allows to contribute event listeners (Java classes) or event handlers (defined in Studio) that subscribe to Platform events such as user login/logout, document creation, document update, document move,  workflow started, etc. Those Java classes or automation chains are then executed either synchronously in the transaction, or synchronously out of the transaction, or asynchronously.

The event bus system is a nice way to implement some triggers in the document repository, and to let the repository notify other elements of your IT system of the changes that happen in the repository.

Concepts

A core event has a source which is usually the document model currently being manipulated. It can also store the event identifier, that gives information about the kind of event that is happening, as well as the principal connected when performing the operation, an attached comment, the event category, etc.

Events sent to the event service have to follow the org.nuxeo.ecm.core.event.Event interface.

A core event listener has a name, an order, and may have a set of event identifiers it is supposed to react to. Its definition also contains the operations it has to execute when receiving an interesting event.

Event listeners have to follow the org.nuxeo.ecm.core.event.EventListener interface.

Several event listeners exist by default in the nuxeo platform, for instance:

  • DublinCoreListener: it listens to document creation/modification events and sets some Dublin Core metadata accordingly (date of creation, date of last modification, document contributors...)

  • DocUIDGeneratorListener: it listens to document creation events and adds an identifier to the document if an UID pattern has been defined for this document type.

  • DocVersioningListener: it listens to document versioning change events and changes the document version numbers accordingly.

Registering an Event Listener

Event listeners can be registered using extension points. Here are example event listeners registrations from the Nuxeo Platform:

<component name="DublinCoreStorageService">
  <extension target="org.nuxeo.ecm.core.event.EventServiceComponent" point="listener">
    <listener name="dclistener" async="false" postCommit="false" priority="120"
      class="org.nuxeo.ecm.platform.dublincore.listener.DublinCoreListener">
    </listener>
  </extension>
</component>

<component name="org.nuxeo.ecm.platform.annotations.repository.listener">
  <extension target="org.nuxeo.ecm.core.event.EventServiceComponent" point="listener">
    <listener name="annotationsVersionEventListener" async="true" postCommit="true"
        class="org.nuxeo.ecm.platform.annotations.repository.service.VersionEventListener">
      <event>documentCreated</event>
      <event>documentRemoved</event>
      <event>versionRemoved</event>
      <event>documentRestored</event>
    </listener>
  </extension>
</component>

When defining an event listener, you should specify:

  • a name, useful to identify the listener and let other components disable/override it,
  • whether the listener is synchronous or asynchronous (default is synchronous),
  • whether the listener runs post-commit or not (default is false),
  • an optional priority among similar listeners,
  • the implementation class, that must implement org.nuxeo.ecm.core.event.EventListener,
  • an optional list of event ids for which this listener must be called.

There are several kinds of listeners:

  • synchronous inline listeners are run immediately in the same transaction and same thread, this is useful for listeners that must modify the state of the application like the beforeDocumentModification event.
  • synchronous post-commit listeners are run after the transaction has committed, in a new transaction but in the same thread, this is useful for logging.
  • asynchronous listeners are run after the transaction has committed, in a new transaction and a separate thread, this is useful for any long-running operations whose result doesn't have to be seen immediately in the user interface.

Processing an Event Using an Event Listener

Here is a simple event listener:

import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventContext;
import org.nuxeo.ecm.core.event.EventListener;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;

public class BookEventListener implements EventListener {

    public void handleEvent(Event event) throws ClientException {
        EventContext ctx = event.getContext();
        if (!(ctx instanceof DocumentEventContext)) {
            return;
        }
        DocumentModel doc = ((DocumentEventContext) ctx).getSourceDocument();
        if (doc == null) {
            return;
        }
        String type = doc.getType();
        if ("Book".equals(type)) {
            process(doc);
        }
    }

    public void process(DocumentModel doc) throws ClientException {
        ...
    }
}

Note that if a listener expects to modify the document upon save or creation, it must use events emptyDocumentModelCreated or beforeDocumentModification, and not save the document, as these events are themselves fired during the document save process.

If a listener expects to observe a document after it has been saved to do things on other documents, it can use events documentCreated or documentModified.

Sending an Event

It is not as common as having new listeners, but sometimes it's useful to send new events. To do this you have to create an event bundle containing the event, then send it to the event producer service:

EventProducer eventProducer;
try {
    eventProducer = Framework.getService(EventProducer.class);
} catch (Exception e) {
    log.error("Cannot get EventProducer", e);
    return;
}

DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), doc);
ctx.setProperty("myprop", "something");

Event event = ctx.newEvent("myeventid");
try {
    eventProducer.fireEvent(event);
} catch (ClientException e) {
    log.error("Cannot fire event", e);
    return;
}

You can also have events be sent automatically at regular intervals using the Scheduling Periodic Events, see that section for mor

Handling Errors

Sometimes, you may want to handle errors that occurred in an inline listener in the UI layer. This is a little bit tricky but do-able.

In the listener, you should register the needed information in a place that is shared with the UI layer. You can use the document context map for this.

...
   DocumentEventContext ctx = (DocumentEventContext)event.getContext();
   DocumentModel doc = ctx.getSourceDocument();
   ScopedMap data = doc.getContextData();
   data.putScopedValue(MY_ERROR_KEY, "some info");
...

Another thing is to insure that the current transaction will be roll-backed. Marking the event as rollback makes the event service throwing a runtime exception when returning from the listener.

...
  TransactionHelper.setTransactionRollbackOnly();
  event.markRollBack();
  throw new ClientException("rollbacking");
...

Then the error handling in the UI layer can be managed like this

...
  DocumentModel doc = ...;
  try {
     // doc related operations
  } catch (Exception e) {
     Serializable info = doc.getContextData(MY_ERROR_KEY);
     if (info == null) {
       throw e;
     }
     // handle code
  }
...

Asynchronous vs Synchronous Listeners

Asynchronous listeners will run in a separated thread (actually in the Workmanager using a processing queue since 5.6): this means the main transaction, the one that raised the event, won't be blocked by the listener processing.

So, if the processing done by the listener may be long, this is a good candidate for async processing.

However, there are some impacts in moving a sync listener to an async one:

  • the listener signature needs to change

    • rather than receiving a single event, it will receive an EventBundle that contains all event inside the transaction
  • because the listener runs in a separated transaction it can not rollback the source transaction (it is too late anyway)
  • the listener code need to be aware that it may have to deal with new cases

    • typically, the listener may receive an event about a document that has since then been deleted

However, it is easy to have a single listener class that exposes both interfaces and can be use both synchronously and asynchronously.

Performances and Monitoring 

Using listeners, especially synchronous one may impact the global performance of the system.

Typically, having synchronous listeners that do long processing will reduce the scalability of the system.

In that kind of case, using asynchronous listener is the recommended approach:

  • the interactive transaction is no longer tried to listener execution
  • Workmanager allow to configure how many async listeners can run in concurrency

To monitor execution of the listeners, you can use the Admin Center / Monitoring / Nuxeo Event Bus to

  • activate the tracking of listeners execution,
  • see how much time is spent in each listener.

For diagnostic and testing purpose, you can use the EventAdminService to activate / deactivate listeners one by one.

The EventServiceAdmin is accessible :