Name: BankingTutorial Main class: org.drools.tutorials.banking.Example*.java Type: Java application Rules file: org.drools.tutorials.banking.*.drl Objective: Demonstrate pattern matching, basic sorting and calculation rules.
This tutorial demonstrates the process of developing a complete personal banking application to handle credits and debits on multiple accounts. It uses a set of design patterns that have been created for the process.
The class RuleRunner
is a simple harness to execute
one or more DRL files against a set of data. It compiles the Packages
and creates the Knowledge Base for each execution, allowing us to
easily execute each scenario and inspect the outputs. In reality this
is not a good solution for a production system, where the Knowledge Base
should be built just once and cached, but for the purposes of this
tutorial it shall suffice.
Example 8.28. Banking Tutorial: RuleRunner
public class RuleRunner { public RuleRunner() { } public void runRules(String[] rules, Object[] facts) throws Exception { KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase(); KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); for ( int i = 0; i < rules.length; i++ ) { String ruleFile = rules[i]; System.out.println( "Loading file: " + ruleFile ); kbuilder.add( ResourceFactory.newClassPathResource( ruleFile, RuleRunner.class ), ResourceType.DRL ); } Collection<KnowledgePackage> pkgs = kbuilder.getKnowledgePackages(); kbase.addKnowledgePackages( pkgs ); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession(); for ( int i = 0; i < facts.length; i++ ) { Object fact = facts[i]; System.out.println( "Inserting fact: " + fact ); ksession.insert( fact ); } ksession.fireAllRules(); } }
The first of our sample Java classes loads and executes a single
DRL file, Example.drl
, but without inserting any
data.
Example 8.29. Banking Tutorial : Java Example1
public class Example1 { public static void main(String[] args) throws Exception { new RuleRunner().runRules( new String[] { "Example1.drl" }, new Object[0] ); } }
The first simple rule to execute has a single <kw>eval</kw> condition that will alway be true, so that this rule will match and fire, once, after the start.
Example 8.30. Banking Tutorial: Rule in Example1.drl
rule "Rule 01" when eval( 1==1 ) then System.out.println( "Rule 01 Works" ); endh
The output for the rule is below, showing that the rule matches and executes the single print statement.
The next step is to assert some simple facts and print them out.
Example 8.32. Banking Tutorial: Java Example2
public class Example2 { public static void main(String[] args) throws Exception { Number[] numbers = new Number[] {wrap(3), wrap(1), wrap(4), wrap(1), wrap(5)}; new RuleRunner().runRules( new String[] { "Example2.drl" }, numbers ); } private static Integer wrap( int i ) { return new Integer(i); } }
This doesn’t use any specific facts but instead asserts a set
of java.lang.Integer
objects. This is not considered
"best practice" as a number is not a useful fact, but we use it here
to demonstrate basic techniques before more complexity is added.
Now we will create a simple rule to print out these numbers.
Example 8.33. Banking Tutorial: Rule in Example2.drl
rule "Rule 02" when Number( $intValue : intValue ) then System.out.println( "Number found with value: " + $intValue ); end
Once again, this rule does nothing special. It identifies any
facts that are Number
objects and prints out the values.
Notice the use of the abstract class Number
: we inserted
Integer
objects but we now look for any kind of number.
The pattern matching engine is able to match interfaces and
superclasses of asserted objects.
The output shows the DRL being loaded, the facts inserted and then the matched and fired rules. We can see that each inserted number is matched and fired and thus printed.
Example 8.34. Banking Tutorial: Output of Example2.java
Loading file: Example2.drl Inserting fact: 3 Inserting fact: 1 Inserting fact: 4 Inserting fact: 1 Inserting fact: 5 Number found with value: 5 Number found with value: 1 Number found with value: 4 Number found with value: 1 Number found with value: 3
There are certainly many better ways to sort numbers than using rules, but since we will need to apply some cashflows in date order when we start looking at banking rules we'll develop simple rule based sorting technique.
Example 8.35. Banking Tutorial: Example3.java
public class Example3 { public static void main(String[] args) throws Exception { Number[] numbers = new Number[] {wrap(3), wrap(1), wrap(4), wrap(1), wrap(5)}; new RuleRunner().runRules( new String[] { "Example3.drl" }, numbers ); } private static Integer wrap(int i) { return new Integer(i); } }
Again we insert our Integer
objects, but this time the
rule is slightly different:
Example 8.36. Banking Tutorial: Rule in Example3.drl
rule "Rule 03" when $number : Number( ) not Number( intValue < $number.intValue ) then System.out.println("Number found with value: " + $number.intValue() ); retract( $number ); end
The first line of the rule identifies a Number
and
extracts the value. The second line ensures that there does not exist
a smaller number than the one found by the first pattern. We might
expect to match only one number - the smallest in the set. However,
the retraction of the number after it has been printed means that the
smallest number has been removed, revealing the next smallest number,
and so on.
The resulting output shows that the numbers are now sorted numerically.
Example 8.37. Banking Tutorial: Output of Example3.java
Loading file: Example3.drl Inserting fact: 3 Inserting fact: 1 Inserting fact: 4 Inserting fact: 1 Inserting fact: 5 Number found with value: 1 Number found with value: 1 Number found with value: 3 Number found with value: 4 Number found with value: 5
We are ready to start moving towards our personal accounting
rules. The first step is to create a Cashflow
object.
Example 8.38. Banking Tutorial: Class Cashflow
public class Cashflow { private Date date; private double amount; public Cashflow() { } public Cashflow(Date date, double amount) { this.date = date; this.amount = amount; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } public String toString() { return "Cashflow[date=" + date + ",amount=" + amount + "]"; } }
Class Cashflow
has two simple attributes, a date
and an amount. (Note that using the type double
for
monetary units is generally not a good idea
because floating point numbers cannot represent most numbers accurately.)
There is also an overloaded constructor to set the values, and a
method toString
to print a cashflow. The Java code of
Example4.java
inserts five Cashflow objects,
with varying dates and amounts.
Example 8.39. Banking Tutorial: Example4.java
public class Example4 { public static void main(String[] args) throws Exception { Object[] cashflows = { new Cashflow(new SimpleDate("01/01/2007"), 300.00), new Cashflow(new SimpleDate("05/01/2007"), 100.00), new Cashflow(new SimpleDate("11/01/2007"), 500.00), new Cashflow(new SimpleDate("07/01/2007"), 800.00), new Cashflow(new SimpleDate("02/01/2007"), 400.00), }; new RuleRunner().runRules( new String[] { "Example4.drl" }, cashflows ); } }
The convenience class SimpleDate
extends
java.util.Date
, providing a constructor taking
a String as input and defining a date format. The code is
listed below
Example 8.40. Banking Tutorial: Class SimpleDate
public class SimpleDate extends Date { private static final SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); public SimpleDate(String datestr) throws Exception { setTime(format.parse(datestr).getTime()); } }
Now, let’s look at Example4.drl
to see how
we print the sorted Cashflow
objects:
Example 8.41. Banking Tutorial: Rule in Example4.drl
rule "Rule 04" when $cashflow : Cashflow( $date : date, $amount : amount ) not Cashflow( date < $date) then System.out.println("Cashflow: "+$date+" :: "+$amount); retract($cashflow); end
Here, we identify a Cashflow
and extract the date
and the amount. In the second line of the rule we ensure that there
is no Cashflow with an earlier date than the one found. In the
consequence, we print the Cashflow
that satisfies the
rule and then retract it, making way for the next earliest
Cashflow
. So, the output we generate is:
Example 8.42. Banking Tutorial: Output of Example4.java
Loading file: Example4.drl Inserting fact: Cashflow[date=Mon Jan 01 00:00:00 GMT 2007,amount=300.0] Inserting fact: Cashflow[date=Fri Jan 05 00:00:00 GMT 2007,amount=100.0] Inserting fact: Cashflow[date=Thu Jan 11 00:00:00 GMT 2007,amount=500.0] Inserting fact: Cashflow[date=Sun Jan 07 00:00:00 GMT 2007,amount=800.0] Inserting fact: Cashflow[date=Tue Jan 02 00:00:00 GMT 2007,amount=400.0] Cashflow: Mon Jan 01 00:00:00 GMT 2007 :: 300.0 Cashflow: Tue Jan 02 00:00:00 GMT 2007 :: 400.0 Cashflow: Fri Jan 05 00:00:00 GMT 2007 :: 100.0 Cashflow: Sun Jan 07 00:00:00 GMT 2007 :: 800.0 Cashflow: Thu Jan 11 00:00:00 GMT 2007 :: 500.0
Next, we extend our Cashflow
, resulting in a
TypedCashflow
which can be a credit or a debit operation.
(Normally, we would just add this to the Cashflow
type, but
we use extension to keep the previous version of the class intact.)
Example 8.43. Banking Tutorial: Class TypedCashflow
public class TypedCashflow extends Cashflow { public static final int CREDIT = 0; public static final int DEBIT = 1; private int type; public TypedCashflow() { } public TypedCashflow(Date date, int type, double amount) { super( date, amount ); this.type = type; } public int getType() { return type; } public void setType(int type) { this.type = type; } public String toString() { return "TypedCashflow[date=" + getDate() + ",type=" + (type == CREDIT ? "Credit" : "Debit") + ",amount=" + getAmount() + "]"; } }
There are lots of ways to improve this code, but for the sake of the example this will do.
Now let's create Example5, a class for running our code.
Example 8.44. Banking Tutorial: Example5.java
public class Example5 { public static void main(String[] args) throws Exception { Object[] cashflows = { new TypedCashflow(new SimpleDate("01/01/2007"), TypedCashflow.CREDIT, 300.00), new TypedCashflow(new SimpleDate("05/01/2007"), TypedCashflow.CREDIT, 100.00), new TypedCashflow(new SimpleDate("11/01/2007"), TypedCashflow.CREDIT, 500.00), new TypedCashflow(new SimpleDate("07/01/2007"), TypedCashflow.DEBIT, 800.00), new TypedCashflow(new SimpleDate("02/01/2007"), TypedCashflow.DEBIT, 400.00), }; new RuleRunner().runRules( new String[] { "Example5.drl" }, cashflows ); } }
Here, we simply create a set of Cashflow
objects
which are either credit or debit operations. We supply them and
Example5.drl
to the RuleEngine.
Now, let’s look at a rule printing the sorted
Cashflow
objects.
Example 8.45. Banking Tutorial: Rule in Example5.drl
rule "Rule 05" when $cashflow : TypedCashflow( $date : date, $amount : amount, type == TypedCashflow.CREDIT ) not TypedCashflow( date < $date, type == TypedCashflow.CREDIT ) then System.out.println("Credit: "+$date+" :: "+$amount); retract($cashflow); end
Here, we identify a Cashflow
fact with a type
of CREDIT
and extract the date and the amount. In the
second line of the rule we ensure that there is no Cashflow
of the same type with an earlier date than the one found. In the
consequence, we print the cashflow satisfying the patterns and then
retract it, making way for the next earliest cashflow of type
CREDIT
.
So, the output we generate is
Example 8.46. Banking Tutorial: Output of Example5.java
Loading file: Example5.drl Inserting fact: TypedCashflow[date=Mon Jan 01 00:00:00 GMT 2007,type=Credit,amount=300.0] Inserting fact: TypedCashflow[date=Fri Jan 05 00:00:00 GMT 2007,type=Credit,amount=100.0] Inserting fact: TypedCashflow[date=Thu Jan 11 00:00:00 GMT 2007,type=Credit,amount=500.0] Inserting fact: TypedCashflow[date=Sun Jan 07 00:00:00 GMT 2007,type=Debit,amount=800.0] Inserting fact: TypedCashflow[date=Tue Jan 02 00:00:00 GMT 2007,type=Debit,amount=400.0] Credit: Mon Jan 01 00:00:00 GMT 2007 :: 300.0 Credit: Fri Jan 05 00:00:00 GMT 2007 :: 100.0 Credit: Thu Jan 11 00:00:00 GMT 2007 :: 500.0
Continuing our banking exercise, we are now going to process both
credits and debits on two bank accounts, calculating the account balance.
In order to do this, we create two separate Account
objects
and inject them into the Cashflows
objects before passing
them to the Rule Engine. The reason for this is to provide easy access
to the correct account without having to resort to helper classes. Let’s
take a look at the Account
class first. This is a simple
Java object with an account number and balance:
Example 8.47. Banking Tutorial: Class Account
public class Account { private long accountNo; private double balance = 0; public Account() { } public Account(long accountNo) { this.accountNo = accountNo; } public long getAccountNo() { return accountNo; } public void setAccountNo(long accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public String toString() { return "Account[" + "accountNo=" + accountNo + ",balance=" + balance + "]"; } }
Now let’s extend our TypedCashflow
, resulting in
AllocatedCashflow
, to include an Account
reference.
Example 8.48. Banking Tutorial: Class AllocatedCashflow
public class AllocatedCashflow extends TypedCashflow { private Account account; public AllocatedCashflow() { } public AllocatedCashflow(Account account, Date date, int type, double amount) { super( date, type, amount ); this.account = account; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } public String toString() { return "AllocatedCashflow[" + "account=" + account + ",date=" + getDate() + ",type=" + (getType() == CREDIT ? "Credit" : "Debit") + ",amount=" + getAmount() + "]"; } }
The Java code of Example5.java
creates
two Account
objects and passes one of them into each
cashflow, in the constructor call.
Example 8.49. Banking Tutorial: Example5.java
public class Example6 { public static void main(String[] args) throws Exception { Account acc1 = new Account(1); Account acc2 = new Account(2); Object[] cashflows = { new AllocatedCashflow(acc1,new SimpleDate("01/01/2007"), TypedCashflow.CREDIT, 300.00), new AllocatedCashflow(acc1,new SimpleDate("05/02/2007"), TypedCashflow.CREDIT, 100.00), new AllocatedCashflow(acc2,new SimpleDate("11/03/2007"), TypedCashflow.CREDIT, 500.00), new AllocatedCashflow(acc1,new SimpleDate("07/02/2007"), TypedCashflow.DEBIT, 800.00), new AllocatedCashflow(acc2,new SimpleDate("02/03/2007"), TypedCashflow.DEBIT, 400.00), new AllocatedCashflow(acc1,new SimpleDate("01/04/2007"), TypedCashflow.CREDIT, 200.00), new AllocatedCashflow(acc1,new SimpleDate("05/04/2007"), TypedCashflow.CREDIT, 300.00), new AllocatedCashflow(acc2,new SimpleDate("11/05/2007"), TypedCashflow.CREDIT, 700.00), new AllocatedCashflow(acc1,new SimpleDate("07/05/2007"), TypedCashflow.DEBIT, 900.00), new AllocatedCashflow(acc2,new SimpleDate("02/05/2007"), TypedCashflow.DEBIT, 100.00) }; new RuleRunner().runRules( new String[] { "Example6.drl" }, cashflows ); } }
Now, let’s look at the rule in Example6.drl
to see how we apply each cashflow in date order and calculate and print
the balance.
Example 8.50. Banking Tutorial: Rule in Example6.drl
rule "Rule 06 - Credit" when $cashflow : AllocatedCashflow( $account : account, $date : date, $amount : amount, type == TypedCashflow.CREDIT ) not AllocatedCashflow( account == $account, date < $date) then System.out.println("Credit: " + $date + " :: " + $amount); $account.setBalance($account.getBalance()+$amount); System.out.println("Account: " + $account.getAccountNo() + " - new balance: " + $account.getBalance()); retract($cashflow); end rule "Rule 06 - Debit" when $cashflow : AllocatedCashflow( $account : account, $date : date, $amount : amount, type == TypedCashflow.DEBIT ) not AllocatedCashflow( account == $account, date < $date) then System.out.println("Debit: " + $date + " :: " + $amount); $account.setBalance($account.getBalance() - $amount); System.out.println("Account: " + $account.getAccountNo() + " - new balance: " + $account.getBalance()); retract($cashflow); end
Although we have separate rules for credits and debits, but we do not specify a type when checking for earlier cashflows. This is so that all cashflows are applied in date order, regardless of the cashflow type. In the conditions we identify the account to work with, and in the consequences we update it with the cashflow amount.
Example 8.51. Banking Tutorial: Output of Example6.java
Loading file: Example6.drl Inserting fact: AllocatedCashflow[account=Account[accountNo=1,balance=0.0],date=Mon Jan 01 00:00:00 GMT 2007,type=Credit,amount=300.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=1,balance=0.0],date=Mon Feb 05 00:00:00 GMT 2007,type=Credit,amount=100.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=2,balance=0.0],date=Sun Mar 11 00:00:00 GMT 2007,type=Credit,amount=500.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=1,balance=0.0],date=Wed Feb 07 00:00:00 GMT 2007,type=Debit,amount=800.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=2,balance=0.0],date=Fri Mar 02 00:00:00 GMT 2007,type=Debit,amount=400.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=1,balance=0.0],date=Sun Apr 01 00:00:00 BST 2007,type=Credit,amount=200.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=1,balance=0.0],date=Thu Apr 05 00:00:00 BST 2007,type=Credit,amount=300.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=2,balance=0.0],date=Fri May 11 00:00:00 BST 2007,type=Credit,amount=700.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=1,balance=0.0],date=Mon May 07 00:00:00 BST 2007,type=Debit,amount=900.0] Inserting fact: AllocatedCashflow[account=Account[accountNo=2,balance=0.0],date=Wed May 02 00:00:00 BST 2007,type=Debit,amount=100.0] Debit: Fri Mar 02 00:00:00 GMT 2007 :: 400.0 Account: 2 - new balance: -400.0 Credit: Sun Mar 11 00:00:00 GMT 2007 :: 500.0 Account: 2 - new balance: 100.0 Debit: Wed May 02 00:00:00 BST 2007 :: 100.0 Account: 2 - new balance: 0.0 Credit: Fri May 11 00:00:00 BST 2007 :: 700.0 Account: 2 - new balance: 700.0 Credit: Mon Jan 01 00:00:00 GMT 2007 :: 300.0 Account: 1 - new balance: 300.0 Credit: Mon Feb 05 00:00:00 GMT 2007 :: 100.0 Account: 1 - new balance: 400.0 Debit: Wed Feb 07 00:00:00 GMT 2007 :: 800.0 Account: 1 - new balance: -400.0 Credit: Sun Apr 01 00:00:00 BST 2007 :: 200.0 Account: 1 - new balance: -200.0 Credit: Thu Apr 05 00:00:00 BST 2007 :: 300.0 Account: 1 - new balance: 100.0 Debit: Mon May 07 00:00:00 BST 2007 :: 900.0 Account: 1 - new balance: -800.0