Kie Base components

We have covered some of the most used components in a knowledge base, such as rules, globals, queries, and channels. It is time to move on to more advanced topics that will allow us to create more concise and reusable knowledge.

In this section, we are going to cover topics such as functions, custom operators, and custom accumulate functions. All these components can be used to model our knowledge in a simpler yet powerful way.

Functions

So far, we have covered three of the most common knowledge declarations that we have in Drools: rules, queries, and declared types. There is another kind of knowledge declaration that can be used to express stateless logic in a knowledge base: functions. Functions in Drools are basically isolated pieces of code that will optionally take arguments and may or may not return a value. Functions are useful for situations where we want to define some logic in a knowledge base instead of having it, for example, in an external Java class.

The syntax to define a function is similar to the one used in Java to declare a method with the addition of the keyword function at the beginning. A function has a return type (it could be any Java class including declared types or void), name, and optional set of typed parameters, as follows:

function String formatCustomer(Customer c){
    return String.format("[%s] %s", c.getCategory(), c.getName());
}

In the preceding example, a function called formatCustomer is defined. This function takes a Customer instance as a parameter and returns a String parameter. The body of the function uses a regular Java syntax; in this case, it is using String.format() to concatenate the category and name of the provided customer.

Just as with declared types, functions defined in the knowledge base are a good way to keep logic together in just one place. Functions in Drools also give us the flexibility to modify them without having to recompile any code.

Note

The without having to recompile any code part of the last paragraph is not 100% accurate. Behind the scenes, when the knowledge base gets compiled, Drools will create a helper class containing the functions defined in it.

Even if the use of functions in Drools gives us a certain degree of flexibility, they do have some limitations, which are as follows:

  • A function can't be used outside the package where it is defined. This means that a function defined inside a DRL file can't be used in any other DRL file, unless both have the same package declaration.
  • Functions can't make use of any global variable, fact, or predefined variables such as kcontext. The context of a function is only the set of arguments that are passed when it is invoked.
  • As a corollary of the previous limitation, functions can't insert, modify, or retract facts from the session.

When thinking about reusability and maintainability, the functions declared inside a knowledge base may not be the best approach. Drools, fortunately, also allows us to import a static method from the Java classes as a function and use it in our rules. In order to import a static method, we need to make use of the function keyword combined with the import keyword.

import function org.drools.devguide.chapter05.utils.CustomerUtils.formatCustomer;

As you can see, importing a static method of a class as a function resembles, in a way, how static methods can be imported in Java.

It does not matter if our function is being imported from a Java class or declared inside the knowledge base, the way we invoke them in our rules or from another function is by simply using its name, as shown in the following code:

rule "Prepare Customers List"
when
    $c: Customer()
then
    globalList.add(formatCustomer($c));
end

The preceding example shows the use of the formatCustomer function within the right-hand side of a rule, but functions can be also used in the conditional part of a rule, as follows:

rule "Prepare Customers List"
when
    $c: Customer($formatted: formatCustomer($c))
then
    ...
end

Let's now move to another powerful feature in Drools that allows us to enhance the DRL language with tailored operators that can be used to create more concise, readable, and maintainable rules: custom operators.

Custom operators

In Chapter 4, Improving Our Rule Syntax, we saw most of the comparison operators that can be used when specifying the left-hand side of our rules. Operators such as ==, !=, <, > ,and so on are already supported by Drools, out of the box. There are some situations though, when the available operators are not enough. Comparisons involving complex logic, external services, or semantic reasoning are good examples of situations where the power of Drools falls short. However, there's nothing to be worried about; Drools provides a mechanism for the creation of custom operators that can then be used when authoring our rules.

In Drools, custom operators are defined as Java classes implementing the org.drools.core.base.evaluators.EvaluatorDefinition interface. This interface represents only the definition of the operator. The concrete implementation is delegated to an implementation of the org.drools.core.spi.Evaluator interface.

Before a custom operator can be used as part of a rule, it must be first registered in the knowledge base being used. The registration of a custom operator is performed using a configuration file in the classpath or by specifying it inside the kmodule.xml file. However, before we move on to see how a custom operator is registered, let's see an example first.

In order to clarify what a custom operator is and how it is defined, let's use an example from our eShop use case. For this example, we are going to implement a trivial operator that will tell us whether an Order function contains a specific Item given its ID. This example may not be the most interesting example for custom operators as it can be resolved in many different ways. Nevertheless, it represents a good and concise example to show how custom operators are built.

The idea of our new custom operator is to be able to write rules as the following:

rule "Apply discount to Orders with item 123"
when
    $o: Order(this containsItem 123) @Watch(!*)
Then
    modify ($o){ setDiscount(new Discount(0.1))};
end

The important thing to notice in the previous rule is the use of a custom operator called containsItem. All custom operators—and by extension, any operator in Drools—take two arguments. In this particular case, the first argument is of the Order type and the second is of the Long type. An operator will always evaluate to a boolean value. In this case, the boolean result will indicate whether the specified item is present in the provided Order or not.

The first thing we need to do in order to implement our custom operator is to implement org.drools.core.base.evaluators.EvaluatorDefinition. In our example, the implementation class will be called ContainsItemEvaluatorDefinition:

package org.drools.devguide.chapter05.evaluator;
public class ContainsItemEvaluatorDefinition implements EvaluatorDefinition {

    protected static final String containsItemOp = "containsItem";

    public static Operator CONTAINS_ITEM;
    public static Operator NOT_CONTAINS_ITEM;

    private static String[] SUPPORTED_IDS;

    private ContainsItemEvaluator evaluator;
    private ContainsItemEvaluator negatedEvaluator;

    
    static {
        if (SUPPORTED_IDS == null) {
            CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, false);
            NOT_CONTAINS_ITEM = Operator.addOperatorToRegistry(containsItemOp, true);
            SUPPORTED_IDS = new String[]{containsItemOp};
        }
    }

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

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

    @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.BOTH, Target.BOTH);
    }

    @Override
    public Evaluator getEvaluator(ValueType type, String operatorId,
            boolean isNegated, String parameterText, Target leftTarget,Target rightTarget) {
        return isNegated ?
                negatedEvaluator == null ?
                    new ContainsItemEvaluator(type, isNegated) :negatedEvaluator
                : evaluator == null ?
                    new ContainsItemEvaluator(type, isNegated) : evaluator;
    }

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

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

There is a lot of information to process in the previous class, so let's go by parts. The static block at the beginning registers two new operators into Drools' operators registry. The two new operators are indeed our new containsItem operator and its counterpart not containsItem. The next important method is getEvaluatorsIds(), which tells Drools all the possible IDs for the operator that we are defining. Following this method, comes isNegatable(), which indicates whether the operator that we are creating can be negated or not. Then, four different versions of the getEvaluator() method are defined. These methods will return, at compile time, the concrete instance of org.drools.core.base.evaluators.EvaluatorDefinition, which should be used for each specific scenario. The arguments that are passed to these methods are as follows:

  • type: This is the type of operator's operands.
  • operatorId: This is the identifier of the operator being parsed. A single operator definition can handle multiple IDs.
  • isNegated: This indicates whether the operator being parsed is using the not prefix (is negated) or not.
  • parameterText: An operand in Drools could have fixed the parameters that are defined in the angle brackets. Examples of operators with parameters are the CEP operators from Drools Fusion. Refer to Chapter 6, Complex Event Processing for more information about Drools Fusion.
  • leftTarget/rightTarget: These two arguments specify whether this operator operates on facts, fact handles, or both.

The four versions of getEvaluator() return an instance of ContainsItemEvaluator. The ContainsItemEvaluator is the concrete implementation of Drools' org.drools.core.spi.Evaluator and is the class in charge of the runtime behavior of our operator. This class is where the real logic of our operator—check whether a specific Item is contained by an Order—is implemented:

public class ContainsItemEvaluator extends BaseEvaluator {
    
    private final boolean isNegated;

    public ContainsItemEvaluator(ValueType type, boolean isNegated) {
        super(type ,isNegated?ContainsItemEvaluatorDefinition.NOT_CONTAINS_ITEM :ContainsItemEvaluatorDefinition.CONTAINS_ITEM);
        this.isNegated = isNegated;
    }
    
    @Override
    public boolean evaluate(InternalWorkingMemory workingMemory,InternalReadAccessor extractor, InternalFactHandle factHandle,FieldValue value) {
        Object order = extractor.getValue(workingMemory, factHandle.getObject());
        return this.isNegated ^ this.evaluateUnsafe(order,value.getValue());
    }
    @Override
    public boolean evaluate(InternalWorkingMemory workingMemory,
            InternalReadAccessor leftExtractor, InternalFactHandle left,InternalReadAccessor rightExtractor, InternalFactHandle right) {
        Object order = leftExtractor.getValue(workingMemory,left.getObject());
        Object itemId = rightExtractor.getValue(workingMemory, right.getObject());
        return this.isNegated ^ this.evaluateUnsafe(order,itemId);
    }

    @Override
    public boolean evaluateCachedLeft(InternalWorkingMemory workingMemory,
            VariableRestriction.VariableContextEntry context,
            InternalFactHandle right) {
        Object order = context.getFieldExtractor().getValue(workingMemory,
                right.getObject());
        Object itemId = ((ObjectVariableContextEntry)context).left;
        
        return this.isNegated ^ this.evaluateUnsafe(order, itemId);
    }

    @Override
    public boolean evaluateCachedRight(InternalWorkingMemory workingMemory,
            VariableRestriction.VariableContextEntry context,
            InternalFactHandle left) {
        Object order = ((ObjectVariableContextEntry)context).right;
        Object itemId = context.getFieldExtractor().getValue(workingMemory,
                left.getObject());
        
        return this.isNegated ^ this.evaluateUnsafe(order, itemId);
    }
    
    private boolean evaluateUnsafe(Object order, Object itemId){
        //if the object is not an Order return false.
        if (!(order instanceof Order)){
            throw new IllegalArgumentException(
                    order.getClass()+" can't be casted to type Order");
        }
        
        //if the value we are comparing against is not a Long, return false.
        Long itemIdAsLong;
        try{
            itemIdAsLong = Long.parseLong(itemId.toString());
        } catch (NumberFormatException e){
            throw new IllegalArgumentException(
                    itemId.getClass()+" can't be converted to Long");
        }
        
        return this.evaluate((Order)order, itemIdAsLong);
    }
    
    private boolean evaluate(Order order, long itemId){
        //no order lines -> no item
        if (order.getOrderLines() == null){
            return false;
        }
        
        return order.getOrderLines().stream()
            .map(ol -> ol.getItem().getId())
            .anyMatch(id -> id.equals(itemId));
    }    
}

Instead of implementing org.drools.core.spi.Evaluator, ContainsItemEvaluator extends from org.drools.core.base.BaseEvaluator, which is a class that implements the boilerplate code of the interface, leaving the implementation of the concrete methods where the operator evaluation actually happens to us. There are four methods that we have to implement, as follows:

  • evaluate: There are two versions of this method that need to be implemented. These methods are used by Drools when the operator appears as part of a condition involving a single fact (in Phreak algorithm, these type of conditions are part of the so called alpha network. Phreak will be covered in more detail in Chapter 9, Introduction to Phreak). The first version is used when literal constraints are involved and the second when variable bindings are involved.
  • evaluateCachedLeft/evaluateCachedRight: These two methods are used by Drools when the operator is used in conditions involving multiple facts (in Phreak algorithm, these conditions are part of it's beta network).

Before we can actually use this new operator in our rules, we need to register it in the knowledge base where we want to use it. There are two ways of doing this: using the drools.packagebuilder.conf file or via the kmodule.xml file.

The first way to register a custom operator is by using a special file in Drools called drools.packagebuilder.conf. This file, which must be located under the META-INF directory, is automatically used by the Drools' package builder to read configuration parameters of the knowledge base being created. In order to register a custom operator, we need to add the following line to this file:

drools.evaluator.containsItem= org.drools.devguide.chapter05.evaluator.ContainsItemEvaluatorDefinition

The line must start with drools.evaluator and the ID of the custom operator must follow. After that, the fully qualified name of the class, where the custom operator is defined, must be specified.

The second way to register a custom operator in Drools is to use the kmodule.xml file. A specific section for configurations can be defined in this file, where properties can be specified as key/value pairs. In order to register our created custom operator in a kmodule.xml file, the following configuration section must be added to it:

<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://jboss.org/kie/6.0.0/kmodule">
    <configuration>
        <property key="drools.evaluator.containsItem" value="org.drools.devguide.chapter05.evaluator.ContainsItemEvaluatorDefinition"/>
    </configuration>
</kmodule>

A complete example of a custom operator can be found in the code bundle under the chapter-05 module. This module defines the custom operator covered by this section and it also includes a couple of unit tests showing its behavior. The tests can be found in chapter-05/chapter-05-tests/src/test/java/org/drools/devguide/chapter05/CustomOperatorTest.java.

So far, we have covered how to define our own custom operators to create tailored solutions for our domain and enhance Drools pattern-matching capabilities. Let's move to another way, we have to create a customized logic in order to be used in our rules: the custom accumulate functions.

Custom accumulate functions

In Chapter 4, Improving Our Rule Syntax, we covered the accumulate conditional element and the different ways we can use it. The structure of an accumulate conditional element is composed of a pattern section and one or more accumulate functions. In the previous chapter, we saw two different types of accumulate functions: inline accumulate functions and built-in accumulate functions.

Inline accumulate functions are explicitly defined in the rule being created. These functions have the four sections that are already explained in the previous chapter: init, action, reverse, and result. On the other hand, the built-in functions are supported by Drools, out of the box. These functions include count, sum, avg, collectList, and so on.

Even if the inline accumulate functions are a powerful and flexible way to enhance Drools' capabilities, their definition and maintainability is fairly complex. Inline accumulate functions are cumbersome to write, debug, and maintain. Inline accumulate functions are basically chunks of the Java/MVEL code embedded in DRL. Writing the code in each section can be very confusing if we are not implementing a trivial function. Even worst, debugging what's going on inside an inline accumulate function is almost impossible. However, maybe the worst thing about inline accumulate functions is that they can't be reused. If the same function is required in multiple rules, it has to be redefined in each of them. Due to all of these inconveniences, the use of inline accumulate functions is kind of discouraged. Instead of defining our accumulate functions embedded in DRL, Drools allows us to define them in Java and then import them into our knowledge base. Decoupling the definition of the accumulate function from its usage in the rules solves all the problems that we have mentioned before.

A custom accumulate function is a Java class that implements Drools' org.kie.api.runtime.rule.AccumulateFunction interface. As an example, let's implement a custom accumulate function to retrieve the one item with the biggest total (including discounts) from a group of Orders, as follows:

public class BiggestOrderFunction implements AccumulateFunction{

    public static class Context implements Externalizable{
        public Order maxOrder = null;
        public double maxTotal = -Double.MAX_VALUE;

        public Context() {}
        ...
    }
    
    @Override
    public Serializable createContext() {
        return new Context();
    }

    @Override
    public void init(Serializable context) throws Exception {
    }

    @Override
    public void accumulate(Serializable context, Object value) {
        Context c = (Context)context;
        
        Order order = (Order) value;
        double discount =
                order.getDiscount() == null ? 0 : order.getDiscount().getPercentage();
        double orderTotal = order.getTotal() - (order.getTotal() * discount);

        if (orderTotal > c.maxTotal){
            c.maxOrder = order;
            c.maxTotal = orderTotal;
        }
        
    }

    @Override
    public boolean supportsReverse() {
        return false;
    }
    
    @Override
    public void reverse(Serializable context, Object value) throws Exception {
    }

    @Override
    public Object getResult(Serializable context) throws Exception {
        return ((Context)context).maxOrder;
    }

    @Override
    public Class<?> getResultType() {
        return Order.class;
    }
    ...    
}

The BiggestOrderFunction class can be found in the source bundles of this chapter. Let's analyze the different sections of this class now. The first thing to notice is that this class is implementing Drools' org.kie.api.runtime.rule.AccumulateFunction interface. This interface defines all the methods required to implement a custom accumulate function. However, before we can even start implementing these methods, we need to define a context class. Every time an accumulate function is used in Drools, an individual context will be created for it. The context will contain all the necessary information for the accumulate function to work. In this case, a Context static class is defined, containing an Order instance and a double maxTotal attribute. This context will keep track of the biggest Order class found so far.

Once we have defined our context, we can implement the methods from the AccumulateFunction interface. The name of these methods, except for createContext(), and their semantics are closely related to the name of the different sections of the inline accumulate functions, as shown in the following:

  • createContext: This method is created the first time the accumulate function is being used. The purpose of this method is to create the context that is going to be used for this particular instance of the accumulate function. In our example, it is creating a new instance of our Context class.
  • init: This method is also invoked the first time the accumulate function is used in a rule. The argument of this method is the context created in createContext().
  • accumulate: This is where the real accumulate logic happens. In our case, we are detecting whether current Order being processed is bigger than the one held by the context. If it is, the context is updated accordingly. This method corresponds to the action section of an inline accumulate function.
  • supportsReverse: This method indicates whether this accumulate function supports the reverse operation. In our case, we don't support it (otherwise, we would need to keep the collection of all analyzed Orders in the context).
  • reverse: This method contains the logic involved when a fact that had previously matched the pattern of the accumulate conditional element no longer does. In our case, given that we don't support the reverse operation, this method remains empty.
  • getResult: This method returns the actual result of the accumulate function. In our case, the result is the Order instance contained in our context object.
  • getResultType: This method tells Drools the result type of this accumulate function. In our case, the type is Order.class.

Before our custom accumulate function can be used in our rules, we need to import it in our knowledge package. A custom accumulate function can be imported into a DRL asset in the following way:

import accumulate org.drools.devguide.chapter05.acc.BiggestOrderFunction biggestOrder

The import statement starts with the import accumulate keywords and what follows is the fully qualified name of the class, implementing the function. The last part of the import statement is the name that we want to give to this function in our DRL.

Once the function is imported, we can use it as any of the Drools built-in accumulate functions:

rule "Find Biggest Order"
when
    $bigO: Order() from accumulate (
        $o: Order(),
        biggestOrder($o)
    )
then
    biggestOrder.setObject($bigO);
end
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.93.12