diff --git a/easy-rules-core/src/main/java/org/jeasy/rules/core/DefaultRulesEngine.java b/easy-rules-core/src/main/java/org/jeasy/rules/core/DefaultRulesEngine.java index 8b9dc82..f350524 100644 --- a/easy-rules-core/src/main/java/org/jeasy/rules/core/DefaultRulesEngine.java +++ b/easy-rules-core/src/main/java/org/jeasy/rules/core/DefaultRulesEngine.java @@ -121,7 +121,7 @@ public final class DefaultRulesEngine implements RulesEngine { return result; } - private void apply(Rules rules, Facts facts) { + void apply(Rules rules, Facts facts) { LOGGER.info("Rules evaluation started"); for (Rule rule : rules) { final String name = rule.getName(); diff --git a/easy-rules-core/src/main/java/org/jeasy/rules/core/InferenceRulesEngine.java b/easy-rules-core/src/main/java/org/jeasy/rules/core/InferenceRulesEngine.java new file mode 100644 index 0000000..b09105c --- /dev/null +++ b/easy-rules-core/src/main/java/org/jeasy/rules/core/InferenceRulesEngine.java @@ -0,0 +1,74 @@ +package org.jeasy.rules.core; + +import org.jeasy.rules.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * Inference {@link RulesEngine} implementation. + * + * Rules are selected based on given facts and fired according to their natural order which is priority by default. + * + * The engine continuously select and fire rules until no more rules are applicable. + * + * @author Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com) + */ +public class InferenceRulesEngine implements RulesEngine { + + private static final Logger LOGGER = LoggerFactory.getLogger(InferenceRulesEngine.class); + + private RulesEngineParameters parameters; + private List ruleListeners; + private DefaultRulesEngine delegate; + + public InferenceRulesEngine() { + this(new RulesEngineParameters()); + } + + public InferenceRulesEngine(RulesEngineParameters parameters) { + this(parameters, new ArrayList()); + } + + public InferenceRulesEngine(RulesEngineParameters parameters, List ruleListeners) { + this.parameters = parameters; + this.ruleListeners = ruleListeners; + delegate = new DefaultRulesEngine(parameters, ruleListeners); + } + + @Override + public RulesEngineParameters getParameters() { + return parameters; + } + + @Override + public List getRuleListeners() { + return ruleListeners; + } + + @Override + public void fire(Rules rules, Facts facts) { + Set selectedRules; + do { + LOGGER.info("Selecting candidate rules based on the following {}", facts); + selectedRules = selectCandidates(rules, facts); + delegate.apply(new Rules(selectedRules), facts); + } while (!selectedRules.isEmpty()); + } + + private Set selectCandidates(Rules rules, Facts facts) { + Set candidates = new TreeSet<>(); + for (Rule rule : rules) { + if (rule.evaluate(facts)) { + candidates.add(rule); + } + } + return candidates; + } + + @Override + public Map check(Rules rules, Facts facts) { + return delegate.check(rules, facts); + } +} diff --git a/easy-rules-core/src/test/java/org/jeasy/rules/core/InferenceRulesEngineTest.java b/easy-rules-core/src/test/java/org/jeasy/rules/core/InferenceRulesEngineTest.java new file mode 100644 index 0000000..5b68ab1 --- /dev/null +++ b/easy-rules-core/src/test/java/org/jeasy/rules/core/InferenceRulesEngineTest.java @@ -0,0 +1,115 @@ +package org.jeasy.rules.core; + +import org.jeasy.rules.annotation.*; +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.api.RulesEngine; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InferenceRulesEngineTest { + + @Test + public void testCandidateSelection() throws Exception { + // Given + Facts facts = new Facts(); + facts.put("foo", true); + DummyRule dummyRule = new DummyRule(); + AnotherDummyRule anotherDummyRule = new AnotherDummyRule(); + Rules rules = new Rules(dummyRule, anotherDummyRule); + RulesEngine rulesEngine = new InferenceRulesEngine(); + + // When + rulesEngine.fire(rules, facts); + + // Then + assertThat(dummyRule.isExecuted()).isTrue(); + assertThat(anotherDummyRule.isExecuted()).isFalse(); + } + + @Test + public void testCandidateOrdering() throws Exception { + // Given + Facts facts = new Facts(); + facts.put("foo", true); + facts.put("bar", true); + DummyRule dummyRule = new DummyRule(); + AnotherDummyRule anotherDummyRule = new AnotherDummyRule(); + Rules rules = new Rules(dummyRule, anotherDummyRule); + RulesEngine rulesEngine = new InferenceRulesEngine(); + + // When + rulesEngine.fire(rules, facts); + + // Then + assertThat(dummyRule.isExecuted()).isTrue(); + assertThat(anotherDummyRule.isExecuted()).isTrue(); + assertThat(dummyRule.getTimestamp()).isLessThanOrEqualTo(anotherDummyRule.getTimestamp()); + } + + @Rule + class DummyRule { + + private boolean isExecuted; + private long timestamp; + + @Condition + public boolean when(@Fact("foo") boolean foo) { + return foo; + } + + @Action + public void then(Facts facts) { + isExecuted = true; + timestamp = System.currentTimeMillis(); + facts.remove("foo"); + } + + @Priority + public int priority() { + return 1; + } + + public boolean isExecuted() { + return isExecuted; + } + + public long getTimestamp() { + return timestamp; + } + } + + @Rule + class AnotherDummyRule { + + private boolean isExecuted; + private long timestamp; + + @Condition + public boolean when(@Fact("bar") boolean bar) { + return bar; + } + + @Action + public void then(Facts facts) { + isExecuted = true; + timestamp = System.currentTimeMillis(); + facts.remove("bar"); + } + + @Priority + public int priority() { + return 2; + } + + public boolean isExecuted() { + return isExecuted; + } + + public long getTimestamp() { + return timestamp; + } + } + +} \ No newline at end of file