Wrapping the Rule Engine

The Problem

When you use a rule engine in your applications you have to do a certain amount of initialization and maintenance work, which will be dictated by the specific rule engine:

  • Initialize the rule engine, instantiate working memory
  • Read the rules from a file (or database)
  • Add facts to working memory
  • Tell the rule engine to fire the rules

If you plan to use a rule engine in multiple applications, it makes sense to wrap this infrastructure code in a RuleEngineWrapper to avoid duplication.

(btw: The JBoss-Rules examples duplicate this infrastructure code, at least until 4.0.M1. It can make sense to minimize the number of classes you need to start an example. Imho it smells bad....)

The Solution

Inspired by this article from Ricardo Olivieri I propose the following solution structure:

Figure showing the RuleEngineWrapper

The classes colored green have the following responsibilities:

  • RuleEngineWrapper: initialize the rule engine, load the rules from the file system (usually they are stored in .drl files), execute the rules.
  • FactInserterCallback: enable your own application classes to insert facts into working memory, without managing the working memory by itself. Please note: Your own classes need to implement this interface - you can very well use an anonymous inner class to do so. Read on for some code sample...

A simple JUnit-Test

...to show usage of the RuleEngineWrapper in action. You find the following pieces:

  • A simple class to represent a fact, which is inserted into working memory by the test and is used in the business rules.
  • A simple rule file, with only a single rule. It fires if it finds a RuleEngineWrapperDummyFact in working memory...
  • A JUnit test case... you'll understand, if you have ever seen a JUnit-test before. The only noteworthy thing: I implement the FactInserterCallback as anonymous inner class, something which is ugly, but as Java lacks closures, there's no other way... (yep - Groovy, I know... but not here!)

A very simple fact class:

RuleEngineWrapperDummyFact.java

package arc42.rules.wrapper;

/**
 * A simple class, used to test RuleEngineWrapper
 *  
 * @author Gernot Starke (www.gernotstarke.de)
 *
 */
public class RuleEngineWrapperDummyFact {
    public String name;
    public int  number;


    public RuleEngineWrapperDummyFact() {
        super();
    }
    public RuleEngineWrapperDummyFact(String name, int number) {
        super();
        this.name = name;
        this.number = number;
    }

         // getter and setter deleted for brevity   
}

A very simple rule set

RuleEngineWrapperTest.drl

package arc42.rules.wrapper

#list any import classes here.
import arc42.rules.wrapper.RuleEngineWrapperDummyFact;


rule "Hello, RuleWrapper"

    when
        myFact: RuleEngineWrapperDummyFact( )
    then 
        myFact.setName( "Hello, world");
end

The JUnit testcase

RuleEngineWrapperTest.java

package arc42.rules.wrapper;

/**
 * Test the RuleEngineWrapper Methods with simple rules and simple facts
 * Depends on the RuleEngineWrapperDummyFact-class.
 * 
 * @author Gernot Starke (www.gernotstarke.de)
 */
import org.drools.WorkingMemory;

import junit.framework.TestCase;

public class RuleEngineWrapperTest extends TestCase {

    private RuleEngineWrapper rew;
    RuleEngineWrapperDummyFact rewFact;

    protected void setUp() {
        try {
            rew = new RuleEngineWrapper("RuleEngineWrapperTest.drl");
        } catch (Exception e) {
            System.out.println("exception, cannot handle it...");
        }
    }

    public void testCanIReadTheRuleSet() {
        rew.executeRules();
        assertTrue(true);
    }

    public void testAddFactAndExecute() {
        rewFact = new RuleEngineWrapperDummyFact();
        rew.addFact(new FactInserterCallback() {
            public void insertFacts(WorkingMemory wm) {
                wm.assertObject(rewFact);
            }
        });
        rew.executeRules();

        // the only rule should have fired and set the name accordingly
        assertEquals(rewFact.getName(), "Hello, world");
    }
} // RuleEngineWrapperTest

An implementation of a RuleEngineWrapper

This implementation does not cover debugging and logging issues, nor does it offer any conflict resolution niceties. A caller can either provide its own class, so that rules files are loaded as classpath resources or call the constructor with only a filename - in that case a default file location is assumed (which accidently sits next to the RuleEngineWrapper itself).

You can add facts with the FactInserterCallback (if you have multiple facts to add) or by calling the addFact method. In both cases the rule engine knows about the real types of your objects.

RuleEngineWrapper.java

    package arc42.rules.wrapper;

    /**
     * Encapsulates the (JBoss-Rules) rule engine. 
     * Intended to be reusable across most JBR-applications, as both
     * rule-initialization and rule-execution are handled here.
     * 
     * Currently missing features:
     * - setter for different conflict resolution strategies
     * - logging
     * 
     * @author Gernot Starke (www.gernotstarke.de)
     * @author with contribution from Ricardo Olivieri
     */

    import java.io.InputStreamReader;
    import javax.rules.RuleException;
    import org.drools.RuleBase;
    import org.drools.RuleBaseFactory;
    import org.drools.WorkingMemory;
    import org.drools.compiler.PackageBuilder;
    import org.drools.event.DebugAgendaEventListener;

    /**
     * 
     * @author Gernot Starke (www.gernotstarke.de)
     * 
     */
    public class RuleEngineWrapper {
        private WorkingMemory workingMemory;

        private boolean debugMode = false;

        private DebugAgendaEventListener debugListener;

        /**
         * construct a RuleEngineWrapper with only a rule-file-name, no idea of
         * callers' class. Assume the rule file is located
         * 
         * @param rulesFile
         */
        public RuleEngineWrapper(String rulesFile) {
            this(RuleEngineWrapper.class, rulesFile);
        }

        /**
         * construct a RuleEngineWrapper with only a calling object known
         * 
         * @param caller
         * @param rulesFile
         */
        public RuleEngineWrapper(Object caller, String rulesFile) {
            this(caller.getClass(), rulesFile);
        }

        /**
         * construct a RuleEngineWrapper with Class caller and rule-file-name known.
         * We load the rule-file as classpath resource.
         * 
         * @param caller
         * @param rulesFile
         */
        public RuleEngineWrapper(Class caller, String rulesFile) {
            super();
            try {
                final PackageBuilder builder = new PackageBuilder();
                System.out.println(caller);

                builder.addPackageFromDrl(new InputStreamReader(caller
                        .getResourceAsStream(rulesFile)));

                final RuleBase ruleBase = RuleBaseFactory.newRuleBase();
                ruleBase.addPackage(builder.getPackage());

                workingMemory = ruleBase.newStatefulSession();

                debugListener = new DebugAgendaEventListener();

            } catch (Exception e) {
                System.out.println("cannot read rule file: " + rulesFile);
            }
        }

        /**
         * Allows to add facts via callback. Users can add their own business
         * objects via callback to this ruleBase (or WorkingMemory).
         * 
         * @see org.drools.WorkingMemory
         * @see org.drools.FactHandle
         * 
         */
        public void addFact(FactInserterCallback factInserter) {
            factInserter.insertFacts(workingMemory);
        }

        /**
         * Allow to add arbitrary objects as facts. Users can add any objects to
         * workingMemory
         * 
         * @param o
         *            the fact to be added
         */
        public void addFact(Object o) {
            workingMemory.assertObject(o);
        }

        /**
         * Execute rules.
         * 
         */
        public void executeRules() {
            workingMemory.fireAllRules();
        }

        /**
         * toggle debugging mode (adds a DebugAgendaEventListener if called once.
         * Removes if called again.
         */
        public void setDebugMode( boolean debug ) {
            if (debugMode ) {
                workingMemory.removeEventListener( debugListener );
            }
            else workingMemory.addEventListener(debugListener);

        }

        /**
         * Invoke callback to insert facts to workingMemory AND execute rules.
         * Convenience method, combines addFact and executeRules.
         */
        public void executeRules(FactInserterCallback factInserter) {
            // to be done!!
        }

    }