This “how to” walks through a solution to dynamically create and execute business rules.
Why we need this
It’s common the necessity to implement some kind of rules to choose between one and other action. Often, these rules are expressed in a sort of business thinking. Drools is a Business Rules Management System maintained by Red Hat and JBoss, and the answer when we need a robust, flexible and open source solution!
What we need
- JDK 1.7+
- Drools 6.5+
- Maven 3.0.5+
How to
First of all, is needed to understand what rules mean in this context. Each rule in Drools works like a if statement in a programming language, thus basically we have two parts; conditional and action. This is written by someone who understood the business rule and adapted it in the schema of Drools Language Rule:
rule "Apply 10% off over all items of $4 or more" when Product(price >= 4.0) then product.discount(10); end
Pretty easy, right?!
Line 1 says the friendly name of the business rule.
Line 3 is the condition to satisfy. In this case, the price of product has to be equal or more than 4 bucks .
Line 5 shows the action to be executed, so apply 10% discount over product’s price.
We can write how many rules are needed and all of them are saved in a file with extension .drl
. But, as a file it is static and the goal here is to create something dynamic, therefore writing files is not an option!
Hands-on mode
Let’s think about the supermarket where we work. It’s time for promotions and the manager asked for applying a set of discounts on some products.
- Apply 10% off over all items of $4 or more
- For all items with due date on next 7 days, apply 45%
- Give 5% off over every kind of Beans
There we go…
Before starting code, just make sure to have the following dependencies in your project file pom.xml
:
<dependency> <groupId>org.drools</groupId> <artifactId>drools-compiler</artifactId> <version>6.5.0.Final</version> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-core</artifactId> <version>6.5.0.Final</version> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-decisiontables</artifactId> <version>6.5.0.Final</version> </dependency>
Next approach is to use Rule Templates, where we can write the pattern of our rules, and populate it after with concrete rules. Following is what we need to accomplish our mission, the file Product.drl
:
template header name object conditional action package drools.templates; global net.itfromhell.howit.dummy.Product product; import java.text.SimpleDateFormat; import function net.itfromhell.howit.dynamicdrools.util.DroolsUtility.debug; dialect "java" template "Product" rule "@{row.rowNumber} - @{name}" when @{object}(@{conditional}) then product.discount(@{action}); debug(drools); end end template
The structure used to create a template file is similar to regular rule file. Please, take a look on:
- Line 1: describes this file as a template
- Line 3-6: parameters expected in this template
- Line 15: declares the dialect used as java
- Line 19-25: rule defination
- Line 27: end of template
Now we have a template to handle all rules about the products in promotion, the next step is to write a couple of rules describing how to apply the discount on them. We will do it programmatically, using a set of classes that I coded to make the dirty job:
- Rule.class: encapsulate all that is needed to create one single business rule.
- Condition.class: describe one specific condition to be evaluated.
- DroolsUtility.class: tool to operate everything in memory.
The complete source code, compiling and ready for duty is available in my GitHub.
In my opinion, you wanna see everything working, who knows if it isn’t a joke… in this case, run ShowMeTheDrools.class
:
public class ShowMeTheDrools { public static void main(String args[]) throws Exception { //List to keep all rules List<Rule> rules = new ArrayList<Rule>(); //Load each business rule rules.add(createDiscountOverpriced()); rules.add(createDiscountSoonDueDate()); rules.add(createDiscountBeans()); //Create a session to operate Drools in memory DroolsUtility utility = new DroolsUtility(); StatelessKieSession session = utility.loadSession(rules, "drools/templates/Product.drl"); //Define the products to be processed using our rules Product blackBeans = new Product("Black Beans", 2.20, "30/12/2017"); Product cannelliniBeans = new Product("Cannellini Beans", 4.15, "05/02/2018"); Product kidneyBeans = new Product("Kidney Beans", 2.05, "20/11/2017"); Product rice = new Product("Rice", 1.10, "28/10/2017"); Product milk = new Product("Milk", 3.50, "10/11/2017"); /* Now, the magic happens! For each product to be processed, we have to face it over rules to get, or not, a discounted price. */ System.out.println("Applying over " + rice.getName() + " with price $" + rice.getPrice() + "..."); session.setGlobal("product", rice); session.execute(rice); System.out.println("...price after review: $" + rice.getPrice()); System.out.println("Applying over " + blackBeans.getName() + " with price $" + blackBeans.getPrice() + "..."); session.setGlobal("product", blackBeans); session.execute(blackBeans); System.out.println("...price after review: $" + blackBeans.getPrice()); System.out.println("Applying over " + milk.getName() + " with price $" + milk.getPrice() + "..."); session.setGlobal("product", milk); session.execute(milk); System.out.println("...price after review: $" + milk.getPrice()); System.out.println("Applying over " + kidneyBeans.getName() + " with price $" + kidneyBeans.getPrice() + "..."); session.setGlobal("product", kidneyBeans); session.execute(kidneyBeans); System.out.println("...price after review: $" + kidneyBeans.getPrice()); System.out.println("Applying over " + cannelliniBeans.getName() + " with price $" + cannelliniBeans.getPrice() + "..."); session.setGlobal("product", cannelliniBeans); session.execute(cannelliniBeans); System.out.println("...price after review: $" + cannelliniBeans.getPrice()); }
As result we got:
Applying over Rice with price $1.1...
Triggered rule: 1 - Apply discount on all soon due date
...price after review: $0.605
Applying over Black Beans with price $2.2…
Triggered rule: 2 – Discounting on all beans
…price after review: $2.0900000000000003
Applying over Milk with price $3.5…
…price after review: $3.5
Applying over Kidney Beans with price $2.05…
Triggered rule: 2 – Discounting on all beans
…price after review: $1.9474999999999998
Applying over Cannellini Beans with price $4.15…
Triggered rule: 2 – Discounting on all beans
Triggered rule: 0 – Give some discount on overpriced
…price after review: $3.5482500000000003
In order to verifiy the promotional price, we noted a discount on Rice which started for $1.1 and now is $0.605 (what bargain! don’t forget to check the due date). We know why, it’ s because the rule named “Apply discount on all soon due date” was triggered applying its huge discount.
Black Beans started from $2.2 and finished to $2.09, 5% less because of rule “Discounting on all beans”.
But wait, we got no discount on Milk since the start and final prices are the same 3.5 buks! The simple answer is just because no rule was triggered (you can check it).
Something different happened to Cannellini Beans, there are two rules triggered, is it right? Yes it is. The first rule about Beans is triggered applying 5%, next the rule about overprice is triggered applying more 10%, so the final price is now $3.54. It is intersting since we are able to chain rules, increasing the complexity of our solution.
Back to source code of ShowMeTheDrools
on lines:
- 25-27: all three business rules are created (next section we see details)
- 31: our tool is used to get a session to operate rules
- 34-38: a set of products is defined
- 44-67: we submit each product against all business rules and watch what happens on price
To make it easy to understand, I created a specific method for each business rule. In the real world it doesn’t make any sense. Anyway, let’s see how it works for asked business rule “Apply 10% off over all items of $4 or more”:
private static Rule createDiscountOverpriced() { //First of all, we create a rule giving it a friendly name Rule rule = new Rule("Give some discount on overpriced"); //Here we need to say what kind of object will be processed rule.setDataObject(Product.class.getName()); //As expected, a rule needs condition to exists. So, let's create it... Condition condition = new Condition(); //What data, or property, will be checked condition.setProperty("price"); //What kind of check to do condition.setOperator(Condition.Operator.GREATER_THAN_OR_EQUAL_TO); //What is the value to check condition.setValue(new Double(4.0)); //Next, is needed to set rule's condition rule.setCondition(condition); //Finally, this is what will be done when ours condition is satisfied rule.setAction("10"); return rule; }
What about “For all items with due date on next 7 days, apply 45%”:
private static Rule createDiscountSoonDueDate() throws Exception { Rule rule = new Rule("Apply discount on all soon due date"); rule.setDataObject(Product.class.getName()); //Is possible to create multiple conditions, therefore, data range or more complex situations could be expressed Condition greaterThan = new Condition(); greaterThan.setProperty("dueDate"); greaterThan.setOperator(Condition.Operator.GREATER_THAN); greaterThan.setValue((new SimpleDateFormat("dd/MM/yyyy").parse("23/10/2017"))); Condition lessThan = new Condition(); lessThan.setProperty("dueDate"); lessThan.setOperator(Condition.Operator.LESS_THAN); lessThan.setValue((new SimpleDateFormat("dd/MM/yyyy").parse("30/10/2017"))); //You can define as many as necessary conditions to achieve your necessity rule.setConditions(Arrays.asList(greaterThan, lessThan)); rule.setAction("45"); return rule; }
Finally we have the third business rule “Give 5% off over all Beans”:
private static Rule createDiscountBeans() { Rule rule = new Rule("Discounting on all beans"); rule.setDataObject(Product.class.getName()); //This is the simplest way to define the rule' condition rule.addCondition("name", Condition.Operator.CONTAINS, "Beans"); rule.setAction("5"); return rule; }
Let’s take a look over most important pieces of code of each class, starting from beggining; Rule.class
As we discussed before, the engine understand Drools Rule Language, the method conditionAsDRL
transforms all conditions of the rule into textual expressions.
public String conditionAsDRL() throws IllegalStateException, IllegalArgumentException { if ((conditions == null) || (conditions.isEmpty())) { throw new IllegalStateException("You must declare at least one condition to be evaluated."); } StringBuilder drl = new StringBuilder(); //For each condition of this rule, we create its textual representation for (int i = 0; i < conditions.size(); i++) { Condition condition = conditions.get(i); drl.append("("); drl.append(condition.buildExpression()); drl.append(")"); if ((i + 1) < conditions.size()) { drl.append(" && "); } } return drl.toString(); }
The class Condition.class
has buildExpression
, executed by Rule and responsible to return its own expression of the conditional.
public String buildExpression() throws IllegalArgumentException { StringBuilder drl = new StringBuilder(); if (value instanceof String) { drl.append(expressionForStringValue()); } else if (value instanceof Number) { drl.append(expressionForNumberValue()); } else if (value instanceof Date) { drl.append(expressionForDateValue()); } else { throw new IllegalArgumentException("The class " + value.getClass().getSimpleName() + " of value is not acceptable."); } return drl.toString(); }
For each type of value, there is a specific method responsible to create the specialized expression, as exemple of expressionForNumberValue
, used to transform a Number instance:
private String expressionForNumberValue() throws IllegalArgumentException { StringBuilder drl = new StringBuilder(); if ((operator.isComparable(Short.class)) || (operator.isComparable(Integer.class)) || (operator.isComparable(Long.class)) || (operator.isComparable(Double.class)) || (operator.isComparable(Float.class))) { drl.append(property).append(" ").append(operator.getOperation()).append(" ").append(value); } else { throw new IllegalArgumentException("Is not possible to use the operator " + operator.getDescription() + " to a " + value.getClass().getSimpleName() + " object."); } return drl.toString(); }
Last but not less important, the class DroolsUtility.class
has two methods to see:
/** * Loads a session to execute rules in memory using a template file. * * @param templatePath Relative path to template file describing the rule's pattern. * @param rulesAsParameters List of maps representing each rule as a set of parameters. * @return Session for execution of rules. * @throws Exception */ private StatelessKieSession loadSession(String templatePath, List<Map<String, Object>> rulesAsParameters) throws Exception { ObjectDataCompiler compiler = new ObjectDataCompiler(); //Compiles the list of rules using the template to create a readable Drools Rules Language String drl = compiler.compile(rulesAsParameters, Thread.currentThread().getContextClassLoader().getResourceAsStream(templatePath)); System.out.println("drl:\n" + drl); KieServices services = KieServices.Factory.get(); KieFileSystem system = services.newKieFileSystem(); system.write("src/main/resources/drools/templates/rule.drl", drl); services.newKieBuilder(system).buildAll(); KieContainer container = services.newKieContainer(services.getRepository().getDefaultReleaseId()); StatelessKieSession session = container.getKieBase().newStatelessKieSession(); return session; }
The highlight line 56 is a trick, it will print in console the concrete generated .drl
‘s contents, which is used by the engine in memory. See below:
package drools.templates; global net.itfromhell.howit.dummy.Product product; import java.text.SimpleDateFormat; import function net.itfromhell.howit.dynamicdrools.util.DroolsUtility.debug; dialect "java" rule "2 - Discounting on all beans" when net.itfromhell.howit.dummy.Product((name.toUpperCase().contains("BEANS"))) then product.discount(5); debug(drools); end rule "1 - Apply discount on all soon due date" when net.itfromhell.howit.dummy.Product((dueDate > (new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")).parse("23/10/2017 00:00:00")) && (dueDate < (new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")).parse("30/10/2017 00:00:00"))) then product.discount(45); debug(drools); end rule "0 - Give some discount on overpriced" when net.itfromhell.howit.dummy.Product((price >= 4.0)) then product.discount(10); debug(drools); end
As suggests its name, this method can be used to get some details of the triggered rule when the engine acts.
/** * Debug tool to show what is happening over each triggered execution. * Name of rule trigger as well the object inspected are printed. * * @param helper Injected when a consequence is fired. */ public static void debug(final KnowledgeHelper helper) { System.out.println("Triggered rule: " + helper.getRule().getName()); if (helper.getMatch() != null && helper.getMatch().getObjects() != null) { for (Object object : helper.getMatch().getObjects()) { System.out.println("Data object: " + object); } } }
That’s all folks!
Remember it: living on your own rules 🙂
Download the complete project from my GitHub.