3 min read

Dynamically Create Rules using Drools & Rule Templates

Rules are used for a variety of stuff in the systems we build. Most often these rules are hard-coded in our application logic. The trouble is that sometimes we want to have the end-user the ability to define his own rules. Imagine an order processing system. The supplier wants to be notified on any range of events as they occur throughout the system but the notification rules are not known ahead of time. Such a rule could be for a late payment or a highly lucrative order event. In Java, the latter rule can be modelled as follows [1]:

Rule highValueOrderRule = new Rule();
Condition highValueOrderCondition = new Condition();
highValueOrderCondition.setField("price");
highValueOrderCondition.setOperator(Condition.Operator.GREATER_THAN);
highValueOrderCondition.setValue(5000.0);
// In reality, you would have multiple rules for different types of events.
// The eventType property would be used to find rules relevant to the event
highValueOrderRule.setEventType(Rule.eventType.ORDER);
highValueOrderRule.setCondition(highValueOrderCondition);
view raw Program(1).java hosted with ❤ by GitHub
Supporting conjoined conditions in a rule requires us that we tweak the previous example:

Rule highValueOrderWidgetsIncRule = new Rule();
Condition highValueOrderCondition = new Condition();
highValueOrderCondition.setField("price");
highValueOrderCondition.setOperator(Condition.Operator.GREATER_THAN);
highValueOrderCondition.setValue(5000.0);
Condition widgetsIncCustomerCondition = new Condition();
widgetsIncCustomerCondition.setField("customer");
widgetsIncCustomerCondition.setOperator(Condition.Operator.EQUAL_TO);
widgetsIncCustomerCondition.setValue("Widgets Inc.");
// In reality, you would have multiple rules for different types of events.
// The eventType property would be used to find rules relevant to the event
highValueOrderWidgetsIncRule.setEventType(Rule.eventType.ORDER);
highValueOrderWidgetsIncRule.setConditions(Arrays.asList(highValueOrderCondition, widgetsIncCustomerCondition));
view raw Program(2).java hosted with ❤ by GitHub
I consider it a risky proposition to write your own primitive rules engine to evaluate rules like the above. I much prefer a solution leveraging Drools 6 in combination with Rule Templates. Rule Templates is an awesome Drools feature giving you the ability to define abstract rules at design-time. At run-time, a Drools compiler runs through the rule template and evaluates expressions to generate concrete rules. Given an event type class (e.g., OrderEvent) and a Rule object (e.g., highValueOrderWidgetsIncRule), we can conceive the following rule template:

template header
rule
eventType
package org.ossandme;
global org.ossandme.AlertDecision alertDecision;
template "alert"
rule "alert_@{row.rowNumber}"
when
@{eventType}(@{rule})
then
alertDecision.setDoAlert(Boolean.TRUE);
end
end template
view raw template.drl hosted with ❤ by GitHub
A couple of things to observe:
  • Line 1: Declares that the DRL file is a rule template.
  • Line 3-4: rule and eventType are template parameters.
  • Line 8: alertDecision is global variable which we write the outcome to should the rule evaluate to true.
  • Line 12: @{row.rowNumber} is an in-built expression that makes the rule ID unique. This is useful for situations when you don’t know how many rules you’re going to have ahead of time. Note that this doesn’t apply to our example.
  • Line 14: @{eventType} and @{rule} MVEL expressions that are substituted with the template parameters at run-time.
  • Line 16: Sets the property doAlert to true to signal the application that the notification rule was fired.
Generating a rule from the template is a matter of instantiating ObjectDataCompiler and passing as parameters:
  1. A map consisting of a Rule object (e.g., highValueOrderWidgetsIncRule) and the name of the event class the Rule object pertains to (e.g., org.ossandme.event.OrderEvent)

  2. The template.drl file
package org.ossandme;
import org.drools.template.ObjectDataCompiler;
...
public final class Program {
...
// Event parameter is an interface and OrderEvent implements it
static private String applyRuleTemplate(Event event, Rule rule) throws Exception {
Map<String, Object> data = new HashMap<String, Object>();
ObjectDataCompiler objectDataCompiler = new ObjectDataCompiler();
data.put("rule", rule);
data.put("eventType", event.getClass().getName());
return objectDataCompiler.compile(Arrays.asList(data), Thread.currentThread().getContextClassLoader().getResourceAsStream("template.drl"));
}
}
view raw Program(3).java hosted with ❤ by GitHub
Drools cannot evaluate a Rule object in its current POJO form. In order to evaluate it, we override the Rule class’s toString() method to transform the POJO into a formal statement:

package org.ossandme.rule;
...
public class Rule {
...
@Override
public String toString(){
StringBuilder statementBuilder = new StringBuilder();
for (Condition condition : getConditions()) {
String operator = null;
switch (condition.getOperator()) {
case EQUAL_TO:
operator = "==";
break;
case NOT_EQUAL_TO:
operator = "!=";
break;
case GREATER_THAN:
operator = ">";
break;
case LESS_THAN:
operator = "<";
break;
case GREATER_THAN_OR_EQUAL_TO:
operator = ">=";
break;
case LESS_THAN_OR_EQUAL_TO:
operator = "<=";
break;
}
statementBuilder.append(condition.getField()).append(" ").append(operator).append(" ");
if (condition.getValue() instanceof String) {
statementBuilder.append("'").append(condition.getValue()).append("'");
} else {
statementBuilder.append(condition.getValue());
}
statementBuilder.append(" && ");
}
String statement = statementBuilder.toString();
// remove trailing &&
return statement.substring(0, statement.length() - 4);
}
}
view raw Rule.java hosted with ❤ by GitHub
Before running the data through the template, Drools calls toString() on the template parameters. Calling toString() on highValueOrderWidgetsIncRule returns the statement: price > 5000.0 && customer == ‘Widgets Inc.’. Going even further, if we apply the template to the statement and event type OrderEvent, we would get the following generated rule:

package org.ossandme;
global org.ossandme.AlertDecision alertDecision;
rule "alert_0"
when
org.ossandme.event.OrderEvent(price > 5000.0 && customer == 'Widgets Inc.')
then
alertDecision.setDoAlert(Boolean.TRUE);
end
view raw instance.drl hosted with ❤ by GitHub
The last step is to evaluate the rule:

package org.ossandme;
...
public final class Program {
...
static private AlertDecision evaluate(String drl, Event event) throws Exception {
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
kieFileSystem.write("src/main/resources/rule.drl", drl);
kieServices.newKieBuilder(kieFileSystem).buildAll();
KieContainer kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
StatelessKieSession statelessKieSession = kieContainer.getKieBase().newStatelessKieSession();
AlertDecision alertDecision = new AlertDecision();
statelessKieSession.getGlobals().set("alertDecision", alertDecision);
statelessKieSession.execute(event);
return alertDecision;
}
...
}
view raw Program(4).java hosted with ❤ by GitHub
Finally, let’s put this all together. Don’t worry, a copy of the complete application can be found on GitHub:

package org.ossandme;
...
public final class Program {
static public void main(String[] args) throws Exception {
// Create an event that will be tested against the rule.
// Realistically, the event would be read from some external source.
OrderEvent orderEvent = new OrderEvent();
orderEvent.setPrice(5000.1);
orderEvent.setCustomer("Widgets Inc.");
// Create rule
Rule highValueOrderWidgetsIncRule = new Rule();
// Create condition
Condition highValueOrderCondition = new Condition();
highValueOrderCondition.setField("price");
highValueOrderCondition.setOperator(Condition.Operator.GREATER_THAN);
highValueOrderCondition.setValue(5000.0);
// Create another condition
Condition widgetsIncCustomerCondition = new Condition();
widgetsIncCustomerCondition.setField("customer");
widgetsIncCustomerCondition.setOperator(Condition.Operator.EQUAL_TO);
widgetsIncCustomerCondition.setValue("Widgets Inc.");
// In reality, you would have multiple rules for different types of events.
// The eventType property would be used to find rules relevant to the event
highValueOrderWidgetsIncRule.setEventType(Rule.eventType.ORDER);
// Add conditions to rule
highValueOrderWidgetsIncRule.setConditions(Arrays.asList(highValueOrderCondition, widgetsIncCustomerCondition));
String drl = applyRuleTemplate(orderEvent, highValueOrderWidgetsIncRule);
AlertDecision alertDecision = evaluate(drl, orderEvent);
System.out.println(alertDecision.getDoAlert());
// doAlert is false by default
if (alertDecision.getDoAlert()) {
// do notification
// ...
}
}
// OrderEvent implements Event interface
static private AlertDecision evaluate(String drl, Event event) throws Exception {
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
kieFileSystem.write("src/main/resources/rule.drl", drl);
kieServices.newKieBuilder(kieFileSystem).buildAll();
KieContainer kieContainer = kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
StatelessKieSession statelessKieSession = kieContainer.getKieBase().newStatelessKieSession();
AlertDecision alertDecision = new AlertDecision();
statelessKieSession.getGlobals().set("alertDecision", alertDecision);
statelessKieSession.execute(event);
return alertDecision;
}
// Event parameter is an interface and OrderEvent implements it
static private String applyRuleTemplate(Event event, Rule rule) throws Exception {
Map<String, Object> data = new HashMap<String, Object>();
ObjectDataCompiler objectDataCompiler = new ObjectDataCompiler();
data.put("rule", rule);
data.put("eventType", event.getClass().getName());
return objectDataCompiler.compile(Arrays.asList(data), Thread.currentThread().getContextClassLoader().getResourceAsStream("rule-template.drl"));
}
}
view raw Program.java hosted with ❤ by GitHub

1: I’m ignoring the fact that most likely the rule is retrieved from a data store.