Drools Salience Example
In this article we will look into an example of Drools Salience Example.
So what is salience and why do we need it?
Salience means to be most noticeable and important. We will use salience
keyword against a rule to set the priority. Before we start with the example, let’s work on our setup.
If you want to more know about Drools Introduction or its setup, read here.
This example uses the following frameworks:
In your pom.xml
, you need to add the below dependencies:
knowledge-api
– this provides the interfaces and factoriesdrools-core
– this is the core engine, runtime component. This is the only runtime dependency if you are pre-compiling rules.drools-complier
– this contains the compiler/builder components to take rule source, and build executable rule bases. You don’t need this during runtime, if your rules are pre-compiled.
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javacodegeeks.drools</groupId> <artifactId>droolsHelloWorld</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.drools</groupId> <artifactId>drools-compiler</artifactId> <version>${drools.version}</version> </dependency> </dependencies> <properties> <drools.version>6.2.0.Final</drools.version> <jbpm.version>6.2.0.Final</jbpm.version> </properties> </project>
1. About Salience Feature
Each rule has a condition, if the condition matches, the rule’s action is fired. If we end up with more than one rule, then the all the matched rules will fire arbitrarily. It is a good idea not to count on rules firing in any particular order. But suppose you want the rules to be fired in a certain order you can use the keyword salience
. salience
has one attribute, which takes any expression that returns a number of type int (positive as well as negative numbers are valid). The higher the value, the more likely a rule will be picked up by the conflict resolution strategy.
2. Shopping Cart Domain Model
Customer will add one or more products to the cart.
Salient keyword sets priority to a rule. By default, all rules have a salience of zero. You can specify a negative or positive salience, so in our case, we’ve used a negative one. Why? Because we want our combo
discount to be in addition to any other discounts added to the order, so we need to give it a lower priority to ensure it’s done last.
It’s used to determine the priority of a rule. By default, all rules have a salience of zero, but can be given a positive or negative value.
If a condition results in more than one rules than we can control the order of execution of the rules using their salience levels.
Let’s go through our shopping cart domain model.
Customer will add items to the cart. If a product needs registration, customer need to register else the item will not be processed. Method registerProduct(product)
is used to register the product and we can use isRegistered(product)
to know whether the product is registered.
Customer:
package com.javacodegeeks.drools; import java.util.ArrayList; import java.util.List; public class Customer { private Cart cart; private String coupon; private boolean isNew; private List registeredProducts = new ArrayList(); public static Customer newCustomer() { Customer customer = new Customer(); customer.isNew = true; return customer; } public boolean isNew() { return isNew; } public void addItem(Product product, int qty) { if (cart == null) { cart = new Cart(this); } cart.addItem(product, qty); } public String getCoupon() { return coupon; } public void setCoupon(String coupon) { this.coupon = coupon; } public Cart getCart() { return cart; } public void registerProduct(Product product) { registeredProducts.add(product); } public boolean isRegistered(Product p) { return registeredProducts.contains(p); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Customer new? ") .append(isNew) .append("\nCoupon: ") .append(coupon) .append("\n") .append(cart); return sb.toString(); } }
A cart knows about the customer, the items and the total discount.
Cart:
package com.javacodegeeks.drools; import java.util.ArrayList; import java.util.List; public class Cart { private Customer customer; private List cartItems = new ArrayList(); private double discount; public Cart(Customer customer) { this.customer = customer; } public void addItem(Product p, int qty) { CartItem cartItem = new CartItem(this, p, qty); cartItems.add(cartItem); } public double getDiscount() { return discount; } public void addDiscount(double discount) { this.discount += discount; } public int getTotalPrice() { int total = 0; for (CartItem item : cartItems) { if (item.hasErrors()) { continue; } total += item.getProduct().getPrice() * item.getQty(); } return total; } public Customer getCustomer() { return customer; } public List getCartItems() { return cartItems; } public void setCustomer(Customer customer) { this.customer = customer; } public int getFinalPrice() { return getTotalPrice() - (int) getDiscount(); } public String toString() { StringBuilder sb = new StringBuilder(); for (CartItem cartItem : cartItems) { sb.append(cartItem) .append("\n"); } sb.append("Discount: ") .append(getDiscount()) .append("\nTotal: ") .append(getTotalPrice()) .append("\nTotal After Discount: ") .append(getFinalPrice()); return sb.toString(); } }
A cart item knows about the cart, the product and quantity. If a product requires registration and the customer has not registered, errors
will be set to true
.
CartItem:
package com.javacodegeeks.drools; public class CartItem { private Cart cart; private Product product; private int qty; private boolean errors; public CartItem(Cart cart, Product product, int qty) { this.cart = cart; this.product = product; this.qty = qty; } public Product getProduct() { return product; } public int getQty() { return qty; } public String toString() { return "Is processed? " + !hasErrors() + (!hasErrors() ? " " + product + ", qty: " + qty : ""); } public Cart getCart() { return cart; } public boolean hasErrors() { return errors; } public void setErrors(boolean errors) { this.errors = errors; } }
Here is the product. If a product requires registration requiresRegistration
will be set to true
.
Product:
package com.javacodegeeks.drools; public class Product { private int price; private String desc; private boolean requiresRegistration; public void setRequiresRegistration(boolean requiresRegistration) { this.requiresRegistration = requiresRegistration; } public boolean isRequiresRegisteration() { return requiresRegistration; } public Product(String desc, int price) { this.desc = desc; this.price = price; } public int getPrice() { return price; } public String getDesc() { return desc; } public String toString() { return "product: " + desc + ", price: " + price; } public boolean equals(Object o) { if (o == null) { return false; } if (!(o instanceof Product)) { return false; } Product p = (Product) o; return getDesc().equals(p.getDesc()); } public int hashCode() { return getDesc().hashCode(); } }
3. Shopping Cart Rules
As an Example to understand Drool’s Salience, we will consider an example of shopping cart. Customer will add products to the cart. There will be discounts applied to the cart total price.
- If the customer has just registered to the site, there will be a 2% discount on the first purchase.
- If the customer has a coupon, another 5% discount will be applied on the total price. The coupon code and the percentage amounts may vary.
- A new mobile is launched but to for the shopping site to process the item customer must specifically register for the product
Now we want the last item to be fired first so that we can set a boolean
whether to process the item or not.
4. Define Rules
Based on the above points we will define our rules. Initially we will not use salience
keyword. We want to process the item only when $cartItem.hasErrors()
returns false.
shoppingRules.drl:
package com.javacodegeeks.drools; import com.javacodegeeks.drools.Cart; import com.javacodegeeks.drools.CartItem; import com.javacodegeeks.drools.Product; rule "If has coupon, 5% discount" when $cartItem : CartItem(cart.customer.coupon == 'DISC01') then if (!$cartItem.hasErrors()) { double discount = ((double)$cartItem.getCart().getTotalPrice())*0.05d; System.out.println("Coupon Rule: Process " + $cartItem.getProduct() + ", qty " + $cartItem.getQty() + ", apply discount " + discount); $cartItem.getCart().addDiscount(discount); } end rule "If new, 2% discount" when $cartItem : CartItem(cart.customer.isNew()) then if (!$cartItem.hasErrors()) { double discount = ((double)$cartItem.getCart().getTotalPrice())*0.2d; System.out.println("New Customer Rule: Process " + $cartItem.getProduct() + ", qty " + $cartItem.getQty() + ", apply discount " + discount); $cartItem.getCart().addDiscount(discount); } end rule "Has customer registered for the product?" when $cartItem : CartItem(product.isRequiresRegisteration(), !cart.customer.isRegistered(product)) then System.out.println("Can't process " + $cartItem.getProduct() + ", as requires registration. Customer not registered for the product!"); $cartItem.setErrors(true); end
Let’s run the example and see how the rules are processed. We will create couple of customers. add items and then fire the rules. Product mobile requires registration.
DroolsSalienceExample:
package com.javacodegeeks.drools; import java.util.List; import org.kie.api.KieServices; import org.kie.api.runtime.KieContainer; import org.kie.api.runtime.KieSession; /** * This is a sample class to launch a rule. */ public class DroolsSalienceExample { private String topic; public DroolsSalienceExample(String topic) { this.topic = topic; } public static final void main(String[] args) { try { // load up the knowledge base KieServices ks = KieServices.Factory.get(); KieContainer kContainer = ks.getKieClasspathContainer(); KieSession kSession = kContainer.newKieSession("ksession-rules"); Customer customer = new Customer(); Product p1 = new Product("Laptop", 15000); Product p2 = new Product("Mobile", 5000); p2.setRequiresRegistration(true); Product p3 = new Product("Books", 2000); customer.addItem(p1, 1); customer.addItem(p2, 2); customer.addItem(p3, 5); customer.setCoupon("DISC01"); List cartItems = customer.getCart().getCartItems(); for (CartItem cartItem: cartItems) { kSession.insert(cartItem); } System.out.println("************* Fire Rules **************"); kSession.fireAllRules(); System.out.println("************************************"); System.out.println("Customer cart\n" + customer); Customer newCustomer = Customer.newCustomer(); newCustomer.addItem(p1, 1); newCustomer.addItem(p2, 2); cartItems = newCustomer.getCart().getCartItems(); for (CartItem cartItem: cartItems) { kSession.insert(cartItem); } System.out.println("************* Fire Rules **************"); kSession.fireAllRules(); System.out.println("************************************"); System.out.println("Customer cart\n" + customer); } catch (Throwable t) { t.printStackTrace(); } } public String getTopic() { return topic; } public String introduceYourself() { return "Drools 6.2.0.Final"; } }
As you can see without the use of salience
, it has processed even the mobile item even though the customer was not registered.
Output:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. ************* Fire Rules ************** Coupon Rule: Process product: Books, price: 2000, qty 5, apply discount 1750.0 Coupon Rule: Process product: Mobile, price: 5000, qty 2, apply discount 1750.0 Coupon Rule: Process product: Laptop, price: 15000, qty 1, apply discount 1750.0 Can't process product: Mobile, price: 5000, as requires registration. Customer not registered for the product! ************************************ Customer cart Customer new? false Coupon: DISC01 Is processed? true product: Laptop, price: 15000, qty: 1 Is processed? false Is processed? true product: Books, price: 2000, qty: 5 Discount: 5250.0 Total: 25000 Total After Discount: 19750 ************* Fire Rules ************** New Customer Rule: Process product: Mobile, price: 5000, qty 2, apply discount 5000.0 New Customer Rule: Process product: Laptop, price: 15000, qty 1, apply discount 5000.0 Can't process product: Mobile, price: 5000, as requires registration. Customer not registered for the product! ************************************ Customer cart Customer new? false Coupon: DISC01 Is processed? true product: Laptop, price: 15000, qty: 1 Is processed? false Is processed? true product: Books, price: 2000, qty: 5 Discount: 5250.0 Total: 25000 Total After Discount: 19750
5. Using Salience to set priority
We will add salience
attribute and set to value 1 so that the rule fires first. Since we haven’t used salience
in other rules, their value will be treated as 0.
shoppingRules.drl:
package com.javacodegeeks.drools; import com.javacodegeeks.drools.Cart; import com.javacodegeeks.drools.CartItem; import com.javacodegeeks.drools.Product; rule "If has coupon, 5% discount" when $cartItem : CartItem(cart.customer.coupon == 'DISC01') then if (!$cartItem.hasErrors()) { double discount = ((double)$cartItem.getCart().getTotalPrice())*0.05d; System.out.println("Coupon Rule: Process " + $cartItem.getProduct() + ", qty " + $cartItem.getQty() + ", apply discount " + discount); $cartItem.getCart().addDiscount(discount); } end rule "If new, 2% discount" when $cartItem : CartItem(cart.customer.isNew()) then if (!$cartItem.hasErrors()) { double discount = ((double)$cartItem.getCart().getTotalPrice())*0.2d; System.out.println("New Customer Rule: Process " + $cartItem.getProduct() + ", qty " + $cartItem.getQty() + ", apply discount " + discount); $cartItem.getCart().addDiscount(discount); } end rule "Has customer registered for the product?" salience 1 when $cartItem : CartItem(product.isRequiresRegisteration(), !cart.customer.isRegistered(product)) then System.out.println("Can't process " + $cartItem.getProduct() + ", as requires registration. Customer not registered for the product!"); $cartItem.setErrors(true); end
Now from the output, we can clearly see, mobile is skipped from processing.
Output:
************* Fire Rules ************** Can't process product: Mobile, price: 5000, as requires registration. Customer not registered for the product! Coupon Rule: Process product: Books, price: 2000, qty 5, apply discount 1250.0 Coupon Rule: Process product: Laptop, price: 15000, qty 1, apply discount 1250.0 ************************************ Customer cart Customer new? false Coupon: DISC01 Is processed? true product: Laptop, price: 15000, qty: 1 Is processed? false Is processed? true product: Books, price: 2000, qty: 5 Discount: 2500.0 Total: 25000 Total After Discount: 22500 ************* Fire Rules ************** Can't process product: Mobile, price: 5000, as requires registration. Customer not registered for the product! New Customer Rule: Process product: Laptop, price: 15000, qty 1, apply discount 3000.0 ************************************ Customer cart Customer new? false Coupon: DISC01 Is processed? true product: Laptop, price: 15000, qty: 1 Is processed? false Is processed? true product: Books, price: 2000, qty: 5 Discount: 2500.0 Total: 25000 Total After Discount: 22500
6. Download the Eclipse Project
This was an example about Drools Salience.
You can download the full source code of this example here: DroolsSalienceExample.zip
Correction to code; it should be as follows otherwise the discount to the whole cart is added multiple times: double discount = ((double)$cartItem.getTotalPrice())*0.05d;