Experimental features

This chapter documents experimental features that are part of the Drools projects group. As these features are experimental, they may not be finished or stable enough for common use. All aspects of these features are highly likely to change in the future.

Declarative agenda

The declarative agenda allows to use rules to control which other rules can fire and when. While this will add a lot more overhead than the simple use of salience, the advantage is it is declarative and thus more readable and maintainable and should allow more use cases to be achieved in a simpler fashion.

As this feature is highly experimental and will be subject to change, it is off by default and must be explicitly enabled. It can be activated on a given KieBase by adding the declarativeAgenda="enabled" attribute in the corresponding kbase tag of the kmodule.xml file, as is specified in the following example.

Example 1. Enabling the Declarative Agenda
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns="http://www.drools.org/xsd/kmodule">
      <kbase name="DeclarativeKBase" declarativeAgenda="enabled">
      <ksession name="KSession"/>
      </kbase>
      </kmodule>

The basic idea is:

  • All rule’s matches are inserted into working memory as facts, represented as instances of Match class. So you can now do pattern matching against a Match instance. The rule’s metadata and declarations are available as fields on the Match instance.

  • You can use the kcontext.blockMatch(Match match) call in a rule to block a selected match. Only when that rule becomes false will the match be eligible for firing. If it is already eligible for firing and is later blocked, it will be removed from the agenda until it is unblocked.

  • A match may have multiple blockers, so a count is kept. All blockers must become false for the counter to reach zero to enable the Match to be eligible for firing.

  • kcontext.unblockAllMatches(Match match) is an over-ride rule that will remove all blockers regardless.

  • An internalMatch may also be cancelled using cancelMatch call, so it never fires.

  • An unblocked Match is added to the agenda and obeys normal salience, agenda groups, ruleflow groups etc. definitions.

  • The @Direct annotation allows a rule to fire as soon as it’s matched. This is supposed to be used for rules that block/unblock matches, it is not desirable for these rules to have side effects that impact else where.

Example 2. New RuleContext methods
void blockMatch(Match match);
void unblockAllMatches(Match match);
void cancelMatch(Match match);

Here is a basic example that will block all matches from rules that have metadata @department('sales'). They will stay blocked until the blockerAllSalesRules rule becomes false, i.e. "go2" is retracted.

Example 3. Block rules based on rule metadata
rule "rule1" @Eager @department('sales')
when
    $s: String(this == 'go1')
then
    System.out.println("rule1 fired!");
end

rule "rule2" @Eager @department('sales')
when
    $s: String(this == 'go1')
then
    System.out.println("rule2 fired!");
end

rule "blockerAllSalesRules" @Direct @Eager
when
    $s: String(this == 'go2')
    $i: Match(department == 'sales')
then
    kcontext.blockMatch($i);
end

It is necessary to annotate all the rules that could be potentially blocked by rule with @Direct annotation with the annotation @Eager. This is because the potentially blocked rules must be evaluated immediately, so they produce Match instances, that can be evaluated by the blocking rule.

This example shows how you can use a property to count the number of active or inactive (already fired) matches.

Example 4. Count the number of active/inactive Matches
rule "rule1" @Eager @department('sales')
when
    $s: String(this == 'go1')
then
    System.out.println("rule1 fired!");
end

rule "rule2" @Eager @department('sales')
when
    $s: String(this == 'go1')
then
    System.out.println("rule2 fired!");
end

rule "rule3" @Eager @department('sales')
when
    $s: String(this == 'go1')
then
    System.out.println("rule3 fired!");
end

rule "countActivateInActive" @Direct @Eager
when
    $s: String(this == 'go2')
    $active: Number(this == 1) from accumulate(
        $a: Match(department == 'sales', active == true),
        count($a))
    $inActive: Number(this == 2) from accumulate(
        $a : Match(department == 'sales', active == false),
        count($a))
then
    kcontext.halt();
end

Traits

The same fact may have multiple dynamic types which do not fit naturally in a class hierarchy. Traits allow to model this very common scenario. A trait is a type that can be applied to (and eventually removed from) an individual object at runtime. To create a trait rather than a traditional bean, one has to declare it explicitly as in the following example:

declare trait GoldenCustomer
    // fields will map to getters/setters
    code     : String
    balance  : long
    discount : int
    maxExpense : long
end

At runtime, this declaration results in an interface, which can be used to write patterns, but can not be instantiated directly. In order to apply a trait to an object, we provide the new don keyword, which can be used as simply as this:

when
    $c : Customer()
then
    GoldenCustomer gc = don( $c, GoldenCustomer.class );
end

When a trait is applied to a fact, a proxy class is created on the fly (one such class will be generated lazily for each fact/trait class combination). The proxy instance, which wraps the fact and implements the trait interface, is inserted automatically and will possibly activate other rules. An immediate advantage of declaring and using interfaces and getting the implementation proxy is that multiple inheritance hierarchies can be exploited when writing rules. The fact declarations, however, need not implement any of those interfaces statically. To be possible to assign a trait to a fact, the fact type definition must contain the annotation @Traitable.

import org.drools.core.factmodel.traits.Traitable;
declare Customer
    @Traitable
    code: String
    balance: long
end

The only connection between fact classes and trait interfaces is at the proxy level: a trait is not specifically tied to a fact class. This means that the same trait can be applied to totally different facts. For this reason, the trait does not transparently expose the fields of its fact object. So, when writing a rule using a trait interface, only the fields of the interface will be available, as usual. However, any field in the interface that corresponds to a fact field, will be mapped by the proxy class:

when
    $o: OrderItem($p: price, $code: custCode)
    $c: GoldenCustomer(code == $code,$a : balance,$d: discount)
then
    $c.setBalance($a - $p*$d);
end

In this case, the code and balance would be read from the underlying Customer object. Likewise, the setAccount will modify the underlying object, preserving a strongly typed access to the data structures. A hard field is a field that must have the same name and type both in the fact class and all trait interfaces. The name is used to establish the mapping: if two fields have the same name, then they must also have the same declared type. The annotation @org.drools.core.factmodel.traits.Alias allows to relax this restriction. If an @Alias is provided, its value string will be used to resolve mappings instead of the original field name. @Alias can be applied both to traits and core beans.

import org.drools.core.factmodel.traits.*;
declare trait GoldenCustomer
    balance: long @Alias("org.acme.foo.accountBalance")
end

declare Person
    @Traitable
    name: String
    savings: long @Alias("org.acme.foo.accountBalance")
end

when
    GoldenCustomer(balance > 1000) // will react to new Person(2000)
then
end

More work is being done on relaxing this constraint (see the experimental section on "logical" traits later). Now, one might wonder what happens when a fact class does NOT provide the implementation for a field defined in an interface. We call hard fields those trait fields which are also fact fields and thus readily available, while we define soft those fields which are NOT provided by the fact class. Hidden fields, instead, are fields in the fact class not exposed by the interface.

So, while hard field management is intuitive, there remains the problem of soft and hidden fields. Hidden fields are normally only accessible using the fact class directly. However, one can also use the "fields" Map on a trait interface to access a hidden field. If the field can’t be resolved, null will be returned. Notice that this feature is likely to change in the future.

when
    $sc: GoldenCustomer(fields["age"] > 18)  // age is declared by the underlying fact class, but not by GoldenCustomer
then

Soft fields, instead, are stored in a Map-like data structure that is specific to each fact object and referenced by the proxy(es), so that they are effectively shared even when an object has multiple traits assigned.

when
    $sc: GoldenCustomer($c: code, // hard getter
                        $maxExpense: maxExpense > 1000 // soft getter)
then
    $sc.setDiscount(...); // soft setter
end

A fact object also holds a reference to all its proxies, so that it is possible to track which traits have been added to an object, using a sort of dynamic "instanceof" operator, which we named isA. The operator can accept a String, a class literal or a list of class literals. In the latter case, the constraint is satisfied only if all the specified traits have been assigned.

$sc: GoldenCustomer($maxExpense: maxExpense > 1000,
                    this isA "SeniorCustomer",
                    this isA [NationalCustomer.class, OnlineCustomer.class])

Eventually, the business logic may require that a trait is removed from a fact object. For this, we provide two options. The first is a "logical don", which will result in a logical insertion of the proxy resulting from the trait assignment. The Truth Maintenance System will ensure that the trait is removed when its logical support is removed.

then
    don($x, // core object
        Customer.class, // trait class
        true // optional flag for logical insertion)

The second option is the use of the shed keyword, which causes the removal of any type that is a subtype (or equivalent) of the one passed as an argument.

then
    Thing t = shed($x, GoldenCustomer.class)

This operation returns another proxy implementing the org.drools.core.factmodel.traits.Thing interface, where the getFields() and getCore() methods are defined. Internally, in fact, all declared traits are generated to extend this interface (in addition to any others specified). This allows to preserve the wrapper with the soft fields which would otherwise be lost.

A trait and its proxies are also correlated in another way. Whenever a fact object is "modified", its proxies are "modified" automatically as well, to allow trait-based patterns to react to potential changes in hard fields. Likewise, whenever a trait proxy (matched by a trait pattern) is modified, the modification is propagated to the fact class and the other traits. Moreover, whenever a don operation is performed, the fact object is also modified automatically, to reevaluate any isA operation which may be triggered.

Potentially, this may result in a high number of modifications, impacting performance (and correctness) heavily. So two solutions are currently implemented. First, whenever a fact object is modified, only the most specific traits (in the sense of inheritance between trait interfaces) are updated and an internal blocking mechanism is in place to ensure that each potentially matching pattern is evaluated once and only once. So, in the following situation:

declare trait GoldenCustomer end
declare trait NationalGoldenCustomer extends GoldenCustomer end
declare trait SeniorGoldenCustomer extends GoldenCustomer end

A modification of an object that is both a GoldenCustomer, a NationalGoldenCustomer and a SeniorGoldenCustomer would cause only the latter two proxies to be actually modified. The first would match any pattern for GoldenCustomer and NationalGoldenCustomer, the latter would instead be prevented from rematching GoldenCustomer, but would be allowed to match SeniorGoldenCustomer patterns. It is not necessary, instead, to modify the GoldenCustomer proxy since it is already covered by at least one other more specific trait.

The second method, up to the user, is to mark traits as @PropertyReactive. Property reactivity is trait-enabled and takes into account the trait field mappings, so to block unnecessary propagations.

Cascading traits

WARNING : This feature is extremely experimental and subject to changes.

Normally, a hard field must be exposed with its original type by all traits assigned to an object, to prevent situations such as

declare Person
  @Traitable
  name: String
  id: String
end

declare trait Customer
  id: String
end

declare trait Patient
  id: long  // Person can't don Patient, or an exception will be thrown
end

Should a Person get assigned both Customer and Patient traits, the type of the hard field id would be ambiguous. However, consider the following example, where GoldenCustomers refer their best friends so that they become Customers as well:

declare Person
  @Traitable(logical = true)
  bestFriend: Person
end

declare trait Customer end

declare trait GoldenCustomer extends Customer
  refers: Customer @Alias("bestFriend")
end

Aside from the @Alias, a Person-as-GoldenCustomer’s best friend may be compatible with the requirements of the trait GoldenCustomer, provided that they are some kind of Customer themselves. Marking a Person as "logically traitable" - i.e. adding the annotation @Traitable(logical = true) - will instruct the Drools rule engine to try and preserve the logical consistency rather than throwing an exception due to a hard field with different type declarations (Person vs Customer). The following operations would then work:

Person p1 = new Person();
Person p2 = new Person();
p1.setBestFriend(p2);
...
Customer c2 = don(p2, Customer.class);
...
GoldenCustomer gc1 = don(p1, GoldenCustomer.class);
...
p1.getBestFriend(); // returns p2
gc1.getRefers(); // returns c2, a Customer proxy wrapping p2

Notice that, by the time p1 becomes GoldenCustomer, p2 must have already become a Customer themselves, otherwise a runtime exception will be thrown since the very definition of GoldenCustomer would have been violated.

In some cases, however, one may want to infer, rather than verify, that p2 is a Customer by virtue that p1 is a GoldenCustomer. This modality can be enabled by marking Customer as "logical", using the annotation @org.drools.core.factmodel.traits.Trait(logical = true). In this case, should p2 not be a Customer by the time that p1 becomes a GoldenCustomer, it will be automatically assigned the trait Customer to preserve the logical integrity of the system.

Notice that the annotation on the fact class enables the dynamic type management for its fields, whereas the annotation on the traits determines whether they will be enforced as integrity constraints or cascaded dynamically.

import org.drools.factmodel.traits.*;

declare trait Customer
    @Trait(logical = true)
end

Impact analysis

The impact analysis feature analyzes the relationships between the rules and generates a graph. When you specify a rule to be changed, the impact analysis feature analyzes the rules that are impacted by the change and renders the rules in the generated graph.

The generated graph supports DOT, SVG, and PNG formats with simple text output.

Using the impact analysis feature

You can find an example usage in ExampleUsageTest.java under drools-impact-analysis/drools-impact-analysis-itests

  1. Configure the following dependency.

        <dependency>
          <groupId>org.drools</groupId>
          <artifactId>drools-impact-analysis-graph-graphviz</artifactId>
          <version>${drools.version}</version>
        </dependency>
  2. Create a KieFileSystem to store your assets and call KieBuilder.buildAll(ImpactAnalysisProject.class) method.

          // set up KieFileSystem
          ...
          KieBuilder kieBuilder = KieServices.Factory.get().newKieBuilder(kfs).buildAll(ImpactAnalysisProject.class);
          ImpactAnalysisKieModule analysisKieModule = (ImpactAnalysisKieModule) kieBuilder.getKieModule();
          AnalysisModel analysisModel = analysisKieModule.getAnalysisModel();

    You get AnalysisModel.

  3. Convert the AnalysisModel to Graph using ModelToGraphConverter.

          ModelToGraphConverter converter = new ModelToGraphConverter();
          Graph graph = converter.toGraph(analysisModel);
  4. Specify a rule that you plan to change. The ImpactAnalysisHelper generates a graph, containing the changed rule and the impacted rules.

          ImpactAnalysisHelper impactFilter = new ImpactAnalysisHelper();
          Graph impactedSubGraph = impactFilter.filterImpactedNodes(graph, "org.drools.impact.analysis.example.PriceCheck_11");
  5. Generate a graph image using GraphImageGenerator. You can choose the format from DOT, SVG, and PNG.

          GraphImageGenerator generator = new GraphImageGenerator("example-impacted-sub-graph");
          generator.generateSvg(impactedSubGraph);
  6. Simple text output is also available using TextReporter. You can choose the format from HierarchyText and FlatText.

          String hierarchyText = TextReporter.toHierarchyText(impactedSubGraph);
          System.out.println(hierarchyText);

In a generated graph, red node represents a changed rule and yellow nodes represent the impacted rules. A solid arrow in a generated graph indicates a positive impact, in which the source rule activates the target rule. However, a dashed arrow indicates a negative impact, in which the source rule deactivates the target rule. Also, a dotted arrow represents an unknown impact, in which the source rule might activate or deactivate the target rule.

impactAnalysis1

You can collapse a graph based on the rule name prefix or RuleSet in a spreadsheet using the GraphCollapsionHelper. This enables you to view the overview of a graph. Also, you can use ImpactAnalysisHelper to the collapsed graph.

      Graph collapsedGraph = new GraphCollapsionHelper().collapseWithRuleNamePrefix(graph);
      Graph impactedCollapsedSubGraph = impactFilter.filterImpactedNodes(collapsedGraph, "org.drools.impact.analysis.example.PriceCheck");

If you only want to view the positive relations in a graph, set the positiveOnly to true for ModelToGraphConverter, ImpactAnalysisHelper, and GraphCollapsionHelper constructor.

      ModelToGraphConverter converter = new ModelToGraphConverter(true);
      Graph graph = converter.toGraph(analysisModel);
      ImpactAnalysisHelper impactFilter = new ImpactAnalysisHelper(true);
      Graph impactedSubGraph = impactFilter.filterImpactedNodes(graph, "org.drools.impact.analysis.example.PriceCheck_11");

Text output is useful for a large number of rules. In a text output, [*] represents a changed rule, and [+] represents impacted rules.

--- toHierarchyText ---
Inventory shortage[+]
PriceCheck_11[*]
  StatusCheck_12[+]
  (Inventory shortage)
  StatusCheck_13[+]
  StatusCheck_11[+]
    (PriceCheck_11)

--- toFlatText ---
Inventory shortage[+]
PriceCheck_11[*]
StatusCheck_11[+]
StatusCheck_12[+]
StatusCheck_13[+]

Troubleshooting

If you get the warning message when rendering SVG or PNG:

graphviz-java failed to render an image. Solutions would be:
1. Install graphviz tools in your local machine. graphviz-java will use graphviz command line binary (e.g. /usr/bin/dot) if available.
2. Consider generating a graph in DOT format and then visualize it with an external tool.

You would need to install graphviz tools in your local machine. If not possible, you would need to generate the graph in DOT format so that you can render it with another tool later on.