Hippo Event Bus 

Introduction

Goal

Understand how the Hippo Event Bus can be used to listen for and respond to local or cluster-wide events.

Background

Noteworthy events happening in Hippo CMS, such as a user logging in or a document being published, may require additional processing in an implementation project. For example, a notification email may need to be sent when an author requests publication of a document. For this purpose, such events are posted to the Hippo Event Bus. It allows any web application running in the same JVM and on the same cluster node to subscribe and respond to events. In addition, all events are stored in the repository event log, indirectly enabling applications running in a different JVM and/or on another cluster node to listen for and respond to events as well.

This page describes the general mechanism used to listen for and respond to events. A detailed example is provided in Respond to Workflow Events.

Local vs. Cluster-Wide

The Hippo Event Bus is local to the JVM it runs in. This means that a listener which subscribes to the event bus directly only receives events posted within the same JVM and, within a clustered environment, the same cluster node. In most use cases, this is actually desirable. This method is described below in Local Event Listener.

However, it is possible to respond to cluster-wide events by listening for persisted events in the repository event log. This method is described below in Cluster-Wide Event Listener.

The HippoEvent Class

Events are posted on the event bus as org.onehippo.cms7.event.HippoEvent objects.

Event Properties

HippoEvent object provides some useful information about itself through the following methods:

Method Description
action The action that caused the event.
category The category of the event (see below).
user The user that initiated the action.

See the Hippo Commons API javadoc for org.onehippo.cms7.event.HippoEvent for more information.

Subclasses of HippoEvent may define additional methods.

Event Categories

Events posted to the event bus are of one of five categories. The categories are defined by constants in the class org.onehippo.cms7.event.HippoEventConstants. Some categories have a specific subclass of HippoEvent.

Event category Constant Class
Workflow

CATEGORY_WORKFLOW

org.onehippo.repository.events.HippoWorkflowEvent
Security

CATEGORY_SECURITY

org.onehippo.cms7.event.HippoSecurityEvent
User management CATEGORY_USER_MANAGEMENT org.onehippo.cms7.event.HippoEvent
Group management

CATEGORY_GROUP_MANAGEMENT

org.onehippo.cms7.event.HippoEvent
Permissions management

CATEGORY_PERMISSIONS_MANAGEMENT

org.onehippo.cms7.event.HippoEvent

Local Event Listener

Listener Implementation

A listener must have a method public void handleEvent(HippoEvent event) annotated with org.onehippo.cms7.services.eventbus.Subscribe, as shown in the example below.

cms/src/main/java/org/example/MyListener.java

package org.example;

import org.onehippo.cms7.event.HippoEvent;
import org.onehippo.cms7.event.HippoEventConstants;
import org.onehippo.cms7.services.eventbus.Subscribe;

public class MyListener {

    @Subscribe
    public void handleEvent(HippoEvent event) {
        if (HippoEventConstants.CATEGORY_WORKFLOW.equals(event.category())) {
            // respond to event
        }
    }

}

It's also possible to subscribe only to a specific subclass of HippoEvent, such as HippoWorkflowEvent:

    @Subscribe
    public void handleEvent(HippoWorkflowEvent event) {
        // respond to event
    }

Listener Registration

The Hippo Event Bus uses the whiteboard pattern for listener registration. This pattern decouples the lifecycles of the applications that host the listener and the event bus, so it does not matter which application is started first.

A listener must be registered with the HippoServiceRegistry using org.onehippo.cms7.services.HippoServiceRegistry.registerService(Object, Class<?>). The listener is registered as a whiteboard service at the HippoEventBus interface. The event bus implementation will retrieve all listeners from the service registry when posting an event. Because the listener is registered in the service registry it is not necessary to check whether an event bus implementation has been registered already.

Using a Repository-Managed Component

Typically, a repository-managed component (a.k.a. daemon module) packaged with the cms application is used to register and unregister a listener, as in the example below:

cms/src/main/java/org/example/ListenerModule.java

package org.example;

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.example.MyListener;
import org.onehippo.cms7.services.HippoServiceRegistry;
import org.onehippo.cms7.services.eventbus.HippoEventBus;
import org.onehippo.repository.modules.DaemonModule;

public class ListenerModule implements DaemonModule {

    private MyListener listener;

    @Override
    public void initialize(Session session) throws RepositoryException {
        listener = new MyListener();
        HippoServiceRegistry.registerService(listener, HippoEventBus.class);
    }

    @Override
    public void shutdown() {
        HippoServiceRegistry.unregisterService(listener, HippoEventBus.class);
    }

}

The component is configured in the repository at /hippo:configuration/hippo:modules:

/hippo:configuration/hippo:modules
  + listener [hipposys:module]
    - hipposys:className = org.example.ListenerModule

Using a Delivery Tier Component

Instead of in the cms application (which hosts the event bus implementation), it is also possible to implement and register a listener in the site application, because it typically runs in the same JVM. This can be useful when a listener needs access to the HST API. In this case, a Spring component can take care of registering the listener:

site/src/main/java/org/example/MyComponent.java

package org.example;

import org.onehippo.cms7.services.HippoServiceRegistry;
import org.onehippo.cms7.services.eventbus.HippoEventBus;

public class MyComponent {

    private MyListener listener;

    public void init() {
      listener = new MyListener();
      HippoServiceRegistry.registerService(listener, HippoEventBus.class); 
    }
   
    public void destroy() {
      HippoServiceRegistry.unregisterService(listener, HippoEventBus.class); 
    }

}

site/src/main/resources/META-INF/hst-assembly/overrides/listener.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="org.example.MyComponent" class="org.example.MyComponent"
    init-method="init" destroy-method="destroy" />

</beans>

Cluster-Wide Event Listener

Although the event bus itself only works locally within an individual cluster node, there may be use cases which require responding to events across the entire cluster.

All events posted to the event bus are persisted as JCR nodes in the repository event log. It's possible to register listeners for these persisted events. Such listeners will receive all events, including actions that are executed on other cluster nodes and actions that were executed when the listener was not registered.

Be aware that a cluster-wide event listener responds to the same event multiple times (once for each cluster node). For many use cases, this should be avoided because duplicate responses (such as email notifications) are undesirable. For those use cases, use a local event listener instead.

Listener Implementation

cms/src/main/java/org/example/MyPersistedEventListener.java

package org.example;

import org.onehippo.cms7.event.HippoEvent;
import org.onehippo.cms7.event.HippoEventConstants;
import org.onehippo.repository.events.PersistedHippoEventListener;

public class MyPersistedEventListener implements PersistedHippoEventListener {

    @Override
    public String getEventCategory() {
        return HippoEventConstants.CATEGORY_WORKFLOW;
    }

    @Override
    public String getChannelName() {
        return "my-publication-listener";
    }

    @Override
    public boolean onlyNewEvents() {
        return true;
    }

    @Override
    public void onHippoEvent(final HippoEvent event) {
        // respond to event
    }
}

The listener's channel name is used to identify the listener after a restart, and it should be unique in the container. In other words, no listener in the same cluster node instance should use the same channel name. The event category limits the events that are passed to the listener to those within the required category.

Listener Registration

Registration of a cluster-wide listener is similar to that of a local listener. The listener is registered as a service at the PersistedHippoEventsService interface instead of the HippoEventBus interface.

cms/src/main/java/org/example/ListenerModule.java

package org.example;

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import org.onehippo.cms7.services.HippoServiceRegistry;
import org.onehippo.cms7.services.eventbus.HippoEventBus;
import org.onehippo.repository.events.PersistedHippoEventsService;
import org.onehippo.repository.modules.DaemonModule;

public class ListenerModule implements DaemonModule {

    private MyPersistedEventListener persistedListener;

    @Override
    public void initialize(Session session) throws RepositoryException {
        persistedListener = new MyPersistedEventListener();
        HippoServiceRegistry.registerService(persistedListener, PersistedHippoEventsService.class);
    }

    @Override
    public void shutdown() {
        HippoServiceRegistry.unregisterService(persistedListener, PersistedHippoEventsService.class);
    }

}

Cluster-Wide Event Dispatch Configuration

Events happening while the cluster node is offline are delivered locally when it comes back online.

The repository stores the timestamp of the last-processed event for each combination (repository-cluster-id, persisted-listener-channel-name). In principle, all events with a greater timestamp will be dispatched to the listener. In practice, there are some bounds that ensure that the system doesn't end up spending all its time dispatching events.

It is possible to tune the broadcasting mechanism that's used for dispatching the persisted events. The node

/hippo:configuration/hippo:modules/broadcast/hippo:moduleconfig

contains a number of parameters for configuring the broadcast module (defaults are between brackets). 

  • pollingTime (5000)
    The interval, in milliseconds, between polls for updates.  Every poll will execute a query for recent events. 
  • queryLimit  (500)
    The maximum number of events to retrieve and deliver at a time. 
  • maxEventAge  (24)
    The maximum age, in hours, of events to publish.