We've already seen various operators that can be used within rule conditions. These include ==
and !=
; relational operators such as >
, <
, and >=
; temporal operators such as after
, during
, and finishes
; or others such as matches
, which does regular expression matching. In this section we'll define our own custom operator.
The ==
operators uses the Object.equals
or hashCode
methods for comparing objects. However, sometimes we need to test if two objects are actually referring to the same instance. This is slightly faster than Object.equals
or hashCode
comparison (only slightly faster, because the hash code is calculated once for object and then it is cached).
Imagine that we have a rule, which matches on an Account
fact and a Customer
fact. We want to test if the
owner
property of Account
contains the same instance of a Customer
fact as the Customer
fact that was matched. The rule might look like this:
rule accountHasCustomer when $customer : Customer( ) Account( owner instanceEquals $customer ) then //.. end
Code listing 1: Rule with a custom operator such as instanceEquals in the custom_operator.drl file
From the previous rule we can see the use of a custom operator named instanceEquals
. Most, if not all, Drools operators support a negated version with not
:
Account( owner not instanceEquals $customer )
Code listing 2: Condition that uses the negated version of the custom operator
This condition will match on the Account
fact whose owner
property is of a different instance than the fact bound under the $customer
binding.
Some operators support parameters. They can be passed within the angle brackets as we've already seen in Chapter 7, Complex Event Processing, when we were discussing temporal operators.
Based on our requirements we can now write the following unit test for our new instanceEquals
operator:
@Test public void instancesEqualsBeta() throws Exception { Customer customer = new Customer(); Account account = new Account(); session.execute(Arrays.asList(customer, account)); assertNotFired("accountHasCustomer"); account.setOwner(new Customer()); session.execute(Arrays.asList(customer, account)); assertNotFired("accountHasCustomer"); account.setOwner(customer); session.execute(Arrays.asList(customer, account)); assertFired("accountHasCustomer"); }
Code listing 3: Unit test for the accountHasCustomer rule
It tests three use cases. The first one is an account with no customer. The test verifies that the rule didn't fire. In the second use case, the owner
property of Account
is set to a different customer than what is in the rule session. The rule isn't fired either. Finally, in the last use case, the owner
property of Account
is set to the right Customer
object and the rule fires.
Before we can successfully execute this test, we have to implement our operator and tell Drools about it. We can tell Drools through the PackageBuilderConfiguration
method. This configuration is fed into the familiar PackageBuilder
instance. The following code listing, which is in fact the unit test setup method, shows how to do it:
@BeforeClass public static void setUpClass() throws Exception { KnowledgeBuilderConfiguration builderConf = KnowledgeBuilderFactory.newKnowledgeBuilderConfiguration(); builderConf.setOption(EvaluatorOption.get( "instanceEquals", new InstanceEqualsEvaluatorDefinition())); knowledgeBase = DroolsHelper.createKnowledgeBase(null, builderConf, "custom_operator.drl"); }
Code listing 4: Unit test setup for custom operator test
A new instance of the PackageBuilderConfiguration
method is created and a new evaluator definition is added. It represents our new instanceEquals
operator, InstanceEqualsEvaluatorDefinition
. This configuration is then used to create a KnowledgeBase
object.
We can now implement our operator. This will be done in two steps:
Create
EvaluatorDefinition
, which will be responsible for creating evaluators based on actual rules.Create the actual evaluator (please note that the implementation should be stateless).
The evaluator definition will be used at rule compile time and the evaluator at rule runtime.
All evaluator definitions must implement the org.drools.base.evaluators.EvaluatorDefinition
interface. It contains all methods that Drools needs to work with our operator. We'll now look at the InstanceEqualsEvaluatorDefinition
interface. The contents of this class is as follows:
public class InstanceEqualsEvaluatorDefinition implements EvaluatorDefinition { public static final Operator INSTANCE_EQUALS = Operator .addOperatorToRegistry("instanceEquals", false); public static final Operator NOT_INSTANCE_EQUALS = Operator .addOperatorToRegistry("instanceEquals", true); private static final String[] SUPPORTED_IDS = { INSTANCE_EQUALS.getOperatorString() }; 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) { if (evaluator == null) { evaluator = new Evaluator[2]; } int index = isNegated ? 0 : 1; if (evaluator[index] == null) { evaluator[index] = new InstanceEqualsEvaluator(type, isNegated); } return evaluator[index]; } @Override public String[] getEvaluatorIds() { return SUPPORTED_IDS; } @Override public boolean isNegatable() { return true; } @Override public Target getTarget() { return Target.FACT; } @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); }
Code listing 5: Implementation of custom EvaluatorDefinition
The InstanceEqualsEvaluatorDefinition
instance contains various information that Drools requires, for example, the operator's ID—whether this operator can be negated and what types it supports.
At the beginning two operators are registered using the Operator.addOperatorToRegistry
static method. The method takes two arguments: operatorId
and a flag indicating whether this operator can be negated.
Then there are few getEvaluator
methods. Drools will call these methods during the rule compilation step. The last getEvaluator
method gets passed in the following arguments:
type
: This is the type of operator's operands.operatorId
: This is the identifier of the operator (one evaluator definition can handle multiple IDs).isNegated
: This specifies whether this operator can be used withnot
parameterText
: This is essentially the text in angle brackets; the evaluator definition is responsible for parsing this text. In our case it is simply ignored.leftTarget
andrightTarget
: These specify whether this operator operates on facts, fact handles, or both.
Then the method lazily initializes two implementations of the operator itself, InstanceEqualsEvaluator
. Since our operator will operate only on facts and we don't care about the parameter text, we need to cater only for two cases: non-negated operations and negated operations. These evaluators are then cached for another use.
It is worth noting the supportsType
method always returns true
, since we want to compare any facts regardless of their type.
All Drools evaluators must extend the org.drools.spi.Evaluator
interface. Drools provides an BaseEvaluator
abstract that we can extend to simplify our implementation. Now we have to implement few evaluate
methods for executing the operator under various circumstances. Using the operator with a literal (for example, Account( owner instanceEquals "some literal value" )
) or variable (for example, Account( owner instanceEquals $customer )
). The implementation of InstanceEqualsEvaluatoroperator
is as follows (please note that it is implemented as a static inner class):
public static class InstanceEqualsEvaluator extends BaseEvaluator { public InstanceEqualsEvaluator(final ValueType type, final boolean isNegated) { super(type, isNegated ? NOT_INSTANCE_EQUALS : INSTANCE_EQUALS); } @Override public boolean evaluate( InternalWorkingMemory workingMemory, InternalReadAccessor extractor, Object object, FieldValue value) { final Object objectValue = extractor.getValue( workingMemory, object); return this.getOperator().isNegated() ^ (objectValue == value.getValue()); } @Override public boolean evaluate( InternalWorkingMemory workingMemory, InternalReadAccessor leftExtractor, Object left, InternalReadAccessor rightExtractor, Object right) { final Object value1 = leftExtractor.getValue( workingMemory, left); final Object value2 = rightExtractor.getValue( workingMemory, right); return this.getOperator().isNegated() ^ (value1 == value2); } @Override public boolean evaluateCachedLeft( InternalWorkingMemory workingMemory, VariableContextEntry context, Object right) { return this.getOperator().isNegated() ^ (right == ((ObjectVariableContextEntry) context).left); } @Override public boolean evaluateCachedRight( InternalWorkingMemory workingMemory, VariableContextEntry context, Object left) { return this.getOperator().isNegated() ^ (left == ((ObjectVariableContextEntry) context).right); } @Override public String toString() { return "InstanceEquals instanceEquals"; } }
Code listing 6: Implementation of a custom Evaluator
The operator's implementation just defines various versions of the evaluate
method. The first one is executed when evaluating alpha nodes with literal constraints. The extractor is used to extract the field from a fact and the value represents the actual literal. The ^
operator is the standard bitwise exclusive or a Java operator.
The second evaluate
method is used when evaluating alpha nodes with variable bindings. In this case the input parameters include left
/right
extractor and left
/right fact (please note that the left
and right
facts represent the same fact instance).
The third one, evaluateCachedLeft
, and the fourth one, evaluateCachedRight
, will be executed when evaluating beta node constraints.
For more information please refer to the API and parent class org.drools.base.BaseEvaluator
.
Both the evaluator definition and the evaluator should be serializable.