Monday, June 07, 2010

Creating pluggable operators

Drools supports creation of pluggable operators. This is a small tutorial on how to create a "str" operator that can be used to compare string values in LHS patterns. The operator adds the ability to write patterns such as:


$m : Message( routingValue str[startsWith] "R1" )


$m : Message( routingValue str[endsWith] "R2" )

or

$m : Message( routingValue str[length] 17 )

First thing that needs to be done is to write your evaluator definition. Your Definition class has to implement org.drools.base.evaluators.EvaluatorDefinition which contains all methods needed to work with this new "str" operator, so we have:

public class StrEvaluatorDefinition implements EvaluatorDefinition {
public static final Operator STR_COMPARE = Operator.addOperatorToRegistry(
"str", false);
public static final Operator NOT_STR_COMPARE = Operator
.addOperatorToRegistry("str", true);
private static final String[] SUPPORTED_IDS = { STR_COMPARE
.getOperatorString() };

public enum Operations {
startsWith, endsWith, length;
}

private Evaluator[] evaluator;

@Override
public Evaluator getEvaluator(ValueType type, Operator operator) {
return this.getEvaluator(type, operator.getOperatorString(), operator
.isNegated(), null);
}

@Override
public Evaluator getEvaluator(ValueType type, Operator operator,
String parameterText) {
return this.getEvaluator(type, operator.getOperatorString(), operator
.isNegated(), parameterText);
}

@Override
public Evaluator getEvaluator(ValueType type, String operatorId,
boolean isNegated, String parameterText) {
return getEvaluator(type, operatorId, isNegated, parameterText,
Target.FACT, Target.FACT);
}

@Override
public Evaluator getEvaluator(ValueType type, String operatorId,
boolean isNegated, String parameterText, Target leftTarget,
Target rightTarget) {
StrEvaluator evaluator = new StrEvaluator(type, isNegated);
evaluator.setParameterText(parameterText);
return evaluator;
}

@Override
public String[] getEvaluatorIds() {
return SUPPORTED_IDS;
}

@Override
public Target getTarget() {
return Target.FACT;
}

@Override
public boolean isNegatable() {
return true;
}

@Override
public boolean supportsType(ValueType type) {
return true;
}

@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
evaluator = (Evaluator[]) in.readObject();
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(evaluator);
}
...
}

The first thing StrEvaluatorDefinition does is registers two operators STR_COMPARE, and NOT_STR_COMPARE using the operator id and a boolean flag which determines if this operator can be negated or not. It also defines the Operations enum which lists all possible operations we can perform (startsWith, endsWith, length). These operations are passed to the operator through angled brackets, similar to what you are already familiar with when writing Drools Fusion patterns, such as for example

this after[0, 3m] $t

The last getEvaluator method gets passed parameters type (type of the operator's operands), operatorId (self explanatory), isNegated (specifies if you can negate, or use "not" with this operator), parameterText (input parameters in the angle brackets), leftTarget (target fact on left), rightTarget (target fact on right).
This method then creates a new instance of the actual Evaluator Implemention class (StrEvaluator) and passes it the parameterText.

Next thing as you guessed it is to create the actual Evaluator implementation code which has to extend org.drools.base.BaseEvaluator:

public static class StrEvaluator extends BaseEvaluator {
private Operations parameter;

public void setParameterText(String parameterText) {
this.parameter = Operations.valueOf(parameterText);
}

public Operations getParameter() {
return parameter;
}

public StrEvaluator(final ValueType type, final boolean isNegated) {
super(type, isNegated ? NOT_STR_COMPARE : STR_COMPARE);
}

@Override
public boolean evaluate(InternalWorkingMemory workingMemory,
InternalReadAccessor extractor, Object object, FieldValue value) {
final Object objectValue = extractor
.getValue(workingMemory, object);
switch (parameter) {
case startsWith:
return this.getOperator().isNegated() ^ (((String)objectValue).startsWith( (String)value.getValue() ));
case endsWith:
return this.getOperator().isNegated() ^ (((String)objectValue).endsWith( (String)value.getValue() ));
case length:
return this.getOperator().isNegated() ^ (((String)objectValue).length() == ((Long) value.getValue()).longValue() );
default:
throw new IllegalAccessError("Illegal str comparison parameter");
}
}
...
}

The implementation code of an Evaluator defines a number of "evaluate" methods is different circumstances. You can get a more detailed description of this by looking at the code for org.drools.base.BaseEvaluator.

Next thing to do is to actually let our KnowledgeBuilder know about this new operator. For this we can just use KnowledgeBuilderConfiguration and pass it to the kbuilder instance:

KnowledgeBuilderConfiguration builderConf = KnowledgeBuilderFactory
.newKnowledgeBuilderConfiguration();
builderConf.setOption(EvaluatorOption.get("str",
new StrEvaluatorDefinition()));
KnowledgeBuilder kbuilder = KnowledgeBuilderFactory
.newKnowledgeBuilder(builderConf);

Another way of configuring this is using a configuration file in the classpath with the following path and name:

META-INF/drools.packagebuilder.conf

Doing this has the advantage of the Drools Eclipse plugin being able to discover our new operator definition and supporting it in the rules. The file itself is a regular properties file and to configure our str operator we can do:

drools.evaluator.str = com.myproject.StrEvaluatorDefinition

And that's it. Now you can use the new operator in your rules, for example:

rule routeToR1
when
$m : Message( routingValue str[startsWith] "R1" )
then
# routing to destination R1
end

or

rule routeToDefault
when
$m : Message( routingValue not str[startsWith] "R1" )
then
# routing to default destination
end

or even

rule routeToSpecial
when
$m : Message( routingValue str[startsWith] "R1" && str[endsWith] "R2" && str[length] 17)
then
# routing to super special destination
end

2 comments:

  1. Hi there,

    Thanks for this quick starter on customer operators in Drools.
    I followed the steps mentioned by you and also looking at the existing Drools 5.0.1 implementation of the Evaluators and was able to get the custom operators working.

    However trying out somethings as per our business requirements wherein I needed to evaluate the value present in some collection its failing.

    say for example:
    Mail ( messages["from"] startsWith "Tom")

    can someone of you Guru's in Drools out there help me out in cracking this one.

    Thanks in advance.
    Akshay

    ReplyDelete
  2. Hi tsurdilovic,

    Thanks a lot for this post¡¡

    When I tried to let my KnowledgeBuilder know about my new operator using KnowledgeBuilderConfiguration and passing it to the kbuilder instance (exactly as in the example) I obtain a NullPointerException in line 1067 of PatternBuilder, but it seems to be a drools bug:

    http://grepcode.com/file/repository.jboss.org/maven2/org.drools/drools-compiler/5.1.0.M1/org/drools/rule/builder/PatternBuilder.java#1067

    This NullPointer is caused because the EvaluatorDefinition can not be obtained from de context, because, although I added it using KnowledgeBuilderConfiguration, it is not in the context¡¡

    I have no more time to investigate about it, so I'm using the configuration file, but have you got any idea about this issue???? I would like to know the reason.

    Thanks in advance,
    Chiguita

    ReplyDelete