Add composite rule support for MVEL from a YAML file

Resolves #155
pull/200/head
Kate Rose 7 years ago committed by Mahmoud Ben Hassine
parent 00f52b388c
commit f88a2cda5b

@ -0,0 +1,60 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
public class MVELActivationRuleGroup extends MVELCompositeRule {
private Rule selectedRule;
public MVELActivationRuleGroup() { super(); }
/**
* Set rule name.
*
* @param name of the rule
* @return this rule
*/
public MVELActivationRuleGroup name(String name) {
this.name = name;
return this;
}
/**
* Set rule description.
*
* @param description of the rule
* @return this rule
*/
public MVELActivationRuleGroup description(String description) {
this.description = description;
return this;
}
/**
* Set rule priority.
*
* @param priority of the rule
* @return this rule
*/
public MVELActivationRuleGroup priority(int priority) {
this.priority = priority;
return this;
}
@Override
public boolean evaluate(Facts facts) {
for (Rule rule : rules) {
if (rule.evaluate(facts)) {
selectedRule = rule;
return true;
}
}
return false;
}
@Override
public void execute(Facts facts) throws Exception {
selectedRule.execute(facts);
}
}

@ -0,0 +1,54 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.core.RuleProxy;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public abstract class MVELCompositeRule extends MVELRule {
protected Set<Rule> rules;
private Map<Object, Rule> proxyRules;
/**
* Create a new MVEL composite rule.
*/
public MVELCompositeRule() {
super();
rules = new TreeSet<>();
proxyRules = new HashMap<>();
}
/**
* Add a rule to the composite rule.
* @param rule the rule to add
*/
public void addRule(final Object rule) {
Rule proxy = RuleProxy.asRule(rule);
rules.add(proxy);
proxyRules.put(rule, proxy);
}
/**
* Remove a rule from the composite rule.
* @param rule the rule to remove
*/
public void removeRule(final Object rule) {
Rule proxy = proxyRules.get(rule);
if (proxy != null) {
rules.remove(proxy);
}
}
/**
* Get the member rules in this composite.
*/
public Set<Rule> getRules() {
return rules;
}
}

@ -0,0 +1,93 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import java.util.*;
public class MVELConditionalRuleGroup extends MVELCompositeRule {
private Set<Rule> successfulEvaluations;
private Rule conditionalRule;
public MVELConditionalRuleGroup() { super(); }
/**
* Set rule name.
*
* @param name of the rule
* @return this rule
*/
public MVELConditionalRuleGroup name(String name) {
this.name = name;
return this;
}
/**
* Set rule description.
*
* @param description of the rule
* @return this rule
*/
public MVELConditionalRuleGroup description(String description) {
this.description = description;
return this;
}
/**
* Set rule priority.
*
* @param priority of the rule
* @return this rule
*/
public MVELConditionalRuleGroup priority(int priority) {
this.priority = priority;
return this;
}
@Override
public boolean evaluate(Facts facts) {
successfulEvaluations = new HashSet<>();
conditionalRule = getRuleWithHighestPriority();
if (conditionalRule.evaluate(facts)) {
for (Rule rule : rules) {
if (rule != conditionalRule && rule.evaluate(facts)) {
successfulEvaluations.add(rule);
}
}
return true;
}
return false;
}
@Override
public void execute(Facts facts) throws Exception {
conditionalRule.execute(facts);
for (Rule rule : successfulEvaluations) {
rule.execute(facts);
}
}
private Rule getRuleWithHighestPriority() {
List<Rule> copy = sortRules();
// make sure that we only have one rule with the highest priority
Rule highest = copy.get(0);
if (copy.size() > 1 && copy.get(1).getPriority() == highest.getPriority()) {
throw new IllegalArgumentException("Only one rule can have highest priority");
}
return highest;
}
private List<Rule> sortRules() {
List<Rule> copy = new ArrayList<>(rules);
Collections.sort(copy, new Comparator<Rule>() {
@Override
public int compare(Rule o1, Rule o2) {
Integer i2 = o2.getPriority();
return i2.compareTo(o1.getPriority());
}
});
return copy;
}
}

@ -24,7 +24,10 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.api.Rules;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class MVELRuleDefinition {
@ -34,6 +37,11 @@ class MVELRuleDefinition {
private int priority = Rule.DEFAULT_PRIORITY;
private String condition;
private List<String> actions;
private Rules subrules;
private String compositeRuleType;
private List<String> allowedCompositeTypes = new ArrayList<>(
Arrays.asList("UnitRuleGroup", "ConditionalRuleGroup", "ActivationRuleGroup")
);
public String getName() {
return name;
@ -75,15 +83,62 @@ class MVELRuleDefinition {
this.actions = actions;
}
public void setSubrules(List<MVELRuleDefinition> subruleDefinitions, String compositeRuleType) {
subrules = new Rules();
for (MVELRuleDefinition ruleDef : subruleDefinitions) {
MVELRule r = ruleDef.create();
subrules.register(r);
}
setCompositeRuleType(compositeRuleType);
}
public void setCompositeRuleType(String compositeRuleType) { this.compositeRuleType = compositeRuleType; }
public String getCompositeRuleType() { return compositeRuleType; }
public Rules getSubrules() { return subrules; }
MVELRule create() {
MVELRule mvelRule = new MVELRule()
.name(name)
.description(description)
.priority(priority)
.when(condition);
for (String action : actions) {
mvelRule.then(action);
if (subrules == null) {
MVELRule mvelRule = new MVELRule()
.name(name)
.description(description)
.priority(priority)
.when(condition);
for (String action : actions) {
mvelRule.then(action);
}
return mvelRule;
} else {
if (allowedCompositeTypes.contains(compositeRuleType)) {
MVELCompositeRule compositeRule;
switch (compositeRuleType) {
case "UnitRuleGroup":
compositeRule = new MVELUnitRuleGroup();
break;
case "ActivationRuleGroup":
compositeRule = new MVELActivationRuleGroup();
break;
case "ConditionalRuleGroup":
compositeRule = new MVELConditionalRuleGroup();
break;
default:
throw new IllegalArgumentException("Invalid composite rule type");
}
compositeRule.name(name);
compositeRule.description(description);
compositeRule.priority(priority);
for (Rule rule : subrules) {
compositeRule.addRule(rule);
}
return compositeRule;
} else {
throw new IllegalArgumentException("Invalid composite rule type");
}
}
return mvelRule;
}
}

@ -64,18 +64,34 @@ class MVELRuleDefinitionReader {
Integer priority = (Integer) map.get("priority");
ruleDefinition.setPriority(priority != null ? priority : Rule.DEFAULT_PRIORITY);
String compositeRuleType = (String) map.get("compositeType");
String condition = (String) map.get("condition");
if (condition == null ) {
if (condition == null && compositeRuleType == null) {
throw new IllegalArgumentException("The rule condition must be specified");
}
ruleDefinition.setCondition(condition);
List<String> actions = (List<String>) map.get("actions");
if (actions == null || actions.isEmpty()) {
if ((actions == null || actions.isEmpty()) && compositeRuleType == null) {
throw new IllegalArgumentException("The rule action(s) must be specified");
}
ruleDefinition.setActions(actions);
List<Object> subrules = (List<Object>) map.get("subrules");
if (subrules != null && compositeRuleType == null) {
throw new IllegalArgumentException("Non-composite rules cannot have subrules");
} else if (subrules == null && compositeRuleType != null) {
throw new IllegalArgumentException("Composite rules must have subrules specified");
} else if (subrules != null) {
List<MVELRuleDefinition> subruleDefinitions = new ArrayList<>();
for (Object rule : subrules){
Map<String, Object> subruleMap = (Map<String, Object>) rule;
subruleDefinitions.add(createRuleDefinitionFrom(subruleMap));
}
ruleDefinition.setSubrules(subruleDefinitions, compositeRuleType);
}
return ruleDefinition;
}
}

@ -0,0 +1,64 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
public class MVELUnitRuleGroup extends MVELCompositeRule {
public MVELUnitRuleGroup() {
super();
}
/**
* Set rule name.
*
* @param name of the rule
* @return this rule
*/
public MVELUnitRuleGroup name(String name) {
this.name = name;
return this;
}
/**
* Set rule description.
*
* @param description of the rule
* @return this rule
*/
public MVELUnitRuleGroup description(String description) {
this.description = description;
return this;
}
/**
* Set rule priority.
*
* @param priority of the rule
* @return this rule
*/
public MVELUnitRuleGroup priority(int priority) {
this.priority = priority;
return this;
}
@Override
public boolean evaluate(Facts facts) {
if (!rules.isEmpty()) {
for (Rule rule : rules) {
if (!rule.evaluate(facts)) {
return false;
}
}
return true;
}
return false;
}
@Override
public void execute(Facts facts) throws Exception {
for (Rule rule : rules) {
rule.execute(facts);
}
}
}

@ -0,0 +1,26 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Facts;
@org.jeasy.rules.annotation.Rule(name = "myRule", description = "my rule description")
public class AnnotatedMVELRule extends MVELRule {
private boolean executed;
private boolean evaluated;
public boolean evaluate(Facts facts) {
evaluated = true;
return super.evaluate(facts);
}
public void execute(Facts facts) throws Exception {
super.execute(facts);
executed = true;
}
public boolean isExecuted() { return executed; }
public boolean isEvaluated() { return evaluated; }
}

@ -0,0 +1,88 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.annotation.Action;
import org.jeasy.rules.annotation.Condition;
import org.jeasy.rules.annotation.Priority;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import org.jeasy.rules.api.Rules;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class MVELActivationRuleGroupTest {
private Facts facts = new Facts();
private MVELActivationRuleGroup activationRuleGroup;
private MVELRule subrule1 = new AnnotatedMVELRule()
.name("subrule1")
.priority(1)
.when("person.age > 18")
.then("person.setAdult(true);");
private MVELRule subrule2 = new AnnotatedMVELRule()
.name("subrule2")
.priority(2)
.when("person.age > 13")
.then("person.setTeenager(true);");
@Before
public void setUp() throws Exception {
activationRuleGroup = new MVELActivationRuleGroup()
.name("activation rule group")
.description("description")
.priority(1);
}
@Test
public void whenTheRuleIsTriggered_conditionEvaluatesTrue() throws Exception {
activationRuleGroup.addRule(subrule1);
activationRuleGroup.addRule(subrule2);
// given
facts.put("person", new Person("foo", 20));
// when
boolean evaluationResult = activationRuleGroup.evaluate(facts);
// then
assertThat(evaluationResult).isTrue();
assertThat(((AnnotatedMVELRule) subrule1).isEvaluated()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isEvaluated()).isFalse();
}
@Test
public void whenTheRuleIsTriggered_conditionEvaluatesFalse() throws Exception {
activationRuleGroup.addRule(subrule1);
activationRuleGroup.addRule(subrule2);
// given
facts.put("person", new Person("foo", 12));
// when
boolean evaluationResult = activationRuleGroup.evaluate(facts);
// then
assertThat(evaluationResult).isFalse();
assertThat(((AnnotatedMVELRule) subrule1).isEvaluated()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isEvaluated()).isTrue();
}
@Test
public void whenTheConditionIsTrue_thenActionsShouldBeExecuted() throws Exception {
activationRuleGroup.addRule(subrule1);
activationRuleGroup.addRule(subrule2);
// given
Person foo = new Person("foo", 20);
facts.put("person", foo);
// when
activationRuleGroup.evaluate(facts);
activationRuleGroup.execute(facts);
// then
assertThat(foo.isAdult()).isTrue();
assertThat(((AnnotatedMVELRule) subrule1).isExecuted()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isExecuted()).isFalse();
}
}

@ -0,0 +1,83 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Facts;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class MVELConditionalRuleGroupTest {
private Facts facts = new Facts();
private MVELConditionalRuleGroup conditionalRuleGroup;
private MVELRule subrule1 = new AnnotatedMVELRule()
.name("subrule1")
.priority(1)
.when("person.age > 18")
.then("person.orderDrink();");
private MVELRule subrule2 = new AnnotatedMVELRule()
.name("subrule2")
.priority(2)
.when("person.age > 18")
.then("person.setAdult(true);");
@Before
public void setUp() throws Exception {
conditionalRuleGroup = new MVELConditionalRuleGroup()
.name("conditional rule group")
.description("description")
.priority(1);
}
@Test
public void whenTheRuleIsTriggered_conditionEvaluatesTrue() throws Exception {
conditionalRuleGroup.addRule(subrule1);
conditionalRuleGroup.addRule(subrule2);
// given
facts.put("person", new Person("foo", 20));
// when
boolean evaluationResult = conditionalRuleGroup.evaluate(facts);
// then
assertThat(evaluationResult).isTrue();
assertThat(((AnnotatedMVELRule) subrule1).isEvaluated()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isEvaluated()).isTrue();
}
@Test
public void whenTheRuleIsTriggered_conditionEvaluatesFalse() throws Exception {
conditionalRuleGroup.addRule(subrule1);
conditionalRuleGroup.addRule(subrule2);
// given
facts.put("person", new Person("foo", 16));
// when
boolean evaluationResult = conditionalRuleGroup.evaluate(facts);
// then
assertThat(evaluationResult).isFalse();
assertThat(((AnnotatedMVELRule) subrule1).isEvaluated()).isFalse();
assertThat(((AnnotatedMVELRule) subrule2).isEvaluated()).isTrue();
}
@Test
public void whenTheConditionIsTrue_thenActionsShouldBeExecuted() throws Exception {
conditionalRuleGroup.addRule(subrule1);
conditionalRuleGroup.addRule(subrule2);
// given
Person foo = new Person("foo", 20);
facts.put("person", foo);
// when
conditionalRuleGroup.evaluate(facts);
conditionalRuleGroup.execute(facts);
// then
assertThat(foo.isAdult()).isTrue();
assertThat(((AnnotatedMVELRule) subrule1).isExecuted()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isExecuted()).isTrue();
}
}

@ -153,4 +153,35 @@ public class MVELRuleDefinitionReaderTest {
// then
assertThat(ruleDefinitions).hasSize(0);
}
@Test
public void testRuleDefinitionReading_withCompositeAndBasicRules() throws Exception {
// given
File compositeRuleDescriptor = new File("src/test/resources/composite-rule.yml");
// when
List<MVELRuleDefinition> ruleDefinitions = ruleDefinitionReader.readAll(new FileReader(compositeRuleDescriptor));
// then
assertThat(ruleDefinitions).hasSize(2);
// then
MVELRuleDefinition ruleDefinition = ruleDefinitions.get(0);
assertThat(ruleDefinition).isNotNull();
assertThat(ruleDefinition.getName()).isEqualTo("Movie id rule");
assertThat(ruleDefinition.getDescription()).isEqualTo("description");
assertThat(ruleDefinition.getCompositeRuleType()).isEqualTo("UnitRuleGroup");
assertThat(ruleDefinition.getSubrules().isEmpty()).isFalse();
for (Rule subrule : ruleDefinition.getSubrules()) {
assertThat(subrule.getPriority()).isEqualTo(1);
}
ruleDefinition = ruleDefinitions.get(1);
assertThat(ruleDefinition).isNotNull();
assertThat(ruleDefinition.getName()).isEqualTo("weather rule");
assertThat(ruleDefinition.getDescription()).isEqualTo("when it rains, then take an umbrella");
assertThat(ruleDefinition.getSubrules()).isNull();
assertThat(ruleDefinition.getCondition()).isEqualTo("rain == True");
assertThat(ruleDefinition.getActions()).isEqualTo(Collections.singletonList("System.out.println(\"It rains, take an umbrella!\");"));
}
}

@ -91,4 +91,29 @@ public class MVELRuleFactoryTest {
assertThat(adultRule.getDescription()).isEqualTo("when age is greater then 18, then mark as adult");
assertThat(adultRule.getPriority()).isEqualTo(1);
}
@Test
public void testRuleCreationFromFileReader__withCompositeRules() throws Exception{
// given
File rulesDescriptor = new File("src/test/resources/composite-rule.yml");
// when
Rules rules = MVELRuleFactory.createRulesFrom(new FileReader(rulesDescriptor));
// then
assertThat(rules).hasSize(2);
Iterator<Rule> iterator = rules.iterator();
Rule rule = iterator.next();
assertThat(rule).isNotNull();
assertThat(rule.getName()).isEqualTo("Movie id rule");
assertThat(rule.getDescription()).isEqualTo("description");
assertThat(rule.getPriority()).isEqualTo(1);
rule = iterator.next();
assertThat(rule).isNotNull();
assertThat(rule.getName()).isEqualTo("weather rule");
assertThat(rule.getDescription()).isEqualTo("when it rains, then take an umbrella");
assertThat(rule.getPriority()).isEqualTo(1);;
}
}

@ -0,0 +1,87 @@
package org.jeasy.rules.mvel;
import org.jeasy.rules.api.Facts;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class MVELUnitRuleGroupTest {
private Facts facts = new Facts();
private MVELUnitRuleGroup unitRuleGroup;
private MVELRule subrule1 = new AnnotatedMVELRule()
.name("subrule1")
.priority(1)
.when("person.age > 18")
.then("person.setAdult(true);");
private MVELRule subrule2 = new AnnotatedMVELRule()
.name("subrule2")
.priority(1)
.when("person.age >= 21")
.then("person.setCanDrinkInUS(true);");
@Before
public void setUp() throws Exception {
unitRuleGroup = new MVELUnitRuleGroup()
.name("unit rule group")
.description("description")
.priority(1);
}
@Test
public void whenTheRuleIsTriggered_conditionEvaluatesTrue() throws Exception {
unitRuleGroup.addRule(subrule1);
unitRuleGroup.addRule(subrule2);
// given
facts.put("person", new Person("foo", 21));
// when
boolean evaluationResult = unitRuleGroup.evaluate(facts);
// then
assertThat(evaluationResult).isTrue();
assertThat(((AnnotatedMVELRule) subrule1).isEvaluated()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isEvaluated()).isTrue();
}
@Test
public void whenTheRuleIsTriggered_conditionEvaluatesFalse() throws Exception {
MVELRule subrule3 = new AnnotatedMVELRule()
.name("subrule3")
.priority(1)
.when("person.age < 18")
.then("System.out.println(\"Not an adult\");");
unitRuleGroup.addRule(subrule1);
unitRuleGroup.addRule(subrule3);
// given
facts.put("person", new Person("foo", 20));
// when
boolean evaluationResult = unitRuleGroup.evaluate(facts);
// then
assertThat(evaluationResult).isFalse();
assertThat(((AnnotatedMVELRule) subrule1).isEvaluated()).isTrue();
assertThat(((AnnotatedMVELRule) subrule3).isEvaluated()).isTrue();
}
@Test
public void whenTheConditionIsTrue_thenActionsShouldBeExecuted() throws Exception {
unitRuleGroup.addRule(subrule1);
unitRuleGroup.addRule(subrule2);
// given
Person foo = new Person("foo", 20);
facts.put("person", foo);
// when
unitRuleGroup.execute(facts);
// then
assertThat(foo.isAdult()).isTrue();
assertThat(((AnnotatedMVELRule) subrule1).isExecuted()).isTrue();
assertThat(((AnnotatedMVELRule) subrule2).isExecuted()).isTrue();
}
}

@ -0,0 +1,23 @@
name: Movie id rule
compositeType: UnitRuleGroup
priority: 1
subrules:
- name: Time is evening
description: If it's later than 7pm
priority: 1
condition: "day.hour > 19"
actions:
- "person.shouldProvideId(true);"
- name: Movie is rated R
description: If the movie is rated R
priority: 1
condition: "movie.rating == R"
actions:
- "person.shouldProvideId(true);"
---
name: weather rule
description: when it rains, then take an umbrella
priority: 1
condition: "rain == True"
actions:
- "System.out.println(\"It rains, take an umbrella!\");"

@ -52,7 +52,7 @@
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>${parent.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>

Loading…
Cancel
Save