JBoss.orgCommunity Documentation

Chapter 5. Authoring

5.1. Decision Tables in Spreadsheets
5.1.1. When to use Decision tables
5.1.2. Overview
5.1.3. How decision tables work
5.1.4. Keywords and Syntax
5.1.5. Creating and integrating Spreadsheet based Decision Tables
5.1.6. Managing business rules in decision tables.
5.1.7. Rule Templates
5.2. Templates
5.2.1. The Rule Template File
5.2.2. Expanding a Template
5.2.3. Example

Decision tables are a "precise yet compact" (ref. Wikipedia) way of representing conditional logic, and are well suited to business level rules.

Drools supports managing rules in a spreadsheet format. Supported formats are Excel (XLS), and CSV, which means that a variety of spreadsheet programs (such as Microsoft Excel, OpenOffice.org Calc amongst others) can be utilized. It is expected that web based decision table editors will be included in a near future release.

Decision tables are an old concept (in software terms) but have proven useful over the years. Very briefly speaking, in Drools decision tables are a way to generate rules driven from the data entered into a spreadsheet. All the usual features of a spreadsheet for data capture and manipulation can be taken advantage of.

The key point to keep in mind is that in a decision table each row is a rule, and each column in that row is either a condition or action for that rule.

The spreadsheet looks for the RuleTable keyword to indicate the start of a rule table (both the starting row and column). Other keywords are also used to define other package level attributes (covered later). It is important to keep the keywords in the one column. By convention the second column ("B") is used for this, but it can be any column (convention is to leave a margin on the left for notes). In the following diagram, C is actually the column where it starts. Everything to the left of this is ignored.

If we expand the hidden sections, it starts to make more sense how it works; note the keywords in column C.

Now the hidden magic which makes it work can be seen. The RuleSet keyword indicates the name to be used in the rule package that will encompass all the rules. This name is optional, using a default, but it must have the RuleSet keyword in the cell immediately to the right.

The other keywords visible in Column C are Import and Sequential which will be covered later. The RuleTable keyword is important as it indicates that a chunk of rules will follow, based on some rule templates. After the RuleTable keyword there is a name, used to prefix the names of the generated rules. The row numbers are appended to guarantee unique rule names. The column of RuleTable indicates the column in which the rules start; columns to the left are ignored.

Referring to row 14 (the row immediately after RuleTable), the keywords CONDITION and ACTION indicate that the data in the columns below are for either the LHS or the RHS parts of a rule. There are other attributes on the rule which can also be optionally set this way.

Row 15 contains declarations of ObjectTypes. The content in this row is optional, but if this option is not in use, the row must be left blank; however this option is usually found to be quite useful. When using this row, the values in the cells below (row 16) become constraints on that object type. In the above case, it will generate Person(age=="42") and Cheese(type=="stilton"), where 42 and "stilton" come from row 18. In the above example, the "==" is implicit; if just a field name is given it will assume that it is to look for exact matches.

Row 16 contains the rule templates themselves. They can use the "$para" place holder to indicate where data from the cells below will be populated ($param can be sued or $1, $2 etc to indicate parameters from a comma separated list in a cell below). Row 17 is ignored as it is textual descriptions of the rule template.

Rows 18 and 19 show data, which will be combined (interpolated) with the templates in row 15, to generate rules. If a cell contains no data, then its template is ignored. (This would mean that some condition or action does not apply for that rule row.) Rule rows are read until there is a blank row. Multiple RuleTables can exsist in a sheet. Row 20 contains another keyword, and a value. The row positions of keywords like this do not matter (most people put them at the top) but their column should be the same one where the RuleTable or RuleSet keywords should appear. In our case column C has been chosen to be significant, but column A could be used instead.

In the above example, rules would be rendered like the following (as it uses the "ObjectType" row):

//row 18
			rule "Cheese_fans_18"
			when
			Person(age=="42")
			Cheese(type=="stilton")
			then
			list.add("Old man stilton");
			end

The syntax of what goes in the templates is dependent on if it is a CONDITION column or ACTION column. In most cases, it is identical to "vanilla" DRL for the LHS or RHS respectively. This means in the LHS, the constraint language must be used, and in the RHS it is a snippet of code to be executed.

The $param place holder is used in templates to indicate where data form the cell will be interpolated. You can also use $1 to the same effect. If the cell contains a comma separated list of values. Symbols $1, $2, etc. may be used to indicate which positional parameter from the list of values in the cell will be used.


For conditions: How snippets are rendered depends on the presence of an entry in the row above, where ObjectType declarations may appear. If there is such an entry, the snippets are rendered as individual constraints on that ObjectType. If there isn't, then they are just rendered as is (with values substituted). If just a plain field is entered (as in the example above) then it will assume that this means equality. If another operator is placed at the end of the snippet, then the values will be interpolated at the end of the constraint, otherwise it will look for $param as outlined previously.

For consequences: How snippets are rendered also depends on the presence of an entry in the row immediately above it. If there is no entry, the output is simply the interpolated snippets. If there is something there (which would typically be a bound variable or a global like in the example above) then it will append it as a method call on that object (refer to the above example).

This may be easiest to understand with some examples, given below.

The above shows how the Person ObjectType declaration spans 2 columns in the spreadsheet, thus both constraints will appear as Person(age == ... , type == ...). As before, since only the field names are present in the snippet, they imply an equality test.

The above condition example shows how you use interpolation to place the values in the snippet (in this case it would result in Person(age == "42")).

The above condition example shows that if you put an operator on the end by itself, the values will be placed after the operator automatically.

A binding can be put in before the column (the constraints will be added from the cells below). Anything can be placed in the ObjectType row. (For instance, this could be a precondition for the columns in the spreadsheet columns that follow).

This shows how the consequence could be done by simple interpolation: just leave the cell above blank. (The same applies to condition columns.) With this style anything can be placed in the consequence, not just one method call.

The following table describes the keywords that are pertinent to the rule table structure.

Table 5.1. Keywords

KeywordDescriptionInclusion Status
RuleSetThe cell to the right of this contains the ruleset name.One only; if left out, it will default.
SequentialThe cell to the right of this can be true or false. If true, then salience is used to ensure that rules fire from the top down.Optional
ImportThe cell to the right contains a comma separated list of Java classes to import.Optional
RuleTableA cell starting with RuleTable indicates the start of a definition of a rule table. The actual rule table starts the next row down. The rule table is read left-to-right, and top-down, until the next blank row.At least one; if there are more, then they are all added to the one ruleset.
CONDITIONIndicates that this column will be for rule conditions.At least one per rule table
ACTIONIndicates that this column will be for rule consequences.At least one per rule table
PRIORITYIndicates that this column's values will set the 'salience' values for the rule row. Over-rides the 'Sequential' flag.Optional
DURATIONIndicates that this column's values will set the duration values for the rule row.Optional
NAMEIndicates that this column's values will set the name for the rule generated from that row.Optional
FunctionsThe cell immediately to the right can contain functions which can be used in the rule snippets. Drools supports functions defined in the DRL, allowing logic to be embedded in the rule, and changed without hard coding, use with care. Same syntax as regular DRL.Optional
VariablesThe cell immediately to the right can contain global declarations which Drools supports. This is a type, followed by a variable name. (If multiple variables are needed, separate them with commas.)Optional
No-loop or UnloopPlaced in the header of a table, no-loop or unloop will both complete the same function of not allowing a rule (row) to loop. For this option to function correctly, there must be a value (true or false) in the cell for the option to take effect. If the cell is left blank then this option will not be set for the row.Optional
XOR-GROUPCell values in this column mean that the rule row belongs to the given Activation group . An Activation group means that only one rule in the named group will fire (i.e., the first one to fire cancels the other rules' activations).Optional
AGENDA-GROUPCell values in this column mean that the rule row belongs to the given Agenda group. (This is one way of controlling flow between groups of rules - see also "rule flow").Optional
RULEFLOW-GROUPCell values in this column mean that the rule row belongs to the given rule-flow group.Optional
WorksheetBy default, the first worksheet is only looked at for decision tables.N/A

Below you will find examples of using the HEADER keyword, which affects the rules generated for each row. Note that the header name is what is important in most cases. If no value appears in the cells below it, then the attribute will not apply (it will be ignored) for that specific row.

The following is an example of Import (comma delimited), Variables (gloabls) - also comma delimited, and a function block (can be multiple functions - just the usual drl syntax). This can appear in the same column as the "RuleSet" keyword, and can be below all the rule rows if you desire.

Related to decision tables (but not necessarily requiring a spreadsheet) are "Rule Templates" (in the drools-templates module). These use any tablular data source as a source of rule data - populating a template to generate many rules. This can allow both for more flexible spreadsheets, but also rules in existing databases, for instance (at the cost of developing the template up front to generate the rules).

With Rule Templates the data is separated from the rule, and there are no restrictions on which part of the rule is data-driven. So whilst you can do everything you could do in decision tables you can also do the following:

As an example, a more classic decision table is shown, but without any hidden rows for the rule meta data (so the spreadsheet only contains the raw data to generate the rules).

See the "ExampleCheese.xls" in the examples download for the above spreadsheet.

If this was a regular decision table there would be hidden rows before row 1 and between rows 1 and 2 containing rule metadata. With rule templates the data is completely separate from the rules. This has two handy consequences - you can apply multiple rule templates to the same data and your data is not tied to your rules at all. So what does the template look like?

1  template header
2  age
3  type
4  log
5
6  package org.drools.examples.templates;
7
8  global java.util.List list;
9
10 template "cheesefans"
11
12 rule "Cheese fans_@{row.rowNumber}"
13 when
14    Person(age == @{age})
15    Cheese(type == "@{type}")
16 then
17    list.add("@{log}");
18 end
19
20 end template
	

Referring to the above:

Line 1: all rule templates start with "template header"
Lines 2-4: following the header is the list of columns in the order they appear in the data. In this case we are calling the first column "age", the second "type" and the third "log".
Lines 5: empty line signifying the end of the column definitions
Lines 6-9: standard rule header text. This is standard rule DRL and will appear at the top of the generated DRL. Put the package statement and any imports and global definitions
Line 10: The "template" keyword signals the start of a rule template. There can be more than one template in a template file. The template should have a unique name.
Lines 11-18: The rule template - see below
Line 20: "end template" signifies the end of the template.

The rule templates rely on MVEL to do substitution using the syntax @{token_name}. There is currently one built-in expression, @{row.rowNumber} which gives a unique number for each row of data and enables you to generate unique rule names. For each row of data a rule will be generated with the values in the data substituted for the tokens in the template. With the example data above the following rule file would be generated:

package org.drools.examples.templates;

global java.util.List list;

rule "Cheese fans_1"
when
  Person(age == 42)
  Cheese(type == "stilton")
then
  list.add("Old man stilton");
end

rule "Cheese fans_2"
when
  Person(age == 21)
  Cheese(type == "cheddar")
then
  list.add("Young man cheddar");
end

The code to run this is simple:

KnowledgeBuilder kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
DecisionTableConfiguration dtconf = KnowledgeBuilderFactory.newDecisionTableConfiguration();
dtconf.setInputType( DecisionTableInputType.XLS );
dtconf.setWorksheetName( "Tables_2" );
kbuilder.add( ResourceFactory.newInputStreamResource( getSpreadsheetStream() ),
              ResourceType.DTABLE,
              dtconf );

Collection<KnowlegePackage> kpkg = kbuilder.getKnowlegePackages();

We create an ExternalSpreadsheetCompiler object and use it to merge the spreadsheet with the rules. The two integer parameters indicate the column and row where the data actually starts - in our case column 2, row 2 (i.e. B2)

If you discover that you have a group of rules following the same arrangement of patterns, constraints and actions on the RHS, differing only in constants or names for objects or fields, you might think of employing Drool's rule template feature for generating the actual rules. You would write a rule template file, containing the textual skeleton of your rule and use the Drools template compiler in combination with a collection of objects providing the actual values for the "flesh" of the rules for their instantiation.

The mechanism is very similar to what a macro processor does. The major advantage proffered by template expansion is that it's nicely integrated in the overall handling of Knowledge Resources.

A rule template file begins with a header defining the placeholders, or formal template parameters for the strings that are to be inserted during instantiation. After the first line, which invariably contains template header, you should write a number of lines, each of which contains a single parameter name.


The template header is followed by the text that is to be replicated and interpolated with the actual parameters. It may begin with a package statement, followed by some additional lines. These may be sectioned into one or more templates, each of them between a pair of matching template and end template statements. The template takes an argument, which puts a name to the template. The name can be a simple unquoted name or an arbitrary string enclosed in double quotes. The template text between these lines may contain one or more rules, constituting the "raw material" for the expansion.


The resulting text will begin with the package line and the header text following it, if present. Then, each template text will be expanded individually, yielding one set of rules for each of the actual parameter sets. Therefore, the structure of the template sections affect the order of the generated rules, since the generator iterates over the sections and then over the set of actual parameters.

Any interpolation takes place between a pair of template and end template statements, when this template is expanded. The template text is scanned for occurrences of parameter expansions written according to:

@{parameter-name}

The name between '@{' and '}' should be one of the parameter names defined in the template header. The substitution is effected anywhere, even within string literals.

An important parameter is available without having to be included in the data source providing the actual values. The parameter substitution

@{row.rowNumber}

expands to the integers 0, 1, 2, etc., providing a unique distinction for the instantiation derived from a parameter set. You would use this as part of each rule name, because, without this precaution, there would be duplicate rule names. (You are, of course, free to use your own identification included as an extra parameter.)

To expand a template, you must prepare a data source. This can be a spreadsheet, as explained in the previous section. Here, we'll concentrate on expansion driven by Java objects. There are two straightforward ways of supplying values for a fixed set of names: Java objects, in the JavaBeans style, and Maps. Both of them can be arranged in a Collection, whose elements will be processed during the expansion, resulting in an instantiation for each element.

You may use a Java object that provides getter methods corresponding to all of the parameter names of your template file. If, for instance, you have defined a header

template header
type
limit
word

the following Java class could be used:

public class ParamSet {
    //...
    public ParamSet( String t, int l, boolean w ) {
        //...
    }
    public String  getType(){...}
    public int     getLimit(){...}
    public boolean isWord(){...}
}

Although interpolation is pure text manipulation, the actual values supplied may be of any type, just as long as this type provides a reasonable toString() method. (For simple types, the eponymous static method of the related class from java.lang is used.)

Assuming that we have created a Collection<ParamSet> for a template file template.drl, we can now proceed to request its expansion.

Collection<ParamSet> paramSets = new ArrayList<ParamSet>();
// populate paramSets
paramSets.add( new ParamSet( "Foo", 42, true ) );
paramSets.add( new ParamSet( "Bar", 13, false ) );
ObjectDataCompiler converter = new ObjectDataCompiler();
InputStream templateStream =
    this.getClass().getResourceAsStream( "template.drl" );
String drl = converter.compile( objs, templateStream );
The resulting string contains the expanded rules text. You could write it to a file and proceed as usual, but it's also possible to feed this to a KnowledgeBuilder and continue with the resulting Knowledge Packages.
KnowledgeBase kBase = KnowledgeBaseFactory.newKnowledgeBase();
KnowledgeBuilder kBuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
Reader rdr = new StringReader( drl );
kBuilder.add( ResourceFactory.newReaderResource( rdr ), ResourceType.DRL );
if( kBuilder.hasErrors() ){
    // ...
    throw new IllegalStateException( "DRL errors" );
}
kBase.addKnowledgePackages( kBuilder.getKnowledgePackages() );

The following example illustrates template expansion. It is based on simple objects of class Item containing a couple of integer fields and an enum field of type ItemCode.

public class Item {
    // ...
    public Item( String n, int p, int w, ItemCode c ){...}

    public String   getName() {...}
    public int      getWeight() {...}
    public int      getPrice() {...}
    public ItemCode getCode() {...}
}

public enum ItemCode {
    LOCK,
    STOCK,
    BARREL;
}

The rule template contains a single rule. Notice that the field name for the range test is a parameter, which enables us to instantiate the template for different fields.

template header
field
lower
upper
codes

package range;
template "inRange"
rule "is in range @{row.rowNumber}"
when
    Item( $name : name, $v : @{field} >= @{lower} && <= @{upper}, $code : code @{codes} )
then
    System.out.println( "Item " + $name + " @{field} in range: " + $v + " code: " + $code );
end
end template

The next code snippet is from the application, where several parameter sets have to be set up. First, there is class ParamSet, for storing a set of actual parameters.

public class ParamSet {
    //...
    private EnumSet<ItemCode> codeSet;
	
    public ParamSet( String f, int l, int u, EnumSet<ItemCode> cs ){...}

    public String getField() { return field; }
    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public String getCodes(){
        StringBuilder sb = new StringBuilder();
        String conn = "";
        for( ItemCode ic: codeSet ){
             sb.append( conn ).append( " == ItemCode." ).append( ic );
             conn = " ||";
        }
        return sb.toString();
    }	
}

Note that the method getCodes() does returns the EnumSet<ItemCode> field value as a String value representing a multiple restriction, i.e., a test for one out of a list of values.

The task of expanding a template, passing the resulting DRL text to a Knowledge Builder and adding the resulting Knowledge Packages to a Knowledge Base is generic. The utility class Expander takes care of this, using a Knowledge Base, the InputStream with the rule template and the collection of parameter sets.

public class Expander {
	
    public void expand( KnowledgeBase kBase, InputStream is, Collection<?> act )
        throws Exception {
        ObjectDataCompiler converter = new ObjectDataCompiler();		
        String drl = converter.compile( act, is );
        
        KnowledgeBuilder kBuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
        Reader rdr = new StringReader( drl );
        kBuilder.add( ResourceFactory.newReaderResource( rdr ), ResourceType.DRL );
        if( kBuilder.hasErrors() ){
            for( KnowledgeBuilderError err: kBuilder.getErrors() ){
                System.err.println( err.toString() );
            }
            throw new IllegalStateException( "DRL errors" );
        }
        kBase.addKnowledgePackages( kBuilder.getKnowledgePackages() );
    }
}

We are now all set to prepare the Knowledge Base with some generated rules. First, we define several parameter sets, constructed as ParamSet objects, and add them to a List, which is passed to the expand method shown above. Then we launch a stateful session, insert a few Item, and watch what happens.

Collection<ParamSet> cfl = new ArrayList<ParamSet>();
cfl.add( new ParamSet( "weight",  10,  99, EnumSet.of( ItemCode.LOCK, ItemCode.STOCK ) ) );
cfl.add( new ParamSet( "price",   10,  50, EnumSet.of( ItemCode.BARREL ) ) );

KnowledgeBase kBase = KnowledgeBaseFactory.newKnowledgeBase();
Expander ex = new Expander();
InputStream dis = new FileInputStream( new File( "rangeTemp.drl" ) );
ex.expand( kBase, dis, cfl );
        
StatefulKnowledgeSession session = kBase.newStatefulKnowledgeSession();
session.insert( new Item( "A", 130,  42, ItemCode.LOCK ) );
session.insert( new Item( "B",  44, 100, ItemCode.STOCK ) );
session.insert( new Item( "C", 123, 180, ItemCode.BARREL ) );
session.insert( new Item( "D",  85,   9, ItemCode.LOCK ) );
        
session.fireAllRules();

Notice that the two resulting rules deal with different fields, one with an item's weight, the other one with its price. - Below is the output.

Item E price in range: 25 code: BARREL
Item A weight in range: 42 code: LOCK