Drools is an open source rules engine that allows you to externalize the business rules.
This tutorial assumes that you have gone through “Setting up Java, Maven, and Eclipse.” Once you have gone through the following steps, you should have a project structure as shown below.
Step 1: Create a new Maven project
1 | C:\Temp\Java\projects>mvn archetype:generate -DgroupId=com.mytutorial -DartifactId=simpleDrools |
Note: press “enter” through all prompts. The project will be created under C:\Temp\Java\projects\simpleDrools with a default pom.xml file.
Step 2: Import it into eclipse as a Maven project. File –> Import –> Existing Maven Projects, and select the folder where the pom.xml file is.
Step 3: Update the pom.xml file as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <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.mytutorial</groupId> <artifactId>simpleDrools</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>simpleDrools</name> <url>http://maven.apache.org</url> <properties> <drools.version>5.3.1.Final</drools.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <!-- drools library --> <dependency> <groupId>org.drools</groupId> <artifactId>drools-core</artifactId> <version>${drools.version}</version> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-compiler</artifactId> <version>${drools.version}</version> </dependency> <!-- required for drools and spring integration --> <dependency> <groupId>org.drools</groupId> <artifactId>drools-spring</artifactId> <version>${drools.version}</version> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.2.2</version> </dependency> </dependencies> </project> |
Step 4: The OrderItem domain object class with getter and setter methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | package com.mytutorial; import java.math.BigDecimal; public class OrderItem { public enum PROD_TYPE { BASIC, LUXURY } private PROD_TYPE type; private int units; private BigDecimal unitPrice; private BigDecimal effectiveDiscountRate; private BigDecimal discountedUnitPrice; public PROD_TYPE getType() { return type; } public void setType(PROD_TYPE type) { this.type = type; } public int getUnits() { return units; } public void setUnits(int units) { this.units = units; } public BigDecimal getUnitPrice() { return unitPrice; } public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; } public BigDecimal getEffectiveDiscountRate() { return effectiveDiscountRate; } public void setEffectiveDiscountRate(BigDecimal effectiveDiscountRate) { this.effectiveDiscountRate = effectiveDiscountRate; } public BigDecimal getDiscountedUnitPrice() { return discountedUnitPrice; } public void setDiscountedUnitPrice(BigDecimal discountedUnitPrice) { this.discountedUnitPrice = discountedUnitPrice; } @Override public String toString() { return "OrderItem [type=" + type + ", units=" + units + ", unitPrice=" + unitPrice + ", effectiveDiscountRate=" + effectiveDiscountRate + ", discountedUnitPrice=" + discountedUnitPrice + "]"; } } |
Step 5: The RuleMap class, which is Map extension to store rules.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package com.mytutorial; import java.util.HashMap; public class RuleMap<K, V> extends HashMap<String, Object> { private static final long serialVersionUID = 1L; @Override public Object put(String key, Object value) { return super.put(key, value); } } |
Step 6: The TemplateExpander utility class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package com.mytutorial; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.Collection; import org.drools.io.Resource; import org.drools.io.ResourceFactory; import org.drools.template.ObjectDataCompiler; public final class TemplateExpander { private TemplateExpander() {} public static Resource expand(Resource template, Collection<?> rules) throws IOException { ObjectDataCompiler converter = new ObjectDataCompiler(); String drl = converter.compile(rules, template.getInputStream()); Reader rdr = new StringReader(drl); return ResourceFactory.newReaderResource(rdr); } } |
Step 7: The KnowledgeBaseBuilder that binds the classes defined above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package com.mytutorial; import java.io.IOException; import java.util.List; import org.drools.KnowledgeBase; import org.drools.KnowledgeBaseFactory; import org.drools.builder.KnowledgeBuilder; import org.drools.builder.KnowledgeBuilderError; import org.drools.builder.KnowledgeBuilderFactory; import org.drools.builder.ResourceType; import org.drools.io.Resource; import org.drools.io.ResourceFactory; public class KnowledgeBaseBuilder { public static KnowledgeBase build(List<RuleMap<String, Object>> ruleAttributes) throws IOException { KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); Resource rules = loadRuleFile("product.drl"); rules = TemplateExpander.expand(rules, ruleAttributes); kbuilder.add(rules, ResourceType.DRL); // handle errors if (kbuilder.hasErrors()) { for (KnowledgeBuilderError err : kbuilder.getErrors()) { System.out.println("Errors: " + err); } throw new IllegalArgumentException("Could not parse knowledge."); } kbase = KnowledgeBaseFactory.newKnowledgeBase(); // add packages to the knowledgebase kbase.addKnowledgePackages(kbuilder.getKnowledgePackages()); return kbase; } protected static Resource loadRuleFile(String ruleFile) { if (ruleFile.contains("://")) { return ResourceFactory.newUrlResource(ruleFile); } else { return ResourceFactory.newClassPathResource(ruleFile, KnowledgeBaseBuilder.class); } } } |
Step 8: The “product.drl” contains the rules to calculate the discounts and apply to each item. The extension “drl” stands for Drools Rule Language. For example, different products have different bulk discount in addition to the normal discount.
Read this along with Step: 9 which supplies the rule data “id, normalDiscountRate, productType, bulkDiscountQty, and bulkDiscountRate ” via the values created from the createDummyRuleMap() method. In commercial projects, these rule map or data is supplied via database tables or a excel spreadsheets. The business users maintain this rule data. They could modify the bulk discount quantity from time to time. The whole beauty of drools is that rules are maintained separately. These values are referred within the drl file with “@“. Each item from the “getOrderItems()” is validated against the rule map. The items are prefixed with “$”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | template header id normalDiscountRate productType bulkDiscountQty bulkDiscountRate package com.mytutorial import com.mytutorial.*; import java.math.*; import java.text.*; import java.util.*; template "product-discount" rule "@{id}" when $item : OrderItem(type == OrderItem.PROD_TYPE.@{productType}); then BigDecimal disc = new BigDecimal("@{normalDiscountRate}"); if($item.getUnits() >= @{bulkDiscountQty}){ disc = disc.add(BigDecimal.valueOf(@{bulkDiscountRate})); } $item.setEffectiveDiscountRate(disc); $item.setDiscountedUnitPrice($item.getUnitPrice().multiply(BigDecimal.ONE.subtract(disc))); end end template |
Step 9: Finally the “App” class with the main method to run. In commercial applications, the actual rules will be stored in a database server table or a an Excel spreadsheet. This allows the business to view and maintain the rules. If the rules change, the database table or spreadsheet can be modified. In this tutorial, for simplicity supplied as dummy rules via createDummyRuleMap() method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | package com.mytutorial; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import org.drools.KnowledgeBase; import org.drools.runtime.StatefulKnowledgeSession; public class App { private static final String KEY_ID = "id"; private static final String KEY_PRODUCT_TYPE = "productType"; private static final String KEY_DISCOUNT_RATE = "normalDiscountRate"; private static final String KEY_BULK_DISCOUNT_QTY = "bulkDiscountQty"; private static final String KEY_BULK_DISCOUNT_RATE = "bulkDiscountRate"; public static void main(String[] args) throws IOException { List<RuleMap<String, Object>> ruleAttributes = createDummyRuleMap(); KnowledgeBase kbase = KnowledgeBaseBuilder.build(ruleAttributes); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); List<OrderItem> orderItems = getOrderItems(); for (OrderItem orderItem : orderItems) { System.out.println("Before firing the rules:" + orderItem); ksession.insert(orderItem); ksession.fireAllRules(); System.out.println("After firing the rules:" + orderItem); } ksession.dispose(); } /** * In commercial apps, can be read from a database table or Excel spreadsheet * @return */ private static List<RuleMap<String, Object>> createDummyRuleMap() { RuleMap<String, Object> ruleSet1 = new RuleMap<String, Object>(); ruleSet1.put(KEY_ID, 1); ruleSet1.put(KEY_PRODUCT_TYPE, OrderItem.PROD_TYPE.BASIC); ruleSet1.put(KEY_DISCOUNT_RATE, 0.1); ruleSet1.put(KEY_BULK_DISCOUNT_QTY, 5); ruleSet1.put(KEY_BULK_DISCOUNT_RATE, 0.2); RuleMap<String, Object> ruleSet2 = new RuleMap<String, Object>(); ruleSet2.put(KEY_ID, 2); ruleSet2.put(KEY_PRODUCT_TYPE, OrderItem.PROD_TYPE.LUXURY); ruleSet2.put(KEY_DISCOUNT_RATE, 0.2); ruleSet2.put(KEY_BULK_DISCOUNT_QTY, 10); ruleSet2.put(KEY_BULK_DISCOUNT_RATE, 0.3); List<RuleMap<String, Object>> rules = new ArrayList<RuleMap<String, Object>>(); rules.add(ruleSet1); rules.add(ruleSet2); return rules; } /** * Create some OrderItems * @return */ private static List<OrderItem> getOrderItems() { OrderItem orderItem1 = new OrderItem(); orderItem1.setType(OrderItem.PROD_TYPE.BASIC); orderItem1.setUnits(3); orderItem1.setUnitPrice(BigDecimal.valueOf(2.50)); OrderItem orderItem2 = new OrderItem(); orderItem2.setType(OrderItem.PROD_TYPE.LUXURY); orderItem2.setUnits(12); orderItem2.setUnitPrice(BigDecimal.valueOf(5.00)); List<OrderItem> listOfItems = new ArrayList<OrderItem>(); listOfItems.add(orderItem1); listOfItems.add(orderItem2); return listOfItems; } } |
When you run the above “App.java”
Output:
1 2 3 4 | Before firing the rules:OrderItem [type=BASIC, units=3, unitPrice=2.5, effectiveDiscountRate=null, discountedUnitPrice=null] After firing the rules:OrderItem [type=BASIC, units=3, unitPrice=2.5, effectiveDiscountRate=0.1, discountedUnitPrice=2.25] Before firing the rules:OrderItem [type=LUXURY, units=12, unitPrice=5.0, effectiveDiscountRate=null, discountedUnitPrice=null] After firing the rules:OrderItem [type=LUXURY, units=12, unitPrice=5.0, effectiveDiscountRate=0.5, discountedUnitPrice=2.50] |