A note from 2026: This article was published in 2018 and uses the former SAP hybris branding; the product is now SAP Commerce Cloud. The JBoss Tohu project referenced here has remained abandoned, and SAP Commerce Cloud platform versions, dependency versions, and extension architecture have changed significantly since the Drools 7-era examples shown below.

Web applications rely heavily on forms. Questionnaires and comprehensive registration forms are common components of HR, financial, and service solutions. The business rules behind these forms are complex and frequently updated. A well-known example is a job application form. Depending on the user input, some components of the form expand to gather more data from the applicant in a timely, contextual way.

This article is about my experiments with rule-engine-driven forms. I used Drools, the default rule engine that comes with hybris out of the box. I also used the code from the JBoss Tohu project, which has long been abandoned.

At the end of the article, you will find a link to the source code of the project. This is an alpha release of the system, so it needs some polishing before it can be used in production.

I set myself the following objectives:

The first thing that comes to mind is Angular Forms. This topic deserves a separate article. Many choose this path, and it is a good solution. However, it has some drawbacks. Angular is a front-end technology, so to interact with server-side entities you need to build an API layer. Secondly, you need to redeploy and retest the Angular code every 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 the 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 obsolete. 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.

To demonstrate the capabilities of the solution, I needed a comprehensive example. I took some examples from Tohu, refactored and expanded them, and this is what I got:

Dynamic form structure with conditional pages and sections

Let’s put all the business rules together:

This form and its business rules are implemented without a single line of custom Java code, only with Drools rules.

Let’s take a closer look at the rules.

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 1: Loyalty Questionnaire page setup

Rule #2. Initial Setup: Contact Details Page Rule

This rule says that the page contains three sections: PersonalDetails, MainAddress, and PostalAddress.

There is no when part in the rule, which means the rule is a kind of 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 2: Contact Details page sections

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 the 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

Personal Details section fields

Rule #4. Initial Setup: Definition of MainAddress section

Similar code is used for the MainAddress section. It is collapsed by default because there is nothing new.

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

Main Address section fields

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

Postal Address section with Alternative Postal Address checkbox

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 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 them becomes false. So, once you uncheck the “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 need arises.

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

Additional postal fields displayed when Alternative Postal Address is selected

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, which means the rule is executed as part of the initial setup.

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 8: Demographic Details page sections

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).

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

Income Details section with income bracket and own home fields

Rule #10. Definition of Household

The next rule creates the question about the number of people and the “placeholder” for the question about 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 enters a number larger than 1. Once it is created, the system will use the placeholder for the question. It is exactly what 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

Household section with number of people field

Rule #11. Definition of childrenQuestion

In this rule, you can see the condition numberOfPeople > 1. The components are inserted using insertLogical, which means 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

Children question displayed when household size is greater than one

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, and other. Note that they are considered as a group.

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 “Promotional Materials” section with two questions: a checkbox and the promotional types group (promotionalTypesGroup).

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 promotionalTypesGroup

In this rule, we only create the fields that always appear; others are created in different rules. However, all of them 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

Promotional material options displayed after selecting receive materials

Rule #14. Defining Home Improvement

This rule adds an item to the promotional type list if two conditions are true:

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 an 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 select box.

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 toys item is added only if the user has children and has agreed to receive 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

Rule 18: Other Loyalty Programs page

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 to create similar subforms. There are two rules per checkbox. The first rule creates the form, and the other rule creates NavigationBranch, a kind 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 the 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