Matteo Baldi
Matteo Baldi

Reputation: 5828

Drools rule - null check and accumulate condition

I'm currently doing some fix on a java batch which run a set of Drools (yeuch!) rules.

The rule I have to fix is this:

rule "Insert a too old condition"
        salience -1
    when
        $person : Person()
        $tooOldInstant : DateTime() from now.minusDays(10)
        DateTime( this < $tooOldInstant ) from accumulate (
            LastData( $date : lastDate ) from $person.personLastDatas,
            maxValue($date)
        )
    then
        insert(new Condition("submitTooOldCondition"));
end

where for simplification Person is a simple bean with a personLastDatas Set<LastData> AND LastData has a org.joda.time.DateTime lastDate property.

Question: How do I insert a new condition where if $person.personLastDatas is null the rule apply?

Something like:

rule "Insert a too old condition modified"
        salience -1
    when
        $person : Person()
        $tooOldInstant : DateTime() from now.minusDays(10)
        $maxLastDate : DateTime() from accumulate (
            LastData( $date : lastDate ) from $person.personLastDatas,
            maxValue($date)
        )
        ($maxLastDate == null || $maxLastDate < $tooOldInstant)
    then
        insert(new Condition("submitTooOldCondition"));
end

Upvotes: 2

Views: 2037

Answers (2)

Mykhaylo Adamovych
Mykhaylo Adamovych

Reputation: 20966

Option 1

rule "Insert a too old condition"
        salience -1
    when
        Person(personLastDatas == null)
        or
        $person : Person()
        and $tooOldInstant : DateTime() from now.minusDays(10)
        and DateTime( this < $tooOldInstant ) from accumulate (
            LastData( $date : lastDate ) from $person.personLastDatas,
            maxValue($date)
        )
    then
        System.out.println("submitTooOldCondition");
end

Option 2

Make your aggregate function work the way you need. From the business you expressed in the rule, null DateTime should be treated as a lowest possible value. If this is true for all other rules, you can encapsulate this logic in your maxValue function.

public Object getResult(HashSet<DateTime> context) throws Exception {
    return context.isEmpty() ? new DateTime(0) /*null*/ : context.iterator().next();
}

With the logic above your original rule will work as expected without any modification.


test

@DroolsSession(resources = "classpath:/test.drl")
public class PlaygroundTest {

    @Rule
    public DroolsAssert drools = new DroolsAssert();

    @Test
    @TestRules(expectedCount = { "2", "Insert a too old condition" })
    public void testIt() {
        drools.setGlobal("now", now());
        drools.insertAndFire(new Person(newHashSet(new LastData(now().minusDays(100)), new LastData(now().minusDays(5)))));
        drools.insertAndFire(new Person(newHashSet(new LastData(now().minusDays(100)), new LastData(now().minusDays(15)))));
        drools.insertAndFire(new Person(null));
    }
}

test output

00:00:00 --> inserted: Person[personLastDatas=[org.droolsassert.LastData@25243bc1, org.droolsassert.LastData@1e287667]]
00:00:00 --> fireAllRules
00:00:00 --> inserted: Person[personLastDatas=[org.droolsassert.LastData@76f10035, org.droolsassert.LastData@5ab9b447]]
00:00:00 --> fireAllRules
00:00:00 <-- 'Insert a too old condition' has been activated by the tuple [Person, DateTime, DateTime]
submitTooOldCondition
00:00:00 --> inserted: Person[personLastDatas=<null>]
00:00:00 --> fireAllRules
00:00:00 <-- 'Insert a too old condition' has been activated by the tuple [Person]
submitTooOldCondition

function source

public class MaxValueAccumalateFunction implements AccumulateFunction<HashSet<DateTime>> {

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    }

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

    @Override
    public HashSet<DateTime> createContext() {
        return new HashSet<>();
    }

    @Override
    public void init(HashSet<DateTime> context) throws Exception {
    }

    @Override
    public void accumulate(HashSet<DateTime> context, Object value) {
        if (context.isEmpty() || context.iterator().next().isBefore((DateTime) value)) {
            context.clear();
            context.add((DateTime) value);
        }
    }

    @Override
    public void reverse(HashSet<DateTime> context, Object value) throws Exception {
    }

    @Override
    public Object getResult(HashSet<DateTime> context) throws Exception {
        return context.isEmpty() ? new DateTime(0) /*null*/ : context.iterator().next();
    }

    @Override
    public boolean supportsReverse() {
        return false;
    }

    @Override
    public Class<?> getResultType() {
        return null;
    }
}

rule source

import org.joda.time.DateTime;
import accumulate org.droolsassert.MaxValueAccumalateFunction maxValue;

global DateTime now;

rule "Insert a too old condition"
        salience -1
    when
        Person(personLastDatas == null)
        or
        $person : Person()
        and $tooOldInstant : DateTime() from now.minusDays(10)
        and DateTime( this < $tooOldInstant ) from accumulate (
            LastData( $date : lastDate ) from $person.personLastDatas,
            maxValue($date)
        )
    then
        System.out.println("submitTooOldCondition");
end

Upvotes: 1

Roddy of the Frozen Peas
Roddy of the Frozen Peas

Reputation: 15180

You should have two rules, one for the null condition and one for the one that compares the dates.

Here is the null condition rule; it verifies that the Person exists but that it has no personLastDatas property:

rule "Insert a too old condition modified - null case"
salience -1
when
  $person: Person( personLastDatas == null )
then
  insert(new Condition("submitTooOldCondition"));
end

Your existing rule is sufficient for the date comparison check.

In general, if you find yourself trying to do complex if-else logic on either side of the rule, it's a good indication that you should have two rules. Since these two rules cannot both be true, you'll only have one condition of this type inserted.

That being said, if you're using a modern version of drools, you can use conditional and namespaced consequences. The documentation covers this in detail (I've linked 7.37.0.Final; most of the recent 7.30+ versions have this functionality.) Below is an example of what your rule might look like:

rule "Insert a too old condition"
salience -1
when
  $person : Person( $lastDatas: personLastDatas )
  if ( $lastDatas == null ) break[noData]
  $tooOldInstant : DateTime() from now.minusDays(10)
  DateTime( this < $tooOldInstant ) from accumulate (
            LastData( $date : lastDate ) from $person.personLastDatas,
            maxValue($date)
  )
then
  insert(new Condition("submitTooOldCondition"));
then[noData]
  // any special logic for the null case goes here
  insert(new Condition("submitTooOldCondition"));
end

(This is pseudo-code; I don't have a drools project on this computer but it should be similar.)

Basically this syntax, though harder to read, will allow you to handle these sorts of repetitive/partial shared case rules. They're usually recommended for cases where you have two rules, where one extends the other, so a subset of the common conditions can trigger one consequence, while the full set of the conditions can trigger another consequence. That's not quite what you have here, but the functionality can be bastardized for your use case.

The break keyword tells the engine to stop evaluating the left hand side once the condition is met; there's also a do keyword which allows for continued evaluation.

Upvotes: 1

Related Questions