issue #122 : add ConditionalRuleGroup implementation
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…
Reference in New Issue