Search code examples
javadroolsrulesrule-engine

Create new Map<String, String> in drools file on the fly


Scenario:
Awarding the students based on few rules. Here each student is eligible for multiple rewards like student with marks 80 is eligible for both award_65 and award_75. So after evaluating the result I want a list of awards a student is eligible for..
This use-case is bit unrealistic, but I tried to put an analogy to my problem (couldn't post as it contains sensitive information).
Inputs to drools would be returned by following method: and these returned list of objects would be the facts to drools.

List<Object> getDroolsFacts(Student student, List<StudentAward> studentAwards) {
..........
}

Sample rules as below:

//Assuming all the imports are perfectly done
import java.util.*;

rule "award_65"
when
  $Student: Student(status == "active" && marks > 65);
  $studentAwards: List();    // DOUBT - I
  Map<String, String> metadata = new HashMap<>();   // DOUBT - II
  metadata.put("status", "active");
  metadata.put("marks", "65");
then
  final StudentAward studentAward = StudentAward.from("Award65" , metadata);
  $studentAwards.add(studentAward);
end

rule "award_75"
when
  $Student: Student(status == "active" && marks > 75);
  $studentAwards: List();    // DOUBT - I
  Map<String, String> metadata = new HashMap<>();   // DOUBT - II
  metadata.put("status", "active");
  metadata.put("marks", "75");
then
  final StudentAward studentAward = StudentAward.from("Award75" , metadata);
  $studentAwards.add(studentAward);
end


As per the current scenario DOUBT-I line is existing in prod and metadata related lines were added by me.

If a student is having 80 marks then as per the current drools (i.e without metadata related lines) I'm able to get two objects in the result list studentAwards .

DOUBT - I: (Refer to the code snipped mentioned as DOUBT-I)

  • Does $studentAwards: List(); correspond to the list provided to drools or is it the new list getting created? Because currently after evaluating the rules I'm able to fetch the multiple responses as expected in it.

DOUBT - II: (Refer to the code snipped mentioned as DOUBT-II)

  • MetaData related 3 lines are added by me newly which are not working. I want to achieve a scenario where I create an HashMap on the fly and add some data and that will be set as part of StudentAwards object. New map needs to be created for each of the rule and multiple rules will be executed for each student object and result studentAwards contains all the applicable awards.
  • So how can I create new Map<String, String> on the fly for each of the rule and add it to the object getting created?

Solution

  • You're kind of going about this in a funny way. Part of the reason your rules don't work is because you're trying to put consequences into the conditional section.

    We can break down your problem into the following.

    Given:

    • a Student, who has a status and marks.

    Rule 1:

    • When status = active and marks > 65, a student is eligible for Award65.

    Rule 2:

    • When status = active and marks > 75, a student is eligible for Award75.

    This is pretty straight forward.

    So far, our rules will look like this:

    rule "Student is eligible for Award65"
    when
      Student( status == "active", marks > 65 )
    then 
      // TODO
    end
    
    rule "Student is eligible for Awards75"
    when
      Student( status == "active", marks > 75)
    then
      // TODO
    end
    

    So far, so good.

    Now, how do we actually do the "is eligible for awardXX" party? Looking at your example, your rule inputs include a List<StudentAward>; when a student is eligible, we add a new instance of StudentAward to that list. The StudentAward constructor takes the award name, and a Map of some data.

    We can now update the // TODO comments in the above rules to do just that:

    rule "Student is eligible for Award65"
    when
      $awards: List()
      Student( status == "active", marks > 65 )
    then 
      Map<String, String> metadata = new HashMap<>();
      metadata.put("status", "active");
      metadata.put("marks", "65");
      StudentAward studentAward = StudentAward.from("Award65" , metadata);
      $awards.add(studentAward);
    end
    
    rule "Student is eligible for Awards75"
    when
      $awards: List()
      Student( status == "active", marks > 75)
    then
      Map<String, String> metadata = new HashMap<>();
      metadata.put("status", "active");
      metadata.put("marks", "75");
      StudentAward studentAward = StudentAward.from("Award75" , metadata);
      $awards.add(studentAward);
    end
    

    And that's it. All that's left is to actually call the rules.

    public List<StudentAward> getAwardsForStudent(Student student) {
        KieServices kieServices = KieServices.Factory.get();
        KieContainer kContainer = kieServices.getKieClasspathContainer();
        KieBase kBase = kContainer.getKieBase(kbaseName);
        
        List<StudentAward> awards = new ArrayList<>();
    
        KieSession session = kBase.newKieSession();
        session.insert(student); // insert the Student 
        session.insert(awards); // insert the list of awards
        session.fireAllRules();
    
        return awards; // at this point, awards contains all of the values from the rules
    }
    

    And that's it. Given a student, we execute the rules and get out a list of awards.

    The only real problem with your rules was that you were instatiating the metadata map on the wrong side of the rule (it's a part of the rule's consequences, not conditions), and also how you were getting the list of awards.

    There's a lot of duplicate code on the rules in this answer, but since we're working off of a "toy" example I didn't want to waste too much energy in refactoring into a shared function. If you do use something like, you'd want to see if you could use shared functions or even inheritance to simplify your rules.


    You asked two specific questions. The only one I didn't answer explicitly was this one:

    Does $studentAwards: List() correspond to the list provided to drools or is it the new list getting created?

    Nothing gets created in the "when" clause. Those are conditions. This line is saying "get the List in working memory and assign it to the variable $studentAwards."

    If you have multiple lists in working memory, this becomes problematic, which is why I usually recommend not putting collections like this into memory. My usual recommendation is to create some sort of "results" object which holds the calculated data that you expect the rules to be outputting.


    Note that in some versions of Drools I've gotten errors while trying to work with parameterized types -- if that happens to you, try doing Map foo = new HashMap() instead of Map<String, String>. I've never managed to figure out what causes this problem.