Testing Oracle 11g Business Rules directly from Java


In my earlier post Testing Business Rules with JDeveloper Rule Designer I presented an example of testing rules using the JDeveloper Rules Designer.

While that’s nice, testing rules from a simple Java Class is even better.

This article presents an example of how the 11g rule engine can be called locally from a standalone Java application.

With this technique, the rules and decision functions authored using JDeveloper are loaded from the file system and called directly from Java.

Tests can be run from a command line or from within JDeveloper,  No Application Server is required. 

Let me repeat that part,  because I really like it,
Rule Tests can be run without a running WebLogic Server

This provides a simple and efficient way to write rules, rapidly test them and analyze the results without the need to deploy the composite. 

The approach uses two Java Classes,

The RuleTester.java Class is a generic testing harness that will call the Rules Engine, pass input data and return results.
The RuleTester.java class does not need to be modified by the developer.

The second Java Class must be implemented by the developer.
This class creates the required test data and invokes RuleTester.
An example of both classes are provided below.

The following example illustrates the following points;

  • Loading a rule dictionary from the file system.
  • Creating the necessary Java and XML test facts.
  • Calling the decision service and running the rules
  • Output of the decision service results

RuleTester.java

package rulesproject;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import oracle.rules.sdk2.decisionpoint.*;
import oracle.rules.sdk2.dictionary.RuleDictionary;
import oracle.rules.sdk2.exception.SDKWarning;
import oracle.rules.rl.RuleSession;
import oracle.rules.rl.extensions.trace.RuleEngineState;
import oracle.rules.rl.extensions.trace.TraceAnalysis;
import oracle.rules.rl.trace.DecisionTrace;
import oracle.rules.rl.trace.FactTrace;
import oracle.rules.rl.trace.RuleTrace;
import oracle.rules.rl.trace.TraceEntry;

/**
 * A Java Class that demonstrates how to load an Oracle 11g Rules Dictionary
 * from a file system location and execute a decision Function.
 * The class is intended to be used only as Rule development / prototyping tool.
 * The rules and decision service to be tested are expected to located in a .rules
 * file on the file system created using the JDeveloper 11g Rules Editor.
 * The rules should be contained in a JDeveloper SOA Rules Composite Application,
 * for example a "Composite Business Rule" application.
 * Rules are tested in a slave JVM automatically started by JDeveloper.
 * A running WebLogic Application Server is not required.
 */
public class RuleTester {
 public RuleTester() {
 super();
 }
 
 /* Initialize loads the Rule dictionary from the specified file system location and creates a Rule Decision Point.
 * @param dictionaryLocation The full path to the .rules file.
 * @param decisionFunctionName The name of the Decision Function that will be called.
 * The function is defined using the JDevleloper rule editor 
 */
 private static DecisionPointInstance initialize(String dictionaryLocation, String decisionFunctionName) throws Exception {
 
 DecisionPointInstance pointInstance = null;
 if(dictionaryLocation == null || decisionFunctionName == null)
 throw new Exception("RuleProcessor must have all input properties to successfully initialize.");
 
 // Load Decision Point using Dictionary on File System
 DecisionPoint decisionPoint = new DecisionPointBuilder()
 .with(decisionFunctionName)
 .with(loadRuleDictionary(dictionaryLocation)) 
 .build();
 pointInstance = decisionPoint.getInstance(); 
 RuleSession session = pointInstance.ruleSession();
 session.callFunctionWithArgument("setDecisionTraceLevel", RuleSession.DECISION_TRACE_DEVELOPMENT);
 
 System.out.println("RuleTester Initialized");
 return pointInstance;
 }
 
 
 
 /**
 * Loads the rule dictionary from the specified dictionaryPath
 * @param dictionaryLocation The full path to the .rules file.
 * @return A rule dictionary object
 * 
 */
 private static RuleDictionary loadRuleDictionary(String dictionaryLocation) throws Exception{
 RuleDictionary dict = null;
 Reader reader = null;
 Writer writer = null;
 
 try {
 reader = new FileReader(new File(dictionaryLocation));
 dict = RuleDictionary.readDictionary(reader, new DecisionPointDictionaryFinder(null));
 List<SDKWarning> warnings = new ArrayList<SDKWarning>();
 
 dict.update(warnings);
 if (warnings.size() > 0 ) { 
 System.err.println("Validation warnings: " + warnings);
 }
 
 } finally {
 if (reader != null) { try { reader.close(); } 
 catch (IOException ioe) {ioe.printStackTrace();}}
 if (writer != null) { try { writer.close(); } 
 catch (IOException ioe) {ioe.printStackTrace();}}
 }
 
 return dict;
 }
 
 /**
 * Produces a trace of the Rules Session to the Standard Output device
 * @param pointInstance The decision point instance for the test session.
 
 */
 private static void outputTrace(DecisionPointInstance pointInstance) throws Exception
 {
 DecisionTrace trace = pointInstance.decisionTrace();
 TraceAnalysis tAnalysis = new TraceAnalysis(trace);
 
 List<TraceEntry> traceList = trace.getTraceEntries();
 System.out.println("Trace list size is " + traceList.size() );
 
 if(traceList.size() > 0) {
 // Iterator<TraceEntry> it = traceList.iterator();
 for(int t=0; t<traceList.size(); t++)
 {
 TraceEntry entry = traceList.get(t);
 
 RuleEngineState engineState = tAnalysis.getRuleEngineState(entry);
 
 // Fact Trace List for Trace Entry
 List<FactTrace> factTraceList = engineState.getFacts();
 System.out.println("Trace entry " + (t+1) + " Engine has " + factTraceList.size() + " Facts");
 for(int i=0; i< factTraceList.size(); i++)
 { 
 FactTrace fTrace = (FactTrace) factTraceList.get(i);
 System.out.println("Fact " + (i+1) + ": " + fTrace.getFactType());
 }
 
 // Output Fired Rules for Trace Entry
 List<RuleTrace> firedRulesList = engineState.getFiredRules();
 for(int i=0; i< firedRulesList.size(); i++)
 { 
 RuleTrace frTrace = (RuleTrace) firedRulesList.get(i);
 System.out.println("Fired rule " + frTrace.getRuleName());
 }
 }
 }
 }
 
 /**
 * Execute the Rule Decision Function and return the results.
 * @param dictionaryLocation The fully qualified path of the .rules file containing the rules.
 * Created by JDeveloper rule editor.
 * @param decisionFunctionName The name of the Decision Function in the Rules Dictionary that will be called.
 * @param inputs An ArrayList containing the parameters required by the decision service in order or appearance.
 * @param trace A boolean flag indicating whether a trace for the session should be output to the standard out device.
 * @return A modified list of objects returned by the specified rule decision service
 */
 public static List runRules(String dicrtionaryLocation, String decisionFunctionName, ArrayList inputs, boolean trace) throws Exception { 
 
 List<Object> ruleResult = null; 

 DecisionPointInstance pInstance = initialize(dicrtionaryLocation, decisionFunctionName);
 
 if(pInstance == null)
 throw new Exception("RuleTester not intialized.");
 
 System.out.println("Running Rules");
 
 if (inputs != null)
 { 
 pInstance.setInputs(inputs);
 
 // invoke the decision point with our inputs
 ruleResult = pInstance.invoke();
 
 if (ruleResult == null || ruleResult.isEmpty()){
 System.out.println("RuleTester: No results returned by rules"); 
 }
 else System.out.println("RuleTester: " + ruleResult.size() + " result(s) returned.");
 
 if(trace)
 outputTrace(pInstance);
 }
 return ruleResult;
 }
 
}

Download RuleTester.java

MyShippingCostTester.java

package rulesproject;

import example.rules.OrderT;
import example.rules.ObjectFactory;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class MyShippingCostTester {
 public MyShippingCostTester() {
 super();
 }
 
 /**
 * Creates a list of Shipping Codes Java Facts for input to the rule engine. 
 * @return A populated List of ShipCode objects
 */
 public static List<ShipCode> createShipCodesTestData()
 { 
 ShipCode row1 = new ShipCode();
 row1.setProductId("001");
 row1.setShipCode("C");
 
 ShipCode row2 = new ShipCode();
 row2.setProductId("002");
 row2.setShipCode("A");
 
 List<ShipCode> codes = new ArrayList<ShipCode>();
 codes.add(row1);
 codes.add(row2);
 
 return codes;
 }
 
 /**
 * Creates a sample order for input to the rule engine. Order is an XML type Fact.
 * @return A populated sample OrderT object
 * @throws JAXBException If unmarshalling is unsuccessful
 */
 public static OrderT createOrderTestData() throws JAXBException {
 
 String sampleOrder =
 "<Order xmlns=\"http://www.rules.example\">\n" + 
 " <orderId></orderId>\n" + 
 " <items>\n" + 
 " <item>\n" + 
 " <productId>001</productId>\n" + 
 " <quantity>3</quantity>\n" + 
 " <unitPrice>5.99</unitPrice>\n" + 
 " <extraCharge>false</extraCharge>\n" + 
 " </item>\n" + 
 " <item>\n" + 
 " <productId>123</productId>\n" + 
 " <quantity>1</quantity>\n" + 
 " <unitPrice>34.99</unitPrice>\n" + 
 " <extraCharge>false</extraCharge>\n" + 
 " </item>\n" + 
 " </items>\n" + 
 "</Order>";
 
 OrderT order = null;
 
 // XML Facts are represented by JAXB types. 
 // Use the Generated types to parse an xml test message
 JAXBContext jaxbContext2 = JAXBContext.newInstance("example.rules");
 Unmarshaller unMarsh = jaxbContext2.createUnmarshaller();
 ByteArrayInputStream is = new ByteArrayInputStream(sampleOrder.getBytes());
 Object obj = unMarsh.unmarshal(is);
 JAXBElement jobj = (JAXBElement) obj;
 order = (OrderT) jobj.getValue();
 
 // Write input Order to stdout for later comparison to Order returned by rule engine 
 System.out.println("Input Order Is:");
 Marshaller marshaller2 = jaxbContext2.createMarshaller();
 marshaller2.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
 marshaller2.marshal( obj, System.out );
 
 return order; 
 } 
 
 public static void main(String args[]) throws Exception {

 final String dictionaryLocation = "C:\\projects\\BobsRulesApp\\oracle\\rules\\rulesproject\\OracleRules1.rules";
 final String decisionServiceName = "DetermineShippingCosts";
 
 // Create an array of inputs that match the Rules Decision Function parameters
 ArrayList inputs = new ArrayList();
 inputs.add(createOrderTestData()); // OrderT
 inputs.add(createShipCodesTestData()); // Ship Code List
 
 // Execute the Rules passing in the Test Order and Shipping Codes
 List resultList = RuleTester.runRules(dictionaryLocation, decisionServiceName, inputs, true); // trace on
 
 // Output the results
 if(resultList != null)
 {
 // Only a single Order object returned from rules decision function
 OrderT result = (OrderT) resultList.get(0); 
 
 // Output Modified Order returned from Rules Engine to stdout 
 JAXBContext jaxbContext = JAXBContext.newInstance(OrderT.class);
 Marshaller marshaller = jaxbContext.createMarshaller();
 marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
 ObjectFactory objF = new ObjectFactory();
 marshaller.marshal( objF.createOrder(result), System.out );
 }
 }
 
 

}


Download MyShippingCostTester.java


Running a test
The rules are tested within JDeveloper by right clicking on the MyShippingCostTester.java file and choosing Run.

JDeveloper launches a JVM using the application class path which includes the required rule engine jars.

When run within JDeveloper, the test output is displayed in the Log window as shown below.

In the modified order that is returned, rules have updated the extraCharge element of the first item to indicate additional shipping charges will apply for this item.


A Note about Java and XML Rule Types

If the rules to be tested are based on Java facts then the Java Classes that were provided to the rule engine during fact creation will be on the classpath at runtime.
If the rules to be tested are based on XML, then JDeveloper has generated JAXB content model classes for the input types sent to the rules engine.
The java types for XML facts are generated by JDeveloper during rule creation in the rule editor.
The JAXB types and stored in the  .rulesdesigner folder under the project, which is not visible from within jdeveloper.
These are the types that must be used to create input test data for xml facts.

If XML Facts are being tested from within JDeveloper,
The .rulesdesign/Jaxb_classes folder must be added to project classpath so a run of MyShippingCostTester.java and RuleTester.java can find the types at run time.

Download the complete JDeveloper Project

This provided zip contains a JDeveloper 11.1.1.5 project.

Localizing the Project

The MyShippingCostTester.java file contains a fully qualified path to the .rules file.
The path must be updated in the source to point to the location of the .rules file on the local file system.
To run a test, right click on the MyShippingCostTester.java file and choose Run
The JDeveloper Log window should display the results shown above in the screen snapshot.

The complete example can be downloaded in a zip file from GitHub here

Comments:

Hi Bob,

I am working on a project with the below requirement.

Requirement:
Update or Create Buckets,BucketSets and Java Facts in the existing Rules.

Is it achievable by Rules API.
Any sample project if achievable.

Kindly help me on this.............

Posted by guest on February 05, 2013 at 02:08 AM EST #

yes it is possible to create rules and bucket sets using the Rules SDK.
The javadoc for the 11g API is here
http://docs.oracle.com/cd/E23943_01/apirefs.1111/e10663/toc.htm

The 10g version of the Rules User guide provided a nice overview of the basics of using the SDK to create rules.
Unfortunately this section does not appear in the 11g manual.
Here is the link to the 10g guide which should be correct but lacking a bucketset example
http://docs.oracle.com/cd/E11036_01/web.1013/b28965/sdk.htm#BJFHJBIC

Creating buckets and bucketsets should also be possible using the 11g SDK.
Sorry but at this point I don't have a working example.

Bob

Posted by Bob Webster on February 05, 2013 at 11:20 AM EST #

Hi Bob,

Thanks for your Reply......

I referred the Rules SDK previously but could not able to update or create the Buckets,BucketSets and Facts because the api's doc which was provided by Oracle was not so informative and could not able to find the correct method which will be used for the creation and updation of rules through java.

Posted by Ganesh on February 05, 2013 at 11:42 AM EST #

Hi Ganesh,
I put together an example of creating a bucketset with the SDK, which I can provide to you.
Please email your contact info to bob.webster@oracle.com

thanks
Bob

Posted by Bob Webster on February 05, 2013 at 01:27 PM EST #

Post a Comment:
Comments are closed for this entry.
About

Picture of Bob

I am an Oracle Architect specializing in Service Oriented Architecture and Business Process Management.


Any code presented is for educational purposes only, no warranty or support is implied.

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today