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