Experimental features
This chapter documents experimental features that are part of the Drools projects group. As these features are experimental, they may not be finished or stable enough for common use. All aspects of these features are highly likely to change in the future.
Declarative agenda
The declarative agenda allows to use rules to control which other rules can fire and when. While this will add a lot more overhead than the simple use of salience, the advantage is it is declarative and thus more readable and maintainable and should allow more use cases to be achieved in a simpler fashion.
As this feature is highly experimental and will be subject to change, it is off by default and must be explicitly enabled. It can be activated on a given KieBase by adding the declarativeAgenda="enabled"
attribute in the corresponding kbase
tag of the kmodule.xml file, as is specified in the following example.
<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.drools.org/xsd/kmodule">
<kbase name="DeclarativeKBase" declarativeAgenda="enabled">
<ksession name="KSession"/>
</kbase>
</kmodule>
The basic idea is:
-
All rule’s matches are inserted into working memory as facts, represented as instances of
Match
class. So you can now do pattern matching against aMatch
instance. The rule’s metadata and declarations are available as fields on theMatch
instance. -
You can use the
kcontext.blockMatch(Match match)
call in a rule to block a selected match. Only when that rule becomes false will the match be eligible for firing. If it is already eligible for firing and is later blocked, it will be removed from the agenda until it is unblocked. -
A match may have multiple blockers, so a count is kept. All blockers must become false for the counter to reach zero to enable the
Match
to be eligible for firing. -
kcontext.unblockAllMatches(Match match)
is an over-ride rule that will remove all blockers regardless. -
An internalMatch may also be cancelled using
cancelMatch
call, so it never fires. -
An unblocked
Match
is added to the agenda and obeys normal salience, agenda groups, ruleflow groups etc. definitions. -
The
@Direct
annotation allows a rule to fire as soon as it’s matched. This is supposed to be used for rules that block/unblock matches, it is not desirable for these rules to have side effects that impact else where.
void blockMatch(Match match);
void unblockAllMatches(Match match);
void cancelMatch(Match match);
Here is a basic example that will block all matches from rules that have metadata @department('sales')
. They will stay blocked until the blockerAllSalesRules rule becomes false, i.e. "go2" is retracted.
rule "rule1" @Eager @department('sales')
when
$s: String(this == 'go1')
then
System.out.println("rule1 fired!");
end
rule "rule2" @Eager @department('sales')
when
$s: String(this == 'go1')
then
System.out.println("rule2 fired!");
end
rule "blockerAllSalesRules" @Direct @Eager
when
$s: String(this == 'go2')
$i: Match(department == 'sales')
then
kcontext.blockMatch($i);
end
It is necessary to annotate all the rules that could be potentially blocked by rule with |
This example shows how you can use a property to count the number of active or inactive (already fired) matches.
rule "rule1" @Eager @department('sales')
when
$s: String(this == 'go1')
then
System.out.println("rule1 fired!");
end
rule "rule2" @Eager @department('sales')
when
$s: String(this == 'go1')
then
System.out.println("rule2 fired!");
end
rule "rule3" @Eager @department('sales')
when
$s: String(this == 'go1')
then
System.out.println("rule3 fired!");
end
rule "countActivateInActive" @Direct @Eager
when
$s: String(this == 'go2')
$active: Number(this == 1) from accumulate(
$a: Match(department == 'sales', active == true),
count($a))
$inActive: Number(this == 2) from accumulate(
$a : Match(department == 'sales', active == false),
count($a))
then
kcontext.halt();
end
Traits
The same fact may have multiple dynamic types which do not fit naturally in a class hierarchy. Traits allow to model this very common scenario. A trait is a type that can be applied to (and eventually removed from) an individual object at runtime. To create a trait rather than a traditional bean, one has to declare it explicitly as in the following example:
declare trait GoldenCustomer
// fields will map to getters/setters
code : String
balance : long
discount : int
maxExpense : long
end
At runtime, this declaration results in an interface, which can be used to write patterns, but can not be instantiated directly. In order to apply a trait to an object, we provide the new don
keyword, which can be used as simply as this:
when
$c : Customer()
then
GoldenCustomer gc = don( $c, GoldenCustomer.class );
end
When a trait is applied to a fact, a proxy class is created on the fly (one such class will be generated lazily for each fact/trait class combination). The proxy instance, which wraps the fact and implements the trait interface, is inserted automatically and will possibly activate other rules. An immediate advantage of declaring and using interfaces and getting the implementation proxy is that multiple inheritance hierarchies can be exploited when writing rules. The fact declarations, however, need not implement any of those interfaces statically. To be possible to assign a trait to a fact, the fact type definition must contain the annotation @Traitable
.
import org.drools.core.factmodel.traits.Traitable;
declare Customer
@Traitable
code: String
balance: long
end
The only connection between fact classes and trait interfaces is at the proxy level: a trait is not specifically tied to a fact class. This means that the same trait can be applied to totally different facts. For this reason, the trait does not transparently expose the fields of its fact object. So, when writing a rule using a trait interface, only the fields of the interface will be available, as usual. However, any field in the interface that corresponds to a fact field, will be mapped by the proxy class:
when
$o: OrderItem($p: price, $code: custCode)
$c: GoldenCustomer(code == $code,$a : balance,$d: discount)
then
$c.setBalance($a - $p*$d);
end
In this case, the code
and balance
would be read from the underlying Customer
object. Likewise, the setAccount
will modify the underlying object, preserving a strongly typed access to the data structures. A hard field is a field that must have the same name and type both in the fact class and all trait interfaces. The name is used to establish the mapping: if two fields have the same name, then they must also have the same declared type. The annotation @org.drools.core.factmodel.traits.Alias
allows to relax this restriction. If an @Alias
is provided, its value string will be used to resolve mappings instead of the original field name. @Alias
can be applied both to traits and core beans.
import org.drools.core.factmodel.traits.*;
declare trait GoldenCustomer
balance: long @Alias("org.acme.foo.accountBalance")
end
declare Person
@Traitable
name: String
savings: long @Alias("org.acme.foo.accountBalance")
end
when
GoldenCustomer(balance > 1000) // will react to new Person(2000)
then
end
More work is being done on relaxing this constraint (see the experimental section on "logical" traits later). Now, one might wonder what happens when a fact class does NOT provide the implementation for a field defined in an interface. We call hard fields those trait fields which are also fact fields and thus readily available, while we define soft those fields which are NOT provided by the fact class. Hidden fields, instead, are fields in the fact class not exposed by the interface.
So, while hard field management is intuitive, there remains the problem of soft and hidden fields. Hidden fields are normally only accessible using the fact class directly. However, one can also use the "fields" Map
on a trait interface to access a hidden field. If the field can’t be resolved, null
will be returned. Notice that this feature is likely to change in the future.
when
$sc: GoldenCustomer(fields["age"] > 18) // age is declared by the underlying fact class, but not by GoldenCustomer
then
Soft fields, instead, are stored in a Map-like data structure that is specific to each fact object and referenced by the proxy(es), so that they are effectively shared even when an object has multiple traits assigned.
when
$sc: GoldenCustomer($c: code, // hard getter
$maxExpense: maxExpense > 1000 // soft getter)
then
$sc.setDiscount(...); // soft setter
end
A fact object also holds a reference to all its proxies, so that it is possible to track which traits have been added to an object, using a sort of dynamic "instanceof" operator, which we named isA
. The operator can accept a String
, a class literal or a list of class literals. In the latter case, the constraint is satisfied only if all the specified traits have been assigned.
$sc: GoldenCustomer($maxExpense: maxExpense > 1000,
this isA "SeniorCustomer",
this isA [NationalCustomer.class, OnlineCustomer.class])
Eventually, the business logic may require that a trait is removed from a fact object. For this, we provide two options. The first is a "logical don", which will result in a logical insertion of the proxy resulting from the trait assignment. The Truth Maintenance System will ensure that the trait is removed when its logical support is removed.
then
don($x, // core object
Customer.class, // trait class
true // optional flag for logical insertion)
The second option is the use of the shed
keyword, which causes the removal of any type that is a subtype (or equivalent) of the one passed as an argument.
then
Thing t = shed($x, GoldenCustomer.class)
This operation returns another proxy implementing the org.drools.core.factmodel.traits.Thing
interface, where the getFields()
and getCore()
methods are defined. Internally, in fact, all declared traits are generated to extend this interface (in addition to any others specified). This allows to preserve the wrapper with the soft fields which would otherwise be lost.
A trait and its proxies are also correlated in another way. Whenever a fact object is "modified", its proxies are "modified" automatically as well, to allow trait-based patterns to react to potential changes in hard fields. Likewise, whenever a trait proxy (matched by a trait pattern) is modified, the modification is propagated to the fact class and the other traits. Moreover, whenever a don
operation is performed, the fact object is also modified automatically, to reevaluate any isA
operation which may be triggered.
Potentially, this may result in a high number of modifications, impacting performance (and correctness) heavily. So two solutions are currently implemented. First, whenever a fact object is modified, only the most specific traits (in the sense of inheritance between trait interfaces) are updated and an internal blocking mechanism is in place to ensure that each potentially matching pattern is evaluated once and only once. So, in the following situation:
declare trait GoldenCustomer end
declare trait NationalGoldenCustomer extends GoldenCustomer end
declare trait SeniorGoldenCustomer extends GoldenCustomer end
A modification of an object that is both a GoldenCustomer
, a NationalGoldenCustomer
and a SeniorGoldenCustomer
would cause only the latter two proxies to be actually modified. The first would match any pattern for GoldenCustomer
and NationalGoldenCustomer
, the latter would instead be prevented from rematching GoldenCustomer
, but would be allowed to match SeniorGoldenCustomer
patterns. It is not necessary, instead, to modify the GoldenCustomer
proxy since it is already covered by at least one other more specific trait.
The second method, up to the user, is to mark traits as @PropertyReactive
.
Property reactivity is trait-enabled and takes into account the trait field mappings, so to block unnecessary propagations.
Cascading traits
WARNING : This feature is extremely experimental and subject to changes.
Normally, a hard field must be exposed with its original type by all traits assigned to an object, to prevent situations such as
declare Person
@Traitable
name: String
id: String
end
declare trait Customer
id: String
end
declare trait Patient
id: long // Person can't don Patient, or an exception will be thrown
end
Should a Person
get assigned both Customer
and Patient
traits, the type of the hard field id
would be ambiguous. However, consider the following example, where GoldenCustomers
refer their best friends so that they become Customers
as well:
declare Person
@Traitable(logical = true)
bestFriend: Person
end
declare trait Customer end
declare trait GoldenCustomer extends Customer
refers: Customer @Alias("bestFriend")
end
Aside from the @Alias
, a Person-as-GoldenCustomer’s best friend may be compatible with the requirements of the trait GoldenCustomer
, provided that they are some kind of Customer
themselves. Marking a Person
as "logically traitable" - i.e. adding the annotation @Traitable(logical = true)
- will instruct the Drools rule engine to try and preserve the logical consistency rather than throwing an exception due to a hard field with different type declarations (Person
vs Customer
). The following operations would then work:
Person p1 = new Person();
Person p2 = new Person();
p1.setBestFriend(p2);
...
Customer c2 = don(p2, Customer.class);
...
GoldenCustomer gc1 = don(p1, GoldenCustomer.class);
...
p1.getBestFriend(); // returns p2
gc1.getRefers(); // returns c2, a Customer proxy wrapping p2
Notice that, by the time p1
becomes GoldenCustomer
, p2
must have already become a Customer
themselves, otherwise a runtime exception will be thrown since the very definition of GoldenCustomer
would have been violated.
In some cases, however, one may want to infer, rather than verify, that p2
is a Customer
by virtue that p1
is a GoldenCustomer
. This modality can be enabled by marking Customer
as "logical", using the annotation @org.drools.core.factmodel.traits.Trait(logical = true)
. In this case, should p2
not be a Customer
by the time that p1
becomes a GoldenCustomer
, it will be automatically assigned the trait Customer
to preserve the logical integrity of the system.
Notice that the annotation on the fact class enables the dynamic type management for its fields, whereas the annotation on the traits determines whether they will be enforced as integrity constraints or cascaded dynamically.
import org.drools.factmodel.traits.*;
declare trait Customer
@Trait(logical = true)
end
Impact analysis
The impact analysis feature analyzes the relationships between the rules and generates a graph. When you specify a rule to be changed, the impact analysis feature analyzes the rules that are impacted by the change and renders the rules in the generated graph.
The generated graph supports DOT, SVG, and PNG formats with simple text output.
Using the impact analysis feature
You can find an example usage in ExampleUsageTest.java
under drools-impact-analysis/drools-impact-analysis-itests
-
Configure the following dependency.
<dependency> <groupId>org.drools</groupId> <artifactId>drools-impact-analysis-graph-graphviz</artifactId> <version>${drools.version}</version> </dependency>
-
Create a
KieFileSystem
to store your assets and callKieBuilder.buildAll(ImpactAnalysisProject.class)
method.// set up KieFileSystem ... KieBuilder kieBuilder = KieServices.Factory.get().newKieBuilder(kfs).buildAll(ImpactAnalysisProject.class); ImpactAnalysisKieModule analysisKieModule = (ImpactAnalysisKieModule) kieBuilder.getKieModule(); AnalysisModel analysisModel = analysisKieModule.getAnalysisModel();
You get
AnalysisModel
. -
Convert the
AnalysisModel
toGraph
usingModelToGraphConverter
.ModelToGraphConverter converter = new ModelToGraphConverter(); Graph graph = converter.toGraph(analysisModel);
-
Specify a rule that you plan to change. The
ImpactAnalysisHelper
generates a graph, containing the changed rule and the impacted rules.ImpactAnalysisHelper impactFilter = new ImpactAnalysisHelper(); Graph impactedSubGraph = impactFilter.filterImpactedNodes(graph, "org.drools.impact.analysis.example.PriceCheck_11");
-
Generate a graph image using
GraphImageGenerator
. You can choose the format from DOT, SVG, and PNG.GraphImageGenerator generator = new GraphImageGenerator("example-impacted-sub-graph"); generator.generateSvg(impactedSubGraph);
-
Simple text output is also available using
TextReporter
. You can choose the format fromHierarchyText
andFlatText
.String hierarchyText = TextReporter.toHierarchyText(impactedSubGraph); System.out.println(hierarchyText);
In a generated graph, red node represents a changed rule and yellow nodes represent the impacted rules. A solid arrow in a generated graph indicates a positive impact, in which the source rule activates the target rule. However, a dashed arrow indicates a negative impact, in which the source rule deactivates the target rule. Also, a dotted arrow represents an unknown impact, in which the source rule might activate or deactivate the target rule.
You can collapse a graph based on the rule name prefix or RuleSet in a spreadsheet using the GraphCollapsionHelper
. This enables you to view the overview of a graph. Also, you can use ImpactAnalysisHelper
to the collapsed graph.
Graph collapsedGraph = new GraphCollapsionHelper().collapseWithRuleNamePrefix(graph);
Graph impactedCollapsedSubGraph = impactFilter.filterImpactedNodes(collapsedGraph, "org.drools.impact.analysis.example.PriceCheck");
If you only want to view the positive relations in a graph, set the positiveOnly
to true
for ModelToGraphConverter
, ImpactAnalysisHelper
, and GraphCollapsionHelper
constructor.
ModelToGraphConverter converter = new ModelToGraphConverter(true);
Graph graph = converter.toGraph(analysisModel);
ImpactAnalysisHelper impactFilter = new ImpactAnalysisHelper(true);
Graph impactedSubGraph = impactFilter.filterImpactedNodes(graph, "org.drools.impact.analysis.example.PriceCheck_11");
Text output is useful for a large number of rules. In a text output, [*]
represents a changed rule, and [+]
represents impacted rules.
--- toHierarchyText ---
Inventory shortage[+]
PriceCheck_11[*]
StatusCheck_12[+]
(Inventory shortage)
StatusCheck_13[+]
StatusCheck_11[+]
(PriceCheck_11)
--- toFlatText ---
Inventory shortage[+]
PriceCheck_11[*]
StatusCheck_11[+]
StatusCheck_12[+]
StatusCheck_13[+]
Troubleshooting
If you get the warning message when rendering SVG or PNG:
graphviz-java failed to render an image. Solutions would be: 1. Install graphviz tools in your local machine. graphviz-java will use graphviz command line binary (e.g. /usr/bin/dot) if available. 2. Consider generating a graph in DOT format and then visualize it with an external tool.
You would need to install graphviz tools in your local machine. If not possible, you would need to generate the graph in DOT format so that you can render it with another tool later on.