Creating rules and listeners

November 14, 2023

This document is an extension of the initial rules getting started information. If you are not familiar with that one, we recommend you read it first.

Architecture

As Jahia uses a Java Content repository, it uses the standard JCRObservationManager. This manager observes events on the content repository and passes them to several event listeners implementing the abstract DefaultEventListener class. In the JCRStoreService, Jahia configures several default event listeners which are necessary to make Jahia work. These event listeners are then registered in the JCRObservationManager.

One of these event listeners is the JBoss Rules Listener, which allows integrators to use rules to process events. These rules can be deployed in modules and Jahia also includes default rules.

rules-and-listeners.png

Content events can be observed through the JCRObservationManager. However, system events (such as the login event) are not routed through the JCRObservationManager, but instead are sent and received through the system framework.

Rules

Each module in Jahia can define its own rules, extend the rule domain-specific language, and add its own set of global objects.

Adding rules in a module

You can add rules to a module simply by creating a rules.drl file in the src/main/resources/META-INF folder of the module. Here is an example from the OSGi rule sample module that defines two rules:

package org.foo.modules.rules

// list any import classes here. This is needed for OSGi to calculate the proper dependencies
import org.jahia.services.content.rules.*
import org.jahia.services.content.JCRContentUtils
import org.slf4j.Logger

global User user
global Service service
global Logger logger
global JCRContentUtils jcrUtils
global Service1 service1
global Service2 service2

rule "Hello bigText"
    when
        A new node is created
        - the node has the type jnt:bigText
    then
        Say hello to node using service1
end

rule "Hello contentList"
    when
        A new node is created
        - the node has the type jnt:contentList
    then
        Say hello to node using service2
end

Please note that the following limitations must be respected:

  • You should only use a given package name in a single module, do not reuse them across modules or you may have problems
  • Rule titles are globally unique to the platform they are deployed on, so make sure you don't re-use titles or you may as well running into problems.

You can find information about the rule language syntax and possibilities in the JBoss Drools Reference documentation.

Finally, modules may also insert objects in a global object shared by all the rules. By default, Jahia already includes a few global objects, such as: 

logger : org.slf4j.Logger
user : org.jahia.services.content.rules.User
workspace : java.lang.String
service : org.jahia.services.content.rules.Service
imageService : org.jahia.services.content.rules.ImageService
extractionService : org.jahia.services.content.rules.ExtractionService
notificationService : org.jahia.services.content.rules.RulesNotificationService

Different rule files for different workspaces

By default, Jahia will look for the following rule files in the module's META-INF directory : 

  • rules.drl : for rules to be executed on any event in any workspace
  • default-rules.drl : for rules to be executed on any event only in the default (edit & preview) workspace
  • live-rules.drl : for rules to be executed on any event only in the live workspace
  • rules.dsl : for DSL extensions

Extending the domain-specific language (DSL) for rules

Jahia allows you to extend the DSL by adding your own conditions and consequences inside modules. You only need to add a rules.dsl file in the src/main/resources/META-INF folder in order to achieve this.

Looking at the above example rule, you might have noticed that we use DSL statements to simplify it. We can take a look at the built-in Jahia DSL that contains the following conditions :

[condition][]A new node is created=node : AddedNodeFact ($node : this)
[condition][]- it has the type {type}=types contains "{type}"

Fact classes are inserted by the Jahia RuleListener and contain contextual information about the event that triggered the rules. Basically, these are objects called "facts," and you may find the built-in facts in Jahia's core here (look at the classes ending in "Fact"). As you can see, the DSL is just a way of making rule statements easier to read.

In the "then" (consequence) part of a rule, you may execute any Java code or use DSL language consequence to perform the needed action when the conditions of a rule are satisfied.

In the case of our example module, we have the following content in our META-INF/rules.dsl file:

[consequence][]Say hello to {node} using service1=service1.hello({node});
[consequence][]Say hello to {node} using service2=service2.hello({node});

So in our above example, the "Say hello to node using serviceX" statements are simply Java method calls to services that are provided as global objects to the rule execution context.

Here is an example of using the consequences in another module. In the pom.xml you should have:

<Jahia-Depends>rules-sample</Jahia-Depends>

And the META-INF/rules.drl file should look like this:

package org.foo.modules.rules.consumer

#list any import classes here.
import org.slf4j.Logger
import org.jahia.services.content.rules.*
import org.foo.modules.rules.*

#declare any global variables used here
global User user
global Service service
global Logger logger
global Service1 service1

rule "Notification about new text node"
   when
       A new node is created
       - the node has the type jnt:text
   then
       Say hello to node with service1
end

Note that the rule file must import the org.foo.modules.rules.* package and have a definition for the service1 global object, as highlighted here:

...

import org.foo.modules.rules.*

...
global Service1 service1
...

You may also be interested to view the full list of conditions and consequences provided by Jahia Core

Adding your own global objects to your rule context for execution

It is quite easy to add global objects to the rules execution context using OSGi annotations. First, we declare an OSGi service we will want to use as a global object, as in the example below:

package org.foo.modules.rules;

import org.jahia.services.content.rules.AddedNodeFact;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;

@Component(service=Service1.class)
public class Service1 {
    private static Logger logger = LoggerFactory.getLogger(Service1.class);
    public void hello(AddedNodeFact node) throws RepositoryException {
        logger.info("Hello 1 " + node.getName());
    }
}

Then we need to create a class that extends the ModuleGlobalObject class that will be recognized by the rules engine upon module deployment as something to add to the rule engine's global context. Here's the code to do that:

package org.foo.modules.rules;

import org.jahia.services.content.rules.ModuleGlobalObject;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

/**
 * Module global rules object to register custom OSGi services
 */
@Component(service = ModuleGlobalObject.class)
public class TestRules extends ModuleGlobalObject {

    @Reference
    public void setService1(Service1 service1) {
        getGlobalRulesObject().put("service1", service1);
    }

    public void unsetService1(Service1 service1) {
        getGlobalRulesObject().remove("service1");
    }

}

As you can see, this class just uses OSGi reference listener methods (@Reference) to put and remove the service1 instance into the global rules object map.

You should note that it is very important to remember to export the packages that you wish to make usable to other modules - even if they are only used in DSL files. You can do so by adding the following lines to your module's pom.xml file : 

    <properties>
        <!-- This property is important we declare that we want to export our package that contains our services, -->
        <!-- we could provide multiple package if needed to do so just separate them with a comma. -->
        <export-package>org.foo.modules.rules</export-package>
    </properties>

You can find a complete example of how to register global objects using OSGi here: https://github.com/Jahia/OSGi-modules-samples/tree/master/rules-samples/src/main/java/org/foo/modules/rules

Salience

It is possible to use salience on a rule, to control the priority of the rule's execution:

rule "Hello bigText"
    salience 100
    when
        A new node is created
        - the node has the type jnt:bigText
    then
        Say hello to node using service1
end

default value: 0

type: integer

Each rule has an integer salience attribute which defaults to zero and can be negative or positive. Salience is a form of priority where rules with higher salience values are given higher priority when ordered in the Activation queue.

You can find more details about salience in the JBoss Drools documentation.

Tools for rules

In Jahia's Tool, you may find a tool to list all the deployed business rules, available at the following URL:

http://localhost:8080/modules/tools/rules.jsp

It is notably possible to temporarily deactivate some rules from this tool, but users should be very careful when doing this as this may break built-in functionality. 

System events

To listen to events sent through the system framework, you need to implement an OSGi event handler. See the example available here: https://github.com/Jahia/OSGi-modules-samples/blob/master/system-events-samples/src/main/java/org/foo/modules/events/OsgiSystemEventHandler.java.

Login and logout events

Login and logout event can be activated, if your application needs to listen to these events. You'll have to add fireLoginEvent and/or fireLogoutEvent properties in your jahia.properties file. An event will then be sent every time a user logs in or out.

JCR Events

You can extend the org.jahia.services.content.DefaultEventListener class to create a listener on JCR events. The event listener needs to be registered from a module using the following API call: jahiaTemplateManagerService.getTemplatePackageRegistry().handleJCREventListener(listener, register);

The jahiaTemplateManagerService is accessible through the OSGi service registry with the following interface: org.jahia.api.templates.JahiaTemplateManagerService

See JCR documentation for details on the different event types.