Table of Contents
So where do we get started, there are so many use cases and so much functionality in a rule engine such as Drools that it becomes beguiling. Have no fear my intrepid adventurer, the complexity is layered and you can ease yourself into with simple use cases.
Stateless session, not utilising inference, forms the simplest use case. A stateless session can be called like a function passing it some data and then receiving some results back. Some common use cases for stateless sessions are, but not limited to:
Validation
Is this person eligible for a mortgage?
Calculation
Compute a mortgage premium.
Routing and Filtering
Filter incoming messages, such as emails, into folders.
Send incoming messages to a destination.
So let's start with a very simple example using a driving license application.
public class Applicant { private String name; private int age; private boolean valid; // getter and setter methods here }
Now that we have our data model we can write our first rule. We assume that the application uses rules to refute invalid applications. As this is a simple validation use case we will add a single rule to disqualify any applicant younger than 18.
package com.company.license rule "Is of valid age" when $a : Applicant( age < 18 ) then $a.setValid( false ); end
To make the engine aware of data, so it can be processed against
the rules, we have to insert the data, much like
with a database. When
the Applicant instance is inserted into the engine it is evaluated
against the constraints of the rules, in this case just two constraints
for one rule. We say two because the type Applicant
is the first object
type constraint, and age < 18
is the second field constraint.
An object type constraint plus its zero or more field constraints is
referred to as a pattern. When an inserted instance satisfies both the
object type constraint and all the field constraints, it is said to be
matched. The $a
is a binding variable which permits us to reference
the matched object in the consequence. There its properties can be
updated. The dollar character ('$') is optional, but it helps to
differentiate variable names from field names. The process of
matching patterns against the inserted data is, not surprisingly,
often referred to as pattern matching.
Let's assume that the rules are in the same folder as the classes,
so we can use the classpath resource loader to build our first
KnowledgeBase
. A Knowledge Base is what we call our collection of
compiled rules, which are compiled using the KnowledgeBuilder
.
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); kbuilder.add( ResourceFactory.newClassPathResource( "licenseApplication.drl", getClass() ), ResourceType.DRL ); if ( kbuilder.hasErrors() ) { System.err.println( builder.getErrors().toString() ); } kbase.addKnowledgePackages( kbuilder.getKnowledgePackages() );
The above code snippet looks on the classpath for the
licenseApplication.drl
file, using the method newClassPathResource()
. The resource type
is DRL, short for "Drools Rule Language". Once the DRL file has been added
we can check the Knowledge Builder object for any errors. If there are no
errors, we can add the resulting packages to our Knowledge Base.
Now we are ready to build our session and execute against some
data:
StatelessKnowledgeSession ksession = kbase.newStatelessKnowledgeSession(); Applicant applicant = new Applicant( "Mr John Smith", 16 ); assertTrue( applicant.isValid() ); ksession.execute( applicant ); assertFalse( applicant.isValid() );
The preceding code executes the data against the rules. Since the applicant is under the age of 18, the application is marked as invalid.
So far we've only used a single instance, but what if we want to use
more than one? We can execute against any object implementing Iterable, such as
a collection. Let's add another class called Application
, which has the
date of the application, and we'll also move the boolean valid field to the
Application
class.
public class Applicant { private String name; private int age; // getter and setter methods here } public class Application { private Date dateApplied; private boolean valid; // getter and setter methods here }
We can also add another rule to validate that the application was made within a period of time.
package com.company.license rule "Is of valid age" when Applicant( age < 18 ) $a : Application() then $a.setValid( false ); end rule "Application was made this year" when $a : Application( dateApplied > "01-jan-2009" ) then $a.setValid( false ); end
Unfortunately a Java array does not implement the
Iterable
interface, so we have to use the JDK converter method
Arrays.asList(...)
. The code
shown below executes against an iterable list, where all collection
elements are inserted before any matched rules are fired.
StatelessKnowledgeSession ksession = kbase.newStatelessKnowledgeSession(); Applicant applicant = new Applicant( "Mr John Smith", 16 ); Application application = new Application(); assertTrue( application() ); ksession.execute( Arrays.asList( new Object[] { application, applicant } ) ); assertFalse( application() );
The two execute methods execute(Object object)
and
execute(Iterable objects)
are actually convenience methods for the
interface BatchExecutor
's method execute(Command command)
.
A CommandFactory
is used to create commands, so that the following is
equivalent to execute(Iterable it)
:
ksession.execute( CommandFactory.newInsertIterable( new Object[] { application, applicant } ) );
Batch Executor and Command Factory are particularly useful when working with multiple Commands and with output identifiers for obtaining results.
List<Command> cmds = new ArrayList<Command>(); cmds.add( CommandFactory.newInsert( new Person( "Mr John Smith" ), "mrSmith" ); cmds.add( CommandFactory.newInsert( new Person( "Mr John Doe" ), "mrDoe" ); BatchExecutionResults results = ksession.execute( CommandFactory.newBatchExecution( cmds ) ); assertEquals( new Person( "Mr John Smith" ), results.getValue( "mrSmith" ) );
CommandFactory
supports many other Commands that can be used in
the BatchExecutor
like StartProcess
, Query
, and
SetGlobal
.
Stateful Sessions are longer lived and allow iterative changes over time. Some common use cases for Stateful Sessions are, but not limited to:
Monitoring
Stock market monitoring and analysis for semi-automatic buying.
Diagnostics
Fault finding, medical diagnostics
Logistics
Parcel tracking and delivery provisioning
Compliance
Validation of legality for market trades.
In contrast to a Stateless Session, the dispose()
method must be called
afterwards to ensure there are no memory leaks, as the Knowledge Base
contains references to Stateful Knowledge Sessions when they are created.
StatefulKnowledgeSession
also supports the BatchExecutor
interface, like StatelessKnowledgeSession
, the only difference
being that the FireAllRules
command is not automatically called at the
end for a Stateful Session.
We illustrate the monitoring use case with an example for raising a
fire alarm. Using just four classes, we represent rooms in a house, each of which
has one sprinkler. If a fire starts in a room, we
represent that with a single Fire
instance.
public class Room { private String name // getter and setter methods here } public classs Sprinkler { private Room room; private boolean on; // getter and setter methods here } public class Fire { private Room room; // getter and setter methods here } public class Alarm { }
In the previous section on Stateless Sessions the concepts of inserting and matching against data was introduced. That example assumed that only a single instance of each object type was ever inserted and thus only used literal constraints. However, a house has many rooms, so rules must express relationships between objects, such as a sprinkler being in a certain room. This is best done by using a binding variable as a constraint in a pattern. This "join" process results in what is called cross products, which are covered in the next section.
When a fire occurs an instance of the Fire
class is created, for
that room, and inserted into the session. The rule uses a binding on the
room
field of the Fire
object to constrain matching to the sprinkler for that room,
which is currently off. When this rule fires and the consequence is executed
the sprinkler is turned on.
rule "When there is a fire turn on the sprinkler" when Fire($room : room) $sprinkler : Sprinkler( room == $room, on == false ) then modify( $sprinkler ) { setOn( true ) }; System.out.println( "Turn on the sprinkler for room " + $room.getName() ); end
Whereas the Stateless Session uses standard Java syntax to modify a field, in the above rule we use the <kw>modify</kw> statement, which acts as a sort of "with" statement. It may contain a series of comma separated Java expressions, i.e., calls to setters of the object selected by the <kw>modify</kw> statement's control expression. This modifies the data, and makes the engine aware of those changes so it can reason over them once more. This process is called inference, and it's essential for the working of a Stateful Session. Stateless Sessions typically do not use inference, so the engine does not need to be aware of changes to data. Inference can also be turned off explicitly by using the sequential mode.
So far we have rules that tell us when matching data exists, but
what about when it does not exist? How do we determine
that a fire has been extinguished, i.e., that there isn't a Fire
object any more? Previously the constraints have been sentences according
to Propositional Logic,
where the engine is constraining against individual intances. Drools also has
support for First Order Logic that allows you to look at sets of data.
A pattern under the keyword <kw>not</kw> matches when something does not exist.
The rule given below turns the sprinkler off as soon as the fire in that
room has disappeared.
rule "When the fire is gone turn off the sprinkler" when $room : Room( ) $sprinkler : Sprinkler( room == $room, on == true ) not Fire( room == $room ) then modify( $sprinkler ) { setOn( false ) }; System.out.println( "Turn off the sprinkler for room " + $room.getName() ); end
While there is one sprinkler per room, there is just a single alarm
for the building. An Alarm
object is created when a fire occurs,
but only one Alarm
is needed for the entire building, no matter
how many fires occur. Previously <kw>not</kw> was introduced to match the absence of
a fact; now we use its complement <kw>exists</kw> which matches for one or more
instances of some category.
rule "Raise the alarm when we have one or more fires" when exists Fire() then insert( new Alarm() ); System.out.println( "Raise the alarm" ); end
Likewise, when there are no fires we want to remove the alarm, so the <kw>not</kw> keyword can be used again.
rule "Cancel the alarm when all the fires have gone" when not Fire() $alarm : Alarm() then retract( $alarm ); System.out.println( "Cancel the alarm" ); end
Finally there is a general health status message that is printed when the application first starts and after the alarm is removed and all sprinklers have been turned off.
rule "Status output when things are ok" when not Alarm() not Sprinkler( on === true ) then System.out.println( "Everything is ok" ); end
The above rules should be placed in a single DRL file and saved to
some directory on the classpath and using the file name
fireAlarm.drl
,
as in the Stateless Session example. We can then build a Knowledge Base,
as before, just using the new name fireAlarm.drl
.
The difference is that
this time we create a Stateful Session from the Knowledge Base, whereas
before we created a Stateless Session.
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder(); kbuilder.add( ResourceFactory.newClassPathResource( "fireAlarm.drl", getClass() ), ResourceType.DRL ); if ( kbuilder.hasErrors() ) { System.err.println( builder.getErrors().toString() ); } kbase.addKnowledgePackages( kbuilder.getKnowledgePackages() ); StatefulKnowledgeSession ksession = kbase.newStatefulKnowledgeSession();
With the session created it is now possible to iteratvely work
with it over time. Four Room
objects are created and
inserted, as well as one Sprinkler
object for
each room. At this point the engine has done all of its
matching, but no rules have fired yet. Calling ksession.fireAllRules()
allows the matched rules to fire, but without a fire that will
just produce the health message.
String[] names = new String[]{"kitchen", "bedroom", "office", "livingroom"}; Map<String,Room> name2room = new HashMap<String,Room>(); for( String name: names ){ Room room = new Room( name ); name2room.put( name, room ); ksession.insert( room ); Sprinkler sprinkler = new Sprinkler( room ); ksession.insert( sprinkler ); } ksession.fireAllRules()
> Everything is ok
We now create two fires and insert them; this time a reference is
kept for the returned FactHandle
. A Fact Handle is an
internal engine reference to the inserted instance and allows instances to be
retracted or modified at a later point in time. With the fires now in
the engine, once fireAllRules()
is called, the alarm is raised and the
respective sprinklers are turned on.
Fire kitchenFire = new Fire( name2room.get( "kitchen" ) ); Fire officeFire = new Fire( name2room.get( "office" ) ); FactHandle kitchenFireHandle = ksession.insert( kitchenFire ); FactHandle officeFireHandle = ksession.insert( officeFire ); ksession.fireAllRules();
> Raise the alarm > Turn on the sprinkler for room kitchen > Turn on the sprinkler for room office
After a while the fires will be put out and the Fire
instances are retracted. This results in the sprinklers being turned off, the alarm
being cancelled, and eventually the health message is printed again.
ksession.retract( kitchenFireHandle ); ksession.retract( officeFireHandle ); ksession.fireAllRules();
> Turn on the sprinkler for room office > Turn on the sprinkler for room kitchen > Cancel the alarm > Everything is ok
Everyone still with me? That wasn't so hard and already I'm hoping you can start to see the value and power of a declarative rule system.