1

I'm trying to work with lists in drools. I'm passing in a request which has a purchase list as part of it. I want to do several rules including checking if the size is correct, then if all elements are the same, if all purchases are authorized, ... I have the following code but I'm running into problems working with the list. Is this the right approach? Especially when checking for the size?

import com.rules.Purchase
import com.rules.PurchaseRequest

dialect  "mvel"

global Boolean eligibleForRefund

rule "Check for list not equal to two elements" salience 10
    when
        PurchaseRequest(getPurchases != null, getPurchases.size() != 2)

    then
        drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end

rule "Check for two purchases" salience 9
    when:
        $purchaseRequest: PurchaseRequest()
        Number(intValue != 2) from accumulate(Purchase(getStatus() == "Approved") from $purchaseRequest.getPurchases(), count(1))

    then
        drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end

rule "Check for the same purchases" salience 8
    when:
        $purchaseRequest: PurchaseRequest()

    then
        firstPurchase = $purchaseRequest.getPurchases().get(0).getCost();
        hasAllElements = true;

        for (Purchase purchase : $purchaseRequest.getPurchases()) {
            if (purchase.getCost() != firstPurchase) {
                hasAllElements = false;
            }
        }

        drools.getKieRuntime().setGlobal("eligibleForRefund", hasAllElements);
end
user3509528
  • 97
  • 10

1 Answers1

2

Assuming that your class definition looks like this:

class PurchaseRequest {
  private List<Purchase> purchases;
  
  public List<Purchase> getPurchases() { return this.purchases; }
}

You should be pulling references out of the holder instead of constantly interacting with things via the getters. In other projects this helps with keep data consistent especially with shared resources. Recall that if you have a getter whose name matches the format getXyz, you can refer to it simply as xyz and drools will automagically map it to the getter function. This allows us to get the purchases via PurchaseRequest( $purchases: purchases ) since purchases will be mapped to getPurchases(). (Note that if purchases happened to be a public variable, it would have mapped to that first; but since it's private it falls back on the public getter that follows bean naming conventions.)

Second you use an accumulate in a very simple scenario where a collect would probably be more appropriate. Generally you'd use accumulate for more complicated "get things that look like this" sort of situations; but for simple matching, a collect works just as well.

The third rule needs the most work. You do not want to do this kind of business logic on the right hand side of your rule. There's a whole lot of ways you could go about checking that all the elements are the same -- if you've implemented equals/hashCode you could just shove everything into a set and confirm that the length of the set is still the length of the list; you could invert the rule to instead check for at least one item that's different; you could use accumulate or collect; ...

Finally --

  • Avoid saliences. They're bad design. Your rules should stand alone. You only need saliences here because your third rule sets both true and false. If instead you defaulted to true and then used the rules to override it to false, you could get away with having absolutely no saliences at all.
  • It's very unusual to use primitives for a global variable. I'm frankly not convinced that this will even work with a primitive. Globals work because the object is passed in by reference, and updated in the rules, and therefore the caller which retains the reference to the object will get the updated value. That doesn't work with primitives.
rule "Check for list not equal to two elements" 
salience 1
when
  PurchaseRequest($purchases: purchases != null)
  List(size != 2) from $purchases
then
  drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end

rule "Check for two purchases"
salience 1
when:
  PurchaseRequest( $purchases: purchases != null)
  List( size != 2 ) from collect( Purchase(status == "Approved") from $purchases)
then
  drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end

// I've no idea what data type `getCost()` returns; I'm assuming "String"
rule "Check for the same purchases"
when:
  PurchaseRequest($purchases: purchases != null)

  // accumulate all of the costs into a set. if all costs are the same, set size = 1
  $costs: Set() from accumulate( Purchase( $cost: cost ) from $purchases;
                                 collectSet($cost))
then
  drools.getKieRuntime().setGlobal("eligibleForRefund", $costs.size() == 1);
end
Roddy of the Frozen Peas
  • 14,380
  • 9
  • 49
  • 99
  • Thanks for providing an incredibly detailed response. Yeah my request looks like that. I couldn't get List(size != 2) from $purchases to work, but eval($purchases.size() != 2) worked great. Thanks for pointing out collect, that works well with sets and just comparing the size (double for the type btw). Changed all the getters to holder. So for saliences, I was under the impression that you needed to use them to exit when the comparison works out. I.e. "Check for two purchases", if the statement is true (where there's not 2 elements) I wanted it to quit instead of going on. Part 1. – user3509528 Aug 05 '21 at 21:33
  • For globals, I have kieSession.setGlobal when I create it. Then fire the rules from there expecting a global value returned. Is passing it in by ref the better way of doing that then? – user3509528 Aug 05 '21 at 21:35
  • Don't ever use `eval` if possible; Drools does some fantastic optimization of the left-hand side but it can't optimize eval calls. (At my last job our custom litnter actually flagged evals; they're code smell.) Try `ArrayList(size != 2)` if `List` "doesn't work" (though I'm guessing since you don't indicate what exactly is not working.) – Roddy of the Frozen Peas Aug 05 '21 at 21:46
  • For getting the result out of the rules, you can see [this other answer](https://stackoverflow.com/questions/60504675/how-to-return-the-value-from-cosequence-of-drl-file-to-java/60536151#60536151). Since the usual syntax for interacting with globals generally does not involve interacting with the runtime, I'm not entirely familiar with the limitations of how you're doing it; but it's generally not considered good practice (globals are pretty much the same as static variables in Java; you don't want to put calculated values in a static var.) – Roddy of the Frozen Peas Aug 05 '21 at 21:49
  • I tried both ArrayList and List. I pass in ImmuatableList.of() (and one with one element) into the request. I'm running a unit test which builds and fires all rules to check if it quits after that evaluates true. It keeps going to the next rule when I use List/ArrayList. With eval, it quits after that rule as expected. Just a heads up, I'm building a proof of concept as part of a bigger service. So there's no requirements / limitations besides pass in a rules file which determine of it's eligible for something (refund as an example). I'm hoping to start with good practices as a result. – user3509528 Aug 05 '21 at 22:03