We've already seen various operators that can be used within rule conditions. These include, for example, ==
, !=
, relational operators such as >
, <
, >=
, temporal operators such as after
, during
, finishes
, or others such as matches
, which perform regular expression matching. In this section, we'll define our own custom operator.
The ==
operator uses Object.equals
/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
/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 Account owner
property contains the same instance of Customer
fact as the Customer
fact that was matched. The rule is as follows:
rule accountHasCustomer when $customer : Customer( ) Account( owner instanceEquals $customer ) then // end
Code listing 1: Rule with instanceEquals
custom operator in custom_operator.drl
file.
From the rule in code listing 1, we can see the use of the instanceEquals
custom operator . Most, if not all Drools operators support a negated version with not
:
Code listing 2: Condition that uses the negated version of the custom operator.
This condition will match Account
fact, whose owner
property is of different instance than the fact bound under $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 (for example, this after[0, 3m] $event2
).
Based on our requirements, we can now write this 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, Account owner
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, Account owner
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 KnowledgeBuilderConfiguration
. This configuration is fed into the familiar KnowledgeBuilder
. 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 KnowledgeBuilderConfiguration
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
.
We can now implement our operator. This will be done it 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 in order to work with our operator. We'll now look at InstanceEqualsEvaluatorDefinition
. The contents of this class are 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
class contains various information that Drools requires—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. This 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
: It is the type of operator's operands.operatorId
: It is the identifier of the operator (one evaluator definition can handle multiple IDs).isNegated
:
It is specified if this operator can be used withnot
.parameterText
: It 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
: It specifies if this operator operates on facts, fact handles, or both.
Then the method lazily initializes two implementations of the operator itself—InstanceEqualsEvaluator
. As our operator will operate only on facts and we don't care about the parameter text, we need to cater to only two cases—for non-negated operations and for negated operations. These evaluators are then cached for another use.
It is worth noting that the supportsType
method always returns true
, as we want to compare any facts regardless of their type.
All Drools evaluators must extend the org.drools.spi.Evaluator
interface (Note that this is the old API that will be replaced with org.drools.runtime.rule.Evaluator
interface in the future). Drools provides an abstract BaseEvaluator
that we can extend in order to simplify our implementation. Now, we have to implement a 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 a variable (for example, Account( owner instanceEquals $customer )
). The InstanceEqualsEvaluator
operator's implementation 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 custom Evaluator
.
Operator's implementation just defines various versions of the evaluate
method. The first one is executed when evaluating alpha nodes with literal constraints. extractor
is used to extract the field from a fact and the value
represents the actual literal. ^
is a standard 'bitwise exclusive or' Java operator.
The second evaluate
method is used when evaluating alpha nodes with variable bindings. In this case, the input parameters include left
/rightExtractor
and left
/right
fact (please note that the left and right facts represent the same fact instance).
The third method (evaluateCachedLeft
) and fourth method (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.