To invoke a rule we need to go through a number of steps. First we must create a session with the rules engine, then we can assert one or more facts, before executing the rule set and finally we can retrieve the results.
We do this in BPEL via a Decision Service; this is essentially a web service wrapper around a rules dictionary, which takes cares of managing the session with the rules engine as well as governing which rule set we wish to apply.
The wrapper allows a BPEL process to assert one or more facts, execute a rule set against the asserted facts, retrieve the results and then reset the session. This can be done within a single invocation of an operation, or over multiple operations.
Creating a Rule Engine Connection
Before you can create a Decision Service you need to create a connection to the repository in which the required rule set is stored. In the Connections panel within JDeveloper, right-click on the Rule Engines folder and select New Rule Engine Connection… as shown in the following screenshot:
This will launch the Create Rule Engine Connection dialogue; first you need to specify whether the connection is for a file repository or WebDAV repository.
Using a file based repository
If you are using a file repository, all we need to specify is the location of the actual file. Once the connection has been created, we can use this to create a decision service for any of the rule sets contained within that repository.
However, it is important to realize that when you create a decision service based on this connection, JDeveloper will take a copy of the repository and copy this into the BPEL project.
When you deploy the BPEL process, then the copy of this repository will be deployed with the BPEL process. This has a number of implications; first if you want to modify the rule set used by the BPEL Process you need to modify the copy of the repository deployed with the BPEL Process.
To modify the rule set deployed with a BPEL Process, log onto the BPEL console, from here click on the BPEL Processes tab, and then select the process that uses the decision service. Next click on the Descriptor tab; this will list all the Partner Links for that process, including the Decision Service (for example LeaveApprovalDecisionServicePL) as shown in the following screenshot:
This PartnerLink will have the property decisionServiceDetails, with the link Rule Service Details (circled in the previous screenshot); click on this and the console will display details of the decision service. From here click on the link Open Rule Author; this will open the Rule Author complete with a connection to the file based rule repository.
The second implication is that if you use the same rule set within multiple BPEL Processes, each process will have its own copy of the rule set. You can work round this by either wrapping each rule set with a single BPEL process, which is then invoked by any other process wishing to use that rule set. Or once you have deployed the rule set for one process, then you can access it directly via the WSDL for the deployed rule set, for example LeaveApprovalDecisionService.wsdl in the above screenshot.
Using a WebDAV repository
For the reasons mentioned above, it often makes sense to use a WebDAV based repository to hold your rules. This makes it far simpler to share a rule set between multiple clients, such as BPEL and Java.
Before you can create a Rule Engine Connection to a WebDAV repository, you must first define a WebDAV connection to JDeveloper, which is also created from the Connections palette.
Creating a Decision Service
To create a decision service within our BPEL process, select the Services page from the Component Palette and drag a Decision Service onto your process, as shown in the following screenshot:
This will launch the Decision Service Wizard dialogue, as shown:
Give the service a name, and then select Execute Ruleset as the invocation pattern. Next click on the flashlight next to Ruleset to launch the Rule Explorer. This allows us to browse any previously defined rule engine connection and select the rule set we wish to invoke via the decision service.
For our purposes, select the LeaveApprovalRules as shown below, and click OK.
This will bring us back to the Decision Service Wizard which will be updated to list the facts that we can exchange with the Rule Engine, as shown in the following screenshot:
This dialogue will only list XML Facts that map to global elements in the XML Schema. Here we need to define which facts we want to assert, that is which facts we pass as inputs to the rule engine from BPEL, and which facts we want to watch, that is which facts we want to return in the output from the rules engine back to our BPEL process.
For our example, we will pass in a single leave request. The rule engine will then apply the rule set we defined earlier and update the status of the request to Approved if appropriate. So we need to specify that Assert and Watch facts of type LeaveRequest.
Finally, you will notice the checkbox Check here to assert all descendants from the top level element; this is important when an element contains nested elements (or facts) to ensure that nested facts are also evaluated by the rules engine. For example if we had a fact of type LeaveRequestList which contained a list of multiple LeaveRequests, if we wanted to ensure the rules engine evaluated these nested facts, then we would need to check this checkbox.
Once you have specified the facts to Assert and Watch, click Next and complete the dialogue; this will then create a decision service partner link within your BPEL process.
Adding a Decide activity
We are now ready to invoke our rule set from within our BPEL process. From the Component Palette, drag a Decide activity onto our BPEL process (at the point before we execute the LeaveRequest Human Task).
This will open up the Edit Decide window (shown in the following screenshot). Here we need to specify a Name for the activity, and select the Decision Service we want to invoke (that is the LeaveApprovalDecisionService that we just created).
Once we've specified the service, we need to specify how we want to interact with it. For example, whether we want to incrementally assert a number of facts over a period of time, before executing the rule set and retrieving the result or whether we want to assert all the facts, execute the rule set and get the result within a single invocation.
We specify this through the Operation attribute. For our purpose we just need to assert a single fact and run the rule set, so select the value of Assert facts, execute rule set, retrieve results.
Once we have selected the operation to invoke on the decision service, the Decision Service Facts will be updated to allow you to assign input and output facts as appropriate.
The final step to invoke our business rules is to assign BPEL variables to the input and output facts. Click on Create, which will launch the Decision Fact Map window, as shown in the following screenshot:
At first glance this will look 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 Decision Service 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 above screenshot, and then map the output fact which will contain our updated leaveRequest back into our inputVariable.
We have now wired the invocation of the rule 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 activity onto your process, 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 1 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 if 1 day in duration and its start date is 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 doesn't provide functionality for this, but the Rule Author allows us to write our own functions to do exactly this. For our purpose we want to create two functions:
- leaveDuration: It returns the number of days between the start and end date, excluding weekends
- startsIn: It returns the number of days before the specified startDate
These functions are very similar in nature, so we will actually write a base function, durationInDays, which takes 3 parameters. The startDate and endDate of the period to be calculated, as well a Boolean includeWeekends, to control whether weekends are to be counted.
Importing Java classes as facts
Each of the functions will take arguments which hold dates, if you examine the TLeaveRequest fact that the Rule Author created when we imported out XML Schema, you will see that those elements of type xsd:Date are mapped to properties of type java.util.Calendar. So our functions should take arguments of this type.
Rules Author allows you to create facts based on Java objects; however before you can do this you need to import them as Java Facts. The process for this is similar to the one we followed earlier to import XML Facts.
Select the Definitions tab within Rule Author and then under the Fact folder click on the JavaFact branch. This will list all Java facts currently defined. If you click on Create, it will take you to the class selector page, listing the top level hierarchy of all the classes on the class path, as shown in the following screenshot:
Click on the + symbol to expand the relevant nodes; for our example, expand the java node, then the util node and then select the Calendar class. Once selected, click Import and this class will be added to your list of Java facts. Once imported, the classes and their methods will be available to the Rule Author as facts.
By default the Rule Author has visibility of classes in the java, javax, and org packages; if you need a class that is not included within those packages then enter the classpath for the required classes in the User Classpath field and click Add.
Creating a function
We are now ready to create our first function, durationInDays. Within the Definitions tab, select the RLFunctions folder, as highlighted in the following screenshot. This will bring up the RLFunction Summary, which lists all the functions currently defined.
Click Create to bring up the RLFunction editor page as shown:
First give the function a name, for example DM.durationInDays, plus a corresponding alias and 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; select Create and this will add an argument to our list. Here, we can specify the argument name (for example startDate), a corresponding alias for it and from the drop down the argument type. At the top of this list will be the Calendar fact that we just imported.
We can then add parameters for endDate, and includeWeekends as shown in the previous screenshot.
The final step is to implement the business logic of our function; we enter this in the Function Body text box. Defining the function here is painful, since the Rule Author doesn't validate the body of the function. A consequence of this is you won't know if the function is valid until you try to execute it at run time, which if it contains an error will typically result in the rule engine throwing an exception, which is not always easy to debug.
However, when implementing a function you are effectively writing a static Java method. Therefore, it is more effective to write this using JDeveloper, where we can then use the development tools to compile, run, and test our function. Once we are satisfied that the function performs as expected we can cut and paste the function body from JDeveloper into the Rule Author.
When using this approach you need to allow for the restriction that you can't use the Java import statement within an RLFunction (in the same way you can't import within the body of a Java method). This means whenever you reference a Java Class you need to prefix it with its package name, for example when we use the Calendar class within our function we always have to specify java.util.Calendar.
So the durationInDays function implemented in Java looks as follows:
We can then just cut and paste this straight from JDeveloper into the Rule Author and click OK to create our function.
To implement the other two functions leaveDuration and startsIn, we follow the same approach.
Invoking a function from within a rule
The final step is to invoke the functions as required from our rule set. Before writing the additional rules for vacation of less than 3, 5, and 10 days respectively, we will update our first rule OneDayVacation to use these new functions.
Go back to the Rulesets tab and click on the OneDayVacation branch within the LeaveApprovalRules and then click on the pencil icon for the If part of the rule. This will bring us back to the Pattern Definition window for the rule.
Previously, when we defined our test for a pattern we defined a Standard Test. With this approach the Rule Author lets us define one or more simple tests. Each simple test allows us to compare one variable with either another variable or a fixed value, and for the pattern to evaluate to true all simple tests must evaluate to true.
However, if we want to define more complex expressions or use functions, then we need to define this as an Advanced Test. When Advanced Test is selected, rather than enter every test as a single row within the pattern, Rule Author presents a single free format text entry box where we can directly enter the test pattern.
If when selecting Advanced Test we already have a simple test defined, Rule Author will automatically convert its free format equivalent, as shown in the following screenshot:
Below the Advanced Test text entry box are three drop downs: Operator, Variable, and Function, which we can use to help build the expression. For example, to modify our test, first of all delete the comparison request.startDate == request.endDate. Next from the Function drop select the leaveDuration function and click Insert, as shown in the following screenshot:
This will insert leaveDuration (startDate: Calendar, endDate: Calendar) at the current cursor location within the test text box. We then need to modify the parameters to pass in the actual request.startDate and request.endDate.
We can either enter this manually, or use the Variable drop down to insert the required variables in a similar fashion to the Function drop down.
We can then repeat these steps to apply the startsIn function to test that the start date for the leave request is two or more weeks away. Once completed our test pattern for approving a 1 day vacation should look as follows:
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 less than 3, 5, and 10 days respectively.
When completed, save your dictionary and rerun the leave approval process; you should now see that vacations which match our leave approval rules are automatically approved.
In this article, we have looked at how to create a decision Service. It's worth noting that you are not restricted to calling these rules from just BPEL, 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.