Chapter 8. Working With Rules

8.1. About Rule Files

8.1.1. Rule File

A rule file is typically a file with a .drl extension. In a DRL file you can have multiple rules, queries and functions, as well as some resource declarations like imports, globals, and attributes that are assigned and used by your rules and queries. However, you are also able to spread your rules across multiple rule files (in that case, the extension .rule is suggested, but not required) - spreading rules across files can help with managing large numbers of rules. A DRL file is simply a text file.

8.1.2. Structure of Rule Files

The overall structure of a rule file is the following:

Example 8.1. Rule File

package package-name

imports

globals

functions

queries

rules

The order in which the elements are declared is not important, except for the package name that, if declared, must be the first element in the rules file. All elements are optional, so you will use only those you need.

8.2. Operating on Facts

Facts are domain model objects that BRMS uses to evaluate conditions and execute consequences. A rule specifies that when a particular set of conditions occur, then the specified list of actions must be executed. The inference engine matches facts against rules, and when matches are found, rule actions are placed on the agenda. The agenda is the place where rules are queued ready to have their actions fired. The rule engine then determines which eligible rules on the agenda must fire.

8.2.1. Accessing Working Memory

The working memory is a stateful object that provides temporary storage and enables manipulation of facts. The working memory includes an API that contains methods which enable access to the working memory from rule files. The available methods are:

  • update(OBJECT, HANDLE)

    Used to inform the engine that an object has changed and rules can need to be reconsidered.

  • update(OBJECT)

    This method causes KieSession to search for a fact handle of the passed object using an identity check. You do not have to call this method when the object changes if property change listeners are provided. For more infomartion, see Section 8.12.15, “Fine Grained Property Change Listeners”.

    If field values of a fact have changed, call this method or use the modify keyword before changing another fact to avoid issues with indexing within the engine.

  • insert(OBJECT)

    Used to place a new object into the working memory.

  • insertLogical(OBJECT)

    This method is similar to the insert method. The newly inserted object is automatically retracted from the working memory if there are no more facts supporting the truth of the rule that inserted the fact.

  • retract(HANDLE)

    Used to remove an object from the working memory. This method is mapped to the delete method in KieSession.

  • halt()

    Used to terminate a rule execution immediately. Calling fireUntilHalt() causes continuous firing of the rules. To stop the firing, call halt().

  • getKieRuntime()

    The whole KIE API is exposed through a predefined kcontext variable of type RuleContext. The inherited getKieRuntime() method returns a KieRuntime object that provides access to various methods, many of which are useful for coding the rule logic.

    For example, calling kcontext.getKieRuntime().halt() terminates a rule execution immediately.

8.3. Using Rule Keywords

8.3.1. Hard Keywords

Hard keywords are words which you cannot use when naming your domain objects, properties, methods, functions, and other elements that are used in the rule text. The hard keywords are true, false, and null.

8.3.2. Soft Keywords

Soft keywords can be used for naming domain objects, properties, methods, functions, and other elements. The rules engine recognizes their context and processes them accordingly.

8.3.3. List of Soft Keywords

Rule attributes can be both simple and complex properties that provide a way to influence the behavior of the rule. They are usually written as one attribute per line and can be optional to the rule. Listed below are various rule attributes:

Figure 8.1. Rule Attributes

6124
no-loop BOOLEAN

When a rule’s consequence modifies a fact, it may cause the rule to activate again, causing an infinite loop. Setting no-loop to true will skip the creation of another activation for the rule with the current set of facts.

Default value: false.

lock-on-active BOOLEAN

Whenever a ruleflow-group becomes active or an agenda-group receives the focus, any rule within that group that has lock-on-active set to true will not be activated any more. Regardless of the origin of the update, the activation of a matching rule is discarded. This is a stronger version of no-loop because the change is not only caused by the rule itself. It is ideal for calculation rules where you have a number of rules that modify a fact, and you do not want any rule re-matching and firing again. Only when the ruleflow-group is no longer active or the agenda-group loses the focus, those rules with lock-on-active set to true become eligible again for their activations to be placed onto the agenda.

Default value: false.

salience INTEGER

Each rule has an integer salience attribute which defaults to zero and can be negative or positive. Salience is a form of priority where rules with higher salience values are given higher priority when ordered in the activation queue.

Default value: 0.

Red Hat JBoss BRMS also supports dynamic salience where you can use an expression involving bound variables like the following:

rule "Fire in rank order 1,2,.."
salience(-$rank)
when
  Element($rank : rank,...)
then
  ...
end
ruleflow-group STRING
Ruleflow is a BRMS feature that lets you exercise control over the firing of rules. Rules that are assembled by the same ruleflow-group identifier fire only when their group is active. This attribute has been merged with agenda-group and the behaviours are basically the same.
agenda-group STRING

Agenda groups enable you to partition the agenda, which provides more execution control. Only rules in the agenda group that have acquired the focus are allowed to fire. This attribute has been merged with ruleflow-group and the behaviours are basically the same.

Default value: MAIN.

auto-focus BOOLEAN

When a rule is activated where the auto-focus value is true and the rule’s agenda group does not have focus yet, it is automatically given focus, allowing the rule to potentially fire.

Default value: false.

activation-group STRING
Rules that belong to the same activation-group identified by this attribute’s String value, will only fire exclusively. More precisely, the first rule in an activation-group to fire will cancel all pending activations of all rules in the group, for example stop them from firing.
dialect STRING

Java and MVEL are the possible values of the dialect attribute. This attribute specifies the language to be used for any code expressions in the LHS or the RHS code block. While the dialect can be specified at the package level, this attribute allows the package definition to be overridden for a rule.

Default value: specified by the package.

date-effective STRING

A rule can only activate if the current date and time is after the date-effective attribute. Note that STRING is a date and time definition. An example date-effective attribute is displayed below:

rule "Start Exercising"
date-effective "4-Sep-2014"
when
  $m : org.drools.compiler.Message()
then
  $m.setFired(true);
end
date-expires STRING

A rule cannot activate if the current date and time is after the date-expires attribute. Note that STRING is a date and time definition. An example date-expires attribute is displayed below:

rule "Run 4km"
date-effective "4-Sep-2014"
date-expires "9-Sep-2014"
when
  $m : org.drools.compiler.Message()
then
  $m.setFired(true);
end
duration LONG
If a rule is still true, the duration attribute will dictate that the rule will fire after a specified duration.
Note

The attributes ruleflow-group and agenda-group have been merged and now behave the same. The GET methods have been left the same, for deprecations reasons, but both attributes return the same underlying data.

8.4. Adding Comments to Rule File

Comments are sections of text that are ignored by the rule engine. They are stripped out when they are encountered, except inside semantic code blocks (like a rule’s RHS).

8.4.1. Single Line Comment Example

This is what a single line comment looks like. To create single line comments, you can use //. The parser will ignore anything in the line after the comment symbol:

rule "Testing Comments"
when
  // this is a single line comment
  eval(true) // this is a comment in the same line of a pattern
then
  // this is a comment inside a semantic code block
end

8.4.2. Multi-Line Comment Example

This is what a multi-line comment looks like. This configuration comments out blocks of text, both in and outside semantic code blocks:

rule "Test Multi-Line Comments"
when
  /* this is a multi-line comment
     in the left hand side of a rule */
  eval( true )
then
  /* and this is a multi-line comment
     in the right hand side of a rule */
end

8.5. Error Messages in Rules

Red Hat JBoss BRMS provides standardized error messages. This standardization aims to help users to find and resolve problems in a easier and faster way.

8.5.1. Error Message Format

This is the standard error message format.

Figure 8.2. Error Message Format Example

1598

1st Block: This area identifies the error code.

2nd Block: Line and column information.

3rd Block: Some text describing the problem.

4th Block: This is the first context. Usually indicates the rule, function, template, or query where the error occurred. This block is not mandatory.

5th Block: Identifies the pattern where the error occurred. This block is not mandatory.

8.5.2. Error Message Description

[ERR 101] Line 4:4 no viable alternative at input 'exits' in rule one

Indicates when the parser came to a decision point but couldn’t identify an alternative. For example:

1: rule one
2:  when
3:    exists Foo()
4:    exits Bar()
5:  then
6: end
[ERR 101] Line 3:2 no viable alternative at input 'WHEN

This message means the parser has encountered the token WHEN (a hard keyword) which is in the wrong place, since the rule name is missing. For example:

1: package org.drools;
2: rule
3:   when
4:     Object()
5:   then
6:     System.out.println("A RHS");
7: end
[ERR 101] Line 0:-1 no viable alternative at input '<eof>' in rule simple_rule in pattern [name]

Indicates an open quote, apostrophe or parentheses. For example:

1: rule simple_rule
2:   when
3:     Student(name == "Andy)
4:   then
5: end
[ERR 102] Line 0:-1 mismatched input '<eof>' expecting ')' in rule simple_rule in pattern Bar

Indicates that the parser was looking for a particular symbol that it didn’t end at the current input position.

1: rule simple_rule
2:   when
3:     foo3 : Bar(
[ERR 102] Line 0:-1 mismatched input '<eof>' expecting ')' in rule simple_rule in pattern [name]

This error is the result of an incomplete rule statement. Usually when you get a 0:-1 position, it means that parser reached the end of source. To fix this problem, it is necessary to complete the rule statement.

1: package org.drools;
2:
3: rule "Avoid NPE on wrong syntax"
4:   when
5:     not(Cheese((type == "stilton", price == 10) \|\| (type == "brie", price == 15)) from $cheeseList)
6:   then
7:     System.out.println("OK");
8: end
[ERR 103] Line 7:0 rule 'rule_key' failed predicate: {(validateIdentifierKey( DroolsSoftKeywords.RULE ))}? in rule

A validating semantic predicate evaluated to false. Usually these semantic predicates are used to identify soft keywords.

 1: package nesting;
 2: dialect "mvel"
 3:
 4: import org.drools.Person
 5: import org.drools.Address
 6:
 7: fdsfdsfds
 8:
 9: rule "test something"
10:   when
11:     p: Person(name=="Michael")
12:   then
13:     p.name = "other";
14:     System.out.println(p.name);
15: end
[ERR 104] Line 3:4 trailing semi-colon not allowed in rule simple_rule

This error is associated with the eval clause, where its expression may not be terminated with a semicolon. This problem is simple to fix: just remove the semi-colon.

1: rule simple_rule
2:   when
3:     eval(abc();)
4:   then
5: end
[ERR 105] Line 2:2 required (…​)+ loop did not match anything at input 'aa' in template test_error

The recognizer came to a subrule in the grammar that must match an alternative at least once, but the subrule did not match anything. To fix this problem it is necessary to remove the numeric value as it is neither a valid data type which might begin a new template slot nor a possible start for any other rule file construct.

1: template test_error
2:   aa s 11;
3: end

8.6. Packaging

A package is a collection of rules and other related constructs, such as imports and globals. The package members are typically related to each other, such as HR rules. A package represents a namespace, which ideally is kept unique for a given grouping of rules. The package name itself is the namespace, and is not related to files or folders in any way.

It is possible to assemble rules from multiple rule sources, and have one top-level package configuration that all the rules are kept under (when the rules are assembled). It is not possible to merge into the same package resources declared under different names. A single Rulebase may, however, contain multiple packages built on it. A common structure is to have all the rules for a package in the same file as the package declaration (so that is it entirely self-contained).

8.6.1. Import Statements

Import statements work like import statements in Java. You need to specify the fully qualified paths and type names for any objects you want to use in the rules. Red Hat JBoss BRMS automatically imports classes from the Java package of the same name, and also from the package java.lang.

8.6.2. Using Globals

In DRL files, globals represent global variables. To use globals in rules:

  1. Declare the global variable:

    global java.util.List myGlobalList;
    
    rule "Using a Global"
    when
      eval(true)
    then
      myGlobalList.add("Hello World");
    end
  2. Set the global value in the working memory. The best practice is to set all global values before asserting any fact into the working memory. For example:

    List list = new ArrayList();
    KieSession kieSession = kieBase.newKieSession();
    kieSession.setGlobal("myGlobalList", list);

8.6.3. From Element

The from element allows you to pass a Hibernate session as a global. It also lets you pull data from a named Hibernate query.

8.6.4. Using Globals with E-Mail Service

Procedure: Task

  1. Open the integration code that is calling the rule engine.
  2. Obtain your emailService object and then set it in the working memory.
  3. In the DRL, declare that you have a global of type emailService and give it the name email.
  4. In your rule consequences, you can use things like email.sendSMS(number, message).

    Warning

    Globals are not designed to share data between rules and they should never be used for that purpose. Rules always reason and react to the working memory state, so if you want to pass data from rule to rule, assert the data as facts into the working memory.

    Important

    Do not set or change a global value from inside the rules. We recommend to you always set the value from your application using the working memory interface.

8.7. Functions in Rules

Functions are a way to put semantic code in a rule source file, as opposed to in normal Java classes. The main advantage of using functions in a rule is that you can keep the logic all in one place. You can change the functions as needed.

Functions are most useful for invoking actions on the consequence (then) part of a rule, especially if that particular action is used repeatedly.

A typical function declaration looks like the following:

function String hello(String name) {
  return "Hello " + name + "!";
}
Note

Note that the function keyword is used, even though it is not technically part of Java. Parameters to the function are defined as for a method. You do not have to have parameters if they are not needed. The return type is defined just like in a regular method.

8.7.1. Importing Static Method Example

In the following example, a static method Foo.hello() from a helper class is imported as a function. To import a method, enter the following into your DRL file:

import function my.package.Foo.hello

8.7.2. Calling Function Declaration Example

Irrespective of the way the function is defined or imported, you use a function by calling it by its name, in the consequence or inside a semantic code block. This is shown below:

rule "Using a Static Function"
when
  eval(true)
then
  System.out.println(hello("Bob"));
end

8.7.3. Type Declarations

Type declarations have two main goals in the rules engine: to allow the declaration of new types, and to allow the declaration of metadata for types.

Table 8.1. Type Declaration Roles

RoleDescription

Declaring new types

Red Hat JBoss BRMS uses plain Java objects as facts out of the box. However, if you wish to define the model directly to the rules engine, you can do so by declaring a new type. You can also declare a new type when there is a domain model already built and you want to complement this model with additional entities that are used mainly during the reasoning process.

Declaring metadata

Facts may have meta information associated to them. Examples of meta information include any kind of data that is not represented by the fact attributes and is consistent among all instances of that fact type. This meta information may be queried at runtime by the engine and used in the reasoning process.

8.7.4. Declaring New Types

To declare a new type, the keyword declare is used, followed by the list of fields and the keyword end. A new fact must have a list of fields, otherwise the engine will look for an existing fact class in the classpath and raise an error if not found.

8.7.5. Declaring New Fact Type Example

In this example, a new fact type called Address is used. This fact type will have three attributes: number, streetName and city. Each attribute has a type that can be any valid Java type, including any other class created by the user or other fact types previously declared:

declare Address
  number : int
  streetName : String
  city : String
end

8.7.6. Declaring New Fact Type Additional Example

This fact type declaration uses a Person example. dateOfBirth is of the type java.util.Date (from the Java API) and address is of the fact type Address.

declare Person
  name : String
  dateOfBirth : java.util.Date
  address : Address
end

8.7.7. Using Import Example

To avoid using fully qualified class names, use the import statement:

import java.util.Date

declare Person
  name : String
  dateOfBirth : Date
  address : Address
end

8.7.8. Generated Java Classes

When you declare a new fact type, Red Hat JBoss BRMS generates bytecode that implements a Java class representing the fact type. The generated Java class is a one-to-one Java Bean mapping of the type definition.

8.7.9. Generated Java Class Example

This is an example of a generated Java class using the Person fact type:

public class Person implements Serializable {
  private String name;
  private java.util.Date dateOfBirth;
  private Address address;

  // empty constructor
  public Person() {...}

  // constructor with all fields
  public Person(String name, Date dateOfBirth, Address address) {...}

  // if keys are defined, constructor with keys
  public Person( ...keys... ) {...}

  // getters and setters
  // equals/hashCode
  // toString
}

8.7.10. Using Declared Types in Rules Example

Since the generated class is a simple Java class, it can be used transparently in the rules like any other fact:

rule "Using a declared Type"
when
  $p : Person(name == "Bob")
then
  // Insert Mark, who is Bob's manager.
  Person mark = new Person();
  mark.setName("Mark");
  insert(mark);
end

8.7.11. Declaring Metadata

Metadata may be assigned to several different constructions in Red Hat JBoss BRMS, such as fact types, fact attributes and rules. Red Hat JBoss BRMS uses the at sign (@) to introduce metadata and it always uses the form:

@metadata_key(metadata_value)

The parenthesized metadata_value is optional.

8.7.12. Working with Metadata Attributes

Red Hat JBoss BRMS allows the declaration of any arbitrary metadata attribute. Some have special meaning to the engine, while others are available for querying at runtime. Red Hat JBoss BRMS allows the declaration of metadata both for fact types and for fact attributes. Any metadata that is declared before the attributes of a fact type are assigned to the fact type, while metadata declared after an attribute are assigned to that particular attribute.

8.7.13. Declaring Metadata Attribute with Fact Types Example

This is an example of declaring metadata attributes for fact types and attributes. There are two metadata items declared for the fact type (@author and @dateOfCreation) and two more defined for the name attribute (@key and @maxLength). The @key metadata has no required value, and so the parentheses and the value were omitted:

import java.util.Date

declare Person
  @author(Bob)
  @dateOfCreation(01-Feb-2009)

  name : String @key @maxLength(30)
  dateOfBirth : Date
  address : Address
end

8.7.14. @position Attribute

The @position attribute can be used to declare the position of a field, overriding the default declared order. This is used for positional constraints in patterns.

8.7.15. @position Example

This is what the @position attribute looks like in use:

declare Cheese
  name : String @position(1)
  shop : String @position(2)
  price : int @position(0)
end

8.7.16. Predefined Class Level Annotations

@role( <fact\|event>)
This attribute can be used to assign roles to facts and events.
@typesafe(<boolean>)
By default, all type declarations are compiled with type safety enabled. @typesafe(false) provides a means to override this behavior by permitting a fall-back, to type unsafe evaluation where all constraints are generated as MVEL constraints and executed dynamically. This is useful when dealing with collections that do not have any generics or mixed type collections.
@timestamp(<attribute name>)
Creates a timestamp.
@duration(<attribute name>)
Sets a duration for the implementation of an attribute.
@expires(<time interval>)
Allows you to define when the attribute should expire.
@propertyChangeSupport
Facts that implement support for property changes as defined in the Javabean spec can now be annotated so that the engine register itself to listen for changes on fact properties.
@propertyReactive
Makes the type property reactive.

8.7.17. @key Attribute Functions

Declaring an attribute as a key attribute has two major effects on generated types:

  1. The attribute is used as a key identifier for the type, and thus the generated class implements the equals() and hashCode() methods taking the attribute into account when comparing instances of this type.
  2. Red Hat JBoss BRMS generates a constructor using all the key attributes as parameters.

8.7.18. @key Declaration Example

This is an example of @key declarations for a type. Red Hat JBoss BRMS generates equals() and hashCode() methods that checks the firstName and lastName attributes to determine if two instances of Person are equal to each other. It does not check the age attribute. It also generates a constructor taking firstName and lastName as parameters:

declare Person
  firstName : String @key
  lastName : String @key
  age : int
end

8.7.19. Creating Instance with Key Constructor Example

This is what creating an instance using the key constructor looks like:

Person person = new Person("John", "Doe");

8.7.20. Positional Arguments

Patterns support positional arguments on type declarations and are defined by the @position attribute.

Positional arguments are when you do not need to specify the field name, as the position maps to a known named field. That is, Person(name == "mark") can be rewritten as Person("mark";). The semicolon ; is important so that the engine knows that everything before it is a positional argument. You can mix positional and named arguments on a pattern by using the semicolon ; to separate them. Any variables used in a positional that have not yet been bound will be bound to the field that maps to that position.

8.7.21. Positional Argument Example

Observe the example below:

declare Cheese
  name : String
  shop : String
  price : int
end

The default order is the declared order, but this can be overridden using @position.

declare Cheese
  name : String @position(1)
  shop : String @position(2)
  price : int @position(0)
end

8.7.22. @position Annotation

The @position annotation can be used to annotate original pojos on the classpath. Currently only fields on classes can be annotated. Inheritance of classes is supported, but not interfaces of methods.

8.7.23. Example Patterns

These example patterns have two constraints and a binding. The semicolon ; is used to differentiate the positional section from the named argument section. Variables and literals and expressions using just literals are supported in positional arguments, but not variables:

Cheese("stilton", "Cheese Shop", p;)
Cheese("stilton", "Cheese Shop"; p : price)
Cheese("stilton"; shop == "Cheese Shop", p : price)
Cheese(name == "stilton"; shop == "Cheese Shop", p : price)

8.8. Backward-Chaining

8.8.1. Backward-Chaining Systems

Backward-Chaining is a feature recently added to the BRMS Engine. This process is often referred to as derivation queries, and it is not as common compared to reactive systems since BRMS is primarily reactive forward chaining. That is, it responds to changes in your data. The backward-chaining added to the engine is for product-like derivations.

8.8.2. Cloning Transitive Closures

Figure 8.3. Reasoning Graph

6135

The previous chart demonstrates a House example of transitive items. A similar reasoning chart can be created by implementing the following rules:

Configuring Transitive Closures

  1. First, create some java rules to develop reasoning for transitive items. It inserts each of the locations.
  2. Next, create the Location class; it has the item and where it is located.
  3. Type the rules for the House example as depicted below:

    ksession.insert(new Location("office", "house"));
    ksession.insert(new Location("kitchen", "house"));
    ksession.insert(new Location("knife", "kitchen"));
    ksession.insert(new Location("cheese", "kitchen"));
    ksession.insert(new Location("desk", "office"));
    ksession.insert(new Location("chair", "office"));
    ksession.insert(new Location("computer", "desk"));
    ksession.insert(new Location("drawer", "desk"));
  4. A transitive design is created in which the item is in its designated location such as a "desk" located in an "office."

    Figure 8.4. Transitive Reasoning Graph of House

    An example transitive closure graph.
Note

Notice compared to the previous graph, there is no "key" item in a "drawer" location. This will become evident in a later topic.

8.8.3. Defining Query

  1. Create a query to search for data inserted into the rule engine:

    query isContainedIn(String x, String y)
      Location(x, y;)
      or
      (Location(z, y;) and isContainedIn(x, z;))
    end

    Note that the query in the example above is recursive, calling isContainedIn.

  2. To see implementation details, create a rule similar to the following for printing each string inserted into the system:

    rule "go" salience 10
    when
      $s : String()
    then
      System.out.println($s);
    end
  3. Create a rule that uses the isContainedIn query from the first step.

    rule "go1"
    when
      String(this == "go1")
      isContainedIn("office", "house";)
    then
      System.out.println("office is in the house");
    end

    The rule checks whether the item office is in the location house. The query created in the first step is triggered when the string go1 is inserted.

  4. Insert a fact into the engine and call fireAllRules().

    ksession.insert("go1");
    ksession.fireAllRules();

    The output of the engine should look like the following:

    go1
    office is in the house

    The following holds:

    • The salience ensures that the go rule is fired first and the message output is printed.
    • The go1 rule matches the query and office is in the house is printed.

8.8.4. Transitive Closure Example

Creating Transitive Closure

  1. Create a transitive closure by implementing the following rule:

    rule "go2"
    when
      String(this == "go2")
      isContainedIn("drawer", "house";)
    then
      System.out.println("Drawer in the House");
    end
  2. Recall from the cloning transitive closure topic, there was no instance of "drawer" in "house." "Drawer" was located in "desk."

    Figure 8.5. Transitive Reasoning Graph of a Drawer.

    An example transitive closure graph.
  3. Use the previous query for this recursive information.

    query isContainedIn(String x, String y)
      Location(x, y;)
      or
      (Location(z, y;) and isContainedIn(x, z;))
    end
  4. Create the go2, insert it into the engine, and call the fireAllRules.

    ksession.insert( "go2" );
    ksession.fireAllRules();
    ---
    go2
    Drawer in the House

    When the rule is fired, it correctly tells you go2 has been inserted and that the "drawer" is in the "house."

  5. Check how the engine determined this outcome.

    • The query has to recurse down several levels to determine this.
    • Instead of using Location(x, y;), the query uses the value of (z, y;) since "drawer" is not in "house."
    • The z is currently unbound which means it has no value and will return everything that is in the argument.
    • y is currently bound to "house," so z will return "office" and "kitchen."
    • Information is gathered from "office" and checks recursively if the "drawer" is in the "office." The following query line is being called for these parameters: isContainedIn(x ,z;)

      There is no instance of "drawer" in "office"; therefore, it does not match. With z being unbound, it will return data that is within the "office", and it will gather that z == desk.

      isContainedIn(x==drawer, z==desk)

      isContainedIn recurses three times. On the final recurse, an instance triggers of "drawer" in the "desk".

      Location(x==drawer, y==desk)

      This matches on the first location and recurses back up, so we know that "drawer" is in the "desk", the "desk" is in the "office", and the "office" is in the "house"; therefore, the "drawer" is in the "house" and returns true.

8.8.5. Reactive Transitive Queries

Creating a Reactive Transitive Query

  1. Create a reactive transitive query by implementing the following rule:

    rule "go3"
    when
      String( this == "go3" )
      isContainedIn("key", "office"; )
    then
      System.out.println( "Key in the Office" );
    end

    Reactive transitive queries can ask a question even if the answer can not be satisfied. Later, if it is satisfied, it will return an answer.

    Note

    Recall from the cloning transitive closures example that there was no key item in the system.

  2. Use the same query for this reactive information.

    query isContainedIn(String x, String y)
      Location(x, y;)
      or
      (Location(z, y;) and isContainedIn(x, z;))
    end
  3. Create the go3, insert it into the engine, and call the fireAllRules.

    ksession.insert("go3");
    ksession.fireAllRules();
    ---
    go3
    • go3 is inserted
    • fireAllRules(); is called

    The first rule that matches any String returns go3 but nothing else is returned because there is no answer; however, while go3 is inserted in the system, it will continuously wait until it is satisfied.

  4. Insert a new location of "key" in the "drawer":

    ksession.insert( new Location("key", "drawer"));
    ksession.fireAllRules();
    ---
    Key in the Office

    This new location satisfies the transitive closure because it is monitoring the entire graph. In addition, this process now has four recursive levels in which it goes through to match and fire the rule.

8.8.6. Queries with Unbound Arguments

Creating Unbound Argument Query

  1. Create a query with unbound arguments by implementing the following rule:

    rule "go4"
    when
      String(this == "go4")
      isContainedIn(thing, "office";)
    then
      System.out.println("thing" + thing + "is in the office");
    end

    This rule is asking for everything in the "office", and it will tell everything in all the rows below. The unbound argument (out variable thing) in this example will return every possible value; accordingly, it is very similar to the z value used in the reactive transitive query example.

  2. Use the query for the unbound arguments.

    query isContainedIn(String x, String y)
      Location(x, y;)
      or
      (Location(z, y;) and isContainedIn(x, z;))
    end
  3. Create the go4, insert it into the engine, and call the fireAllRules.

    ksession.insert( "go4" );
    ksession.fireAllRules();
    ---
    go4
    thing Key is in the Office
    thing Computer is in the Office
    thing Drawer is in the Office
    thing Desk is in the Office
    thing Chair is in the Office

    When go4 is inserted, it returns all the previous information that is transitively below "office."

8.8.7. Multiple Unbound Arguments

Creating Multiple Unbound Arguments

  1. Create a query with multiple unbound arguments by implementing the following rule:

    rule "go5"
    when
      String(this == "go5")
      isContainedIn(thing, location;)
    then
      System.out.println("thing" + thing + "is in" + location);
    end

    Both thing and location are unbound out variables, and without bound arguments, everything is called upon.

  2. Use the query for multiple unbound arguments.

    query isContainedIn(String x, String y)
      Location(x, y;)
      or
      (Location(z, y;) and isContainedIn(x, z;))
    end
  3. Create the go5, insert it into the engine, and call the fireAllRules.

    ksession.insert("go5");
    ksession.fireAllRules();
    ---
    go5
    thing Knife is in House
    thing Cheese is in House
    thing Key is in House
    thing Computer is in House
    thing Drawer is in House
    thing Desk is in House
    thing Chair is in House
    thing Key is in Office
    thing Computer is in Office
    thing Drawer is in Office
    thing Key is in Desk
    thing Office is in House
    thing Computer is in Desk
    thing Knife is in Kitchen
    thing Cheese is in Kitchen
    thing Kitchen is in House
    thing Key is in Drawer
    thing Drawer is in Desk
    thing Desk is in Office
    thing Chair is in Office

    When go5 is called, it returns everything within everything.

8.9. Type Declaration

8.9.1. Declaring Metadata for Existing Types

Red Hat JBoss BRMS allows the declaration of metadata attributes for existing types in the same way as when declaring metadata attributes for new fact types. The only difference is that there are no fields in that declaration.

8.9.2. Declaring Metadata for Existing Types Example

This example shows how to declare metadata for an existing type:

import org.drools.examples.Person

declare Person
  @author(Bob)
  @dateOfCreation(01-Feb-2009)
end

8.9.3. Declaring Metadata Using Fully Qualified Class Name Example

This example shows how you can declare metadata using the fully qualified class name instead of using the import annotation:

declare org.drools.examples.Person
  @author(Bob)
  @dateOfCreation(01-Feb-2009)
end

8.9.4. Parametrized Constructors for Declared Types Example

For a declared type like the following:

declare Person
  firstName : String @key
  lastName : String @key
  age : int
end

The compiler will implicitly generate 3 constructors: one without parameters, one with the @key fields and one with all fields.

Person() // parameterless constructor
Person(String firstName, String lastName)
Person(String firstName, String lastName, int age)

8.9.5. Non-Typesafe Classes

The @typesafe(BOOLEAN) annotation has been added to type declarations. By default all type declarations are compiled with type safety enabled. @typesafe(false) provides a means to override this behaviour by permitting a fall-back, to type unsafe evaluation where all constraints are generated as MVEL constraints and executed dynamically. This is useful when dealing with collections that do not have any generics or mixed type collections.

8.9.6. Accessing Declared Types from Application Code

Sometimes applications need to access and handle facts from the declared types. In such cases, Red Hat JBoss BRMS provides a simplified API for the most common fact handling the application wishes to do. A declared fact belongs to the package where it is declared.

8.9.7. Declaring Type

This illustrates the process of declaring a type:

package org.drools.examples

import java.util.Date

declare Address
  street : String
  city : String
  code : String
end

declare Person
  name : String
  dateOfBirth : Date
  address : Address
end

8.9.8. Handling Declared Fact Types Through API Example

This example illustrates the handling of declared fact types through the API:

import java.util.Date;

import org.kie.api.definition.type.FactType;
import org.kie.api.KieBase;
import org.kie.api.runtime.KieSession;

...

// Get a reference to a knowledge base with a declared type:
KieBase kbase = ...

// Get the declared FactType:
FactType personType = kbase.getFactType("org.drools.examples", "Person");

// Handle the type as necessary:
// Create instances:
Object bob = personType.newInstance();

// Set attributes values:
personType.set(bob, "name", "Bob" );
personType.set(bob, "dateOfBirth", new Date());
personType.set(bob, "address", new Address("King's Road","London","404"));

// Insert fact into a session:
KieSession ksession = ...
ksession.insert(bob);
ksession.fireAllRules();

// Read attributes:
String name = (String) personType.get(bob, "name");
Date date = (Date) personType.get(bob, "dateOfBirth");

For a list of Maven dependencies, see example Embedded jBPM Engine Dependencies. If you use Red Hat JBoss BRMS, see Embedded Drools Engine Dependencies.

The API also includes other helpful methods, like setting all the attributes at once, reading values from a Map, or reading all attributes at once, into a Map.

8.9.9. Type Declaration Extends

Type declarations support the extends keyword for inheritance. To extend a type declared in Java by a DRL declared subtype, repeat the supertype in a declare statement without any fields.

8.9.10. Type Declaration Extends Example

This illustrates the use of the extends annotation:

import org.people.Person

declare Person
end

declare Student extends Person
  school : String
end

declare LongTermStudent extends Student
  years : int
  course : String
end

8.9.11. Traits

Traits allow you to model multiple dynamic types which do not fit naturally in a class hierarchy. A trait is an interface that can be applied (and eventually removed) to an individual object at runtime. To create a trait out of an interface, a @format(trait) annotation is added to its declaration in DRL.

8.9.12. Traits Example

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

In order to apply a trait to an object, the new don keyword is added:

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

8.9.13. Core Objects and Traits

When a core object dons a trait, a proxy class is created on the fly (one such class will be generated lazily for each core/trait class combination). The proxy instance, which wraps the core object and implements the trait interface, is inserted automatically and will possibly activate other rules. An immediate advantage of declaring and using interfaces, getting the implementation proxy for free from the engine, is that multiple inheritance hierarchies can be exploited when writing rules. The core classes, however, need not implement any of those interfaces statically, also facilitating the use of legacy classes as cores. Any object can don a trait. For efficiency reasons, however, you can add the @traitable annotation to a declared bean class to reduce the amount of glue code that the compiler will have to generate. This is optional and will not change the behavior of the engine.

8.9.14. @traitable Example

This illustrates the use of the @traitable annotation:

declare Customer
  @traitable
  code    : String
  balance : long
end

8.9.15. Writing Rules with Traits

The only connection between core classes and trait interfaces is at the proxy level. (That is, a trait is not specifically tied to a core class.) This means that the same trait can be applied to totally different objects. For this reason, the trait does not transparently expose the fields of its core object. 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 core object field, will be mapped by the proxy class.

8.9.16. Rules with Traits Example

This example illustrates the trait interface being mapped to a field:

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

8.9.17. Hidden Fields

Hidden fields are fields in the core class not exposed by the interface.

8.9.18. Two-Part Proxy

The two-part proxy has been developed to deal with soft and hidden fields which are not processed intuitively. Internally, proxies are formed by a proper proxy and a wrapper. The former implements the interface, while the latter manages the core object fields, implementing a name/value map to supports soft fields. The proxy uses both the core object and the map wrapper to implement the interface, as needed.

8.9.19. Wrappers

The wrapper provides a looser form of typing when writing rules. However, it has also other uses. The wrapper is specific to the object it wraps, regardless of how many traits have been attached to an object. All the proxies on the same object will share the same wrapper. Additionally, the wrapper contains a back-reference to all proxies attached to the wrapped object, effectively allowing traits to see each other.

8.9.20. Wrapper Example

This is an example of using the wrapper:

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

8.9.21. Wrapper with isA Annotation Example

This illustrates a wrapper in use with the isA annotation:

$sc : GoldenCustomer($maxExpense : maxExpense > 1000, this isA "SeniorCustomer")

8.9.22. Removing Traits

The business logic may require that a trait is removed from a wrapped object. There are two ways to do so:

Logical don

Results in a logical insertion of the proxy resulting from the traiting operation.

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

The shed keyword causes the retraction of the proxy corresponding to the given argument type.

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

This operation returns another proxy implementing the org.drools.factmodel.traits.Thing interface, where the getFields() and getCore() methods are defined. Internally, 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.

8.10. Rule Attributes

For the list of all rule attributes and their description, see Section 8.3.2, “Soft Keywords”.

See an example of rule attributes below:

rule "my rule"
  salience 42
  agenda-group "number-1"
when
  ...

8.10.1. Timer Attribute Example

This is what the timer attribute looks like:

timer(int: INITIAL_DELAY REPEAT_INTERVAL?)
timer(int: 30s)
timer(int: 30s 5m)

timer(cron: CRON_EXPRESSION)
timer(cron:* 0/15 * * * ?)

8.10.2. Timers

The following timers are available in Red Hat JBoss BRMS:

Interval
Interval (indicated by int:) timers follow the semantics of java.util.Timer objects, with an initial delay and an optional repeat interval.
Cron
Cron (indicated by cron:) timers follow standard Unix cron expressions.

A rule controlled by a timer becomes active when it matches, and once for each individual match. Its consequence is executed repeatedly, according to the timer’s settings. This stops as soon as the condition doesn’t match any more.

Consequences are executed even after control returns from a call to fireUntilHalt. Moreover, the Engine remains reactive to any changes made to the Working Memory. For instance, removing a fact that was involved in triggering the timer rule’s execution causes the repeated execution to terminate, or inserting a fact so that some rule matches will cause that rule to fire. But the Engine is not continually active, only after a rule fires, for whatever reason. Thus, reactions to an insertion done asynchronously will not happen until the next execution of a timer-controlled rule.

Disposing a session puts an end to all timer activity.

8.10.3. Cron Timer Example

This is what the Cron timer looks like:

rule "Send SMS every 15 minutes"
  timer (cron:* 0/15 * * * ?)
when
  $a : Alarm(on == true)
then
  channels["sms"].insert(new Sms($a.mobileNumber, "The alarm is still on");
end

8.10.4. Calendars

Calendars are used to control when rules can fire. Red Hat JBoss BRMS uses the Quartz calendar.

8.10.5. Quartz Calendar Example

This is what the Quartz calendar looks like:

Calendar weekDayCal = QuartzHelper.quartzCalendarAdapter(org.quartz.Calendar quartzCal)

8.10.6. Registering Calendar

Procedure: Task

  1. Start a StatefulKnowledgeSession.
  2. Use the following code to register the calendar:

    ksession.getCalendars().set("weekday", weekDayCal);
  3. If you wish to utilize the calendar and a timer together, use the following code:

    rule "Weekdays are high priority"
      calendars "weekday"
      timer (int:0 1h)
    when
      Alarm()
    then
      send("priority high - we have an alarm”);
    end
    
    rule "Weekend are low priority"
      calendars "weekend"
      timer (int:0 4h)
    when
      Alarm()
    then
      send("priority low - we have an alarm”);
    end

8.10.7. Left Hand Side

The Left Hand Side (LHS) is a common name for the conditional part of the rule. It consists of zero or more conditional elements. If the LHS is empty, it will be considered as a condition element that is always true and it will be activated once, when a new WorkingMemory session is created.

8.10.8. Conditional Elements

Conditional elements work on one or more patterns. The most common conditional element is and. It is implicit when you have multiple patterns in the LHS of a rule that is not connected in any way.

8.10.9. Rule Without Conditional Element Example

This is what a rule without a conditional element looks like:

rule "no CEs"
when
  // empty
then
  ... // actions (executed once)
end

// The above rule is internally rewritten as:

rule "eval(true)"
when
  eval( true )
then
  ... // actions (executed once)
end

8.11. Patterns

A pattern element is the most important conditional element. It can potentially match on each fact that is inserted in the working memory. A pattern contains constraints and has an optional pattern binding.

8.11.1. Pattern Example

This is what a pattern looks like:

rule "Two unconnected patterns"
when
  Pattern1()
  Pattern2()
then
    ... // actions
end

// The above rule is internally rewritten as:

rule "Two and connected patterns"
when
  Pattern1()
  and Pattern2()
then
  ... // actions
end
Note

An and cannot have a leading declaration binding. This is because a declaration can only reference a single fact at a time, and when the and is satisfied it matches both facts.

8.11.2. Pattern Matching

A pattern matches against a fact of the given type. The type need not be the actual class of some fact object. Patterns may refer to superclasses or even interfaces, thereby potentially matching facts from many different classes. The constraints are defined inside parentheses.

8.11.3. Pattern Binding

Patterns can be bound to a matching object. This is accomplished using a pattern binding variable such as $p.

8.11.4. Pattern Binding with Variable Example

This is what pattern binding using a variable looks like:

rule ...
when
  $p : Person()
then
  System.out.println("Person " + $p);
end
Note

The prefixed dollar symbol ($) is not mandatory.

8.11.5. Constraints

A constraint is an expression that returns true or false. For example, you can have a constraint that states "five is smaller than six".

8.12. Elements and Variables

8.12.1. Property Access on Java Beans (POJOs)

Any bean property can be used directly. A bean property is exposed using a standard Java bean getter: a method getMyProperty() (or isMyProperty() for a primitive boolean) which takes no arguments and return something.

Red Hat JBoss BRMS uses the standard JDK Introspector class to do this mapping, so it follows the standard Java bean specification.

Warning

Property accessors must not change the state of the object in a way that may effect the rules. The rule engine effectively caches the results of its matching in between invocations to make it faster.

8.12.2. POJO Example

This is what the bean property looks like:

Person(age == 50)

// this is the same as:
Person(getAge() == 50)
The age property
The age property is written as age in DRL instead of the getter getAge().
Property accessors
You can use property access (age) instead of getters explicitly (getAge()) because of performance enhancements through field indexing.

8.12.3. Working with POJOs

Procedure: Task

  1. Observe the example below:

    public int getAge() {
      Date now = DateUtil.now(); // Do NOT do this.
      return DateUtil.differenceInYears(now, birthday);
    }
  2. To solve this, insert a fact that wraps the current date into working memory and update that fact between fireAllRules as needed.

8.12.4. POJO Fallbacks

When working with POJOs, a fallback method is applied. If the getter of a property cannot be found, the compiler will resort to using the property name as a method name and without arguments. Nested properties are also indexed.

8.12.5. Fallback Example

This is what happens when a fallback is implemented:

Person(age == 50)

// If Person.getAge() does not exists, this falls back to:
Person(age() == 50)

This is what it looks like as a nested property:

Person(address.houseNumber == 50)

// this is the same as:
Person(getAddress().getHouseNumber() == 50)
Warning

In a stateful session, care should be taken when using nested accessors as the Working Memory is not aware of any of the nested values and does not know when they change. Consider them immutable while any of their parent references are inserted into the Working Memory. If you wish to modify a nested value you should mark all of the outer facts as updated. In the above example, when the houseNumber changes, any Person with that Address must be marked as updated.

8.12.6. Java Expressions

Table 8.2. Java Expressions

CapabilityExample

You can use any Java expression that returns a boolean as a constraint inside the parentheses of a pattern. Java expressions can be mixed with other expression enhancements, such as property access.

Person(age == 50)

You can change the evaluation priority by using parentheses, as in any logic or mathematical expression.

Person(age > 100 && (age % 10 == 0))

You can reuse Java methods.

Person(Math.round(weight / (height * height)) < 25.0)

Type coercion is always attempted if the field and the value are of different types; exceptions will be thrown if a bad coercion is attempted.

Person(age == "10") // "10" is coerced to 10
Warning

Methods must not change the state of the object in a way that may affect the rules. Any method executed on a fact in the LHS should be a read only method.

Warning

The state of a fact should not change between rule invocations (unless those facts are marked as updated to the working memory on every change):

Person(System.currentTimeMillis() % 1000 == 0) // Do NOT do this.
Important

All operators have normal Java semantics except for == and !=.

The == operator has null-safe equals() semantics:

// Similar to: java.util.Objects.equals(person.getFirstName(), "John")
// so (because "John" is not null) similar to:
// "John".equals(person.getFirstName())
Person(firstName == "John")

The != operator has null-safe !equals() semantics:

// Similar to: !java.util.Objects.equals(person.getFirstName(), "John")
Person(firstName != "John")

8.12.7. Comma-Separated Operators

The comma character (,) is used to separate constraint groups. It has implicit and connective semantics.

The comma operator is used at the top-level constraint as it makes them easier to read and the engine will be able to optimize them.

8.12.8. Comma-Separated Operator Example

The following illustrates comma-separated scenarios in implicit and connective semantics:

// Person is at least 50 and weighs at least 80 kg.
Person(age > 50, weight > 80)
// Person is at least 50, weighs at least 80 kg and is taller than 2 meter.
Person(age > 50, weight > 80, height > 2)
Note

The comma (,) operator cannot be embedded in a composite constraint expression, such as parentheses.

8.12.9. Binding Variables

You can bind properties to variables in Red Hat JBoss BRMS. It allows for faster execution and performance.

8.12.10. Binding Variable Examples

This is an example of a property bound to a variable:

// Two people of the same age:
Person($firstAge : age) // binding
Person(age == $firstAge) // constraint expression
Note

For backwards compatibility reasons, it’s allowed (but not recommended) to mix a constraint binding and constraint expressions as such:

// Not recommended:
Person($age : age * 2 < 100)
// Recommended (separates bindings and constraint expressions):
Person(age * 2 < 100, $age : age)

8.12.11. Unification

You can unify arguments across several properties. While positional arguments are always processed with unification, the unification symbol, :=, exists for named arguments.

8.12.12. Unification Example

This is what unifying two arguments looks like:

Person($age := age)
Person($age := age)

8.12.13. Options and Operators in Red Hat JBoss BRMS

Date literal

The date format dd-mmm-yyyy is supported by default. You can customize this by providing an alternative date format mask as the System property named drools.dateformat. If more control is required, use a restriction.

Cheese(bestBefore < "27-Oct-2009")
List and Map access

You can directly access a List value by index.

// Same as childList(0).getAge() == 18
Person(childList[0].age == 18)
Value key

You can directly access a Map value by key.

// Same as credentialMap.get("jsmith").isValid()
Person(credentialMap["jsmith"].valid)
Abbreviated combined relation condition

This allows you to place more than one restriction on a field using the restriction connectives && or \|\|. Grouping via parentheses is permitted, resulting in a recursive syntax pattern.

// Simple abbreviated combined relation condition using a single &&
Person(age > 30 && < 40)
// Complex abbreviated combined relation using groupings
Person(age ((> 30 && < 40) \|\| (> 20 && < 25)))
// Mixing abbreviated combined relation with constraint connectives
Person(age > 30 && < 40 \|\| location == "london")
Operators

Operators can be used on properties with natural ordering. For example, for Date fields, < means before, for String fields, it means alphabetically lower.

Person(firstName < $otherFirstName)
Person(birthDate < $otherBirthDate)
Operator matches

Matches a field against any valid Java regular expression. Typically that regexp is a string literal, but variables that resolve to a valid regexp are also allowed. It only applies on String properties. Using matches against a null value always evaluates to false.

Cheese(type matches "(Buffalo)?\\S*Mozarella")
Operator not matches

The operator returns true if the String does not match the regular expression. The same rules apply as for the matches operator. It only applies on String properties.

Cheese(type not matches "(Buffulo)?\\S*Mozarella")
The operator contains

The operator contains is used to check whether a field that is a Collection or array and contains the specified value. It only applies on Collection properties.

CheeseCounter(cheeses contains "stilton") // contains with a String literal
CheeseCounter(cheeses contains $var) // contains with a variable
The operator not contains

The operator not contains is used to check whether a field that is a Collection or array and does not contain the specified value. It only applies on Collection properties.

CheeseCounter(cheeses not contains "cheddar") // not contains with a String literal
CheeseCounter(cheeses not contains $var) // not contains with a variable
The operator memberOf

The operator memberOf is used to check whether a field is a member of a collection or array; that collection must be a variable.

CheeseCounter(cheese memberOf $matureCheeses)
The operator not memberOf

The operator not memberOf is used to check whether a field is not a member of a collection or array. That collection must be a variable.

CheeseCounter(cheese not memberOf $matureCheeses)
The operator soundslike

This operator is similar to matches, but it checks whether a word has almost the same sound (using English pronunciation) as the given value.

// match cheese "fubar" or "foobar"
Cheese(name soundslike 'foobar')
The operator str

The operator str is used to check whether a field that is a String starts with or ends with a certain value. It can also be used to check the length of the String.

Message(routingValue str[startsWith] "R1")
Message(routingValue str[endsWith] "R2")
Message(routingValue str[length] 17)
Compound Value Restriction

Compound value restriction is used where there is more than one possible value to match. Currently only the in and not in evaluators support this. The second operand of this operator must be a comma-separated list of values, enclosed in parentheses. Values may be given as variables, literals, return values or qualified identifiers. Both evaluators are actually syntactic sugar, internally rewritten as a list of multiple restrictions using the operators != and ==.

Person($cheese : favouriteCheese)
Cheese(type in ("stilton", "cheddar", $cheese))
Inline Eval Operator (deprecated)

An inline eval constraint can use any valid dialect expression as long as it results to a primitive boolean. The expression must be constant over time. Any previously bound variable, from the current or previous pattern, can be used; autovivification is also used to auto-create field binding variables. When an identifier is found that is not a current variable, the builder looks to see if the identifier is a field on the current object type, if it is, the field binding is auto-created as a variable of the same name. This is called autovivification of field variables inside of inline eval’s.

Person(girlAge : age, sex = "F")
Person(eval(age == girlAge + 2), sex = 'M') // eval() is actually obsolete in this example

8.12.14. Operator Precedence

Table 8.3. Operator Precedence

Operator TypeOperatorsNotes

(nested) property access

.

Not normal Java semantics.

List/Map access

[ ]

Not normal Java semantics.

constraint binding

:

Not normal Java semantics.

multiplicative

* /%

 

additive

+ -

 

shift

<< >> >>>

 

relational

< > <= >= instanceof

 

equality

== !=

Does not use normal Java (not) same semantics: uses (not) equals semantics instead.

non-short circuiting AND

&

 

non-short circuiting exclusive OR

^

 

non-short circuiting inclusive OR

|

 

logical AND

&&

 

logical OR

||

 

ternary

? :

 

comma-separated AND

,

Not normal Java semantics.

8.12.15. Fine Grained Property Change Listeners

This feature allows the pattern matching to only react to modification of properties actually constrained or bound inside of a given pattern. This helps with performance and recursion and avoid artificial object splitting.

Note

By default this feature is off in order to make the behavior of the rule engine backward compatible with the former releases. When you want to activate it on a specific bean you have to annotate it with @propertyReactive.

8.12.16. Fine Grained Property Change Listener Example

DRL example
declare Person
  @propertyReactive
  firstName : String
  lastName : String
end
Java class example
@PropertyReactive
 public static class Person {
 private String firstName;
 private String lastName;
  }

8.12.17. Working with Fine Grained Property Change Listeners

Using these listeners means you do not need to implement the no-loop attribute to avoid an infinite recursion. The engine recognizes that the pattern matching is done on the property while the RHS of the rule modifies other the properties. On Java classes, you can also annotate any method to say that its invocation actually modifies other properties.

8.12.18. Using Patterns with @watch

Annotating a pattern with @watch allows you to modify the inferred set of properties for which that pattern will react. The properties named in the @watch annotation are added to the ones automatically inferred. You can explicitly exclude one or more of them by beginning their name with a ! and to make the pattern to listen for all or none of the properties of the type used in the pattern respectively with the wildcards * and !*.

8.12.19. @watch Example

This is the @watch annotation in a rule’s LHS:

// Listens for changes on both firstName (inferred) and lastName:
Person(firstName == $expectedFirstName) @watch(lastName)

// Listens for all the properties of the Person bean:
Person(firstName == $expectedFirstName) @watch(*)

// Listens for changes on lastName and explicitly exclude firstName:
Person(firstName == $expectedFirstName) @watch(lastName, !firstName)

// Listens for changes on all the properties except the age one:
Person(firstName == $expectedFirstName) @watch(*, !age)
Note

Since it does not make sense to use this annotation on a pattern using a type not annotated with @PropertyReactive the rule compiler will raise a compilation error if you try to do so. Also the duplicated usage of the same property in @watch (for example like in: @watch(firstName, ! firstName)) will end up in a compilation error.

8.12.20. Using @PropertySpecificOption

You can enable @watch by default or completely disallow it using the on option of the KnowledgeBuilderConfiguration. This new PropertySpecificOption can have one of the following 3 values:

  • DISABLED: the feature is turned off and all the other related annotations are just ignored.
  • ALLOWED: this is the default behavior: types are not property reactive unless they are not annotated with @PropertySpecific.
  • ALWAYS: all types are property reactive by default.

Alternatively, you can use the drools.propertySpecific system property. For example, if you use Red Hat JBoss EAP, add the property into EAP_HOME/standalone/configuration/standalone.xml:

<system-properties>
  ...
  <property name="drools.propertySpecific" value="DISABLED"/>
  ...
</system-properties>

8.12.21. Basic Conditional Elements

and

The conditional element and is used to group other conditional elements into a logical conjunction. Red Hat JBoss BRMS supports both prefix and and infix and. It supports explicit grouping with parentheses. You can also use traditional infix and prefix and.

//infixAnd
Cheese(cheeseType : type) and Person(favouriteCheese == cheeseType)
//infixAnd with grouping
(Cheese(cheeseType : type) and (Person(favouriteCheese == cheeseType) or Person(favouriteCheese == cheeseType))

Prefix and is also supported:

(and Cheese(cheeseType : type) Person(favouriteCheese == cheeseType))

The root element of the LHS is an implicit prefix and and does not need to be specified:

when
  Cheese(cheeseType : type)
  Person(favouriteCheese == cheeseType)
then
  ...
or

This is a shortcut for generating two or more similar rules. Red Hat JBoss BRMS supports both prefix or and infix or. You can use traditional infix, prefix and explicit grouping parentheses.

//infixOr
Cheese(cheeseType : type) or Person(favouriteCheese == cheeseType)
//infixOr with grouping
(Cheese(cheeseType : type) or
  (Person(favouriteCheese == cheeseType) and
   Person(favouriteCheese == cheeseType))
(or Person(sex == "f", age > 60)
    Person(sex == "m", age > 65)

Allows for optional pattern binding. Each pattern must be bound separately.

pensioner : (Person(sex == "f", age > 60) or Person(sex == "m", age > 65))
(or pensioner : Person(sex == "f", age > 60)
    pensioner : Person(sex == "m", age > 65))
not

This checks to ensure an object specified as absent is not included in the Working Memory. It may be followed by parentheses around the condition elements it applies to. In a single pattern you can omit the parentheses.

// Brackets are optional:
not Bus(color == "red")
// Brackets are optional:
not (Bus(color == "red", number == 42))
// "not" with nested infix and - two patterns,
// brackets are requires:
not (Bus(color == "red") and
     Bus(color == "blue"))
exists

This checks the working memory to see if a specified item exists. The keyword exists must be followed by parentheses around the CEs that it applies to. In a single pattern you can omit the parentheses.

exists Bus(color == "red")
// brackets are optional:
exists (Bus(color == "red", number == 42))
// "exists" with nested infix and,
// brackets are required:
exists (Bus(color == "red") and
        Bus(color == "blue"))
Note

The behavior of the Conditional Element or is different from the connective || for constraints and restrictions in field constraints. The engine cannot interpret the Conditional Element or. Instead, a rule with or is rewritten as a number of subrules. This process ultimately results in a rule that has a single or as the root node and one subrule for each of its CEs. Each subrule can activate and fire like any normal rule; there is no special behavior or interaction between these subrules.

8.12.22. Conditional Element forall

This element evaluates to true when all facts that match the first pattern match all the remaining patterns. It is a scope delimiter. Therefore, it can use any previously bound variable, but no variable bound inside it will be available for use outside of it.

forall can be nested inside other CEs. For instance, forall can be used inside a not CE. Only single patterns have optional parentheses, so with a nested forall parentheses must be used.

8.12.23. forall Examples

Evaluating to true
rule "All English buses are red"
when
  forall($bus : Bus(type == 'english')
                Bus(this == $bus, color = 'red'))
then
    // all English buses are red
end
Single pattern forall
rule "All buses are red"
when
  forall(Bus(color == 'red'))
then
  // all Bus facts are red
end
Multi-pattern forall
rule "All employees have health and dental care programs"
when
  forall($emp : Employee()
         HealthCare(employee == $emp)
         DentalCare(employee == $emp))
then
  // all employees have health and dental care
end
Nested forall
rule "Not all employees have health and dental care"
when
  not (forall($emp : Employee()
              HealthCare(employee == $emp)
              DentalCare(employee == $emp)))
then
    // not all employees have health and dental care
end

8.12.24. Conditional Element from

The conditional element from enables users to specify an arbitrary source for data to be matched by LHS patterns. This allows the engine to reason over data not in the Working Memory. The data source could be a sub-field on a bound variable or the results of a method call. It is a powerful construction that allows out of the box integration with other application components and frameworks. One common example is the integration with data retrieved on-demand from databases using hibernate named queries.

The expression used to define the object source is any expression that follows regular MVEL syntax. Therefore, it allows you to easily use object property navigation, execute method calls and access maps and collections elements.

Important

Using from with lock-on-active rule attribute can result in rules not being fired.

There are several ways to address this issue:

  • Avoid the use of from when you can assert all facts into working memory or use nested object references in your constraint expressions (shown below).
  • Place the variable assigned used in the modify block as the last sentence in your condition (LHS).
  • Avoid the use of lock-on-active when you can explicitly manage how rules within the same rule-flow group place activations on one another.

8.12.25. from Examples

Reasoning and binding on patterns
rule "Validate zipcode"
when
  Person($personAddress : address)
  Address(zipcode == "23920W") from $personAddress
then
  // zip code is ok
end
Using a graph notation
rule "Validate zipcode"
when
  $p : Person()
  $a : Address(zipcode == "23920W") from $p.address
then
  // zip code is ok
end
Iterating over all objects
rule "Apply 10% discount to all items over US$ 100,00 in an order"
when
  $order : Order()
  $item  : OrderItem( value > 100) from $order.items
then
  // apply discount to $item
end
Use with lock-on-active
rule "Assign people in North Carolina (NC) to sales region 1"
ruleflow-group "test"
lock-on-active true
when
  $p : Person(address.state == "NC")
then
  modify ($p) {} // Assign person to sales region 1 in a modify block
end

rule "Apply a discount to people in the city of Raleigh"
ruleflow-group "test"
lock-on-active true
when
  $p : Person(address.city == "Raleigh")
then
  modify ($p) {} //Apply discount to person in a modify block
end

8.12.26. Conditional Element collect

The conditional element collect allows rules to reason over a collection of objects obtained from the given source or from the working memory. In First Oder Logic terms this is the cardinality quantifier.

The result pattern of collect can be any concrete class that implements the java.util.Collection interface and provides a default no-arg public constructor. You can use Java collections like ArrayList, LinkedList and HashSet or your own class, as long as it implements the java.util.Collection interface and provide a default no-arg public constructor.

Variables bound before the collect CE are in the scope of both source and result patterns and therefore you can use them to constrain both your source and result patterns. Any binding made inside collect is not available for use outside of it.

8.12.27. Conditional Element accumulate

The conditional element accumulate is a more flexible and powerful form of the collect element and allows a rule to iterate over a collection of objects while executing custom actions for each of the elements. The accumulate element returns a result object.

The element accumulate supports the use of predefined accumulate functions, as well as the use of inline custom code. However, using inline custom code is not recommended, as it is harder to maintain and might lead to code duplication. On the other hand, accumulate functions are easier to test and reuse.

The conditional element accumulate supports multiple different syntaxes. The preferred is the top-level syntax (as noted below), but all other syntaxes are supported as well for backward compatibility.

Top-Level accumulate Syntax

The top-level accumulate syntax is the most compact and flexible. The simplified syntax is as follows:

accumulate(SOURCE_PATTERN ; FUNCTIONS [;CONSTRAINTS])

Example 8.2. Top-Level accumulate Syntax Example

rule "Raise Alarm"
when
  $s : Sensor()
  accumulate(Reading(sensor == $s, $temp : temperature);
    $min : min($temp),
    $max : max($temp),
    $avg : average($temp);
    $min < 20, $avg > 70)
then
  // raise the alarm
end

In the example above, min, max, and average are accumulate functions that calculate the minimum, maximum, and average temperature values over all the readings for each sensor.

Built-in accumulate Functions

Only user-defined custom accumulate functions have to be explicitly imported. The following accumulate functions are imported automatically by the engine:

  • average
  • min
  • max
  • count
  • sum
  • collectList
  • collectSet

These common functions accept any expression as an input. For instance, if you want to calculate an average profit on all items of an order, you can write a rule using the average function as follows:

rule "Average Profit"
when
  $order : Order()
  accumulate(
    OrderItem(order == $order, $cost : cost, $price : price);
    $avgProfit : average(1 - $cost / $price))
then
  // average profit for $order is $avgProfit
end
Accumulate Functions Pluggability

Accumulate functions are all pluggable; if needed, custom and domain-specific functions can be easily added to the engine and rules can start to use them without any restrictions.

To implement a new accumulate function, create a Java class that implements the org.kie.api.runtime.rule.AccumulateFunction interface. To use the function in the rules, import it using the import accumulate statement:

import accumulate CLASS_NAME FUNCTION_NAME

Example 8.3. Importing and Using Custom Accumulate Function

import accumulate some.package.VarianceFunction variance

rule "Calculate Variance"
when
  accumulate(Test($s : score), $v : variance($s))
then
  // variance of the test scores is $v
end

Example 8.4. Implementation of average Function

As an example of an accumulate function, see the following implementation of the average function:

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;

import org.kie.api.runtime.rule.AccumulateFunction;

/**
 * Implementation of an accumulator capable of calculating average values.
 */
public class AverageAccumulateFunction implements AccumulateFunction {

  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {}

  public void writeExternal(ObjectOutput out) throws IOException {}

  public static class AverageData implements Externalizable {
    public int    count = 0;
    public double total = 0;

    public AverageData() {}

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
      count = in.readInt();
      total = in.readDouble();
    }

    public void writeExternal(ObjectOutput out) throws IOException {
      out.writeInt(count);
      out.writeDouble(total);
    }
  }

  /* (non-Javadoc)
   * @see org.kie.base.accumulators.AccumulateFunction#createContext()
   */
  public Serializable createContext() {
    return new AverageData();
  }

  /* (non-Javadoc)
   * @see org.kie.base.accumulators.AccumulateFunction#init(java.lang.Object)
   */
  public void init(Serializable context) throws Exception {
    AverageData data = (AverageData) context;
    data.count = 0;
    data.total = 0;
  }

  /* (non-Javadoc)
   * @see org.kie.base.accumulators.AccumulateFunction#accumulate(java.lang.Object,
   * java.lang.Object)
   */
  public void accumulate(Serializable context, Object value) {
    AverageData data = (AverageData) context;
    data.count++;
    data.total += ((Number) value).doubleValue();
  }

  /* (non-Javadoc)
   * @see org.kie.base.accumulators.AccumulateFunction#reverse(java.lang.Object,
   * java.lang.Object)
   */
  public void reverse(Serializable context, Object value) throws Exception {
    AverageData data = (AverageData) context;
    data.count--;
    data.total -= ((Number) value).doubleValue();
  }

  /* (non-Javadoc)
   * @see org.kie.base.accumulators.AccumulateFunction#getResult(java.lang.Object)
   */
  public Object getResult(Serializable context) throws Exception {
    AverageData data = (AverageData) context;
    return new Double(data.count == 0 ? 0 : data.total / data.count);
  }

  /* (non-Javadoc)
   * @see org.kie.base.accumulators.AccumulateFunction#supportsReverse()
   */
  public boolean supportsReverse() {
    return true;
  }

  /**
   * {@inheritDoc}
   */
  public Class< ? > getResultType() {
    return Number.class;
  }
}

For a list of Maven dependencies, see example Embedded jBPM Engine Dependencies. If you use Red Hat JBoss BRMS, see Embedded Drools Engine Dependencies.

Alternative Syntax

Previous accumulate syntaxes are still supported for backward compatibility.

In case the rule uses a single accumulate function on a given accumulate element, you can add a pattern for the result object and use the from keyword to link it to the accumulate result. See the following example:

Example 8.5. Rule with Alternative Syntax

rule "Apply 10% Discount on Orders over US $100.00"
when
  $order : Order()
  $total : Number(doubleValue > 100)
    from accumulate(OrderItem(order == $order, $value : value), sum($value))
then
  # apply discount on $order
end

In this example, the element accumulate uses only one function – sum. In this case, it is possible to write a pattern for the result type of the accumulate function with the constraints inside.

Important

Note that it is not possible to use both the return type and the function binding in the same accumulate statement.

accumulate with Inline Custom Code

Instead of using the accumulate functions, you can define inline custom code.

Warning

The use of accumulate with inline custom code is not recommended. It is difficult to maintain and test the rules, as well as reuse the code. Implementing your own accumulate functions allows you to test and use them easily.

The general syntax of the accumulate with inline custom code is as follows:

RESULT_PATTERN from accumulate(
	SOURCE_PATTERN,
	init(INIT_CODE),
	action(ACTION_CODE),
	reverse(REVERSE_CODE),
	result(RESULT_EXPRESSION))
RESULT_PATTERN

A regular pattern that the engine tries to match against the object returned from the RESULT_EXPRESSION.

If the attempt succeeds, the accumulate conditional element returns true and the engine proceeds with an evaluation of the next conditional element in the rule. In the second case, accumulate returns false and the engine stops evaluating conditional elements for this rule.

SOURCE_PATTERN
A regular pattern that the engine tries to match against each of the source objects.
INIT_CODE
A semantic block of code in the selected dialect that is executed once for each tuple before iterating over the source objects.
ACTION_CODE
A semantic block of code in the selected dialect that is executed for each of the source objects.
REVERSE_CODE

An optional semantic block of code in the selected dialect that is executed for each source object that no longer matches the source pattern.

The objective of this code block is to undo any calculation done in the ACTION_CODE block, so that the engine can do decremental calculation when a source object is modified or retracted. This significantly improves the performance of these operations.

RESULT_EXPRESSION
A semantic expression in the selected dialect that is executed after all source objects are iterated.

Example 8.6. Example of Inline Custom Code

rule "Apply 10% Discount on Orders over US $100.00"
when
  $order : Order()
  $total : Number(doubleValue > 100)
    from accumulate(OrderItem(order == $order, $value : value),
      init(double total = 0;),
      action(total += $value;),
      reverse(total -= $value;),
      result(total))
then
  # apply discount on $order
end

In this example, the engine executes the INIT_CODE for each Order in the working memory, initializing the total variable to zero. The engine then iterates over all OrderItem objects for that Order, executing the action for each one. After the iteration, the engine returns the value corresponding to the RESULT_EXPRESSION (in this case, a value of the total variable). Finally, the engine tries to match the result with the Number pattern. If the doubleValue is greater than 100, the rule fires.

The example is using Java programming language as a semantic dialect. In this case, a semicolon as a statement delimiter is mandatory in the init, action, and reverse code blocks. However, since the result is an expression, it does not require a semicolon. If you want to use any other dialect, note that you have to observe the principles of its specific syntax.

Custom Objects

The accumulate conditional element can be used to execute any action on source objects. The following example instantiates and populates a custom object:

Example 8.7. Instantiating Custom Objects

rule "accumulate Using Custom Objects"
when
  $person : Person($likes : likes)
  $cheesery : Cheesery(totalAmount > 100)
    from accumulate($cheese : Cheese(type == $likes),
      init(Cheesery cheesery = new Cheesery();),
      action(cheesery.addCheese($cheese);),
      reverse(cheesery.removeCheese($cheese);),
      result(cheesery));
then
  // do something
end

8.12.28. Conditional Element eval

The conditional element eval is essentially a catch-all which allows any semantic code (that returns a primitive boolean) to be executed. This code can refer to variables that were bound in the LHS of the rule, and functions in the rule package. Overuse of eval reduces the declarativeness of your rules and can result in a poorly performing engine. While eval can be used anywhere in the patterns, the best practice is to add it as the last conditional element in the LHS of a rule.

Evals cannot be indexed and thus are not as efficient as field constraints. However this makes them ideal for being used when functions return values that change over time, which is not allowed within field constraints.

8.12.29. eval Conditional Element Examples

This is what eval looks like in use:

p1 : Parameter()
p2 : Parameter()
eval(p1.getList().containsKey( p2.getItem()))
p1 : Parameter()
p2 : Parameter()
// call function isValid in the LHS
eval(isValid( p1, p2))

8.12.30. Right Hand Side

The Right Hand Side (RHS) is a common name for the consequence part of a rule. The main purpose of the RHS is to insert, retract (delete), or modify working memory data. The RHS usually contains a list of actions to be executed and should be kept small, thus keeping it declarative and readable.

Note

In case you need imperative or conditional code in the RHS, divide the rule into more rules.

8.12.31. RHS Convenience Methods

See the following list of the RHS convenience methods:

  • update(OBJECT, HANDLE);
  • update(OBJECT);
  • insert(OBJECT);
  • insertLogical(OBJECT);
  • retract(HANDLE);

For more information, see Section 8.2.1, “Accessing Working Memory”.

8.12.32. Convenience Methods Using Drools Variable

  • The call drools.halt() terminates rule execution immediately. This is required for returning control to the point whence the current session was put to work with fireUntilHalt().
  • Methods insert(Object o), update(Object o) and retract(Object o) can be called on drools as well, but due to their frequent use they can be called without the object reference.
  • drools.getWorkingMemory() returns the WorkingMemory object.
  • drools.setFocus(String s) sets the focus to the specified agenda group.
  • drools.getRule().getName(), called from a rule’s RHS, returns the name of the rule.
  • drools.getTuple() returns the Tuple that matches the currently executing rule, and drools.getActivation() delivers the corresponding Activation. (These calls are useful for logging and debugging purposes.)

8.12.33. Convenience Methods Using kcontext Variable

  • The call kcontext.getKieRuntime().halt() terminates rule execution immediately.
  • The accessor getAgenda() returns a reference to the session’s Agenda, which in turn provides access to the various rule groups: activation groups, agenda groups, and rule flow groups. A fairly common paradigm is the activation of some agenda group, which could be done with the lengthy call:

    // Give focus to the agenda group CleanUp:
    kcontext.getKieRuntime().getAgenda().getAgendaGroup("CleanUp").setFocus();

    You can achieve the same using drools.setFocus("CleanUp").

  • To run a query, you call getQueryResults(String query), whereupon you may process the results.
  • A set of methods dealing with event management lets you add and remove event listeners for the Working Memory and the Agenda.
  • Method getKieBase() returns the KieBase object, the backbone of all the Knowledge in your system, and the originator of the current session.
  • You can manage globals with setGlobal(…​), getGlobal(…​) and getGlobals().
  • Method getEnvironment() returns the runtime’s Environment.

8.12.34. Modify Statement

modify

This provides a structured approach to fact updates. It combines the update operation with a number of setter calls to change the object’s fields.

modify (FACT_EXPRESSION)
{
 EXPRESSION [, EXPRESSION]*
}

The parenthesized FACT_EXPRESSION must yield a fact object reference. The expression list in the block should consist of setter calls for the given object, to be written without the usual object reference, which is automatically prepended by the compiler.

rule "Modify stilton"
when
  $stilton : Cheese(type == "stilton")
then
  modify($stilton){
    setPrice(20),
    setAge("overripe")
  }
end

8.12.35. Query Examples

Note

To return the results use ksession.getQueryResults("name"), where "name" is the query’s name. This returns a list of query results, which allow you to retrieve the objects that matched the query.

Query for people over the age of 30
query "People over the age of 30"
  person : Person(age > 30)
end
Query for people over the age of X, and who live in Y
query "People over the age of x"  (int x, String y)
  person : Person(age > x, location == y)
end

8.12.36. QueryResults Example

We iterate over the returned QueryResults using a standard for loop. Each element is a QueryResultsRow which we can use to access each of the columns in the tuple. These columns can be accessed by bound declaration name or index position:

QueryResults results = ksession.getQueryResults("people over the age of 30");
System.out.println("we have " + results.size() + " people over the age  of 30");

System.out.println("These people are are over 30:");

for (QueryResultsRow row : results) {
  Person person = (Person) row.get("person");
  System.out.println(person.getName() + "\n");
}

8.12.37. Queries Calling Other Queries

Queries can call other queries. This combined with optional query arguments provides derivation query style backward chaining. Positional and named syntax is supported for arguments. It is also possible to mix both positional and named, but positional must come first, separated by a semi colon. Literal expressions can be passed as query arguments, but you cannot mix expressions with variables.

Note

Using the ? symbol in this process means the query is pull only and once the results are returned you will not receive further results as the underlying data changes.

8.12.38. Queries Calling Other Queries Example

Query calling another query
declare Location
  thing : String
  location : String
end

query isContainedIn(String x, String y)
  Location(x, y;)
  or
  (Location(z, y;) and ?isContainedIn(x, z;))
end
Using live queries to reactively receive changes over time from query results
query isContainedIn(String x, String y)
  Location(x, y;)
  or
  (Location(z, y;) and isContainedIn(x, z;))
end

rule look when
  Person($l : likes)
  isContainedIn($l, 'office';)
then
  insertLogical($l 'is in the office');
end

8.12.39. Unification for Derivation Queries

Red Hat JBoss BRMS supports unification for derivation queries. This means that arguments are optional. It is possible to call queries from Java leaving arguments unspecified using the static field org.drools.runtime.rule.Variable.v. You must use v and not an alternative instance of Variable. These are referred to as out arguments.

Note

The query itself does not declare at compile time whether an argument is in or an out. This can be defined purely at runtime on each use.

8.13. Searching Working Memory Using Query

8.13.1. Queries

Queries are used to retrieve fact sets based on patterns, as they are used in rules. Patterns may make use of optional parameters. Queries can be defined in the Knowledge Base, from where they are called up to return the matching results. While iterating over the result collection, any identifier bound in the query can be used to access the corresponding fact or fact field by calling the get method with the binding variable’s name as its argument. If the binding refers to a fact object, its FactHandle can be retrieved by calling getFactHandle, again with the variable’s name as the parameter. Illustrated below is a query example:

QueryResults results = ksession.getQueryResults("my query", new Object[] {"string"});
for (QueryResultsRow row : results) {
  System.out.println(row.get("varName"));
}

8.13.2. Live Queries

Invoking queries and processing the results by iterating over the returned set is not a good way to monitor changes over time.

To alleviate this, Red Hat JBoss BRMS provides live queries, which have a listener attached instead of returning an iterable result set. These live queries stay open by creating a view and publishing change events for the contents of this view. To activate, start your query with parameters and listen to changes in the resulting view. The dispose method terminates the query and discontinues this reactive scenario.

8.13.3. ViewChangedEventListener Implementation Example

final List updated = new ArrayList();
final List removed = new ArrayList();
final List added = new ArrayList();

ViewChangedEventListener listener = new ViewChangedEventListener() {
  public void rowUpdated(Row row) {
    updated.add(row.get("$price"));
  }

  public void rowRemoved(Row row) {
    removed.add(row.get("$price"));
  }

  public void rowAdded(Row row) {
    added.add(row.get("$price"));
  }
}

// Open the LiveQuery:
LiveQuery query = ksession.openLiveQuery("cars", new Object[] {"sedan", "hatchback"}, listener);
...
query.dispose() // calling dispose to terminate the live query
Note

For an example of Glazed Lists integration for live queries, read the Glazed Lists examples for Drools Live Querries article.

8.14. Domain Specific Languages (DSLs)

Domain Specific Languages (or DSLs) are a way of creating a rule language that is dedicated to your problem domain. A set of DSL definitions consists of transformations from DSL "sentences" to DRL constructs, which lets you use of all the underlying rule language and engine features. You can write rules in DSL rule (or DSLR) files, which will be translated into DRL files.

DSL and DSLR files are plain text files and you can use any text editor to create and modify them. There are also DSL and DSLR editors you can use, both in the IDE as well as in the web based BRMS, although they may not provide you with the full DSL functionality.

8.14.1. DSL Editor

The DSL editor provides a tabular view of the mapping of Language to Rule Expressions. The Language Expression feeds the content assistance for the rule editor so that it can suggest Language Expressions from the DSL configuration. The rule editor loads the DSL configuration when the rule resource is loaded for editing.

Note

DSL feature is useful for simple use cases for non technical users to easily define rules based on sentence snippets. For more complex use cases, we recommend you to use other advanced features like decision tables and DRL rules, that are more expressive and flexible.

8.14.2. Using DSLs

DSLs can serve as a layer of separation between rule authoring (and rule authors) and the technical intricacies resulting from the modeling of domain object and the rule engine’s native language and methods. A DSL hides implementation details and focuses on the rule logic proper. DSL sentences can also act as "templates" for conditional elements and consequence actions that are used repeatedly in your rules, possibly with minor variations. You may define DSL sentences as being mapped to these repeated phrases, with parameters providing a means for accommodating those variations.

8.14.3. DSL Example

[when]Something is {colour}=Something(colour=="{colour}")

[when] indicates the scope of the expression (that is, whether it is valid for the LHS or the RHS of a rule).

The part after the bracketed keyword is the expression that you use in the rule.

The part to the right of the equal sign (=) is the mapping of the expression into the rule language. The form of this string depends on its destination, RHS or LHS. If it is for the LHS, then it ought to be a term according to the regular LHS syntax; if it is for the RHS then it might be a Java statement.

8.14.4. About DSL Parser

Whenever the DSL parser matches a line from the rule file written in the DSL with an expression in the DSL definition, it performs three steps of string manipulation:

  • The DSL extracts the string values appearing where the expression contains variable names in brackets.
  • The values obtained from these captures are interpolated wherever that name occurs on the right hand side of the mapping.
  • The interpolated string replaces whatever was matched by the entire expression in the line of the DSL rule file.
Note

You can use (for instance) a ? to indicate that the preceding character is optional. One good reason to use this is to overcome variations in natural language phrases of your DSL. But, given that these expressions are regular expression patterns, this means that all wildcard characters in Java’s pattern syntax have to be escaped with a preceding backslash (\).

8.14.5. About DSL Compiler

The DSL compiler transforms DSL rule files line by line. If you do not wish for this to occur, ensure that the captures are surrounded by characteristic text (words or single characters). As a result, the matching operation done by the parser plucks out a substring from somewhere within the line. In the example below, quotes are used as distinctive characters. The characters that surround the capture are not included during interpolation, just the contents between them.

8.14.6. DSL Syntax Examples

Quotes

Use quotes for textual data that a rule editor may want to enter. You can also enclose the capture with words to ensure that the text is correctly matched.

[when]something is "{color}"=Something(color=="{color}")
[when]another {state} thing=OtherThing(state=="{state}"
Braces

In a DSL mapping, the braces "{" and "}" should only be used to enclose a variable definition or reference, resulting in a capture. If they should occur literally, either in the expression or within the replacement text on the right hand side, they must be escaped with a preceding backslash (\).

[then]do something= if (foo) \{ doSomething(); \}
Mapping with correct syntax example
# This is a comment to be ignored.
[when]There is a person with name of "{name}"=Person(name=="{name}")
[when]Person is at least {age} years old and lives in "{location}"=Person(age >= {age}, location=="{location}")
[then]Log "{message}"=System.out.println("{message}");
[when]And = and
Expanded DSL example
There is a person with name of "Kitty"
   ==> Person(name="Kitty")
Person is at least 42 years old and lives in "Atlanta"
   ==> Person(age >= 42, location="Atlanta")
Log "boo"
   ==> System.out.println("boo");
There is a person with name of "Bob" and Person is at least 30 years old and lives in "Utah"
   ==> Person(name="Bob") and Person(age >= 30, location="Utah")
Note

If you are capturing plain text from a DSL rule line and want to use it as a string literal in the expansion, you must provide the quotes on the right hand side of the mapping.

8.14.7. Chaining DSL Expressions

DSL expressions can be chained together one one line to be used at once. It must be clear where one ends and the next one begins and where the text representing a parameter ends. Otherwise you risk getting all the text until the end of the line as a parameter value. The DSL expressions are tried, one after the other, according to their order in the DSL definition file. After any match, all remaining DSL expressions are investigated, too.

8.14.8. Adding Constraints to Facts

Expressing LHS conditions

The DSL facility allows you to add constraints to a pattern by a simple convention: if your DSL expression starts with a hyphen (minus character, -) it is assumed to be a field constraint and, consequently, is is added to the last pattern line preceding it.

In the example, the class Cheese, has these fields: type, price, age, and country. You can express some LHS condition in normal DRL.

Cheese(age < 5, price == 20, type=="stilton", country=="ch")
DSL definitions

The DSL definitions given in this example result in three DSL phrases which may be used to create any combination of constraint involving these fields.

[when]There is a Cheese with=Cheese()
[when]- age is less than {age}=age<{age}
[when]- type is '{type}'=type=='{type}'
[when]- country equal to '{country}'=country=='{country}'
-

The parser will pick up a line beginning with - and add it as a constraint to the preceding pattern, inserting a comma when it is required.

There is a Cheese with
  - age is less than 42
  - type is 'stilton'
Cheese(age<42, type=='stilton')
Defining DSL phrases

Defining DSL phrases for various operators and even a generic expression that handles any field constraint reduces the amount of DSL entries.

[when][]is less than or equal to=<=
[when][]is less than=<
[when][]is greater than or equal to=>=
[when][]is greater than=>
[when][]is equal to===
[when][]equals===
[when][]There is a Cheese with=Cheese()
DSL definition rule
There is a Cheese with
  - age is less than 42
  - rating is greater than 50
  - type equals 'stilton'

In this specific case, a phrase such as "is less than" is replaced by <, and then the line matches the last DSL entry. This removes the hyphen, but the final result is still added as a constraint to the preceding pattern. After processing all of the lines, the resulting DRL text is:

Cheese(age<42, rating > 50, type=='stilton')
Note

The order of the entries in the DSL is important if separate DSL expressions are intended to match the same line, one after the other.

8.14.9. Tips for Developing DSLs

  • Write representative samples of the rules your application requires and test them as you develop.
  • Rules, both in DRL and in DSLR, refer to entities according to the data model representing the application data that should be subject to the reasoning process defined in rules.
  • Writing rules is easier if most of the data model’s types are facts.
  • Mark variable parts as parameters. This provides reliable leads for useful DSL entries.
  • You may postpone implementation decisions concerning conditions and actions during this first design phase by leaving certain conditional elements and actions in their DRL form by prefixing a line with a greater sign (">"). (This is also handy for inserting debugging statements.)
  • New rules can be written by reusing the existing DSL definitions, or by adding a parameter to an existing condition or consequence entry.
  • Keep the number of DSL entries small. Using parameters lets you apply the same DSL sentence for similar rule patterns or constraints.

8.14.10. DSL and DSLR Reference

A DSL file is a text file in a line-oriented format. Its entries are used for transforming a DSLR file into a file according to DRL syntax:

  • A line starting with # or // (with or without preceding white space) is treated as a comment. A comment line starting with #/ is scanned for words requesting a debug option, see below.
  • Any line starting with an opening bracket ([) is assumed to be the first line of a DSL entry definition.
  • Any other line is appended to the preceding DSL entry definition, with the line end replaced by a space.

8.14.11. DSL Entry Description

A DSL entry consists of the following four parts:

  1. A scope definition, written as one of the keywords when or condition, then or consequence, * and keyword, enclosed in brackets ([ and ]). This indicates whether the DSL entry is valid for the condition or the consequence of a rule, or both. A scope indication of keyword means that the entry has global significance, that is, it is recognized anywhere in a DSLR file.
  2. A type definition, written as a Java class name, enclosed in brackets. This part is optional unless the next part begins with an opening bracket. An empty pair of brackets is valid, too.
  3. A DSL expression consists of a (Java) regular expression, with any number of embedded variable definitions, terminated by an equal sign (=). A variable definition is enclosed in braces ({ and }). It consists of a variable name and two optional attachments, separated by colons (:). If there is one attachment, it is a regular expression for matching text that is to be assigned to the variable. If there are two attachments, the first one is a hint for the GUI editor and the second one the regular expression.

    Note that all characters that are "magic" in regular expressions must be escaped with a preceding backslash (\) if they should occur literally within the expression.

  4. The remaining part of the line after the delimiting equal sign is the replacement text for any DSLR text matching the regular expression. It may contain variable references, for example a variable name enclosed in braces. Optionally, the variable name may be followed by an exclamation mark (!) and a transformation function, see below.

    Note that braces ({ and }) must be escaped with a preceding backslash (\) if they should occur literally within the replacement string.

8.14.12. Debug Options for DSL Expansion

Table 8.4. Debug Options for DSL Expansion

WordDescription

result

Prints the resulting DRL text, with line numbers.

steps

Prints each expansion step of condition and consequence lines.

keyword

Dumps the internal representation of all DSL entries with scope keyword.

when

Dumps the internal representation of all DSL entries with scope when or *.

then

Dumps the internal representation of all DSL entries with scope then or *.

usage

Displays a usage statistic of all DSL entries.

8.14.13. DSL Definition Example

This is what a DSL definition looks like:

# Comment: DSL examples

#/ debug: display result and usage

# keyword definition: replaces "regula" by "rule"
[keyword][]regula=rule

# conditional element: "T" or "t", "a" or "an", convert matched word
[when][][Tt]here is an? {entity:\w+}=${entity!lc}: {entity!ucfirst} ()

# consequence statement: convert matched word, literal braces
[then][]update {entity:\w+}=modify(${entity!lc})\{ \}

8.14.14. Transformation of DSLR File

The transformation of a DSLR file proceeds as follows:

  1. The text is read into memory.
  2. Each of the keyword entries is applied to the entire text. The regular expression from the keyword definition is modified by replacing white space sequences with a pattern matching any number of white space characters, and by replacing variable definitions with a capture made from the regular expression provided with the definition, or with the default (.*?). Then, the DSLR text is searched exhaustively for occurrences of strings matching the modified regular expression. Substrings of a matching string corresponding to variable captures are extracted and replace variable references in the corresponding replacement text, and this text replaces the matching string in the DSLR text.
  3. Sections of the DSLR text between when and then, and then and end, respectively, are located and processed in a uniform manner, line by line, as described below.

    For a line, each DSL entry pertaining to the line’s section is taken in turn, in the order it appears in the DSL file. Its regular expression part is modified: white space is replaced by a pattern matching any number of white space characters; variable definitions with a regular expression are replaced by a capture with this regular expression, its default being .*?. If the resulting regular expression matches all or part of the line, the matched part is replaced by the suitably modified replacement text.

    Modification of the replacement text is done by replacing variable references with the text corresponding to the regular expression capture. This text may be modified according to the string transformation function given in the variable reference; see below for details.

    If there is a variable reference naming a variable that is not defined in the same entry, the expander substitutes a value bound to a variable of that name, provided it was defined in one of the preceding lines of the current rule.

  4. If a DSLR line in a condition is written with a leading hyphen, the expanded result is inserted into the last line, which should contain a pattern CE, that is, a type name followed by a pair of parentheses. if this pair is empty, the expanded line (which should contain a valid constraint) is simply inserted, otherwise a comma (,) is inserted beforehand.

    If a DSLR line in a consequence is written with a leading hyphen, the expanded result is inserted into the last line, which should contain a modify statement, ending in a pair of braces ({ and }). If this pair is empty, the expanded line (which should contain a valid method call) is simply inserted, otherwise a comma (,) is inserted beforehand.

Note

It is currently not possible to use a line with a leading hyphen to insert text into other conditional element forms (for example accumulate) or it may only work for the first insertion (for example eval).

8.14.15. String Transformation Functions

Table 8.5. String Transformation Functions

NameDescription

uc

Converts all letters to upper case.

lc

Converts all letters to lower case.

ucfirst

Converts the first letter to upper case, and all other letters to lower case.

num

Extracts all digits and - from the string. If the last two digits in the original string are preceded by . or ,, a decimal period is inserted in the corresponding position.

a?b/c

Compares the string with string a, and if they are equal, replaces it with b, otherwise with c. But c can be another triplet a, b, c, so that the entire structure is, in fact, a translation table.

8.14.16. Stringing DSL Transformation Functions

.dsl

A file containing a DSL definition is customarily given the extension .dsl. It is passed to the Knowledge Builder with ResourceType.DSL. For a file using DSL definition, the extension .dslr should be used. The Knowledge Builder expects ResourceType.DSLR. The IDE, however, relies on file extensions to correctly recognize and work with your rules file.

# definitions for conditions
[when][]There is an? {entity}=${entity!lc}: {entity!ucfirst}()
[when][]- with an? {attr} greater than {amount}={attr} <= {amount!num}
[when][]- with a {what} {attr}={attr} {what!positive?>0/negative?%lt;0/zero?==0/ERROR}
DSL passing

The DSL must be passed to the Knowledge Builder ahead of any rules file using the DSL.

For parsing and expanding a DSLR file the DSL configuration is read and supplied to the parser. Thus, the parser can "recognize" the DSL expressions and transform them into native rule language expressions.

KnowledgeBuilder kBuilder = new KnowledgeBuilder();
Resource dsl = ResourceFactory.newClassPathResource(dslPath, getClass());
kBuilder.add(dsl, ResourceType.DSL);
Resource dslr = ResourceFactory.newClassPathResource(dslrPath, getClass());
kBuilder.add(dslr, ResourceType.DSLR);