Liquibase Enterprise was formerly known as Datical DB.
Rule Syntax and Construction
Anatomy of a Rule
Package <namespace for the rule>; rule "<name>" <attribute> when <conditional element> then <action> end
- package - The package statement defines the namespace for the rule.
- rule - The rule name must be unique within a given package. This is the name that will be printed out when the rule executes. So, you'll want to make sure it's descriptive.
- when - This is the expression for the rule. Even though you can create complex expressions in the when clause, you should ensure that it codifies only one rule.
- then - This is where your code prepares and saves the response that will be displayed to the user.
- end - This statement ends a rule.
Comments
Note: Comments do not work inside of a "for loop".
Single Line Comments
To create single line comments, you can use '//'. The parser will ignore anything in the line after the comment symbol.
Example:
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
Multi Line Comments
Multi-line comments are used to comment blocks of text, both in and outside semantic code blocks.
Example:
rule "Test Multi-line Comments" when /* this is a multi-line comment* in the left hand side of a rule */ eval( true ) then /* this is another comment */ end
Rule Package
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 - perhaps HR rules, for instance. 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). Although, 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).
The following railroad diagram shows all the components that may make up a package. Note that a package must have a namespace and be declared using standard Java conventions for package names; i.e., no spaces, unlike rule names which allow spaces. In terms of the order of elements, they can appear in any order in the rule file, with the exception of the package statement, which must be at the top of the file. In all cases, the semicolons are optional.
Notice that any rule attribute (as described the section Rule Attributes) may also be written at package level, superseding the attribute's default value. The modified default may still be replaced by an attribute setting within a rule.
Importing Classes For Use in a Rule
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. Drools automatically imports classes from the Java package of the same name, and also from the package java.lang.
Rule Functions
Functions are a way to put semantic code in your rule source file, as opposed to in normal Java classes. They can't do anything more than what you can do with helper classes. (In fact, the compiler generates the helper class for you behind the scenes.) The main advantage of using functions in a rule is that you can keep the logic all in one place, and you can change the functions as needed (which can be a good or a bad thing).
Functions are most useful for invoking actions on the consequence (then) part of a rule, especially if that particular action is used over and over again, perhaps with only differing parameters for each rule.
A typical function declaration looks like:
function String hello(String name) { return "Hello "name"!"; }
Note that the function keyword is used, even though it's not really part of Java. Parameters to the function are defined as for a method, and you don't have to have parameters if they are not needed. The return type is defined just like in a regular method.
Alternatively, you could use a static method in a helper class, e.g., Foo.hello(). Drools supports the use of function imports, so all you would need to do is:
import function my.package.Foo.hello
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. Example:
rule "using a static function" when eval( true ) then System.out.println( hello( "Bob" ) ); end
Rule Attributes
Rule attributes provide a declarative way to influence the behavior of the rule. Some are quite simple, while others are part of complex subsystems such as ruleflow. To get the most from Drools you should make sure you have a proper understanding of each attribute.
no-loop
default value: false
type: 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. Datical DB does not allow for the modification of facts within a rule.
ruleflow-group
default value: N/A
type: String
Ruleflow is a Drools 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.
lock-on-active
default value: false
type: 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; irrespective 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 could now be caused not only by the rule itself. It's ideal for calculation rules where you have a number of rules that modify a fact and you don't 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.
salience
default value: 0
type: 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.
Drools also supports dynamic salience where you can use an expression involving bound variables.
rule "Fire in rank order 1,2,.." salience( -$rank ) when Element( $rank : rank,... ) then ... end
agenda-group
default value: MAIN
type: String
Agenda groups allow the user to partition the Agenda providing more execution control. Only rules in the agenda group that has acquired the focus are allowed to fire.
auto-focus
default value: false
type: Boolean
When a rule is activated where the auto-focus value is true and the rule's agenda group does not have focus yet, then it is given focus, allowing the rule to potentially fire.
activation-group
default value: N/A
type: 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, i.e., stop them from firing.
Note: This used to be called Xor group, but technically it's not quite an Xor. You may still hear people mention Xor group; just swap that term in your mind with activation-group.
dialect
default value: as specified by the package type: String
possible values: "java" or "mvel"
The dialect species the language to be used for any code expressions in the LHS or the RHS code block. Currently two dialects are available, Java and MVEL. While the dialect can be specified at the package level, this attribute allows the package definition to be overridden for a rule.
date-effective
default value: N/A
type: String, containing a date and time definition
A rule can only activate if the current date and time is after date-effective attribute.
date-expires
default value: N/A
type: String, containing a date and time definition
A rule cannot activate if the current date and time is after the date-expires attribute.
duration
default value: no default value
type: long
The duration dictates that the rule will fire after a specified duration, if it is still true.
rule "my rule" salience 42 agenda-group "number 1" when ...
Left Hand Side (when) syntax
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.
The following is an example of a rule without a conditional element.
rule "no CEs" when // empty then ... // actions (executed once) end // The above rule is internally rewritten as: rule "no CEs" when eval( true ) then ... // actions (executed once) end
Conditional elements work on one or more patterns (which are described below). The most common conditional element is "and". Therefore it is implicit when you have multiple patterns in the LHS of a rule that are not connected in any way:
rule "2 unconnected patterns" when Pattern1() Pattern2() then ... // actions end // The above rule is internally rewritten as: rule "2 AND connected patterns" when Pattern1() and Pattern2() then ... // actions end
Note
An "and" cannot have a leading declaration binding (unlike, for example, or). This is obvious, since a declaration can only reference a single fact at a time, and when the "and" is satisfied it matches both facts - so which fact would the declaration bind to?
// Compile error $person : (Person( name == "Romeo" ) and Person( name == "Juliet"))
Pattern (conditional element)
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 of zero or more constraints and has an optional pattern binding. The railroad diagram below shows the syntax for this.
In its simplest form, with no constraints, a pattern matches against a fact of the given type. In the following case the type is Cheese, which means that the pattern will match against all Person objects in the Working Memory:
Person()
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.
Object() // matches all objects in the working memory
Inside of the pattern parenthesis is where all the action happens: it defines the constraints for that pattern. For example, with a age related constraint:
Person( age == 100 )
Note
For backwards compatibility reasons it's allowed to suffix patterns with the ";" character. But it is not recommended to do that.
Pattern binding
For referring to the matched object, use a pattern binding variable such as $p.
Pattern with a binding variable
rule ... when $p : Person() then System.out.println( "Person " + $p ); end
The prefixed dollar symbol ($) is just a convention; it can be useful in complex rules where it helps to easily differentiate between variables and fields, but it is not mandatory.
Constraint (part of a pattern)
A constraint is an expression that returns true or false. This example has a constraint that states 5 is smaller than 6:
Person( 5 < 6 ) // just an example - constraints like this would be useless in a real pattern
In essence, it's a Java expression with some enhancements (such as property access) and a few differences (such as equals() semantics for ==). Let's take a deeper look.
Property access on Java Beans (POJO's)
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. For example: the age property is written as age in DRL instead of the getter getAge():
Person( age == 50 ) // this is the same as: Person( getAge() == 50 )
Drools uses the standard JDK Introspector class to do this mapping, so it follows the standard Java bean specification.
Use property access (age) over using getters explicitly (getAge()) because of performance enhancements through field indexing.
Property accessors must not change the state of the object in a way that may affect the rules. Remember that the rule engine effectively caches the results of its matching in between invocations to make it faster.
public int getAge() { age++; // Do NOT do this return age; }
Nested property access is supported:
Person( address.houseNumber == 50 ) // this is the same as: Person( getAddress().getHouseNumber() == 50 )
Java expression
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 )
It is possible to change the evaluation priority by using parentheses, as in any logic or mathematical expression:
Person( age > 100 && ( age % 10 == 0 ) )
It is possible to reuse Java methods:
Person( Math.round( weight / ( height * height ) ) < 25.0 )
Warnings
As for property accessors, 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.
Person( incrementAndGetAge() == 10 ) // Do NOT do this
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
Normal Java operator precedence applies; see the operator precedence list below.
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" )
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. For instance, if "ten" is provided as a string in a numeric evaluator, an exception is thrown, whereas "10" would coerce to a numeric 10. Coercion is always in favor of the field type and not the value type:
Person( age == "10" ) // "10" is coerced to 10
Comma separated AND
The comma character (',') is used to separate constraint groups. It has 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
Although the && and , operators have the same semantics, they are resolved with different priorities: The && operator precedes the || operator. Both the && and || operator precede the , operator. See the operator precedence list below.
The comma operator should be preferred at the top level constraint, as it makes constraints easier to read and the engine will often be able to optimize them better.
The comma (,) operator cannot be embedded in a composite constraint expression, such as parentheses:
Person( ( age > 50, weight > 80 ) || height > 2 ) // Do NOT do this: compile error // Use this instead Person( ( age > 50 && weight > 80 ) || height > 2 )
Binding variables
A property can be bound to a variable:
// 2 persons of the same age Person( $firstAge : age ) // binding Person( age == $firstAge ) // constraint expression
The prefixed dollar symbol ($) is just a convention; it can be useful in complex rules where it helps to easily differentiate between variables and fields.
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 )
Bound variable restrictions using the operator == provide for very fast execution as it use hash indexing to improve performance.
Unification
Drools does not allow bindings to the same declaration. However this is an important aspect to derivation query unification. While positional arguments are always processed with unification a special unification symbol, ':=', was introduced for named arguments named arguments. The following "unifies" the age argument across two people.
Person( $age := age ) Person( $age := age)
In essence unification will declare a binding for the first occurrence and constrain to the same value of the bound field for sequence occurrences.
Grouped accessors for nested objects
Often it happens that it is necessary to access multiple properties of a nested object as in the following example
Person( name == "mark", address.city == "london", address.country == "uk" )
These accessors to nested objects can be grouped with a '.(...)' syntax providing more readable rules as in
Person( name == "mark", address.( city == "london", country =="uk") )
Note the '.' prefix, this is necessary to differentiate the nested object constraints from a method call.
Inline casts and coercion
When dealing with nested objects, it also quite common the need to cast to a subtype. It is possible to do that via the # symbol as in:
Person( name=="mark", address#LongAddress.country == "uk" )
This example casts Address to LongAddress, making its getters available. If the cast is not possible (instanceof returns false), the evaluation will be considered false. Also fully qualified names are supported:
Person( name=="mark", address#org.domain.LongAddress.country == "uk" )
It is possible to use multiple inline casts in the same expression:
Person( name == "mark", address#LongAddress.country#DetailedCountry.population > 10000000 )
Moreover, since we also support the instanceof operator, if that is used we will infer its results for further uses of that field, within that pattern:
Person( name=="mark", address instanceof LongAddress, address.country == "uk" )
Special literal support
Besides normal Java literals (including Java 5 enums), this literal is also supported:
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
It's possible to directly access a List value by index:
// Same as childList(0).getAge() == 18 Person( childList[0].age == 18 )
It's also possible to 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.
Abbreviated combined relation condition
Abbreviated combined relation condition with parentheses
// 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" )
Special DRL operators
Coercion to the correct value for the evaluator and the field will be attempted.
The operators < <= > >=
These 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 )
Only applies on Comparable properties.
Null-safe dereferencing operator
The !. operator allows derefencing in a null-safe way. More in details the matching algorithm requires the value to the left of the !. operator to be not null in order to give a positive result for pattern matching itself. In other words the pattern:
Person( $streetName : address!.street )
will be internally translated in:
Person( address != null, $streetName : address.street )
The 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.
Cheese( type matches "(Buffalo)?\\S*Mozarella" )
Note: Like in Java, regular expressions written as string literals need to escape '\'.
Only applies on String properties. Using matches against a null value always evaluates to false.
The 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.
Example:
Cheese( type not matches "(Buffulo)?\\S*Mozarella" )
Only applies on String properties. Using not matches against a null value always evaluates to true.
The operator contains
The operator contains is used to check whether a field that is a Collection or elements contains the specified value.
CheeseCounter( cheeses contains "stilton" ) // contains with a String literal
CheeseCounter( cheeses contains $var ) // contains with a variable
Only applies on Collection properties.
The operator not contains
The operator not contains is used to check whether a field that is a Collection or elements does not contain the specified value.
CheeseCounter( cheeses not contains "cheddar" ) // not contains with a String literal
CheeseCounter( cheeses not contains $var ) // not contains with a variable
Only applies on Collection properties.
Note
For backward compatibility, the excludes operator is supported as a synonym for not contains.
The operator memberOf
The operator memberOf is used to check whether a field is a member of a collection or elements; 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 elements; 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. This is based on the Soundex algorithm (see http://en.wikipedia.org/wiki/Soundex).
// match cheese "fubar" or "foobar" Cheese( name soundslike 'foobar' )
The operator str
This 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 )
The operators in and not in (compound value restriction)
The 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 ) )
Operator precedence
Operator type | Operators | Notes |
(nested / null safe) 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 |
Basic conditional elements
Conditional Element and
The Conditional Element "and" is used to group other Conditional Elements into a logical conjunction. Drools supports both prefix and and infix and.
Infix and
Traditional infix and is supported:
//infixAnd Cheese( cheeseType : type ) and Person( favouriteCheese == cheeseType )
Explicit grouping with parentheses is also supported:
//infixAnd with grouping ( Cheese( cheeseType : type ) and ( Person( favouriteCheese == cheeseType ) or Person( favouriteCheese == cheeseType ) )
Note
The symbol && (as an alternative to and) is deprecated. But it is still supported in the syntax for backwards compatibility.
Prefix and
Prefix and is also supported:
Conditional Element or
The Conditional Element or is used to group other Conditional Elements into a logical disjunction. Drools supports both prefix or and infix or.
Infix or
Traditional infix or is supported:
//infixOr Cheese( cheeseType : type ) or Person( favouriteCheese == cheeseType )
Explicit grouping with parentheses is also supported:
//infixOr with grouping ( Cheese( cheeseType : type ) or ( Person( favouriteCheese == cheeseType ) and Person( favouriteCheese == cheeseType ) )
Note
The symbol || (as an alternative to or) is deprecated. But it is still supported in the syntax for backwards compatibility.
Prefix or
Prefix or is also supported:
(or Person( sex == "f", age > 60 ) Person( sex == "m", age > 65 )
Note
The behavior of the Conditional Element or is different from the connective || for constraints and restrictions in field constraints. The engine actually has no understanding of the Conditional Element or. Instead, via a number of different logic transformations, 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. - This can be most confusing to new rule authors.
The Conditional Element or also allows for optional pattern binding. This means that each resulting subrule will bind its pattern to the pattern binding. Each pattern must be bound separately, using eponymous variables:
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 ) )
Since the conditional element or results in multiple subrule generation, one for each possible logically outcome, the example above would result in the internal generation of two rules. These two rules work independently within the Working Memory, which means both can match, activate and fire - there is no shortcutting.
The best way to think of the conditional element or is as a shortcut for generating two or more similar rules. When you think of it that way, it's clear that for a single rule there could be multiple activations if two or more terms of the disjunction are true.
Conditional Element not
The CE not is first order logic's non-existential quantifier and checks for the non-existence of something in the Working Memory. Think of "not" as meaning "there must be none of...".
The keyword not may be followed by parentheses around the CEs that it applies to. In the simplest case of a single pattern (like below) you may optionally omit the parentheses.
No Buses
not Bus()
No Red Buses
// 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 required: not ( Bus(color == "red") and Bus(color == "blue") )
Conditional Element exists
The CE exists is first order logic's existential quantifier and checks for the existence of something in the Working Memory. Think of "exists" as meaning "there is at least one..". It is different from just having the pattern on its own, which is more like saying "for each one of...". If you use exists with a pattern, the rule will only activate at most once, regardless of how much data there is in working memory that matches the condition inside of the exists pattern. Since only the existence matters, no bindings will be established.
The keyword exists must be followed by parentheses around the CEs that it applies to. In the simplest case of a single pattern (like below) you may omit the parentheses.
At least one Bus
exists Bus()
At least one red Bus
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") )
Advanced conditional elements
Conditional Element forall
The Conditional Element forall completes the First Order Logic support in Drools. The Conditional Element forall evaluates to true when all facts that match the first pattern match all the remaining patterns. Example:
rule "All English buses are red" when forall( $bus : Bus( type == 'english') Bus( this == $bus, color = 'red' ) ) then // all English buses are red end
In the above rule, we "select" all Bus objects whose type is "english". Then, for each fact that matches this pattern we evaluate the following patterns and if they match, the forall CE will evaluate to true.
To state that all facts of a given type in the working memory must match a set of constraints, forall can be written with a single pattern for simplicity. Example:
Single Pattern Forall
rule "All Buses are Red" when forall( Bus( color == 'red' ) ) then // all Bus facts are red end
Another example shows multiple patterns inside the forall:
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
Forall can be nested inside other CEs. For instance, forall can be used inside a not CE. Note that only single patterns have optional parentheses, so that with a nested forall parentheses must be used:
Combining Forall with Not CE
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
As a side note, forall( p1 p2 p3...)
is equivalent to writing:
not(p1 and not(and p2 p3...))
Also, it is important to note that forall 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.
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.
Here is a simple example of reasoning and binding on another pattern sub-field:
rule "validate zipcode" when Person( $personAddress : address ) Address( zipcode == "23920W") from $personAddress then // zip code is ok end
With all the flexibility from the new expressiveness in the Drools engine you can slice and dice this problem many ways. This is the same but shows how you can use a graph notation with the 'from':
rule "validate zipcode" when $p : Person( ) $a : Address( zipcode == "23920W") from $p.address then // zip code is ok end
Previous examples were evaluations using a single pattern. The CE from also support object sources that return a collection of objects. In that case, from will iterate over all objects in the collection and try to match each of them individually. For instance, if we want a rule that applies 10% discount to each item in an order, we could do:
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
The above example will cause the rule to fire once for each item whose value is greater than 100 for each given order.
You must take caution, however, when using from, especially in conjunction with the lock-on-active rule attribute as it may produce unexpected results. Consider the example provided earlier, but now slightly modified as follows:
rule "Assign people in North Carolina (NC) to sales region 1" ruleflow-group "test" lock-on-active true when $p : Person( ) $a : Address( state == "NC") from $p.address 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( ) $a : Address( city == "Raleigh") from $p.address then modify ($p) {} // Apply discount to person in a modify block end
In the above example, persons in Raleigh, NC should be assigned to sales region 1 and receive a discount; i.e., you would expect both rules to activate and fire. Instead you will find that only the second rule fires.
If you were to turn on the audit log, you would also see that when the second rule fires, it deactivates the first rule. Since the rule attribute lock-on-active prevents a rule from creating new activations when a set of facts change, the first rule fails to reactivate. Though the set of facts have not changed, the use of from returns a new fact for all intents and purposes each time it is evaluated.
First, it's important to review why you would use the above pattern. You may have many rules across different rule-flow groups. When rules modify working memory and other rules downstream of your RuleFlow (in different rule-flow groups) need to be reevaluated, the use of modify is critical. You don't, however, want other rules in the same rule-flow group to place activations on one another recursively. In this case, the no-loop attribute is ineffective, as it would only prevent a rule from activating itself recursively. Hence, you resort to lock-on-active.
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 (explained below).
The preferred solution is to minimize use of from when you can assert all your facts into working memory directly. In the example above, both the Person and Address instance can be asserted into working memory. In this case, because the graph is fairly simple, an even easier solution is to modify your rules as follows:
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
Now, you will find that both rules fire as expected. However, it is not always possible to access nested facts as above. Consider an example where a Person holds one or more Addresses and you wish to use an existential quantifier to match people with at least one address that meets certain conditions. In this case, you would have to resort to the use of from to reason over the collection.
There are several ways to use from to achieve this and not all of them exhibit an issue with the use of
lock-on-active. For example, the following use of from causes both rules to fire as expected:
rule "Assign people in North Carolina (NC) to sales region 1" ruleflow-group "test" lock-on-active true when $p : Person($addresses : addresses) exists (Address(state == "NC") from $addresses) 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($addresses : addresses) exists (Address(city == "Raleigh") from $addresses) then modify ($p) {} // Apply discount to person in a modify block end
However, the following slightly different approach does exhibit the problem:
rule "Assign people in North Carolina (NC) to sales region 1" ruleflow-group "test" lock-on-active true when $assessment : Assessment() $p : Person() $addresses : List() from $p.addresses exists (Address( state == "NC") from $addresses) then modify ($assessment) {} // Modify assessment in a modify block end rule "Apply a discount to people in the city of Raleigh" ruleflow-group "test" lock-on-active true when $assessment : Assessment() $p : Person() $addresses : List() from $p.addresses exists (Address( city == "Raleigh") from $addresses) then modify ($assessment) {} // Modify assessment in a modify block end
In the above example, the $addresses variable is returned from the use of from. The example also introduces a new object, assessment, to highlight one possible solution in this case. If the $assessment variable assigned in the condition (LHS) is moved to the last condition in each rule, both rules fire as expected.
Though the above examples demonstrate how to combine the use of from with lock-on-active where no loss of rule activations occurs, they carry the drawback of placing a dependency on the order of conditions on the LHS. In addition, the solutions present greater complexity for the rule author in terms of keeping track of which conditions may create issues.
A better alternative is to assert more facts into working memory. In this case, a person's addresses may be asserted into working memory and the use of from would not be necessary.
There are cases, however, where asserting all data into working memory is not practical and we need to find other solutions. Another option is to reevaluate the need for lock-on-active. An alternative to lock-on-active is to directly manage how rules within the same rule-flow group activate one another by including conditions in each rule that prevent rules from activating each other recursively when working memory is modified. For example, in the case above where a discount is applied to citizens of Raleigh, a condition may be added to the rule that checks whether the discount has already been applied. If so, the rule does not activate.
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 Order Logic terms this is the cardinality quantifier. A simple example:
import java.util.ArrayList rule "Raise priority if system has more than 3 pending alarms" when $system : System() $alarms : ArrayList( size >= 3 ) from collect(Alarm(system == $system, status == 'pending')) then // Raise priority, because system $system has // 3 or more alarms pending. The pending alarms // are $alarms. end
In the above example, the rule will look for all pending alarms in the working memory for each given system and group them in ArrayLists. If 3 or more alarms are found for a given system, the rule will fire.
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. This means that you can use Java collections like ArrayList, LinkedList, HashSet, etc., or your own class, as long as it implements the java.util.Collection interface and provide a default no-arg public constructor.
Both source and result patterns can be constrained as any other pattern.
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. But note that collect is a scope delimiter for bindings, so that any binding made inside of it is not available for use outside of it.
Collect accepts nested from CEs. The following example is a valid use of "collect":
import java.util.LinkedList; rule "Send a message to all mothers" when $town : Town( name == 'Paris' ) $mothers : LinkedList() from collect( Person( gender == 'F', children > 0 ) from $town.getPeople() ) then // send a message to all mothers end
Conditional Element accumulate
The Conditional Element accumulate is a more flexible and powerful form of collect, in the sense that it can be used to do what collect does and also achieve results that the CE collect is not capable of achieving.
Accumulate allows a rule to iterate over a collection of objects, executing custom actions for each of the elements, and at the end, it returns a result object.
Accumulate supports both the use of pre-defined accumulate functions, or the use of inline custom code. Inline custom code should be avoided though, as it is harder for rule authors to maintain, and frequently leads to code duplication. Accumulate functions are easier to test and reuse.
The Accumulate CE also supports multiple different syntaxes. The preferred syntax is the top level accumulate, as noted bellow, but all other syntaxes are supported for backward compatibility.
Accumulate CE (preferred syntax)
The top level accumulate syntax is the most compact and flexible syntax. The simplified syntax is as follows:
accumulate( <source pattern>; <functions> [;<constraints>] )
For instance, a rule to calculate the minimum, maximum and average temperature reading for a given sensor and that raises an alarm if the minimum temperature is under 20C degrees and the average is over 70C degrees could be written in the following way, using Accumulate:
Note
The DRL language defines "acc" as a synonym of "accumulate". The author might prefer to use "acc" as a less verbose keyword or the full keyword "accumulate" for legibility.
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 above example, min, max and average are Accumulate Functions and will calculate the minimum, maximum and average temperature values over all the readings for each sensor.
Drools ships with several built-in accumulate functions, including:
- average
- min
- max
- count
- sum
- collectList
- collectSet
These common functions accept any expression as input. For instance, if someone wants to calculate the average profit on all items of an order, a rule could be written using the average function:
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 are all pluggable. That means that if needed, custom, domain specific functions can easily be added to the engine and rules can start to use them without any restrictions. To implement a new Accumulate Function all one needs to do is to create a Java class that implements the org.drools.core.runtime.rule.TypedAccumulateFunction interface. As an example of an Accumulate Function implementation, the following is the implementation of the average function:
/** An implementation of an accumulator capable of calculating average values */ public class AverageAccumulateFunction implements org.drools.core.runtime.rule.TypedAccumulateFunction { 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.drools.base.accumulators.AccumulateFunction#createContext() */ public Serializable createContext() { return new AverageData(); } /* (non-Javadoc) * @see org.drools.core.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.drools.core.base.accumulators.AccumulateFunction#accumulate(java.lang.Object, ja */ public void accumulate(Serializable context, Object value) { AverageData data = (AverageData) context; data.count++; data.total += ((Number) value).doubleValue(); } /* (non-Javadoc) * @see org.drools.core.base.accumulators.AccumulateFunction#reverse(java.lang.Object, java. */ public void reverse(Serializable context, Object value) throws Exception { AverageData data = (AverageData) context; data.count--; data.total -= ((Number) value).doubleValue(); } /* (non-Javadoc) * @see org.drools.core.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.drools.core.base.accumulators.AccumulateFunction#supportsReverse() */ public boolean supportsReverse() { return true; } /** {@inheritDoc} */ public Class< ? > getResultType() { return Number.class; } }
The code for the function is very simple, as we could expect, as all the "dirty" integration work is done by the engine. Finally, to use the function in the rules, the author can import it using the "import accumulate" statement:
import accumulate <class_name> <function_name>
For instance, if one implements the class some.package.VarianceFunction function that implements the variance function and wants to use it in the rules, he would do the following:
Example 7.77. Example of importing and using the custom "variance" accumulate function
import accumulate some.package.VarianceFunction variance rule "Calculate Variance" when accumulate( Test( $s : score ), $v : variance( $s ) ) then // the variance of the test scores is $v end
Note
The built in functions (sum, average, etc) are imported automatically by the engine. Only user- defined custom accumulate functions need to be explicitly imported.
Note
For backward compatibility, Drools still supports the configuration of accumulate functions through configuration files and system properties, but this is a deprecated method. In order to configure the variance function from the previous example using the configuration file or system property, the user would set a property like this:
drools.accumulate.function.variance = some.package.VarianceFunction
Please note that "drools.accumulate.function." is a prefix that must always be used, "variance" is how the function will be used in the drl files, and "some.package.VarianceFunction" is the fully qualified name of the class that implements the function behavior.
Alternate Syntax: single function with return type
The accumulate syntax evolved over time with the goal of becoming more compact and expressive. Nevertheless, Drools still supports previous syntaxes for backward compatibility purposes.
In case the rule is using a single accumulate function on a given accumulate, the author may add a pattern for the result object and use the "from" keyword to link it to the accumulate result. Example: a rule to apply a 10% discount on orders over $100 could be written in the following way:
rule "Apply 10% discount to orders over US$ 100,00" when $order : Order() $total : Number( doubleValue > 100 ) from accumulate( OrderItem( order == $order, $value : value ), sum( $value ) ) then # apply discount to $order end
In the above example, the accumulate element is using only one function (sum), and so, the rules author opted to explicitly write a pattern for the result type of the accumulate function (Number) and write the constraints inside it. There are no problems in using this syntax over the compact syntax presented before, except that is is a bit more verbose. Also note that it is not allowed to use both the return type and the functions binding in the same accumulate statement.
Accumulate with inline custom code
Warning
The use of accumulate with inline custom code is not a good practice for several reasons, including difficulties on maintaining and testing rules that use them, as well as the inability of reusing that code. Implementing your own accumulate functions is very simple and straightforward, they are easy to unit test and to use. This form of accumulate is supported for backward compatibility only.
Another possible syntax for the accumulate is to define inline custom code, instead of using accumulate functions. As noted on the previous warned, this is discouraged though for the stated reasons.
The general syntax of the accumulate CE with inline custom code is:
<result pattern> from accumulate( <source pattern>, init( <init code> ), action( <action code> ), reverse( <reverse code> ), result( <result expression> ) )
The meaning of each of the elements is the following:
- <source pattern>: the source pattern is a regular pattern that the engine will try to match against each of the source objects.
- <init code>: this is a semantic block of code in the selected dialect that will be executed once for each tuple, before iterating over the source objects.
- <action code>: this is a semantic block of code in the selected dialect that will be executed for each of the source objects.
- <reverse code>: this is an optional semantic block of code in the selected dialect that if present will be 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 deleted, hugely improving performance of these operations.
- <result expression>: this is a semantic expression in the selected dialect that is executed after all source objects are iterated.
- <result pattern>: this is a regular pattern that the engine tries to match against the object returned from the
- <result expression>. If it matches, the accumulate conditional element evaluates to true and the engine proceeds with the evaluation of the next CE in the rule. If it does not matches, the accumulate CE evaluates to false and the engine stops evaluating CEs for that rule.
It is easier to understand if we look at an example:
rule "Apply 10% discount to 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 to $order end
In the above example, for each Order in the Working Memory, the engine will execute the init code initializing the total variable to zero. Then it will iterate over all OrderItem objects for that order, executing the action for each one (in the example, it will sum the value of all items into the total variable). After iterating over all OrderItem objects, it will return the value corresponding to the result expression (in the above example, the value of variable total). Finally, the engine will try to match the result with the Number pattern, and if the double value is greater than 100, the rule will fire.
The example used Java as the semantic dialect, and as such, note that the usage of the semicolon as statement delimiter is mandatory in the init, action and reverse code blocks. The result is an expression and, as such, it does not admit ';'. If the user uses any other dialect, he must comply to that dialect's specific syntax.
As mentioned before, the reverse code is optional, but it is strongly recommended that the user writes it in order to benefit from the improved performance on update and delete.
The accumulate CE can be used to execute any action on source objects. The following example instantiates and populates a custom object:
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
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.
For folks who are familiar with Drools 2.x lineage, the old Drools parameter and condition tags are equivalent to binding a variable to an appropriate type, and then using it in an eval node.
p1 : Parameter() p2 : Parameter() eval( p1.getList().containsKey( p2.getItem() ) )
p1 : Parameter() p2 : Parameter() // call function isValid in the LHS eval( isValid( p1, p2 ) )
Copyright © Liquibase 2012-2022 - Proprietary and Confidential