Calling a business rule from BPEL
Save the rule, and then switch back to our composite and double-click the LeaveRequest BPEL process to edit it. Drag a Business Rule from the BPEL Activities and Components palette into your BPEL process (before the Human Task activity). This will open the Business Rule dialog (as shown in the following screenshot):
First, we need to specify a name for the Business Rule activity within our BPEL process, so give it a meaningful name such as LeaveApprovalRules.
Next we need to specify the Business Rule Dictionary that we wish to use. If we click on the drop-down list, it will list all the dictionaries within our composite application, which in our case is LeaveApprovalRules that we have just defined.
Select this and the rule dialog will be updated (as shown in the following screenshot) to enable us to specify additional information about how we want to invoke the rule. First, we need to select the decision service that we want to invoke from BPEL. Our rule only contains a single decision service, LeaveApprovalDecisionService, so select it.
Once we've specified the service, we need to specify how we want to invoke the decision service. We specify this through the Operation attribute. Here we have two options:
- Execute function and reset the session
- Execute function
If we choose the option Execute function and thus don't reset the session, if we were then to call the decision service several times within the same instance of our BPEL process, each new invocation would reuse the same session and would also evaluate facts asserted in any previous invocation. For our purposes, we just need to assert a single fact and run the ruleset, so accept the default value of Execute function and reset the session.
The final step to invoke our business rules is to assign BPEL variables to the input and output facts. Click on the green plus symbol (as shown in the preceding screenshot), and this will launch the Decision Fact Map window, as shown in the following screenshot:
At first glance, this looks like the standard Create Copy Operation window that we use when carrying out assigns within BPEL (which in reality is exactly what it is).
The key difference is that we are using this to assign values to the input facts to be submitted to the rules engines, so the Type on the To side of the copy operation is a Business Rule Facts.
The reverse is true for an output fact, where we use this dialog to map the output from the decision service back into a corresponding BPEL variable.
For our purpose, we just want to map the initial LeaveRequest in the process inputVariable into the corresponding fact, as shown in the preceding screenshot. Then we will map the output fact, which will contain our updated LeaveRequest back into our inputVariable.
When JDeveloper opens the Decision Fact Map window, the Variables folder for the Business Rules Facts (circled in the preceding screenshot) is closed and it appears that there are no input facts. You must double-click on this to open it and expose the facts.
We have now wired the rule invocation into our BPEL process, before finally running our process; we need to modify our process to only invoke the workflow if the leave request hasn't been automatically approved.
To do this, just drag a switch onto your process, and then drag your workflow task into the first branch in the switch and define a test to check that the LeaveRequest hasn't been approved. You are now ready to deploy and run your modified process.
Our current rule only approves vacations of one day in duration, requiring all other leave requests to be manually approved. Ideally, we would like to approve holidays of varying duration as long as sufficient notice has been given, for example:
- Approve vacations of one day in duration with a start date that's two weeks or more in the future
- Approve if for 2-3 days and more than 30 days in the future
- Approve if 5 days or less and more than 60 days in the future
Approve if 10 days or less and more than 120 days in the future
To write these rules, we will need to calculate the duration of the leave period, as well as calculate how long it is before the start date. Out of the box, the rule engine provides the Duration extension methods, which allow us to calculate the number of days between two dates, but doesn't allow us to exclude weekends.
So we will need to write our own logic to calculate these values. Rather than embedding this logic directly in each rule, best practice dictates that we place this logic into a separate function. This not only ensures that we have a single version of the logic to implement but minimizes the size of our rules, thus making them simpler and easier to maintain. For our purposes, we will create the following functions:
- startsIn: Which returns the number of days before the specified start date
- leaveDuration: Which returns the number of days from the start date to the end date, excluding weekends
Creating a function
To create our first function, within the rule editor, click on the Functions tab. This will list all the functions currently defined to our ruleset. To create a new function, click on the green plus icon, as shown in the following screenshot:
This will add a new function with a default name (for example, Function_1) to our list. Click on the function name to select it and update it to startsIn. From the drop-down list, select the Return Type of the function, which is int in our case.
Next, we need to specify the arguments we wish to pass to our function. Click on the green plus sign, as shown in the following screenshot, and this will add an argument to our list. Here we can specify the argument name (for example, startDate), and from the drop-down list, the argument Type, which should be XMLGregorianCalendar (when creating XML facts, the JAXB processor maps the type xsd:date to javax.xml.datatype.XMLGregorianCalendar).
The list of valid types is made up of the basic types (for example, int, double, char, and so on), plus the XML facts (excluding object factories) and the Java Facts (excluding the Rules Extension Method) defined in our rules dictionary.
The final step is to implement the business logic of our function, which consists of one or more actions. We enter these actions in the Body section of the function. The first action we need to create is one that creates a local variable of type calendar, which holds the current date.
To do this, click on <insert action> within the Body section of our function. The rule editor will display a drop-down list that lists all the available actions.
For our purpose, we want to create a new variable and assign a value to it, so select the assign new action, as shown in the preceding screenshot. This will insert atemplate for the assign new action into our function body (as shown in the following screenshot). We then configure the action by clicking on each part within the template and defining it as appropriate.
The first part we need to define is the type of variable we wish to create. Click on the <type> element within our <assign> statement, and the rule editor displays a drop-down list displaying all the available types. For our purposes, select Calendar.
Next, click on var. This will prompt us to enter the name of the variable that we want to create. Specify today, and hit enter.
Finally, we need to specify the value we want to initialize our variable with. Click on the <expression> element. The rule editor will display a drop-down box listing all the valid values we can assign to our variable, as shown in the following screenshot:
Select Calendar.getInstance(), which will initialize our variable to hold the current date.
For our second action, we want to calculate the number of days before the specified start date and place the result into the variable duration. To calculate this, we will make use of the Duration extension method provided with the rules engine.
We will do this by defining another assign new action in a similar way to the previous action. The key difference is how we specify the <expression>. This time, instead of selecting a value from the drop-down list, click on the Expression Builder icon (circled in the preceding screenshot) to launch the Expression Builder for the rules editor.
The Expression Builder provides a graphical tool for writing rule expressions and is accessed from various parts of the rule editor. It consists of the following areas:
- Expression: The top textbox contains the rule expression that you are working on. You can either type data directly in here or use the Expression Builder to insert code fragments to build up the expression required.
- Variables, Functions, Operators, Constants: This part of the Expression Builder lets you browse the various components that you can insert into your expression. Once you've located the component that you wish to use, click the Insert Into Expression button, and this will insert the appropriate code fragment into the expression.<>
The code fragment is inserted at the point within the expression that the cursor is currently positioned.
- Content Preview: This box displays a preview of the content that would be inserted into the expression if you clicked the Insert Into Expression button.
So let's use this to build our rules expression. The expression we want to build is a relatively simple one, namely:
Duration.days between(today,startDate) + 1
To build our expression, carry out the following steps. First, within the Functions tab, locate the function Duration.days between and insert this into the expression (as shown in the previous screenshot).
Next, within the Variables tab, locate the variable today. Then within the expression, highlight the first argument of the function (as shown in the following screenshot), and click Insert Into Expression.
This will update the value of the first argument to contain today; repeat this to update the second argument to contain startDate. Next, manually enter +1 to the end of the expression to complete it and click OK.
Finally add a third action to return the duration. The completed body of our function looks as shown in the following screenshot:
To implement our leaveDuration function, we follow the same approach (for details of this, see the code samples included with the book).
To implement our leaveDuration function, we follow the same approach (for details of this, see the code samples included with the book).
Testing a function
JDeveloper provides a test option that allows us to run a function in JDeveloper without the need to deploy it first. However, it will only allow us to run functions with no input parameters and returns a type of boolean.
In order to test our startsIn function, we need to write a wrapper function (for example, testStartsIn) which creates the required input parameters for our function, invokes it, and then prints out the result. So the body of our test function will look as shown in the following screenshot:
To run this, with the Functions tab, select the testStartsIn function, and click the Test button, as shown in the following screenshot:
If there are any validation errors within our rules dictionary, then the Test button will be disabled.
This will execute the function and open a window displaying the result of the function and any output as shown in the following screenshot:
Testing decision service functions
We can also use this approach to test our decision service. The body for this test function appears as shown in the following screenshot:
A couple of interesting points to note about this: the statement call RL.watch.all() will cause the function to output details about how the facts are being processed and which rules are being activated.
The other point to note is that the decision service return type is a result List, so we need to extract our fact from this list and cast it to the appropriate fact type in order to examine its content. We do this with the statement:
assign leaveRequest = (TLeaveRequest) resultList.get(0)
Invoking a function from within a rule
The final step is to invoke the functions as required from our ruleset. Before writing the additional rules for a vacation of less than 3, 5, and 10 days respectively, we willupdate our existing rule to use these new functions.
Go back to the One Day Vacation rule, and select the first test (so it has an orange box around it). Right-click and select Delete Test from the drop-down list, as shown in the following screenshot:
Next, click on <insert test> to add a new test to our IF clause. Click on the left operand. This time, instead of selecting an item from the drop-down list, click on the calculator icon to launch the Expression Builder and use it to build the expression:
Set the value of the operator to >=. Finally, enter the value of 14 for the second operand. Follow the same approach to add another test to check that the leave duration is only one day. Our updated rule should now looks as shown in the following screenshot:
Once we have completed our test pattern, we can click validate just to check that its syntax is correct. Having completed this test, we can define similar approval rules for vacations of 3, 5, and 10 days respectively.
When completed, save your dictionary and rerun the leave approval process; you should now see that the vacations that match our leave approval rules are automatically approved.
Using decision tables
Our updated ruleset consists of four rules that are very repetitive in nature. It would make more sense to specify the rule just once and then parameterize it in a tabular fashion. This is effectively what decision tables allow you to do.
Before creating your decision table, you will need to delete the rules we have just defined, otherwise we will end up with two versions of the same rules within our ruleset.
Defining a bucket set
When creating a decision table, you are often required to specify a list of values or a range of values that apply to a particular rule. For example, in the case of our vacation approval rule, we will need to specify the following ranges of leave duration values that we are interested in:
- 1 day
- 2-3 days
- 4-5 days
- 6-10 days
We define these in a bucketset. To do this, select the Bucketsets tab in the rule editor, then click on the green plus symbol and select List of Ranges from the drop-down list, as shown in the following screenshot:
This will create a new bucketset called Buckset_1. Click on the name and change it to something more meaningful such as LeaveDuration. By default, the bucketset will have a Datatype of int, which is fine for our purposes.
Click on the pencil icon. This will launch the Edit Bucketset - LeaveDuration window, as shown in the following screenshot:
A bucketset, as its name implies, consists of one or more buckets, each corresponding to a range of values. For each bucket, you specify its Endpoint and whether the endpoint is included within the bucket. The range of values covered by a bucket is from the endpoint of the bucket to the endpoint of the next bucket.
You can also choose whether to include the specified endpoint in its corresponding bucket. If you don't, then the endpoint will be included in the preceding bucket.
For example, in the preceding screenshot, the second bucket (with the endpoint of 5) covers the integer values from 6 (as the endpoint 5 isn't included in the bucket) to 10 (the end point of the next bucket).
It is good practice to specify a meaningful alias for each bucket, as when you reference a bucket in a decision table, you do so using its alias. If you don't specify an alias, then it will default to the description in the Range.
In preparation for defining our decision table, we have defined two bucketsets: LeaveDuration, as shown in the preceding screenshot, and StartsIn.
Creating a decision table
To create a decision table, select the Employee Leave Approval ruleset tab. Click on the green plus icon and select Create Decision Table, as shown in thefollowing screenshot:
This will add an empty decision table to our ruleset, as shown in the following screenshot:
The decision table consists of three areas: the first is for defining our tests (or conditions), the second is for conflict resolution (for resolving overlapping rules within our decision table), and the final area is for defining our actions.
Click on <insert condition>. This will add an empty condition with the name C1 to our ruleset. At the same time, the rule editor will also add an additional column to our decision table. This represents our first rule and is given the name R1. To specify the condition that we want to test, double-click on C1. This will bring up a drop-down list (similar to the one used to define an operand within the test part of a rule), as shown in the following screenshot:
As with our original rule, the first condition we want to test is the type of leave request, so select TLeaveRequest.leaveType from the drop-down list.
For our first rule, we want to check that the leave request is of type Vacation, so click on the appropriate cell (the intersection of C1 and R1). The rule editor will present us with a drop-down listing our options. In this case, directly enter Vacation, as shown in the following screenshot:
The next step is to add a second condition to test the leave duration. To do this, click on the green plus icon and select Conditions. This will add another condition row to our decision table. Click on <edit condition> and use the expression builder to define the following:
For each rule, we need to test the result of this function against the appropriate value in our LeaveDuration bucketset. Before we can do this, we must first associate the condition with that bucketset. To do this, ensure that the condition cell is selected and then click on the drop-down list above it and select LeaveDuration, as shown in the following screenshot:
The next step is0 to check that the leave duration is one day, so click on the appropriate cell (the intersection of C2 and R1). The rule editor will present us with a drop-down listing our options, which will be the list of buckets in the LeaveDuration bucketset. From here, select the option 1 day.
Add three more rules to our decision table (to add a rule, click on the green plus icon and select Rule). For R2, specify a leave duration of 2..3 days, for R3 4..5 days, and R4 6..10 days.
For each of these rules, we want to check that the leave type is Vacation. Rather than specifying this individually for each rule (which we could do), we can merge these into a single cell and specify the test just once. To do this, select each cell (hold down the Ctrl key while you do this) and then right-click. From the drop-down list, select Merge Selected Cells.
Next, we need to add the final condition as follows:
To check whether sufficient notice has been given to automatically approve the vacation request, add this in the normal way and associate the condition with the StartsIn bucketset.
For our first rule, we want to approve the leave request if it starts in 14 or more days time, so select ALL the appropriate buckets from our bucketset (as shown in the following screenshot). Complete the test for rules R2, R3, and R4.
The final step is to specify the action we want to take for each of our rules. Click on <insert action>. This will display a drop-down list where you need to specify the Action Type you wish to carry out. Select Modify. This will insert a modify action into our decision table; double-click on this to open the Action Editor (as shown in the following screenshot):
The Form option allows us to select from the drop-down list which action we want to perform. For the Modify action, we first need to specify the fact we wish to update, so select TLeaveRequest in the Target section.
The Arguments section will then be populated to list all the properties for the selected fact. Select requestStatus and enter a value of Approved. Also select the cell to be parameterized. If you don't specify this, then it forces every rule within our decision table to use the same value.
Finally, ensure that the checkbox Always Selected is unchecked (we will see why in a moment) and click OK. This will return us to our decision table, as shown in the following screenshot:
At this point, the action will contain an identical configuration for each rule, which we can then modify as appropriate.
Each rule has an associated checkbox for the action, which, by default, is unchecked. This specifies whether that action should be taken for that rule. In our case, we want each rule to update the request status, so ensure that the checkbox is selected for every rule (as shown in the preceding screenshot).
If you had checked the Always Selected checkbox in the ActionEditor, then the action would be selected for each rule and would also be read-only to prevent you from modifying it.
The action will also contain a row for every property that we are modifying, which,in our example, is just one (requestStatus). As we selected this property to be parameterized, we could override the specified value for each individual rule.
This almost completes our decision table. However, we will add one more rule to handle any other scenario that isn't covered by our current ruleset. Add one more rule, but don't specify any values for any of the conditions, so the rule will apply to everything. In the actions section, specify a value of Manual to indicate that the request requires manual approval.
Upon doing this, the rule editor will add a row to the conflicts section of the decision table, as shown in the following screenshot:
This is indicating that R5 is in conflict with R1, R2, R3, and R4, that is, that they both apply to the same scenario. Double-click on the conflict warning for R1, and this will launch the Conflict Resolution window, as shown in the following screenshot:
Here, we can specify how we wish to handle the conflict. Click on the drop-down list and select Override to specify that R1 takes precedence over R5. Do the same for rules R2R3 and R4. The decision table will be updated to show no conflicts and that rules R1 to R4 override R5.
This completes our decision table, so save the rules dictionary and redeploy the leave approval composite to test it.
Business rules are a key component of any application. Traditionally, these rules are buried deep within the code of an application, making them very difficult to change.
Yet, in a typical application, it is the business rules that change most frequently, by separating these out as a specialized service, it allows us to change these rules without having to modify the overall application.
In this article, we have looked at how we can use the Oracle Business Rules engine to implement such rules, and how we can invoke these from within BPEL as a decision service.
It's worth noting that you are not restricted to calling these rules from just BPEL, as the rules engine comes with a Java API that allows it to be easily invoked from any Java application, or alternatively, you can expose the rules as web services, which can then be invoked from any web service client.
Finally, while in this article, we have only looked at very simple rules. The Oracle Business Rules engine implements the industry standard Rete Algorithm, making it ideal for evaluating a large number of interdependent rules and facts.