issue #122 : add ConditionalRuleGroup implementation

pull/144/head
Mahmoud Ben Hassine 7 years ago
parent 79e68108be
commit 7bb0962e34

@ -0,0 +1,142 @@
/**
* The MIT License
*
* Copyright (c) 2017, Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jeasy.rules.support;
import org.jeasy.rules.api.Facts;
import org.jeasy.rules.api.Rule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A conditional rule group is a composite rule where the rule with the highest priority acts as a condition:
* if the rule with the highest priority evaluates to true, then we try to evaluate the rest of the rules
* and execute the ones that evaluate to true.
*
* @author Dag Framstad (dagframstad@gmail.com)
*/
public class ConditionalRuleGroup extends CompositeRule {
private Set<Rule> successfulEvaluations;
private Rule conditionalRule;
/**
* Create a conditional rule group.
*/
public ConditionalRuleGroup() {
}
/**
* Create a conditional rule group.
*
* @param name of the conditional rule
*/
public ConditionalRuleGroup(String name) {
super(name);
}
/**
* Create a conditional rule group.
*
* @param name of the conditional rule
* @param description of the conditional rule
*/
public ConditionalRuleGroup(String name, String description) {
super(name, description);
}
/**
* Create a conditional rule group.
*
* @param name of the conditional rule
* @param description of the conditional rule
* @param priority of the composite rule
*/
public ConditionalRuleGroup(String name, String description, int priority) {
super(name, description, priority);
}
/**
* A path rule will trigger all it's rules if the path rule's condition is true.
* @param facts The facts.
* @return true if the path rules condition is true.
*/
@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;
}
/**
* When a conditional rule group is applied, all rules that evaluated to true are performed
* in their natural order, but with the conditional rule (the one with the highest priority) first.
*
* @param facts The facts.
*
* @throws Exception thrown if an exception occurs during actions performing
*/
@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;
}
}

@ -0,0 +1,327 @@
/**
* The MIT License
*
* Copyright (c) 2017, Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jeasy.rules.support;
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.jeasy.rules.core.DefaultRulesEngine;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ConditionalRuleGroupTest {
@Mock
private Rule rule1, rule2, conditionalRule;
private Facts facts = new Facts();
private Rules rules = new Rules();
private DefaultRulesEngine rulesEngine = new DefaultRulesEngine();
private ConditionalRuleGroup conditionalRuleGroup;
@Before
public void setUp() {
when(rule1.evaluate(facts)).thenReturn(false);
when(rule1.getPriority()).thenReturn(2);
when(rule2.evaluate(facts)).thenReturn(true);
when(rule2.getPriority()).thenReturn(3);
when(rule2.compareTo(rule1)).thenReturn(1);
when(conditionalRule.compareTo(rule1)).thenReturn(1);
when(conditionalRule.compareTo(rule2)).thenReturn(1);
when(conditionalRule.getPriority()).thenReturn(100);
conditionalRuleGroup = new ConditionalRuleGroup();
}
@Test
public void rulesMustNotBeExecutedIfConditionalRuleEvaluatesToFalse() throws Exception {
// Given
when(conditionalRule.evaluate(facts)).thenReturn(false);
conditionalRuleGroup.addRule(rule1);
conditionalRuleGroup.addRule(rule2);
conditionalRuleGroup.addRule(conditionalRule);
rules.register(conditionalRuleGroup);
// When
rulesEngine.fire(rules, facts);
// Then
/*
* The composing rules should not be executed
* since the conditional rule evaluate to FALSE
*/
// primaryRule should not be executed
verify(conditionalRule, never()).execute(facts);
//Rule 1 should not be executed
verify(rule1, never()).execute(facts);
//Rule 2 should not be executed
verify(rule2, never()).execute(facts);
}
@Test
public void rulesMustBeExecutedForThoseThatEvaluateToTrueIfConditionalRuleEvaluatesToTrue() throws Exception {
// Given
when(conditionalRule.evaluate(facts)).thenReturn(true);
conditionalRuleGroup.addRule(rule1);
conditionalRuleGroup.addRule(rule2);
conditionalRuleGroup.addRule(conditionalRule);
rules.register(conditionalRuleGroup);
// When
rulesEngine.fire(rules, facts);
// Then
/*
* Some of he composing rules should be executed
* since the conditional rule evaluate to TRUE
*/
// primaryRule should be executed
verify(conditionalRule, times(1)).execute(facts);
//Rule 1 should not be executed
verify(rule1, never()).execute(facts);
//Rule 2 should be executed
verify(rule2, times(1)).execute(facts);
}
@Test
public void whenARuleIsRemoved_thenItShouldNotBeEvaluated() throws Exception {
// Given
when(conditionalRule.evaluate(facts)).thenReturn(true);
conditionalRuleGroup.addRule(rule1);
conditionalRuleGroup.addRule(rule2);
conditionalRuleGroup.addRule(conditionalRule);
conditionalRuleGroup.removeRule(rule2);
rules.register(conditionalRuleGroup);
// When
rulesEngine.fire(rules, facts);
// Then
// primaryRule should be executed
verify(conditionalRule, times(1)).execute(facts);
//Rule 1 should not be executed
verify(rule1, times(1)).evaluate(facts);
verify(rule1, never()).execute(facts);
// Rule 2 should not be evaluated nor executed
verify(rule2, never()).evaluate(facts);
verify(rule2, never()).execute(facts);
}
@Test
public void testCompositeRuleWithAnnotatedComposingRules() throws Exception {
// Given
when(conditionalRule.evaluate(facts)).thenReturn(true);
MyRule rule = new MyRule();
conditionalRuleGroup = new ConditionalRuleGroup("myConditinalRule");
conditionalRuleGroup.addRule(rule);
when(conditionalRule.compareTo(any(Rule.class))).thenReturn(1);
conditionalRuleGroup.addRule(conditionalRule);
rules.register(conditionalRuleGroup);
// When
rulesEngine.fire(rules, facts);
// Then
verify(conditionalRule, times(1)).execute(facts);
assertThat(rule.isExecuted()).isTrue();
}
@Test
public void whenAnnotatedRuleIsRemoved_thenItsProxyShouldBeRetrieved() throws Exception {
// Given
when(conditionalRule.evaluate(facts)).thenReturn(true);
MyRule rule = new MyRule();
MyAnnotatedRule annotatedRule = new MyAnnotatedRule();
conditionalRuleGroup = new ConditionalRuleGroup("myCompositeRule", "composite rule with mixed types of rules");
conditionalRuleGroup.addRule(rule);
conditionalRuleGroup.addRule(annotatedRule);
conditionalRuleGroup.removeRule(annotatedRule);
when(conditionalRule.compareTo(any(Rule.class))).thenReturn(1);
conditionalRuleGroup.addRule(conditionalRule);
rules.register(conditionalRuleGroup);
// When
rulesEngine.fire(rules, facts);
// Then
verify(conditionalRule, times(1)).execute(facts);
assertThat(rule.isExecuted()).isTrue();
assertThat(annotatedRule.isExecuted()).isFalse();
}
@Test(expected = IllegalArgumentException.class)
public void twoRulesWithSameHighestPriorityIsNotAllowed() {
conditionalRuleGroup.addRule(new MyOtherRule(1));
conditionalRuleGroup.addRule(new MyOtherRule(2));
conditionalRuleGroup.addRule(new MyRule());
rules.register(conditionalRuleGroup);
rulesEngine.fire(rules, facts);
}
@Test
public void twoRulesWithSamePriorityIsAllowedIfAnotherRuleHasHigherPriority() {
MyOtherRule rule1 = new MyOtherRule(3);
conditionalRuleGroup.addRule(rule1);
conditionalRuleGroup.addRule(new MyOtherRule(2));
conditionalRuleGroup.addRule(new MyRule());
rules.register(conditionalRuleGroup);
rulesEngine.fire(rules, facts);
assertThat(rule1.isExecuted()).isTrue();
}
@SuppressWarnings("unchecked")
@Test
public void aRuleWithoutPriorityHasAHighPriororty() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MyOtherRule rule1 = new MyOtherRule(3);
conditionalRuleGroup.addRule(rule1);
conditionalRuleGroup.addRule(new UnprioritizedRule());
Method m = conditionalRuleGroup.getClass().getDeclaredMethod("sortRules");
m.setAccessible(true);
List<Rule> sorted = (List<Rule>)m.invoke(conditionalRuleGroup);
assertThat(sorted.get(0).getPriority()).isEqualTo(Integer.MAX_VALUE - 1);
assertThat(sorted.get(1).getPriority()).isEqualTo(3);
}
@org.jeasy.rules.annotation.Rule
public class MyRule {
boolean executed;
@Condition
public boolean when() {
return true;
}
@Action
public void then() {
executed = true;
}
@Priority
public int priority() {
return 2;
}
public boolean isExecuted() {
return executed;
}
}
@org.jeasy.rules.annotation.Rule
public static class MyAnnotatedRule {
private boolean executed;
@Condition
public boolean evaluate() {
return true;
}
@Action
public void execute() {
executed = true;
}
@Priority
public int priority() {
return 3;
}
public boolean isExecuted() {
return executed;
}
}
@org.jeasy.rules.annotation.Rule
public class MyOtherRule {
boolean executed;
private int priority;
public MyOtherRule(int priority) {
this.priority = priority;
}
@Condition
public boolean when() {
return true;
}
@Action
public void then() {
executed = true;
}
@Priority
public int priority() {
return priority;
}
public boolean isExecuted() {
return executed;
}
}
@org.jeasy.rules.annotation.Rule
public class UnprioritizedRule {
boolean executed;
@Condition
public boolean when() {
return false;
}
@Action
public void then() {
executed = true;
}
public boolean isExecuted() {
return executed;
}
}
}
Loading…
Cancel
Save