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.
This is an experimental feature. In particular, the API is subject to change.
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.)
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() );
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 );
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