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]:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
- 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.
- 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)
- The template.drl file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | |
} | |
} |
1: I’m ignoring the fact that most likely the rule is retrieved from a data store.