Templates

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.

Caution

This is an experimental feature. In particular, the API is subject to change.

The Rule Template File

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 <kw>template header</kw>, you should write a number of lines, each of which contains a single parameter name.

Example 5.2. Rule template file: template header

template header
parameter-name-1
...
parameter-name-n
...

The template header is followed by the text that is to be replicated and interpolated with the actual parameters. It may begin with a <kw>package</kw> statement, followed by some additional lines. These may be sectioned into one or more templates, each of them between a pair of matching <kw>template</kw> and <kw>end template</kw> statements. The <kw>template</kw> 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.

Example 5.3. Rule template file: templates

template header
parameter-name-1
...
parameter-name-n
package ...     # optional
header text    # optional
template template-name
...
// template text
...
end template
...

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 <kw>template</kw> and <kw>end template</kw> 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.)

Expanding a Template

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.

Instantiation from Java Objects

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() );

Instantiation from Maps

A Map that provides the values for substituting template parameters should have a (string) key set matching all of the parameter names. Again, values could be from any class, as long as they provide a good toString() method. The expansion would use the same approach, just differing in the way the map collection is composed.

Collection<Map<String,Object>> paramMaps = new ArrayList<Map<String,Object>>();
// populate paramMaps
ObjectDataCompiler converter = new ObjectDataCompiler();
InputStream templateStream =
    this.getClass().getResourceAsStream( "template.drl" );
String drl = converter.compile( objs, templateStream );

Example

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