JBoss Drools - a hello world example

Filed under: J2EE, Java, Maven, — Tags: JBoss Drools — Thomas Sundberg — 2012-10-11

What is JBoss Drools? It is a framework where you can create rules that defines when a specific action should be done. This could be done in code using conditions. Creating them using a rule engine can make it easier to combine many business rules with many actions.

I tried to get a JBoss Drools example up and running. It turned out that the examples I found on the web were either were very complicated, tried to solve all possible problems or was just incomplete. I ended up writing my own example where I have removed everything that I didn't find necessary.

I divided this example in two parts. First part is the simplest possible solution that could work, a Hello World. In the second part I try to do something that actually could be useful. I trigger a rule and instantiates a class that could perform some action.

The simplest possible solution that could work

Learning from Test Driven Development, TDD, I will start with the absolute smallest, and therefore the simplest, possible example that could work. This is usually a Hello World example. It may seem trivial, but just getting the environment up and running is a necessary but not sufficient requirement.

Let's start with the files. I created this project as a Maven project and followed the usual Maven conventions a close as I could.

drools-hello-world
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- se
    |   |       `-- waymark
    |   |           `-- drools
    |   |               `-- message
    |   |                   `-- Message.java
    |   `-- resources
    |       `-- se
    |           `-- waymark
    |               `-- drools
    |                   `-- hello
    |                       `-- helloWorld.drl
    `-- test
        `-- java
            `-- se
                `-- waymark
                    `-- drools
                        `-- HelloWorldRuleTest.java

This example consists of four files. Lets start from the top with the Maven pom:

pom.xml

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.waymark.drools</groupId>
    <artifactId>drools-hello-world</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>5.4.0.Final</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
    </dependencies>
</project>

We need two dependencies, the JBoss Drools compiler and JUnit. JUnit is strictly speaking not necessary; I could have tested this with just a main method instead. But working from a test feels better.

Next file a POJO, Plain Old Java Object, that is sent as a message to the rule som it has something that will can trigger rules.

src/main/java/se/waymark/drools/message/Message.java

package se.waymark.drools.message;

public class Message {
    private String type;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public void printMessage() {
        System.out.println("Type: " + type);
    }
}

The only property here is a type. I will trigger the rule based on the type. I have also added an action, printMessage() that could be triggered from the rule.

The next file is the rule file that will determine if an action should be taken or not. My example looks like this:

src/main/resources/se/waymark/drools/hello/helloWorld.drl

import se.waymark.drools.message.Message;

rule "Hello World"
when
  message:Message (type == 'Hello')
then
  message.printMessage();
end

The only rule defined is a rule called Hello World and it will be triggered when it receives an instance of the message class where the type is set to Hello. When a message is received that trigger the rule, the printMessage() method gets executed.

The final thing to do is to use the rule engine and create a message that will trigger the Hello World rule. I will do this from a test class. Lets start from the top, a test that will trigger the execution. It looks like this:

@Test
public void shouldFireHelloWorld() throws IOException, DroolsParserException {
    RuleBase ruleBase = initialiseDrools();
    WorkingMemory workingMemory = initializeMessageObjects(ruleBase);
    int expectedNumberOfRulesFired = 1;

    int actualNumberOfRulesFired = workingMemory.fireAllRules();

    assertThat(actualNumberOfRulesFired, is(expectedNumberOfRulesFired));
}

Following the usual pattern of a test, I start with setting up the system under test. That is initialise drools, add the objects the rule engine should use to its working memory and define that I expect one rule to be fired.

Next thing I do is fire all rules and save the actual number of rules that was fired.

Last I assert that the system under test, the rules engine, and verify that the expected number of rules fired is the same as the actual number of rules that was fired.

The initialisation of Drool is done like this:

private RuleBase initialiseDrools() throws IOException, DroolsParserException {
    PackageBuilder packageBuilder = readRuleFiles();
    return addRulesToWorkingMemory(packageBuilder);
}

I read the rules and store them in a package builder. The rules are then added to the working memory so Drools can use them.

Reading rules can be done like this:

private PackageBuilder readRuleFiles() throws DroolsParserException, IOException {
    PackageBuilder packageBuilder = new PackageBuilder();

    String ruleFile = "/se/waymark/drools/hello/helloWorld.drl";
    Reader reader = getRuleFileAsReader(ruleFile);
    packageBuilder.addPackageFromDrl(reader);

    assertNoRuleErrors(packageBuilder);

    return packageBuilder;
}

Reading rules is a matter of locating them as resources on the classpath, use a reader and finally add them to a package builder. Before I return the package builder, I verify that there are no parsing errors.

Initialising the message objects is done like this:

private WorkingMemory initializeMessageObjects(RuleBase ruleBase) {
    WorkingMemory workingMemory = ruleBase.newStatefulSession();

    createHelloWorld(workingMemory);

    return workingMemory;
}

It just delegates to another method to add message POJOs to the working memory.

These are the most important parts of the test. I include the complete test class for completeness so you can implement this yourself.

src/test/java/se/waymark/drools/HelloWorldRuleTest.java

package se.waymark.drools;

import org.drools.RuleBase;
import org.drools.RuleBaseFactory;
import org.drools.WorkingMemory;
import org.drools.compiler.DroolsError;
import org.drools.compiler.DroolsParserException;
import org.drools.compiler.PackageBuilder;
import org.drools.compiler.PackageBuilderErrors;
import org.drools.rule.*;
import org.drools.rule.Package;
import org.junit.Test;
import se.waymark.drools.message.Message;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;


public class HelloWorldRuleTest {

    @Test
    public void shouldFireHelloWorld() throws IOException, DroolsParserException {
        RuleBase ruleBase = initialiseDrools();
        WorkingMemory workingMemory = initializeMessageObjects(ruleBase);
        int expectedNumberOfRulesFired = 1;

        int actualNumberOfRulesFired = workingMemory.fireAllRules();

        assertThat(actualNumberOfRulesFired, is(expectedNumberOfRulesFired));
    }

    private RuleBase initialiseDrools() throws IOException, DroolsParserException {
        PackageBuilder packageBuilder = readRuleFiles();
        return addRulesToWorkingMemory(packageBuilder);
    }

    private PackageBuilder readRuleFiles() throws DroolsParserException, IOException {
        PackageBuilder packageBuilder = new PackageBuilder();

        String ruleFile = "/se/waymark/drools/hello/helloWorld.drl";
        Reader reader = getRuleFileAsReader(ruleFile);
        packageBuilder.addPackageFromDrl(reader);

        assertNoRuleErrors(packageBuilder);

        return packageBuilder;
    }

    private Reader getRuleFileAsReader(String ruleFile) {
        InputStream resourceAsStream = getClass().getResourceAsStream(ruleFile);

        return new InputStreamReader(resourceAsStream);
    }

    private RuleBase addRulesToWorkingMemory(PackageBuilder packageBuilder) {
        RuleBase ruleBase = RuleBaseFactory.newRuleBase();
        Package rulesPackage = packageBuilder.getPackage();
        ruleBase.addPackage(rulesPackage);

        return ruleBase;
    }

    private void assertNoRuleErrors(PackageBuilder packageBuilder) {
        PackageBuilderErrors errors = packageBuilder.getErrors();

        if (errors.getErrors().length > 0) {
            StringBuilder errorMessages = new StringBuilder();
            errorMessages.append("Found errors in package builder\n");
            for (int i = 0; i < errors.getErrors().length; i++) {
                DroolsError errorMessage = errors.getErrors()[i];
                errorMessages.append(errorMessage);
                errorMessages.append("\n");
            }
            errorMessages.append("Could not parse knowledge");

            throw new IllegalArgumentException(errorMessages.toString());
        }
    }

    private WorkingMemory initializeMessageObjects(RuleBase ruleBase) {
        WorkingMemory workingMemory = ruleBase.newStatefulSession();

        createHelloWorld(workingMemory);

        return workingMemory;
    }

    private void createHelloWorld(WorkingMemory workingMemory) {
        Message helloMessage = new Message();
        helloMessage.setType("Hello");
        workingMemory.insert(helloMessage);
    }
}

Conclusion

Getting a working Hello World required four files. One project file pom.xml, a rule file helloWorld.drl, a java POJO that the rule could trigger on Message.java and finally a test class that ties everything together HelloWorldRuleTest.java.

To build the project, execute:

mvn clean install

An extended example

The Hello World example above may not be very useful expect for validating your setup. A slightly more useful solution should include some action that is separated from the message and that action should be triggered if a condition is true. I have there fore extended the example so it looks like this now:

drools-with-action
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- se
    |   |       `-- waymark
    |   |           `-- drools
    |   |               |-- action
    |   |               |   `-- Action.java
    |   |               `-- message
    |   |                   `-- Message.java
    |   `-- resources
    |       `-- se
    |           `-- waymark
    |               `-- drools
    |                   `-- hello
    |                       |-- actionRule.drl
    |                       `-- helloWorld.drl
    `-- test
        `-- java
            `-- se
                `-- waymark
                    `-- drools
                        `-- RuleTest.java

The most important difference is a new class I call Action. It will be instantiated by the rule if it should execute something. There is also a new rule actionRule.drl. The rest of the example is more or less similar to the previous example.

Lets start with investigating the new Action class:

src/main/java/se/waymark/drools/action/Action.java

package se.waymark.drools.action;

import se.waymark.drools.message.Message;

public class Action {
    public void performAction(Message message) {
        message.printMessage();
    }
}

It is obvious a very trivial action. But hat is a good thing in my world; there are almost nothing that can go wrong here.

The new rule file looks like this:

src/main/resources/se/waymark/drools/hello/actionRule.drl

import se.waymark.drools.message.Message;
import se.waymark.drools.action.Action;

rule "Act on high value in message"
when
  message:Message (messageValue > 17)
then
  Action action = new Action();
  action.performAction(message);
end

I import the action class. When a value in the message class is high, then I instantiate the Action class and execute is method performAction() with the message as an argument. This separates the message and action but if I need to execute the action, I have the message available so it is possible to determine why the action is executed. This is obviously not used in this example, but it could be used in another circumstance.

The message class has been extended so it now contains a numerical value that is easy to use a threshold. It now looks like this:

src/main/java/se/waymark/drools/message/Message.java

package se.waymark.drools.message;

public class Message {
    private String type;
    private int messageValue;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getMessageValue() {
        return messageValue;
    }

    public void setMessageValue(int messageValue) {
        this.messageValue = messageValue;
    }

    public void printMessage() {
        System.out.println("Type: " + type + " value: " + messageValue);
    }
}

The test class has also been slightly changed and it now contains an array so more then one rule can be read. The obvious extension here would of course to read all drl files on the classpath, but I leave that as an exercise for the reader. The new test class looks like this:

src/test/java/se/waymark/drools/RuleTest.java

package se.waymark.drools;

import org.drools.RuleBase;
import org.drools.RuleBaseFactory;
import org.drools.WorkingMemory;
import org.drools.compiler.DroolsError;
import org.drools.compiler.DroolsParserException;
import org.drools.compiler.PackageBuilder;
import org.drools.compiler.PackageBuilderErrors;
import org.drools.rule.*;
import org.drools.rule.Package;
import org.junit.Test;
import se.waymark.drools.message.Message;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;


public class RuleTest {

    @Test
    public void shouldFireAllRules() throws IOException, DroolsParserException {
        RuleBase ruleBase = initialiseDrools();
        WorkingMemory workingMemory = initializeMessageObjects(ruleBase);
        int expectedNumberOfRulesFired = 2;

        int actualNumberOfRulesFired = workingMemory.fireAllRules();

        assertThat(actualNumberOfRulesFired, is(expectedNumberOfRulesFired));
    }

    private RuleBase initialiseDrools() throws IOException, DroolsParserException {
        PackageBuilder packageBuilder = readRuleFiles();
        return addRulesToWorkingMemory(packageBuilder);
    }

    private PackageBuilder readRuleFiles() throws DroolsParserException, IOException {
        PackageBuilder packageBuilder = new PackageBuilder();

        String[] ruleFiles = {"/se/waymark/drools/hello/helloWorld.drl",
                "/se/waymark/drools/hello/actionRule.drl"};

        for (String ruleFile : ruleFiles) {
            Reader reader = getRuleFileAsReader(ruleFile);
            packageBuilder.addPackageFromDrl(reader);
        }
        assertNoRuleErrors(packageBuilder);

        return packageBuilder;
    }

    private Reader getRuleFileAsReader(String ruleFile) {
        InputStream resourceAsStream = getClass().getResourceAsStream(ruleFile);

        return new InputStreamReader(resourceAsStream);
    }

    private RuleBase addRulesToWorkingMemory(PackageBuilder packageBuilder) {
        RuleBase ruleBase = RuleBaseFactory.newRuleBase();
        Package rulesPackage = packageBuilder.getPackage();
        ruleBase.addPackage(rulesPackage);

        return ruleBase;
    }

    private void assertNoRuleErrors(PackageBuilder packageBuilder) {
        PackageBuilderErrors errors = packageBuilder.getErrors();

        if (errors.getErrors().length > 0) {
            StringBuilder errorMessages = new StringBuilder();
            errorMessages.append("Found errors in package builder\n");
            for (int i = 0; i < errors.getErrors().length; i++) {
                DroolsError errorMessage = errors.getErrors()[i];
                errorMessages.append(errorMessage);
                errorMessages.append("\n");
            }
            errorMessages.append("Could not parse knowledge");

            throw new IllegalArgumentException(errorMessages.toString());
        }
    }

    private WorkingMemory initializeMessageObjects(RuleBase ruleBase) {
        WorkingMemory workingMemory = ruleBase.newStatefulSession();

        createHelloWorld(workingMemory);
        createHighValue(workingMemory);

        return workingMemory;
    }

    private void createHelloWorld(WorkingMemory workingMemory) {
        Message helloMessage = new Message();
        helloMessage.setType("Hello");
        workingMemory.insert(helloMessage);
    }

    private void createHighValue(WorkingMemory workingMemory) {
        Message highValue = new Message();
        highValue.setType("High value");
        highValue.setMessageValue(42);
        workingMemory.insert(highValue);
    }
}

The last two files for this extension are identical to the Hello World example. I include them any way so you can review them easily. Lets begin with the hello world rule:

src/main/resources/se/waymark/drools/hello/helloWorld.drl

import se.waymark.drools.message.Message;

rule "Hello World"
when
  message:Message (type == 'Hello')
then
  message.printMessage();
end

Nothing have changed here.

The Maven pom is also identical to the previous example:

pom.xml


<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>se.waymark.drools</groupId>
    <artifactId>drools-with-action</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>5.4.0.Final</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
    </dependencies>
</project>

Conclusion

Separating the message and action was not very difficult. I am not sure if this is the actual way you want to do it in production. You might want to use actions that already exists in an object pool or similar. Doing that in this example would add a lot of complexity that I would prefer to avoid and instead be as clear as I can.

Credits

I wish to thank Johan Karlsson and Malin Ekholm for the feedback. It as always a pleasure.

Resources



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS