Saturday 13 October 2007

Banking on Drools - Part I

For the last few months I've been working at the European Bank for Reconstruction and Development on a project using DROOLS. We started with DROOLS 3 and have more recently moved to DROOLS 4.

I am not allowed to use actual examples from work so this blog will instead document by example the process of developing a complete personal banking application that will handle credits, debits, currencies and that will use a set of design patterns that I have created for the process.

In order to make the examples documented here clear and modular, I will try and steer away from re-visiting existing code to add new functionality, and will instead extend and inject where appropriate.

From my own personal experience, DROOLS is an excellent choice for banking and financial rules and through use of DROOLS I have adopted my own standards.

- Ideally the Facts that are asserted (I know that in 4 they are inserted, but I prefer the term asserted) into the DROOLS engine should be decoupled from the banking data and I prefer to inject helper objects into the facts being asserted rather than use helper classes or globals for calculations or storage of intermediate values - I refer to this as the Calculator Pattern later in the blog.

- Helper classes I use for logging rule activity and those helper classes can print to a file or use log4j as you wish.

- For the creation of additional Facts at rule-time, I use a RuleFactory object that is supplied as a global - I think in more complicated rulesets, it would be better injecting it into a Fact or Facts somewhere, but so far I have not needed to go that far.

The examples here are simplistic and are presented as demonstration and discussion documents so any feedback is welcome.

Finally, this is my first attempt at blogging so bear with me.

Step 1 - A Simple Example


I am using the latest version of JBOSS Rules, with Eclipse Europa and the JBOSS Rules plug-in for Eclipse.  I haven't got the links to hand at the moment, so I will add them later.

My first task is to write a simple class to interface with the Rule Engine, and a simple rule just to see it all hanging together.  Here we go:

RuleRunner.java

package com.javarepository.rules;
import java.io.InputStreamReader;
import org.drools.RuleBase;
import org.drools.RuleBaseFactory;
import org.drools.WorkingMemory;
import org.drools.compiler.PackageBuilder;
import org.drools.rule.Package;

public class RuleRunner
{
    public RuleRunner(){}
  
    public void runRules(String[] rules, Object... facts)
    throws Exception
    {      
        RuleBase ruleBase = RuleBaseFactory.newRuleBase();
        PackageBuilder builder = new PackageBuilder();      
      
        for(String ruleFile : rules)
        {
            System.out.println("Loading file: "+ruleFile);
            builder.addPackageFromDrl(
                    new InputStreamReader(
                        this.getClass().getResourceAsStream(ruleFile)));
        }
      
        Package pkg = builder.getPackage();      
        ruleBase.addPackage(pkg);
      
        WorkingMemory workingMemory
            = ruleBase.newStatefulSession();  

        for(Object fact : facts)
        {
            System.out.println("Inserting fact: "+fact);
            workingMemory.insert(fact);
        }

        workingMemory.fireAllRules();
    }

    public static void simple1()
    throws Exception
    {
        new RuleRunner().runRules(
                new String[]{"/simple/rule01.drl"});      
    }      

    public static void main(String[] args)
    throws Exception
    {
        simple1();
    }
}

And a simple rule to run:

rule01.drl

package simple
rule "Rule 01"
    when
        eval (1==1)
    then
        System.out.println("Rule 01 Works");      
end

I'm not going to go into too much detail about what is happening here.  Suffice it to say that eval (1==1) will always return true and so we should get an output of:

output:

Loading file: /simple/rule01.drl
Rule 01 Works


Step 2 - Introducing Facts


My next step is to assert some simple facts and print them out.

Let's add the method simple2() to the RuleRunner class (not forgetting to call it in the main method).

simple2() method from RuleRunner.java

public static void simple2()
throws Exception
{
    Number n1=3, n2=1, n3=4, n4=1, n5=5;
    new RuleRunner().runRules(
            new String[]{"/simple/rule02.drl"},
            n1,n2,n3,n4,n5);      
}

This doesn't use any specific facts but instead asserts a set of java.lang.Number's

Now we will create a simple rule to print out these facts.

rule02.drl

package simple
rule "Rule 02 - Number Printer"
    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 Numbers and prints out the values.  So what would we expect and what do we get?

From the inputs, we might expect

output:

Loading file: /simple/rule02.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

but what we actually get is:

output:

Loading file: /simple/rule02.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: 4
Number found with value: 1
Number found with value: 3

My first instinct was the think that this is actually a feature of Drools.  It is instead a feature of autoboxing.  Reading the DROOLS documentation (and an email from Mark Proctor) reveals that by default the DROOLS WorkingMemory uses an IdentityHashMap to store all the asserted Objects.  The following simple test, generates the same output as above, demonstrating that the constant value of 1 is stored only once in the runtime constant pool and two Integers autoboxed from this constant will actually be the same physical object and so will not be duplicated in the IdentityHashMap keyset.

TestIdentityHashMap.java

package com.javarepository.test;
import java.util.IdentityHashMap;

public class TestIdentityHashMap
{
    public static void main(String[] args)
    {
        IdentityHashMap<Object,Object< map = new IdentityHashMap<Object,Object<();
        Number n1=3, n2=1, n3=4, n4=1, n5=5;
        map.put(n1,n1);
        map.put(n2,n2);
        map.put(n3,n3);
        map.put(n4,n4);
        map.put(n5,n5);

        System.out.println(n2==n4);

        for(Object key : map.keySet())
        {
            System.out.println(key);
        }
    }
}

output:

true
4
1
3
5

Further documentation on how the jvm stores primitives and objects can be found at

http://java.sun.com/docs/books/jvms/second_edition/html/Overview.doc.html

Step 3 - Sorting Numbers


There are probably a hundred and one better ways to sort numbers; but we will need to apply some cashflows in date order when we start looking at banking rules so let's look at a simple rule based example.

simple3() method from RuleRunner.java

public static void simple3()
throws Exception
{
    Number n1=3, n2=1, n3=4, n4=1, n5=5;
    new RuleRunner().runRules(
        new String[]{"/simple/rule03.drl"},
        n1,n2,n3,n4,n5);      
}

Actually, this method is exactly the same as simple2() with the exception that it supplies rule03.drl to the Rule Engine.

Now let's look at the rule that will sort our numbers:

rule03.drl

package simple
rule "Rule 03"
    when
        $number : Number( $intValue : intValue )
        not Number( intValue < $intValue)
    then
        System.out.println("Number found with value: "
            +$intValue);  
    retract($number);
end

The first line of the rules identifies a Number and extracts the value.  The second line ensures that there does not exist a smaller number than the one found.  By executing this rule, we might expect to find 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.

So, the output we generate is

output:

Loading file: /simple/rule03.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: 3
Number found with value: 4
Number found with value: 5

I've not tried any timings with this approach but would be interested to compare this with a sorting algorithm.

Step 4 - Sorting Cashflows



Now we want to start moving towards our personal accounting rules.  The first step is to create a Cashflow POJO.

Cashflow.java

package com.javarepository.rules;
import java.util.Date;
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+"]";
    }
}

The Cashflow has two simple attributes, a date and an amount.  I have added a toString method to print it and overloaded the constructor to set the values.

Now, let's add the method simple4() to RuleRunner.

simple4() method from RuleRunner.java

    public static void simple4()
    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[]{"/simple/rule04.drl"},
            cashflows);      
}

Here, we simply create a set of Cashflows and supply them and rule04.drl to the RuleEngine.

SimpleDate is a simple class that extends Date and takes a String as input. The code is listed below

SimpleDate.java

package com.javarepository.rules;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDate extends Date
{
     public SimpleDate(String datestr)
    throws Exception
    {
        SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy");
        this.setTime(format.parse(datestr).getTime());
    }
}

Now, let's look at rule04.drl to see how we print the sorted Cashflows:

rule04.drl

package simple
import com.javarepository.rules.*;
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 rules we ensure that there is not a Cashflow with an earlier date than the one found.  In the consequences, we print the Cashflow that satisfies the rules and then retract it, making way for the next earliest Cashflow.

So, the output we generate is:

output:

Loading file: /simple/rule04.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

Step 5 - Processing Credits



Here we extend our Cashflow to give a TypedCashflow which can be CREDIT or DEBIT.  Ideally, we would just add this to the Cashflow type, but so that we can keep all the examples simple, we will go with the extensions.

TypedCashflow.java

package com.javarepository.rules;
import java.util.Date;

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 add the method simple5() to RuleRunner.

simple5() method from RuleRunner.java

public static void simple5()
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[]{"/simple/rule05.drl"},
        cashflows);      
}

Here, we simply create a set of Cashflows which are either CREDIT or DEBIT Cashflows and supply them and rule05.drl to the RuleEngine.

Now, let's look at rule05.drl to see how we print the sorted Cashflows:

rule05.drl

package simple
import com.javarepository.rules.*;

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 with a type of CREDIT and extract the date and the amount.  In the second line of the rules we ensure that there is not a Cashflow of type CREDIT with an earlier date than the one found.  In the consequences, we print the Cashflow that satisfies the rules and then retract it, making way for the next earliest Cashflow of type CREDIT.

So, the output we generate is

output:

Loading file: /simple/rule05.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

Step 6 - Processing Credits and Debits


Here we are going to process both CREDITs and DEBITs on 2 bank accounts to calculate the account balance.  In order to do this, I am going to create two separate Account Objects and inject them into the Cashflows before passing them to the Rule Engine.  The reason for this is to provide easy access to the correct Bank Accounts without having to resort to Helper classes.

Let's take a look at the Account class first.  This is a simple POJO with an account number and balance:

Account.java

package com.javarepository.rules;

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 to give AllocatedCashflow (allocated to an account).

AllocatedCashflow.java

package com.javarepository.rules;
import java.util.Date;

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()+"]";
    }
}

Now, let's add the method simple6() to RuleRunner.  Here we create two Account objects and inject one into each cashflow as appropriate.  For simplicity I have simply included them in the constructor.

simple6() method from RuleRunner.java

public static void simple6()
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[]{"/simple/rule06.drl"},
        cashflows);      
}

Now, let's look at rule06.drl to see how we apply each cashflow in date order and calculate and print the balance.

rule06.drl

package simple;
import com.javarepository.rules.*;
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

Here, we have separate rules for CREDITs and DEBITs, however we do not specify a type when checking for earlier cashflows.  This is so that all cashflows are applied in date order regardless of which type of cashflow type they are.  In the rule section we identify the correct account to work with and in the consequences we update it with the cashflow amount.

output:

Loading file: /simple/rule06.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

The Source


The source for this blog entry can be downloaded from
here