Chapter 5. Integrating Business Rules with JBoss Rules

This chapter introduces Rule Engines and the JBoss Rules open-source rule engine. We show the ability to integrate the JBoss Rules engine into OSWorkflow. This capability eases the implementation of real-world business processes along with complex business rules. We approach the chapter with an example found in the banking domain, a loan-risk process.

Incorporating Business Logic into Business Processes

Each task in a business process can be performed automatically. When a task can be done automatically, it's because the business process has embedded knowledge about the task and can make logical decisions about it. This knowledge can be in the form of program code or another knowledge representation, such as a decision table.

This knowledge about a process is called business logic or business rules. For instance, a business rule in a bank loan process can be 'validate the amount requested versus the amount limit; if it exceeds the amount limit cancel the loan'. All business rules clearly have two parts—the 'if' or condition part, and the 'then' or consequence part. If the condition is true, then the consequence is executed. This structure will be clearer later on in the chapter.

There are two ways to embed this knowledge inside our OSWorkflow business processes, depending on the type of rules. For most simple rules, OSWorkflow Conditions are more than enough, but for complex logic a Business Rules Management System is needed. In this chapter we will cover both ways—the Condition-based approach and JBoss Rules, an open-source Java Rule Engine designed for the execution and inference of complex and sophisticated rules.

Simple Logic with Conditional Results

Most business processes have a simple logic in the form of decisions: branch points that follow a condition predicate. The Conditions construct in OSWorkflow serves as a branch point for workflows with conditional results.

Remember that Conditions can be BeanShell scripts or Java classes implementing com.opensymphony.workflow.Condition.

In this chapter we'll use an example from the financial domain, a simple loan request processes performed in every bank. This process has been depicted in the following figure.

Simple Logic with Conditional Results

The business process begins when the bank customer hands in the signed loan-application form. This form contains several personal, financial, and employment data such as social security number, loan amount, annual salary, and so on.

This data is used to calculate a risk index (RI) with a loan-risk algorithm. If this index is above a certain threshold the loan is rejected, otherwise it is approved.

This is an oversimplified risk index calculation algorithm for loans from a typical bank:

  • Annual Earnings

    • If annual earnings > amount requested, then risk A= 0.0

    • If annual earnings >= 50% amount requested, then risk A= 0.5

    • If annual earnings < 50% amount, then risk A = 1.0

  • Previous Credit History

    • If previous credit is paid, then risk B = 0.0

    • If previous credit is unpaid, then risk B = 0.5

    • If there is a legal situation, then risk B = 1.0

  • Marital Status

    • If single, then risk C = 0.5

    • If married, then risk C = 0.0

  • Final Risk Index = (risk A + risk B + risk C)/3

If the final risk index is below 0.4, then the loan is approved automatically as the risk involved is low. If the index is between 0.4 and 0.7, then a manual approval or rejection is needed. If the index is over 0.7, then the loan is denied automatically as the risk involved is too high.

All the customer data in the form is passed to OSWorkflow in the form of a Loan JavaBean having the following structure:

package packtpub.osw;
/**
* Money Loan Request
*/
public class LoanRequest {
private float amount;
private String customerName;
private boolean married;
private float annualEarnings;
private boolean creditHistory;
private boolean previousCreditPaid;
private boolean previousLegalSituation;
...getters/setters
}

An instance of this object is passed on to the inputs map of the initialize() and doAction() methods. The OSWorkflow XML definition file is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow PUBLIC "-//OpenSymphony Group//DTD OSWorkflow
2.6//EN" "http://www.opensymphony.com/osworkflow/workflow_2_8.dtd">
<workflow>
<initial-actions>
<action id="100" name="Start Workflow">
<results>
<unconditional-result old-status="Finished"
status="Customer handles form" step="1" />
</results>
</action>
</initial-actions>
<steps>
<step id="1" name="Risk index calculation">
<actions>
<action id="1" name="Calculate" >
<results>
<result old-status="Calculation FinishedA"
status="Approved" step="2">
<conditions type="OR">
<condition type="beanshell">
<arg name="script">
<![CDATA[packtpub.osw.LoanRequest lr
= (packtpub.osw.LoanRequest)transientVars.get
("loanRequest");System.out.println
("Amount:" + lr.getAmount());
float riskA = 0.0f;
float riskB = 0.0f;
float riskC = 0.0f;
if(lr.getAnnualEarnings() > lr.getAmount())
{
riskA = 0.0f;
}else
if(lr.getAnnualEarnings() >= (lr.getAmount()/2))
{
riskA = 0.5f;
} else
{
riskA = 1.0f;
}
if(lr.hasCreditHistory())
{
if(lr.getPreviousCreditPaid())
{
riskB = 0.0f;
}else{
riskB = 0.5f;
}
if(lr.isPreviousLegalSituation())
{
riskB = 1.0f;
}
}
if(!lr.isMarried())
OSWorkflowcustomer data, passing through a JavaBean{
riskC = 0.5f;
}
System.out.println("riskA" + riskA);
System.out.println("riskB" + riskB);
System.out.println("riskC" + riskC);
float finalRisk = ((riskA + riskB + riskC)/3);
System.out.println("finalRisk:" + finalRisk);
if(finalRisk < 0.4f)
{
System.out.println("Approved");
return true;
}
return false;
]]>
</arg>
</condition>
</conditions>
</result>
<unconditional-result old-status="Calculation Finished"
step="2" status="Denied"/>
</results>
</action>
</actions>
</step>
<step id="2" name="Risk index calculation done" >
</step>
</steps>
</workflow>

This is a standard definition consisting of two steps, but the key part of the definition is the conditional result of step one. If the condition is true (the risk index allows automatic approval), then the status of the final step is set to Approved. If the index is high, then the status is set to Denied. The status of the business process is derived from the output of the risk index.

Notice that all the code required to calculate the index is embedded in the definition; we can put it into a Java Class but we will lose dynamicity. Another option available is putting the code into a Rule Engine.

Complex Business Logic

In the previous example, for each business rule, a BeanShell Condition was needed. This can really clutter the definition. To reuse rules and to program more complex logic, we need a more powerful tool. This tool is a Business Rule Management System (BRMS).

BRMS is a component inside a BPMS solution. It emerged out of a need to create, execute, maintain, and organize business logic and rules for use inside and outside business processes. The key part of the BRMS is the rule engine.

A rule engine decouples the business rules from the code and externalizes them outside the code. Each change to a business rule doesn't impact the process definition or the program code. On the other hand, it increases the complexity of the solution.

What is "JBoss Rules"?

JBoss Rules is a Rete-based rule engine. This means that the engine evaluates the condition each time a new fact is introduced in the working memory. A fact is just an object and the working memory is the object scope that the rules are allowed to see. Each rule is composed of a condition and a consequence.

If the condition is true, the consequence is executed. If multiple conditions turn true, then the order of consequence execution is defined by a conflict-resolution strategy. This is better explained in the following figure:

What is "JBoss Rules"?

The JBoss Rules architecture is as follows:

What is "JBoss Rules"?

Rules cannot be fired in isolation. Every time you assert an object in the working memory and call the fireAllRules() method, the engine will try to match each rule predicate with the working memory contents and all that match will be executed. The rules that match are ordered into an Agenda. This agenda changes every time an object is asserted into the working memory.

This is a clearly a paradigm shift from the procedural approach of regular Java code. This is called a declarative approach to logic. You declare the rules, but the order and activation of the rules depend on the data you input to the engine.

Creating a Rule

JBoss Rules has a proprietary format for rule writing, called DRL. DRLs are simply text files. A JBoss Rules rule is an expression of the form:

Rule ""
when
<conditions>
then
<actions>
end

This structure is analogous to:

if ( <LHS> ) {
<RHS>
}

LHS and RHS stand for Left Hand Side and Right Hand Side respectively. Each rule has a namespace associated with it called a package. The RHS is standard Java code, but the LHS has a particular syntax, which is described later.

Executing a Rule

Once you have written the rule, the execution is very easy. The following code snippet shows you how:

try
{
PackageBuilder builder = new PackageBuilder();
builder.addPackageFromDrl(new InputStreamReader(getClass()
.getResourceAsStream("/loan.drl")));

First, we create a PackageBuilder and load the DRL file loan.drl from the classpath containing the rules into the builder.

org.drools.rule.Package pkg = builder.getPackage();
RuleBase ruleBase = RuleBaseFactory.newRuleBase();
ruleBase.addPackage(pkg);

Rules are grouped logically into RuleBases (a logical grouping of rules, which are cohesive) and Packages (these are compiled and optimized rules files). Thus, we get the package object from the rules file and create a RuleBase to execute the rules.

WorkingMemory workingMemory = ruleBase.newWorkingMemory();
Loan loan = new Loan();
loan.setAmount(100000);
FactHandle handle = workingMemory.assertObject(loan);

After the rules and packages are loaded, we create a WorkingMemory from the RuleBase. This WorkingMemory stores the transient objects that are within the scope during the execution of rules. You can create as many WorkingMemories as you want.

The Loan object instantiated is a common JavaBean referenced in the rules and the Amount is a simple JavaBean float type property. Once instantiated, we assert the Loan into the working memory, thus giving the rules sight of the object. The handle object is a reference to the object asserted into the WorkingMemory; it's useful for tracking purposes.

workingMemory.fireAllRules();

We end by firing the rules. This firing searches for the right rule to execute, taking into account the conflict-resolution strategy.

The complete code fragment is as follows:

try
{
PackageBuilder builder = new PackageBuilder();
builder.addPackageFromDrl(new InputStreamReader(getClass()
.getResourceAsStream("/loan.drl")));
org.drools.rule.Package pkg = builder.getPackage();
RuleBase ruleBase = RuleBaseFactory.newRuleBase();
ruleBase.addPackage(pkg);
WorkingMemory workingMemory = ruleBase.newWorkingMemory();
Loan loan = new Loan();
loan.setAmount(100000);
FactHandle handle = workingMemory.assertObject(loan);
workingMemory.fireAllRules();
}
catch (Exception e)
{
e.printStackTrace();
}

Integrating with OSWorkflow

JBoss Rules integration with OSWorkflow is performed by a Condition and a FunctionProvider. Condition gives the rules the flow of control of the process; the FunctionProvider simply executes rules at a designated moment.

RulesCondition

The RulesCondition is like any other OSWorkflow Condition and implements com.opensymphony.workflow.Condition. The helper object RuleConditionalResult is available in every rule set executed through the Condition to mark the return value of the Condition predicate. The return value of the condition is the RuleConditionalResult.getResult() result.

Also, the transientVars and arguments map contents are passed directly to the JBoss Rules WorkingMemory.

The only mandatory parameter of the Condition is the ruleName. This parameter indicates which DRL file from the classpath will be loaded and executed.

You can append other parameters in the invocation, which will be available as objects inside the WorkingMemory. Remember that variable interpolation is also applicable.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE workflow PUBLIC "-//OpenSymphony Group//DTD OSWorkflow
2.6//EN" "http://www.opensymphony.com/osworkflow/workflow_2_8.dtd">
<workflow>
<initial-actions>
<action id="100" name="Start Workflow">
<results>
<unconditional-result old-status="Finished"
status="Customer handles form" step="1" />
</results>
</action>
</initial-actions>
<steps>
<step id="1" name="Risk index calculation">
<actions>
<action id="1" name="Calculate" >
<results>
<result old-status="Calculation FinishedA"
status="Approved" step="2">
<conditions type="OR">
<condition type="class">
<arg name="class.name"packtpub.osw.RuleCondition
</arg>
<arg name="ruleName">/loan.drl
</arg>
</condition>
</conditions>
</result>
<unconditional-result old-status=
"Calculation Finished"step="2" status="Denied" />
</results>
</action>
</actions>
</step>
<step id="2" name="Risk index calculation done" >
</step>
/steps>
</workflow>

Clearly, this definition is smaller and cleaner. We have decoupled the business logic for the index calculation and put it in an external rule file. Even if the algorithm changes, the process definition remains untouched.

The code for the custom JBoss Rules Condition is very small:

package packtpub.osw;
import java.util.Iterator;
import java.util.Map;
import org.drools.WorkingMemory;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.workflow.Condition;
import com.opensymphony.workflow.WorkflowException;
/**
* Drools Condition
*/
public class RuleCondition implements Condition
{
public boolean passesCondition(Map transientVars, Map args,
PropertySet arg2) throws WorkflowException
{
WorkingMemory wm = DroolsHelper.setupRules
((String)args.get("ruleName"));
RuleConditionalResult result = new RuleConditionalResult();
for (Iterator iter = transientVars.values().iterator();
iter.hasNext();)
{
wm.assertObject(iter.next());
}
wm.assertObject(result);
wm.fireAllRules();
System.out.println("Condition:" + result.getResult());
return result.getResult();
}
}

It uses a DroolsHelper to load the DRL file and instantiate a WorkingMemory. Then it puts all the contents of the transientsVars map into the working memory along with an instance of the RuleConditionalResult class. Finally, it fires the rules returning the Boolean result of the RuleConditionalResult instance.

The following code is the helper return holder, the RuleConditionalResult:

package packtpub.osw;
/**
* Holder of rule result for conditions.
*/
public class RuleConditionalResult{
private boolean result;
/**
* @return the result
*/
public boolean getResult() {
return result;
}
/**
* @param result the result to set
*/
public void setResult(boolean result) {
this.result = result;
}
}

It's just a holder for a boolean primitive. A third helper class used in conjunction with the JBoss Rules FunctionProvider and Condition is the DroolsHelper.

package packtpub.osw;
import java.io.InputStreamReader;
import org.drools.RuleBase;
import org.drools.RuleBaseFactory;
import org.drools.WorkingMemory;
import org.drools.compiler.PackageBuilder;
/**
* Helper for JBoss Rules integration.
*/
public class DroolsHelper
{
private static WorkingMemory workingMemory;
public static WorkingMemory setupRules(String ruleFile)
{
try
{
PackageBuilder builder = new PackageBuilder();
builder.addPackageFromDrl(new InputStreamReader
(DroolsHelper.class getResourceAsStream(ruleFile)));
org.drools.rule.Package pkg = builder.getPackage();
RuleBase ruleBase = RuleBaseFactory.newRuleBase();
ruleBase.addPackage(pkg);
workingMemory = ruleBase.newWorkingMemory();
}catch (Exception e)
{
e.printStackTrace();
}
return workingMemory;
}
}

It does all the work of loading the DRL file, adding a Package to a RuleBase, and instantiating a new WorkingMemory for them. Finally, the DRL file for the risk index calculation (almost the same as the previous BeanShell implementation) is as follows:

package LoanRequestRules;
rule "Calculate"
when
lr: packtpub.osw.LoanRequest();
result: packtpub.osw.RuleConditionalResult();
then
System.out.println( "loan" + lr);
System.out.println("Amount:" + lr.getAmount());
double riskA = 0.0;
double riskB = 0.0;
double riskC = 0.0;
if(lr.getAnnualEarnings() > lr.getAmount())
{
riskA = 0.0;
}else
if(lr.getAnnualEarnings() >= (lr.getAmount()/2))
{
riskA = 0.5;
} else {
riskA = 1.0;
}
if(lr.hasCreditHistory())
{
if(lr.getPreviousCreditPaid())
{
riskB = 0.0;
}else
{
riskB = 0.5;
}
if(lr.isPreviousLegalSituation())
{
riskB = 1.0;
}
}
if(!lr.isMarried()){
riskC = 0.5;
}
System.out.println("riskA" + riskA);
System.out.println("riskB" + riskB);
System.out.println("riskC" + riskC);
double finalRisk = ((riskA + riskB + riskC)/3);
System.out.println("finalRisk:" + finalRisk);
if(finalRisk < 0.4){
System.out.println("approved");
result.setResult(true);
}
end

After reading the DRL file, you may notice the when structure:

when
lr: packtpub.osw.LoanRequest();
result: packtpub.osw.RuleConditionalResult();

This tells JBoss Rules to search the WorkingMemory for objects of that type and bind them to the variable name before the colons. After binding, the variables become available for use within the then part.

The then part or consequence is just Java code and in this case it is the same code that the workflow definition has.

Predicates can be a lot more complex than that; please refer to the JBoss Rules documentation for the predicate syntax and more examples.

RulesFunctionProvider

The RulesFunctionProvider executes a DRL file without worrying about the return parameters of the rules.

The mandatory parameter ruleName of the FunctionProvider is the same as that of Condition; it tells it which DRL file to load and execute.

package packtpub.osw;
import java.util.Iterator;
import java.util.Map;
import org.drools.WorkingMemory;
import com.opensymphony.module.propertyset.PropertySet;
import com.opensymphony.workflow.FunctionProvider;
import com.opensymphony.workflow.WorkflowException;
/**
* Rule executing function provider.
*
*/
public class RuleExecutorFunctionProvider implements FunctionProvider {
public void execute(Map transientVars, Map args, PropertySet arg2)
throws WorkflowException
{
WorkingMemory wm = DroolsHelper.setupRules
((String)args.get("ruleName"));
for (Iterator iter = transientVars.values().iterator();
iter.hasNext();)
{
wm.assertObject(iter.next());
}
wm.fireAllRules();
}
}

Declarative programming is a new and exciting approach for business rules. It enables a wealth of new opportunities such as Domain Specific Languages (DSL). For more information about JBoss Rules, refer to its documentation.

Summary

In this chapter, we introduced the location of business logic inside our BPM solution—the BRMS. JBoss Rules is a very powerful open-source rule engine and is the main component of a BRMS. We also learned how to implement simpler logic using OSWorkflow's Conditions.

Finally, we created a RulesCondition and RulesFunctionProvider to make decisions and to execute the set of rules inside our workflow definition.

In the next chapter we'll be using Quartz, an open-source job scheduler to enrich our BPMS with powerful task scheduling capabilities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset