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 Bloomreach Experience Manager, 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
A 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: jcr:primaryType: 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) { HippoWorkflowEvent workflowEvent = new HippoWorkflowEvent(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.
Note that the HippoEvent object passed into the onHippoEvent method can't be cast to a HippoWorkflowEvent. Instead, a new HippoWorkflowInstance must be created, passing the HippoEvent object as parameter into the constructor.
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.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.