Reactive rule-based dynamic forms in hybris using Drools7


Web applications heavily rely on forms. Questionnaires and comprehensive registration forms are common components of the HR, financial and service solutions. The business rules behind these forms are complex and frequently updated. The well-known example is job application forms. Depending on the user input, some components of the form expands to gather more data from the applicant, timely and in context. This article is about my experiments with the rule engine driven forms. I used Drools, as a default rule engine that comes with hybris out of the box. I also used the code of JBoss Tohu project, long abandoned. In the end of the article, you will find a link to source code of the project. This is the alpha release of the system, so it need some polishing to get used in production. I’ve set myself the following objectives:
  • The solution should support
    • dynamic forms:
      • Some fields of the form are available only for some form configurations and/or user groups
        • For example, some fields are available only for the customers with a subscription
        • For example, some fields are visible only if other fields are filled with the particular values
      • Some form elements have different views on the different form configurations and/or user groups
        • For example, a select box can have a separate set of options and it depends of the set of checkboxes from the previous page
    • multi-page forms:
      • Wizard-like, with Prev/Next/Done buttons
      • Conditional (dynamic) pages
    • complex validation rules:
      • Validation rules depend on the form configurations
      • Validation rules depend on the user profile attributes
      • If some fields are not visible, their validation rules should be turned off
    • server-side scripting
    • as lightweight as possible
The first thing that comes to mind is Angular Forms. This topic deserves a separate article. Many choose this way, it is a good solution. However, it has some drawbacks. Angular is a front-end thing, so for interacting with the server-side entities you need to build an API layer.  Secondly, you need to redeploy and retest the angular code each time you decide to change the rules. I did some research and stumbled upon the reactive form framework called Tohu. It was developed more than seven years ago, but both the architecture and code are quite clear, supportable and extensible. I decided to play around with the framework and integrate it with SAP hybris. It was quite challenging because the dependencies of the original code are also obsoleted. For example, Tohu uses Drools 5 while hybris comes with Drools 7. Tohu is built with the old jQuery 1.3.2 while the latest SAP hybris uses jQuery 3.2.1. I refactored the framework to support the latest libraries without touching the original Java code. As a result, I created a sample project, where the dynamic forms are integrated into hybris as an extension. The source code is available on github. In order to demonstrate the capabilities of the solution, I needed a comprehensive example. I took some examples from Tohu, refactored and expanded, and this is what I got:
  • There are six pages
    • Contact Details page. Contains Joint/Single application selectbox.
    • *Joint Contact Details Page (second participant).
    • Demographic details page
    • *Demographic details page for the joint application (second participant)
    • Loyalty data page
    • *Loyalty data page for the joint application (second participant)
  • Contact Details Page
    • Personal Details Group
      • Application type (Select Box) Single/Joint. This selection affects the fields, sections and pages.
      • Given names (Text).
      • Family name (Text).
      • Date of birthday (Calendar)
      • E-mail (Text). Should be validated against the text pattern.
      • Phone (Text).
    • Main Address Group
      • Street Address (Text)
      • Suburb (Text)
      • City (Text)
      • Postcode (Text)
    • Postal Address Group
      • Different Postal Address (Boolean)
      • Postal Address (Text)
      • Postal Suburb (Text)
      • Postal City (Text)
      • Postal Postcode (Text)
  • *Joint Contact Details Page
    • (Only displayed if Application Type = Joint)
    • Joint Personal Details
      • Extra Given names (Text).
      • Extra Surname (Text).
      • Extra Date of birthday (Calendar)
      • Extra e-mail (Text).
      • Extra phone (Text).
  • Demographic Details Page
    • IncomeDetails  (Select Box). The selection affects other fields on this page. Possible Answers:
      • Less than 20,000
      • 20,000 to 39,999
      • 40,000 to 59,999
      • 60,000 to 99,999
      • 100,000 and over
    • Own Home (Boolean).
    • Household: Number of people (Number). Default value: 1
      • If a number of people > 1 the following question is displayed:
        • Children Under 18 (Boolean).
          • If Children Under 18 is yes, the following section is displayed:
            • Preschoolers (boolean)
            • Primary (boolean)
            • Intermediate (boolean)
            • Secondary (boolean)
            • Tertiary (boolean)
            • Other (boolean)
    • Ok to receive Promo Materials? (Boolean)
      • if yes, display a list of topics
        • books (Boolean)
        • music (Boolean)
        • crafts (Boolean)
        • motoring (Boolean)
        • cooking (Boolean)
        • home improvement (Boolean).
          • Only displayed if Own Home is checked
        • travel (Boolean)
          • Only displayed if income > 20000
        • toys (Boolean)
          • Only displayed if Children Under 18 is true
        • wine (Boolean)
        • investing (Boolean)
          • Only displayed if income > 100000
  • Loyalty Programs Page
    • Main Name Summary. Not editable. Copied from Given Names.
    • Last name. Not editable. Copied from Family Name.
    • Loyalty Programs:
      • Budget Airways (Boolean).
        • If selected, the following fields are displayed
          • Membership Number (Text)
          • Membership Name (Text)
          • Ok to receive Special Offers? (Boolean)
          • Membership Type (Radio buttons)
            • Gold
            • Silver
            • Bronze
      • Online Food (Boolean)
        • If selected, the following fields are displayed
          • Membership Number (Text)
          • Membership Name (Text)
          • Ok to receive Special Offers? (Boolean)
      • Mocha Coffee (Boolean)
        • If selected, the following fields are displayed
          • Membership Number (Text)
          • Membership Name (Text)
          • Ok to receive Special Offers? (Boolean)
  • *Joint Loyalty Programs Page
    • The same as previous section, but for the second participant
    • Only displayed if Application Type = Joint
Let’s put all business rules together:
  • “Joint…” pages are displayed only if the application type is “Joint”
  • E-mail should be validated against the pattern
  • Some options from the list of promotional materials are available
    • only for some income ranges
    • only if the  
      children under 18
      checkbox is set
    • only if the
      own home
       checkbox is set
  • the details of the loyalty program are displayed only if the checkbox for the particular loyalty program is set
  • the details of the loyalty program have the same questions for all loyalty programs with one exception:  Budget Airways has a membership type, while others don’t have it.
  • joint contact page has its own set of questions and it is displayed only for the application type = Joint
  • Joint loyalty page is the same as main contact loyalty page in terms of the structure and business rules.
This form with these business rules are implemented without a single line of custom java code, only with the Drools rules. Let’s consider the rules closer.

Rule #1. Initial Setup: Definition of Pages

rule "LoyaltyQuestionnaire"
dialect "mvel"
then
Questionnaire questionnaire = new Questionnaire("LoyaltyQuestionnaire");
questionnaire.setLabel("Solnet Loyalty Card Signup");
questionnaire.setCompletionAction("extract.pdf");

questionnaire.addItem("ContactDetailsPage");
questionnaire.addItem("DemographicDetailsPage");
questionnaire.addItem("MainOtherLoyaltyProgramsPage");

questionnaire.setActiveItem("ContactDetailsPage");

insertLogical(questionnaire);
end

Rule #2. Initial Setup: Contact Details Page Rule

This rule just says that the page contains three sections: PersonalDetails, MainAddress and PostalAddress. There is no “when” part in the rule that means that the rule is a sort of the initial setup as well.
rule "ContactDetailsPage"
dialect "mvel"
then
Group page = new Group("ContactDetailsPage");
page.setLabel("Sign up for the best rewards online!");

Group personalDetails = new Group("PersonalDetails");
personalDetails.setLabel("1");
page.addItem(personalDetails.getId());
personalDetails.setPresentationStyles({"section"});
insertLogical(personalDetails);

Group mainAddress = new Group("MainAddress");
mainAddress.setLabel("2");
page.addItem(mainAddress.getId());
mainAddress.setPresentationStyles({"section"});
insertLogical(mainAddress);

Group postalAddress = new Group("PostalAddress");
postalAddress.setLabel("3");
page.addItem(postalAddress.getId());
postalAddress.setPresentationStyles({"section"});
insertLogical(postalAddress);

insertLogical(page);
end

Rule #3. Initial Setup: Definition of PersonalDetails

This rule creates the fields for the Personal Details section. There is a condition,
(Group.id='PersonalDetails' and items = null)
Once the code from the “then” block is executed, items won’t be empty and the rule won’t be fired again. Drools is based on reactive programming concept and if you empty the items of the group, the rule will be fired again and the form will be populated again.
rule "personalDetailsSection"
dialect "mvel"
when
g : Group(id == "PersonalDetails", items == null)
then
MultipleChoiceQuestion mcQuestion = new MultipleChoiceQuestion("applicationType");
g.addItem(mcQuestion.getId());
mcQuestion.setAnswerType(Question.TYPE_TEXT);
mcQuestion.setPreLabel("Application Type");
mcQuestion.setPossibleAnswers({
new PossibleAnswer("1", "Single"),
new PossibleAnswer("2", "Joint")}
);
mcQuestion.setAnswer("1");
insert(mcQuestion);

Question question = new Question("givenNames");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Given or first names");
insert(question);

question = new Question("surname");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Family name");
insert(question);

question = new Question("dob");
g.addItem(question.getId());
question.setPresentationStyles({"datepicker"});
question.setAnswerType(Question.TYPE_DATE);
question.setPreLabel("Date of birth");
insert(question);

question = new Question("email");
g.addItem(question.getId());
question.setAnswerType("text.email");
question.setPreLabel("Email");
insert(question);

question = new Question("phone");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Phone");
insert(question);

update(g);
end

Rule #4. Initial Setup: Definition of MainAddress section

The similar code for the MainAddress section. It is collapsed by default because there is nothing new. Expand the code
rule "mainAddressSection"
dialect "mvel"
when
g : Group(id == "MainAddress", items == null)
then
Question question = new Question("streetAddress");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Street Address");
insert(question);

question = new Question("suburb");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Suburb");
insert(question);

question = new Question("city");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("City");
insert(question);

question = new Question("postcode");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Post Code");
insert(question);

update(g);
end

Rule #5. Initial Setup: Definition of PostalAddress section

The PostalAddress block has a dynamic component. This component becomes visible only if the “Different Postal Address” checkbox is on. The dynamic component is defined in a separate rule, “additionalPostalFields”.
rule "postalAddressSection"
dialect "mvel"
when
g : Group(id == "PostalAddress", items == null)
then
Question question = new Question("differentPostalAddress");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Alternative Postal Address?");
question.setPostLabel("(select if different to above)");
question.setPresentationStyles({"first"});
question.setAnswer(false);
insert(question);

g.addItem("additionalPostalFields");

update(g);
end

Rule #6. Definition of AdditionalPostalFields

This rule contains the first “real condition” in our tutorial. This condition is true if the checkbox created in the the previous rule is on. In this rule, you can see “insertLogical” rather than “insert”. There is a difference between them. The pieces of data inserted using insertLogical will remain in the session until the condition of the rule that inserted it becomes false. So once you uncheck “additional postal fields” checkbox, this form will disappear. The pieces of data inserted using “insert” are solid, and they need to be removed manually if the needs arise.
rule "postalAddress"
dialect "mvel"
when
Question(id == "differentPostalAddress", answer == "true")
then

Group group = new Group("additionalPostalFields");

Question question = new Question("postalAddress");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Postal Address");
insertLogical(question);

question = new Question("postalSuburb");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Postal Suburb");
insertLogical(question);

question = new Question("postalCity");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Postal City");
insertLogical(question);

question = new Question("postalPostcode");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Post Code");
insertLogical(question);

insertLogical(group);

end

Rule #8. Initial Setup: Definition of DemographicDetailsPage

The code of the rule is collapsed because there is nothing new. It is very similar to ContactsDetailPage. It contains three sections, IncomeDetails, HouseHold and PromotionalMaterial. It doesn’t have a condition that means that the rule is executed as part of Initial Setup. Expand the code
rule "DemographicDetailsPage"
dialect "mvel"
then
Group page = new Group("DemographicDetailsPage");
page.setLabel("Let us know what you really, really want!");

Group incomeDetails = new Group("IncomeDetails");
incomeDetails.setLabel("4");
page.addItem(incomeDetails.getId());
incomeDetails.setPresentationStyles({"section"});
insertLogical(incomeDetails);

Group householdDetails = new Group("Household");
householdDetails.setLabel("5");
page.addItem(householdDetails.getId());
householdDetails.setPresentationStyles({"section"});
insertLogical(householdDetails);

Group promotionalDetails = new Group("PromotionalMaterial");
promotionalDetails.setLabel("6");
page.addItem(promotionalDetails.getId());
promotionalDetails.setPresentationStyles({"section"});
insertLogical(promotionalDetails);

insertLogical(page);
end

Rule #9. Definition of IncomeDetails

The code of the rule is collapsed because there is nothing new. It defines two questions, incomeBracket (select box) and ownHome (checkbox). Expand the code
rule "incomeDetailsSection"
dialect "mvel"
when
g : Group(id == "IncomeDetails", items == null)
then
MultipleChoiceQuestion mcQuestion = new MultipleChoiceQuestion("incomeBracket");
g.addItem(mcQuestion.getId());
mcQuestion.setAnswerType(Question.TYPE_TEXT);
mcQuestion.setPreLabel("Income Bracket");
mcQuestion.setPossibleAnswers({
new PossibleAnswer(null, "Please select ..."),
new PossibleAnswer("1", "Less than 20,000"),
new PossibleAnswer("2", "20,000 to 39,999"),
new PossibleAnswer("3", "40,000 to 59,999"),
new PossibleAnswer("4", "60,000 to 99,999"),
new PossibleAnswer("5", "100,000 and over")}
);
insert(mcQuestion);

Question question = new Question("ownHome");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Do you own your own home?");
insert(question);

update(g);
end

Rule #10. Definition of Household

The next rule creates the question about the number of people and the “placeholder” for the question about the kids. Note: childrenQuestion is listed as an item but it hasn’t been created yet. It is ok. The system will ignore it. It will be created once the user entered a number larger than 1. Once it is created, the system will use the placeholder for the question. It is exactly as we expect.
rule "householdSection"
dialect "mvel"
when
g : Group(id == "Household", items == null)
then
Question question = new Question("numberOfPeople");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_NUMBER);
question.setPreLabel("Number of people in the household");
question.setPresentationStyles({"first"});
question.setAnswer(1L);
insert(question);
g.addItem("childrenQuestion");
update(g);
end

Rule #11. Definition of childrenQuestion

In this rule, you can see a condition “numberOfPeople>1”. The components are inserted using insertLogical that means that the components will be removed automatically if the condition is false (numberOfPeople is NOT larger than 1). This rule creates another section, “childrenAgeRanges”. This section is defined by a separate rule.
rule "children"
dialect "mvel"
when
Question(id == "numberOfPeople", answer > 1)
then

Group group = new Group("childrenQuestion");

Question question = new Question("haveChildren");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Are there children under 18 in the household?");
question.setAnswer(false);
insertLogical(question);

group.addItem("childrenAgeRanges");
insertLogical(group);
end

Rule #12. Definition of childrenAgeSection

The code of the rule is collapsed because there is nothing new. It defines the questions, one per age range: preschool, primary, intermediate, secondary, tertiary, other. Note that they are considered as a group. Expand the code
rule "childrenAgeSection"
dialect "mvel"
when
Question(id == "haveChildren", answer == "true")
then
Group group = new Group("childrenAgeRanges");

Question question = new Question("preschool");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Preschoolers");
question.setPostLabel("(please tick all that apply)");
question.setAnswer(false);
insertLogical(question);

question = new Question("primary");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Primary");
question.setAnswer(false);
insertLogical(question);

question = new Question("intermediate");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Intermediate");
question.setAnswer(false);
insertLogical(question);

question = new Question("secondary");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Secondary");
question.setAnswer(false);
insertLogical(question);

question = new Question("tertiary");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Tertiary");
question.setAnswer(false);
insertLogical(question);

question = new Question("other");
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Other");
question.setAnswer(false);
insertLogical(question);

insertLogical(group);
end

Rule #13. Defining PromotionalMaterial

The code of the rule is collapsed because there is nothing new. It defines the section “Promotional Materials” with two questions, a checkbox and promotional types group (promotionalTypesGroup). Expand the code
rule "promotionalMaterialSection"
dialect "mvel"
when
g : Group(id == "PromotionalMaterial", items == null)
then
Question question = new Question("receiveMaterials");
g.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Are you interested in receiving promotional materials?");
question.setPresentationStyles({"first"});
question.setAnswer(false);
insert(question);
g.addItem("promotionalTypesGroup");
update(g);
end

Rule #13. Defining ptromotionalTypesGroup

In this rule, we only do the fields that always appear – others in different rules. However, all are listed in setItems. We could create different subgroups for the different logic elements but it is easier to revert to the parent specifying the child approach.
rule "promotionalTypes"
dialect "mvel"
when
Question(id == "receiveMaterials", answer == "true")
then

Group group = new Group("promotionalTypesGroup");

group.setItems({
"books",
"music",
"crafts",
"motoring",
"cooking",
"homeImprovement",
"travel",
"toys",
"wine",
"investing"});
insertLogical(group);

Question question = new Question("books");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Books");
question.setPostLabel("(please tick all that apply)");
question.setAnswer(false);
insertLogical(question);

question = new Question("music");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Music");
question.setAnswer(false);
insertLogical(question);

question = new Question("crafts");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Crafts");
question.setAnswer(false);
insertLogical(question);

question = new Question("motoring");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Motoring");
question.setAnswer(false);
insertLogical(question);

question = new Question("cooking");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Cooking");
question.setAnswer(false);
insertLogical(question);

question = new Question("wine");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Wine");
question.setAnswer(false);
insertLogical(question);

end

Rule #14. Defining Home Improvement

This rule adds an item for the promotional type list if the two conditions are true:
  • receiveMaterials checkbox is on
  • ownHome checkbox is on
Also note that the item is added via insertLogical. It means that the item will be removed automatically once any of these checkboxes is unchecked.
rule "homeImprovement"
when
Question(id == "receiveMaterials", answer == "true")
Question(id == "ownHome", answer == "true");
then
Question question = new Question("homeImprovement");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Home Improvement");
question.setAnswer(false);
insertLogical(question);
end

Rule #15. Defining Travel

This rule is similar to the previous one. It is triggered only for users who selected the income range other than the lowest one AND who agreed to receive promotional materials.
rule "travel"
when
Question(id == "receiveMaterials", answer == "true")
Question(id == "incomeBracket", answer > 2);
then
Question question = new Question("travel");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Travel");
question.setAnswer(false);
insertLogical(question);
end

Rule #16. Defining Investing

This rule is similar to the previous rules. As a result, the “investing” item will be added only if you checked “receiveMaterials” and selected the last item in the selectbox.
rule "investing"
when
Question(id == "receiveMaterials", answer == "true")
Question(id == "incomeBracket", answer > 3);
then
Question question = new Question("investing");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Investing");
question.setAnswer(false);
insertLogical(question);
end

Rule #17. Defining Toys

The item “toys” is added only if the user has children and they agreed to receive the promotional materials.
rule "toys"
when
Question(id == "receiveMaterials", answer == "true")
Question(id == "haveChildren", answer == "true");
then
Question question = new Question("toys");
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Toys");
question.setAnswer(false);
insertLogical(question);
end

Rule #18. Defining MainOtherLoyaltyProgramsPage

rule "MainOtherLoyaltyProgramsPage"
dialect "mvel"
then
Group page = new Group("MainOtherLoyaltyProgramsPage");
page.setLabel("Please specify the other loyalty programs for the main applicant.");

Group group = new Group("mainNameSummary");
page.addItem(group.getId());
// facts have already been defined
group.setItems({"givenNames", "surname"});
group.setPresentationStyles({"readonly", "row"});
insertLogical(group);

personalDetails = new Group("LoyaltyPrograms");
personalDetails.setLabel("7");
page.addItem(personalDetails.getId());
personalDetails.setPresentationStyles({"section"});
insertLogical(personalDetails);

Question question = new Question("budgetAirways");
personalDetails.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Budget Airways?");
question.setPresentationStyles({"yesNoButtons"});
question.setAnswer(false);
insertLogical(question);

question = new Question("onlineFood");
personalDetails.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Online Food?");
question.setPresentationStyles({"yesNoButtons"});
question.setAnswer(false);
insertLogical(question);

question = new Question("mochaCoffee");
personalDetails.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Mocha Coffee?");
question.setPresentationStyles({"yesNoButtons"});
question.setAnswer(false);
insertLogical(question);
insertLogical(page);
end

Rules #19-#24. Defining the rules for Loyalty checkboxes

rule "Main BA Yes Clicked support"
dialect "mvel"
when
q : Question(id == "budgetAirways", answer == true);
then
String groupId = q.id + "_details" ;
createDetails(drools, groupId, q.id);
end
rule "Main OF Yes Clicked support"
dialect "mvel"
when
Question(id == "onlineFood", answer == true);
then
String id = "onlineFood";
String groupId = id + "_details" ;
createDetails(drools, groupId, null);
end
rule "Main MC Yes Clicked support"
dialect "mvel"
when
Question(id == "mochaCoffee", answer == true);
then
String id = "mochaCoffee";
String groupId = id + "_details" ;
createDetails(drools, groupId, null);
end

rule "Main BA Yes clicked"
salience 15
no-loop
when
q : Question(id == "budgetAirways");
a : Answer(questionId == q.id, value == "true");
questionnaire : Questionnaire(branched == "false");

then
String groupId = q.getId() + "_details" ;
questionnaire.navigationBranch(new String[]{groupId}, groupId);
update(questionnaire);
end

rule "Main OF Yes clicked"
salience 15
no-loop
when
q : Question(id == "onlineFood");
a : Answer(questionId == q.id, value == "true");
questionnaire : Questionnaire(branched == false);

then
String groupId = q.getId() + "_details" ;
questionnaire.navigationBranch(new String[]{groupId}, groupId);
update(questionnaire);
end

rule "Main MC Yes clicked"
salience 15
no-loop
when
q : Question(id == "mochaCoffee");
a : Answer(questionId == q.id, value == "true");
questionnaire : Questionnaire(branched == false);
then
String groupId = q.getId() + "_details" ;
questionnaire.navigationBranch(new String[]{groupId}, groupId);
update(questionnaire);
end

function void createDetails(KnowledgeHelper drools, String pageId, String baQuestionId) {

Group page = new Group(pageId);
page.setLabel("What are the details of the other loyalty card");
String groupId = pageId + "_group";
page.addItem(groupId);

Group group = new Group(groupId);
group.setLabel("8");
group.setPresentationStyles(new String[]{"section", "programDetails"});

String prefix = groupId + "_";
String id1 = prefix + "membershipNumber";
String id2 = prefix + "membershipName";
String id3 = prefix + "specialOffers";
String id4 = prefix + "membershipType";

group.addItem(id1);
group.addItem(id2);
group.addItem(id3);
if (baQuestionId != null) {
group.addItem(id4);
}
drools.insertLogical(group);

Question question = new Question(id1);
group.addItem(question.getId());
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Membership Number");
drools.insertLogical(question);

question = new Question(id2);
question.setAnswerType(Question.TYPE_TEXT);
question.setPreLabel("Membership Name");
drools.insertLogical(question);

question = new Question(id3);
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Do you want to receive combined special offers");
drools.insertLogical(question);

if (baQuestionId != null) {
MultipleChoiceQuestion mcQuestion = new MultipleChoiceQuestion(id4);
mcQuestion.setAnswerType(Question.TYPE_TEXT);
mcQuestion.setPreLabel("Membership Type");
mcQuestion.setPossibleAnswers(new PossibleAnswer[]{
new PossibleAnswer("1", "Gold"),
new PossibleAnswer("2", "Silver"),
new PossibleAnswer("3", "Bronze")}
);
mcQuestion.setPresentationStyles(new String[]{ "radio" });
drools.insertLogical(mcQuestion);
}

drools.insertLogical(page);

}

Rule #25-#27. Adding Joint Application page

Add Joint Loyalty Programs Page

rule "AddJointLoyaltyProgramsPage"
dialect "mvel"
salience 10
no-loop
when
Question(id == "applicationType", answer == "2")
questionnaire : Questionnaire(branched == false, items not contains "JointLoyaltyProgramsPage");
then
questionnaire.appendItem("JointLoyaltyProgramsPage", "MainOtherLoyaltyProgramsPage");
update(questionnaire);
end

Joint Loyalty Programs Page

rule "JointLoyaltyProgramsPage"
dialect "mvel"
when
Question(id == "applicationType", answer == "2")
then
Group page = new Group("JointLoyaltyProgramsPage");
page.setLabel("Please specify the other loyalty programs for the joint applicant.");

Group group = new Group("jointSummary");
page.addItem(group.getId());
// facts have already been defined
group.setItems({
"extraGivenNames",
"extraSurname"
});
group.setPresentationStyles({"readonly", "row"});
insertLogical(group);

personalDetails = new Group("JointLoyaltyPrograms");
personalDetails.setLabel("7+");
page.addItem(personalDetails.getId());
personalDetails.setPresentationStyles({"section"});
insertLogical(personalDetails);

Question question = new Question("jointBudgetAirways");
personalDetails.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Budget Airways?");
question.setPresentationStyles({"yesNoButtons"});
question.setAnswer(false);
insertLogical(question);

question = new Question("jointOnlineFood");
personalDetails.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Online Food?");
question.setPresentationStyles({"yesNoButtons"});
question.setAnswer(false);
insertLogical(question);

question = new Question("jointMochaCoffee");
personalDetails.addItem(question.getId());
question.setAnswerType(Question.TYPE_BOOLEAN);
question.setPreLabel("Mocha Coffee?");
question.setPresentationStyles({"yesNoButtons"});
question.setAnswer(false);
insertLogical(question);

insertLogical(page);
end

Remove Joint Loyalty Page

rule "RemoveJointLoyaltyProgramsPage"
dialect "mvel"
salience 100
no-loop
when
Question(id == "applicationType", answer == "1")
questionnaire : Questionnaire(items contains "JointLoyaltyProgramsPage");
then
questionnaire.removeItem("JointLoyaltyProgramsPage");
update(questionnaire);
end

Rule #27-#32. Adding Joint Application Support for checkboxes

These rules use the same shared code for creating the similar subforms. There two rules per each checkbox. The first rule creates the form, and another rule creates NavigationBranch, a sort of subform that replaces the main form completely.
rule "Joint OF Yes Clicked support"
dialect "mvel"
when
Question(id == "jointOnlineFood", answer == true);
then
String id = "jointOnlineFood";
String groupId = id + "_details" ;
createDetails(drools, groupId, null);
end
rule "Joint Mocha Yes Clicked support"
dialect "mvel"
when
Question(id == "jointMochaCoffee", answer == true);
then
String id = "jointMochaCoffee";
String groupId = id + "_details" ;
createDetails(drools, groupId, null);
end
rule "Joint BA Yes Clicked support"
dialect "mvel"
when
Question(id == "jointBudgetAirways", answer == true);
then
String id = "jointBudgetAirways";
String groupId = id + "_details" ;
createDetails(drools, groupId, id);
end
rule "Joint BA Yes clicked"
salience 15
no-loop
when
q : Question(id == "jointBudgetAirways");
a : Answer(questionId == q.id, value == "true");
questionnaire : Questionnaire(branched == false);

then
String groupId = q.getId() + "_details" ;
questionnaire.navigationBranch(new String[]{groupId}, groupId);
update(questionnaire);
end

rule "Joint OF Yes clicked"
salience 15
no-loop
when
q : Question(id == "jointOnlineFood");
a : Answer(questionId == q.id, value == "true");
questionnaire : Questionnaire(branched == false);

then
String groupId = q.getId() + "_details" ;
questionnaire.navigationBranch(new String[]{groupId}, groupId);
update(questionnaire);
end

rule "Joint MC Yes clicked"
salience 15
no-loop
when
q : Question(id == "jointMochaCoffee");
a : Answer(questionId == q.id, value == "true");
questionnaire : Questionnaire(branched == false);

then
String groupId = q.getId() + "_details" ;
questionnaire.navigationBranch(new String[]{groupId}, groupId);
update(questionnaire);
end

Adding new validations using hybris API

If we want to add new validators, everything we need is to create a new file with a new rule. In the rule below, the system makes a call to hybris API to validate an e-mail from the form.
package org.tohu.examples.loyalty

import org.tohu.InvalidAnswer;
import org.tohu.Question;
import de.hybris.platform.core.Registry;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery;
import de.hybris.platform.servicelayer.search.SearchResult;

rule "EmailValidatorAgainstTheDatabase"
dialect "mvel"
when
question : Question(answerType == "text.email", answered == true, answer : textAnswer);
eval(!existingEmail(answer));
then
insertLogical(new InvalidAnswer(question.getId(), "This is not an existing email address"));
end

function boolean existingEmail(String email)
{
FlexibleSearchService flexibleSearchService = (FlexibleSearchService) Registry.getApplicationContext().getBean("flexibleSearchService");
FlexibleSearchQuery query = new FlexibleSearchQuery("select {pk} from {Customer} where {uid} = ?email");
query.addQueryParameter("email", email);
SearchResult searchResult = flexibleSearchService.search(query);
System.out.println("delay start..");
Thread.sleep(2000);
System.out.println("delay finish..");
int foundEmails = searchResult.getCount();
if (foundEmails > 0) { return true; } else { return false; }
}

Adding new fields prefilled with hybris data

package org.tohu.examples.loyalty

import java.util.Calendar;

import org.tohu.Group;
import org.tohu.MultipleChoiceQuestion;
import org.tohu.MultipleChoiceQuestion.PossibleAnswer;
import org.tohu.Question;
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery;
import de.hybris.platform.core.Registry;
import de.hybris.platform.core.model.c2l.LanguageModel;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;

rule "Languages"
dialect "mvel"
when
g : Group(id == "PersonalDetails",
items != null,
items not contains "languages")
then
MultipleChoiceQuestion mcQuestion = new MultipleChoiceQuestion("languages");
mcQuestion.setAnswerType(Question.TYPE_TEXT);
mcQuestion.setPreLabel("Languages");

FlexibleSearchService flexibleSearchService = Registry.getApplicationContext().getBean("flexibleSearchService");
answers = flexibleSearchService.search("select {pk} from {Language}").getResult();
i = 0;
for (LanguageModel item : answers) {
mcQuestion.insertPossibleAnswer(new PossibleAnswer(item.getIsocode(), item.getIsocode()), i++);
}
insert(mcQuestion);
g.addItem(mcQuestion.getId());

update(g)

end

Leave a Reply