This is an automated email from the ASF dual-hosted git repository. mattyb149 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/master by this push: new abf223d NIFI-7163 - added RulesEngine and RulesEngineProvider interfaces, enhanced easy rules to support provider interface and refactored to extract rules engine implementation abf223d is described below commit abf223d574f0d5c1d3484374535e6ae1d0f6c6c0 Author: Yolanda M. Davis <yolanda.m.da...@gmail.com> AuthorDate: Mon Feb 24 15:27:39 2020 -0500 NIFI-7163 - added RulesEngine and RulesEngineProvider interfaces, enhanced easy rules to support provider interface and refactored to extract rules engine implementation NIFI-7163 - updated documentation and comments NIFI-7163 - fix checkstyle issues Signed-off-by: Matthew Burgess <mattyb...@apache.org> This closes #4081 --- .../nifi-easyrules-service/pom.xml | 6 + .../org/apache/nifi/rules/RulesMVELCondition.java | 3 +- .../org/apache/nifi/rules/RulesSPELCondition.java | 3 +- ...java => AbstractEasyRulesEngineController.java} | 114 +++--------- .../apache/nifi/rules/engine/EasyRulesEngine.java | 178 +++++++++++++++++++ .../nifi/rules/engine/EasyRulesEngineProvider.java | 37 ++++ .../nifi/rules/engine/EasyRulesEngineService.java | 191 +++------------------ .../org.apache.nifi.controller.ControllerService | 3 +- .../additionalDetails.html | 5 +- .../org/apache/nifi/rules/TestRulesCondition.java | 100 +++++++++++ .../nifi/rules/engine/TestEasyRulesEngine.java | 87 ++++++++++ .../rules/engine/TestEasyRulesEngineProvider.java | 59 +++++++ .../rules/engine/TestEasyRulesEngineService.java | 25 ++- .../src/test/resources/test_nifi_rules_filter.json | 34 ++++ .../main/java/org/apache/nifi/rules/Action.java | 12 +- .../src/main/java/org/apache/nifi/rules/Rule.java | 24 ++- .../rules/{Action.java => engine/RulesEngine.java} | 40 ++--- ...EngineService.java => RulesEngineProvider.java} | 21 +-- .../nifi/rules/engine/RulesEngineService.java | 2 +- 19 files changed, 626 insertions(+), 318 deletions(-) diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/pom.xml b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/pom.xml index c36cfe4..9a0ae5a 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/pom.xml +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/pom.xml @@ -50,6 +50,11 @@ <version>3.3.0</version> </dependency> <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-jexl3</artifactId> + <version>3.1</version> + </dependency> + <dependency> <groupId>org.jeasy</groupId> <artifactId>easy-rules-spel</artifactId> <version>3.3.0</version> @@ -76,6 +81,7 @@ <excludes combine.children="append"> <exclude>src/test/resources/test_nifi_rules.json</exclude> <exclude>src/test/resources/test_nifi_rules.yml</exclude> + <exclude>src/test/resources/test_nifi_rules_filter.json</exclude> <exclude>src/test/resources/test_mvel_rules.json</exclude> <exclude>src/test/resources/test_mvel_rules.yml</exclude> <exclude>src/test/resources/test_spel_rules.json</exclude> diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesMVELCondition.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesMVELCondition.java index 7bd615f..969d445 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesMVELCondition.java +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesMVELCondition.java @@ -19,7 +19,6 @@ package org.apache.nifi.rules; import org.jeasy.rules.api.Condition; import org.jeasy.rules.api.Facts; -import org.jeasy.rules.mvel.MVELCondition; import org.mvel2.MVEL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,7 +27,7 @@ import java.io.Serializable; public class RulesMVELCondition implements Condition { - private static final Logger LOGGER = LoggerFactory.getLogger(MVELCondition.class); + private static final Logger LOGGER = LoggerFactory.getLogger(RulesMVELCondition.class); private String expression; private Serializable compiledExpression; private boolean ignoreConditionErrors; diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesSPELCondition.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesSPELCondition.java index e753f93..881da6b 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesSPELCondition.java +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/RulesSPELCondition.java @@ -18,7 +18,6 @@ package org.apache.nifi.rules; import org.jeasy.rules.api.Condition; import org.jeasy.rules.api.Facts; -import org.jeasy.rules.spel.SpELCondition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.expression.Expression; @@ -28,7 +27,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; public class RulesSPELCondition implements Condition { - private static final Logger LOGGER = LoggerFactory.getLogger(SpELCondition.class); + private static final Logger LOGGER = LoggerFactory.getLogger(RulesSPELCondition.class); private final ExpressionParser parser = new SpelExpressionParser(); private String expression; private Expression compiledExpression; diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/AbstractEasyRulesEngineController.java similarity index 67% copy from nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java copy to nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/AbstractEasyRulesEngineController.java index 187f0fe..b7b5e9d 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/AbstractEasyRulesEngineController.java @@ -16,8 +16,6 @@ */ package org.apache.nifi.rules.engine; -import org.apache.nifi.annotation.documentation.CapabilityDescription; -import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnEnabled; import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; @@ -30,19 +28,9 @@ import org.apache.nifi.controller.ControllerServiceInitializationContext; import org.apache.nifi.expression.ExpressionLanguageScope; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.reporting.InitializationException; -import org.apache.nifi.rules.Action; -import org.apache.nifi.rules.ActionHandler; import org.apache.nifi.rules.Rule; import org.apache.nifi.rules.RulesFactory; -import org.apache.nifi.rules.RulesMVELCondition; -import org.apache.nifi.rules.RulesSPELCondition; import org.apache.nifi.util.StringUtils; -import org.jeasy.rules.api.Condition; -import org.jeasy.rules.api.Facts; -import org.jeasy.rules.api.RuleListener; -import org.jeasy.rules.api.Rules; -import org.jeasy.rules.core.DefaultRulesEngine; -import org.jeasy.rules.core.RuleBuilder; import java.util.ArrayList; import java.util.Collection; @@ -52,15 +40,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -/** - * Implementation of RulesEngineService interface - * - * @see RulesEngineService - */ -@CapabilityDescription("Defines and execute the rules stored in NiFi or EasyRules file formats for a given set of facts. Supports " + - "rules stored as JSON or YAML file types.") -@Tags({ "rules","rules-engine","engine","actions","facts" }) -public class EasyRulesEngineService extends AbstractControllerService implements RulesEngineService { +public abstract class AbstractEasyRulesEngineController extends AbstractControllerService { static final AllowableValue YAML = new AllowableValue("YAML", "YAML", "YAML file configuration type."); static final AllowableValue JSON = new AllowableValue("JSON", "JSON", "JSON file configuration type."); @@ -116,10 +96,23 @@ public class EasyRulesEngineService extends AbstractControllerService implement .allowableValues("true", "false") .build(); + static final PropertyDescriptor FILTER_RULES_MISSING_FACTS = new PropertyDescriptor.Builder() + .name("rules-filter-missing-facts") + .displayName("Filter Rules With Missing Facts") + .description("When set to true, the rules engine will first filter out any rule where fact are not available before " + + "executing a check or firing that rule. When running a check rules this will return only rules " + + "that were evaluated after filtering. NOTE: This is only applicable for the NIFI Rules Format (which allows" + + " specification of fact variables) and will be ignored for other formats.") + .required(true) + .defaultValue("false") + .allowableValues("true", "false") + .build(); + protected List<PropertyDescriptor> properties; - protected volatile List<Rule> rules; + protected List<Rule> rules; protected volatile String rulesFileFormat; - private boolean ignoreConditionErrors; + protected boolean ignoreConditionErrors; + protected boolean filterRules; @Override protected void init(ControllerServiceInitializationContext config) throws InitializationException { @@ -130,6 +123,7 @@ public class EasyRulesEngineService extends AbstractControllerService implement properties.add(RULES_BODY); properties.add(RULES_FILE_FORMAT); properties.add(IGNORE_CONDITION_ERRORS); + properties.add(FILTER_RULES_MISSING_FACTS); this.properties = Collections.unmodifiableList(properties); } @@ -145,6 +139,8 @@ public class EasyRulesEngineService extends AbstractControllerService implement final String rulesFileType = context.getProperty(RULES_FILE_TYPE).getValue(); rulesFileFormat = context.getProperty(RULES_FILE_FORMAT).getValue(); ignoreConditionErrors = context.getProperty(IGNORE_CONDITION_ERRORS).asBoolean(); + filterRules = context.getProperty(FILTER_RULES_MISSING_FACTS).asBoolean(); + try{ if(StringUtils.isEmpty(rulesFile)){ rules = RulesFactory.createRulesFromString(rulesBody, rulesFileType, rulesFileFormat); @@ -174,79 +170,15 @@ public class EasyRulesEngineService extends AbstractControllerService implement results.add(new ValidationResult.Builder().subject("Rules Body or Rules File").valid(false).explanation( "exactly one of Rules File or Rules Body must be set").build()); } - return results; } - - /** - * Return the list of actions what should be executed for a given set of facts - * @param facts a Map of key and facts values, as objects, that should be evaluated by the rules engine - * @return - */ - @Override - public List<Action> fireRules(Map<String, Object> facts) { - final List<Action> actions = new ArrayList<>(); - if (rules == null || facts == null || facts.isEmpty()) { - return null; - }else { - org.jeasy.rules.api.Rules easyRules = convertToEasyRules(rules, (action, eventFacts) -> - actions.add(action)); - Facts easyFacts = new Facts(); - facts.forEach(easyFacts::put); - DefaultRulesEngine rulesEngine = new DefaultRulesEngine(); - rulesEngine.registerRuleListener(new EasyRulesListener()); - rulesEngine.fire(easyRules, easyFacts); - return actions; - } - } - - - protected Rules convertToEasyRules(List<Rule> rules, ActionHandler actionHandler) { - final Rules easyRules = new Rules(); + protected RulesEngine getRulesEngine() { + List<Rule> rulesCopy = new ArrayList<>(); rules.forEach(rule -> { - RuleBuilder ruleBuilder = new RuleBuilder(); - Condition condition = rulesFileFormat.equalsIgnoreCase(SPEL.getValue()) - ? new RulesSPELCondition(rule.getCondition(), ignoreConditionErrors): new RulesMVELCondition(rule.getCondition(), ignoreConditionErrors); - ruleBuilder.name(rule.getName()) - .description(rule.getDescription()) - .priority(rule.getPriority()) - .when(condition); - for (Action action : rule.getActions()) { - ruleBuilder.then(facts -> { - actionHandler.execute(action, facts.asMap()); - }); - } - easyRules.register(ruleBuilder.build()); + rulesCopy.add(rule.clone()); }); - return easyRules; - } - - private class EasyRulesListener implements RuleListener { - @Override - public boolean beforeEvaluate(org.jeasy.rules.api.Rule rule, Facts facts) { - return true; - } - - @Override - public void afterEvaluate(org.jeasy.rules.api.Rule rule, Facts facts, boolean b) { - - } - - @Override - public void beforeExecute(org.jeasy.rules.api.Rule rule, Facts facts) { - - } - - @Override - public void onSuccess(org.jeasy.rules.api.Rule rule, Facts facts) { - getLogger().debug("Rules was successfully processed for: {}",new Object[]{rule.getName()}); - } - - @Override - public void onFailure(org.jeasy.rules.api.Rule rule, Facts facts, Exception e) { - getLogger().warn("Rule execution failed for: {}", new Object[]{rule.getName()}, e); - } + return new EasyRulesEngine(rulesFileFormat, ignoreConditionErrors, filterRules, Collections.unmodifiableList(rulesCopy)); } } diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngine.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngine.java new file mode 100644 index 0000000..3a950a8 --- /dev/null +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngine.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.rules.engine; + +import org.apache.nifi.rules.Action; +import org.apache.nifi.rules.Rule; +import org.apache.nifi.rules.RulesMVELCondition; +import org.apache.nifi.rules.RulesSPELCondition; +import org.jeasy.rules.api.Condition; +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.RuleListener; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.core.BasicRule; +import org.jeasy.rules.core.DefaultRulesEngine; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + + +public class EasyRulesEngine implements RulesEngine { + + protected String rulesFileFormat; + protected boolean ignoreConditionErrors; + protected boolean filterRulesMissingFacts; + protected Rules easyRules; + protected List<RuleListener> ruleListeners; + protected DefaultRulesEngine rulesEngine; + + + public EasyRulesEngine(String rulesFileFormat, boolean ignoreConditionErrors, boolean filterRulesMissingFacts, List<Rule> rules) { + this.rulesFileFormat = rulesFileFormat; + this.ignoreConditionErrors = ignoreConditionErrors; + this.filterRulesMissingFacts = filterRulesMissingFacts; + this.easyRules = convertToEasyRules(rules, rulesFileFormat, ignoreConditionErrors); + this.rulesEngine = new DefaultRulesEngine(); + if (getRuleListeners() != null) { + rulesEngine.registerRuleListeners(getRuleListeners()); + } + } + + /** + * Return the list of actions what should be executed for a given set of facts + * + * @param facts a Map of key and facts values, as objects, that should be evaluated by the rules engine + * @return + */ + @Override + public List<Action> fireRules(Map<String, Object> facts) { + final List<Action> actions = new ArrayList<>(); + Map<Rule, Boolean> checkedRules = checkRules(facts); + checkedRules.forEach((checkedRule, executeRule) -> { + if (executeRule) { + actions.addAll(checkedRule.getActions()); + } + }); + return actions; + } + + /** + * Return a Map with Rule as a key and Boolean as a value indicating that the rule's conditions were met + * + * @param facts Map of keys and values contains facts to evaluate against rules + * @return + */ + @Override + public Map<Rule, Boolean> checkRules(Map<String, Object> facts) { + Map<Rule, Boolean> checkedRules = new HashMap<>(); + if (easyRules == null || facts == null || facts.isEmpty()) { + return null; + } else { + Facts easyFacts = new Facts(); + facts.forEach(easyFacts::put); + + Map<org.jeasy.rules.api.Rule, Boolean> checkedEasyRules = rulesEngine.check(filterRulesMissingFacts ? filterByAvailableFacts(facts) : easyRules, easyFacts); + checkedEasyRules.forEach((checkedRuled, executeAction) -> { + checkedRules.put(((NiFiEasyRule) checkedRuled).getNifiRule(), executeAction); + }); + + } + return checkedRules; + } + + public List<Rule> getRules() { + return StreamSupport.stream(easyRules.spliterator(), false) + .map(easyRule -> ((NiFiEasyRule) easyRule) + .getNifiRule()).collect(Collectors.toList()); + } + + List<RuleListener> getRuleListeners() { + return ruleListeners; + } + + void setRuleListeners(List<RuleListener> ruleListeners) { + this.ruleListeners = ruleListeners; + } + + private org.jeasy.rules.api.Rules convertToEasyRules(List<Rule> rules, String rulesFileFormat, Boolean ignoreConditionErrors) { + final Rules easyRules = new Rules(); + for (Rule rule : rules) { + easyRules.register(new NiFiEasyRule(rule, rulesFileFormat, ignoreConditionErrors)); + } + return easyRules; + } + + private Rules filterByAvailableFacts(Map<String, Object> facts) { + Set<String> factVariables = facts.keySet(); + List<org.jeasy.rules.api.Rule> filteredEasyRules = StreamSupport.stream(easyRules.spliterator(), false) + .filter(easyRule -> ((NiFiEasyRule) easyRule).getNifiRule().getFacts() == null || factVariables.containsAll(((NiFiEasyRule) easyRule) + .getNifiRule().getFacts())).collect(Collectors.toList()); + + return new Rules(new HashSet(filteredEasyRules)); + } + + private static class NiFiEasyRule extends BasicRule { + + private Condition condition; + private Rule nifiRule; + + NiFiEasyRule(Rule nifiRule, String rulesFileFormat, Boolean ignoreConditionErrors) { + super(nifiRule.getName(), nifiRule.getDescription(), nifiRule.getPriority()); + this.condition = rulesFileFormat.equalsIgnoreCase("spel") + ? new RulesSPELCondition(nifiRule.getCondition(), ignoreConditionErrors) : new RulesMVELCondition(nifiRule.getCondition(), ignoreConditionErrors); + this.nifiRule = nifiRule; + } + + public boolean evaluate(Facts facts) { + + final Facts evaluateFacts; + + if (nifiRule.getFacts() != null) { + + List<Map.Entry<String, Object>> filteredFacts = StreamSupport.stream(facts.spliterator(), false) + .filter(fact -> nifiRule.getFacts().contains(fact.getKey())).collect(Collectors.toList()); + + if (filteredFacts.size() > 0) { + evaluateFacts = new Facts(); + filteredFacts.forEach(filteredFact -> { + evaluateFacts.put(filteredFact.getKey(), filteredFact.getValue()); + }); + } else { + evaluateFacts = facts; + } + + } else { + evaluateFacts = facts; + } + + return this.condition.evaluate(evaluateFacts); + + } + + Rule getNifiRule() { + return nifiRule; + } + } + + +} diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineProvider.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineProvider.java new file mode 100644 index 0000000..83b897a --- /dev/null +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineProvider.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.rules.engine; + +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; + + +@CapabilityDescription("Provides an instance of a rules engine to the caller. Supports " + + "rules stored as JSON or YAML file types.") +@Tags({ "rules","rules-engine","engine","actions","facts" }) +public class EasyRulesEngineProvider extends AbstractEasyRulesEngineController implements RulesEngineProvider { + + /** + * Returns a rules engine instance + * @return + */ + @Override + public RulesEngine getRulesEngine() { + return super.getRulesEngine(); + } + +} diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java index 187f0fe..520a0e0 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/java/org/apache/nifi/rules/engine/EasyRulesEngineService.java @@ -19,38 +19,17 @@ package org.apache.nifi.rules.engine; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnEnabled; -import org.apache.nifi.components.AllowableValue; -import org.apache.nifi.components.PropertyDescriptor; -import org.apache.nifi.components.ValidationContext; -import org.apache.nifi.components.ValidationResult; -import org.apache.nifi.components.Validator; -import org.apache.nifi.controller.AbstractControllerService; import org.apache.nifi.controller.ConfigurationContext; -import org.apache.nifi.controller.ControllerServiceInitializationContext; -import org.apache.nifi.expression.ExpressionLanguageScope; -import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.logging.ComponentLog; import org.apache.nifi.reporting.InitializationException; import org.apache.nifi.rules.Action; -import org.apache.nifi.rules.ActionHandler; -import org.apache.nifi.rules.Rule; -import org.apache.nifi.rules.RulesFactory; -import org.apache.nifi.rules.RulesMVELCondition; -import org.apache.nifi.rules.RulesSPELCondition; -import org.apache.nifi.util.StringUtils; -import org.jeasy.rules.api.Condition; import org.jeasy.rules.api.Facts; import org.jeasy.rules.api.RuleListener; -import org.jeasy.rules.api.Rules; -import org.jeasy.rules.core.DefaultRulesEngine; -import org.jeasy.rules.core.RuleBuilder; + import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; /** * Implementation of RulesEngineService interface @@ -60,125 +39,21 @@ import java.util.Set; @CapabilityDescription("Defines and execute the rules stored in NiFi or EasyRules file formats for a given set of facts. Supports " + "rules stored as JSON or YAML file types.") @Tags({ "rules","rules-engine","engine","actions","facts" }) -public class EasyRulesEngineService extends AbstractControllerService implements RulesEngineService { - - static final AllowableValue YAML = new AllowableValue("YAML", "YAML", "YAML file configuration type."); - static final AllowableValue JSON = new AllowableValue("JSON", "JSON", "JSON file configuration type."); - static final AllowableValue NIFI = new AllowableValue("NIFI", "NIFI", "NiFi rules formatted file."); - static final AllowableValue MVEL = new AllowableValue("MVEL", "Easy Rules MVEL", "Easy Rules File format using MVFLEX Expression Language"); - static final AllowableValue SPEL = new AllowableValue("SPEL", "Easy Rules SpEL", "Easy Rules File format using Spring Expression Language"); - - static final PropertyDescriptor RULES_FILE_PATH = new PropertyDescriptor.Builder() - .name("rules-file-path") - .displayName("Rules File Path") - .description("Path to location of rules file. Only one of Rules File or Rules Body may be used") - .required(false) - .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) - .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) - .build(); - - static final PropertyDescriptor RULES_BODY = new PropertyDescriptor.Builder() - .name("rules-body") - .displayName("Rules Body") - .description("Body of rules file to execute. Only one of Rules File or Rules Body may be used") - .required(false) - .addValidator(Validator.VALID) - .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) - .build(); - - static final PropertyDescriptor RULES_FILE_TYPE = new PropertyDescriptor.Builder() - .name("rules-file-type") - .displayName("Rules File Type") - .description("File or Body type for rules definition. Supported types are YAML and JSON") - .required(true) - .allowableValues(JSON,YAML) - .defaultValue(JSON.getValue()) - .build(); - - static final PropertyDescriptor RULES_FILE_FORMAT = new PropertyDescriptor.Builder() - .name("rules-file-format") - .displayName("Rules File Format") - .description("Format for rules. Supported formats are NiFi Rules, Easy Rules files with MVEL Expression Language" + - " and Easy Rules files with Spring Expression Language.") - .required(true) - .allowableValues(NIFI,MVEL,SPEL) - .defaultValue(NIFI.getValue()) - .build(); - - static final PropertyDescriptor IGNORE_CONDITION_ERRORS = new PropertyDescriptor.Builder() - .name("rules-ignore-condition-errors") - .displayName("Ignore Condition Errors") - .description("When set to true, rules engine will ignore errors for any rule that encounters issues " + - "when compiling rule conditions (including syntax errors and/or missing facts). Rule will simply return as false " + - "and engine will continue with execution.") - .required(true) - .defaultValue("false") - .allowableValues("true", "false") - .build(); - - protected List<PropertyDescriptor> properties; - protected volatile List<Rule> rules; - protected volatile String rulesFileFormat; - private boolean ignoreConditionErrors; +public class EasyRulesEngineService extends EasyRulesEngineProvider implements RulesEngineService { - @Override - protected void init(ControllerServiceInitializationContext config) throws InitializationException { - super.init(config); - final List<PropertyDescriptor> properties = new ArrayList<>(); - properties.add(RULES_FILE_TYPE); - properties.add(RULES_FILE_PATH); - properties.add(RULES_BODY); - properties.add(RULES_FILE_FORMAT); - properties.add(IGNORE_CONDITION_ERRORS); - this.properties = Collections.unmodifiableList(properties); - } + private volatile RulesEngine rulesEngine; @Override - protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { - return properties; - } - @OnEnabled public void onEnabled(final ConfigurationContext context) throws InitializationException { - final String rulesFile = context.getProperty(RULES_FILE_PATH).getValue(); - final String rulesBody = context.getProperty(RULES_BODY).getValue(); - final String rulesFileType = context.getProperty(RULES_FILE_TYPE).getValue(); - rulesFileFormat = context.getProperty(RULES_FILE_FORMAT).getValue(); - ignoreConditionErrors = context.getProperty(IGNORE_CONDITION_ERRORS).asBoolean(); - try{ - if(StringUtils.isEmpty(rulesFile)){ - rules = RulesFactory.createRulesFromString(rulesBody, rulesFileType, rulesFileFormat); - }else{ - rules = RulesFactory.createRulesFromFile(rulesFile, rulesFileType, rulesFileFormat); - } - } catch (Exception fex){ - throw new InitializationException(fex); - } - } - - /** - * Custom validation for ensuring exactly one of Script File or Script Body is populated - * - * @param validationContext provides a mechanism for obtaining externally - * managed values, such as property values and supplies convenience methods - * for operating on those values - * @return A collection of validation results - */ - @Override - public Collection<ValidationResult> customValidate(ValidationContext validationContext) { - Set<ValidationResult> results = new HashSet<>(); - - // Verify that exactly one of "script file" or "script body" is set - Map<PropertyDescriptor, String> propertyMap = validationContext.getProperties(); - if (StringUtils.isEmpty(propertyMap.get(RULES_FILE_PATH)) == StringUtils.isEmpty(propertyMap.get(RULES_BODY))) { - results.add(new ValidationResult.Builder().subject("Rules Body or Rules File").valid(false).explanation( - "exactly one of Rules File or Rules Body must be set").build()); - } - - return results; + super.onEnabled(context); + EasyRulesEngine easyRulesEngine = (EasyRulesEngine) getRulesEngine(); + List<RuleListener> ruleListeners = new ArrayList<>(); + ruleListeners.add(new EasyRulesListener(getLogger())); + easyRulesEngine.setRuleListeners(ruleListeners); + rulesEngine = easyRulesEngine; } - /** * Return the list of actions what should be executed for a given set of facts * @param facts a Map of key and facts values, as objects, that should be evaluated by the rules engine @@ -186,43 +61,17 @@ public class EasyRulesEngineService extends AbstractControllerService implement */ @Override public List<Action> fireRules(Map<String, Object> facts) { - final List<Action> actions = new ArrayList<>(); - if (rules == null || facts == null || facts.isEmpty()) { - return null; - }else { - org.jeasy.rules.api.Rules easyRules = convertToEasyRules(rules, (action, eventFacts) -> - actions.add(action)); - Facts easyFacts = new Facts(); - facts.forEach(easyFacts::put); - DefaultRulesEngine rulesEngine = new DefaultRulesEngine(); - rulesEngine.registerRuleListener(new EasyRulesListener()); - rulesEngine.fire(easyRules, easyFacts); - return actions; - } + return rulesEngine.fireRules(facts); } + private static class EasyRulesListener implements RuleListener { - protected Rules convertToEasyRules(List<Rule> rules, ActionHandler actionHandler) { - final Rules easyRules = new Rules(); - rules.forEach(rule -> { - RuleBuilder ruleBuilder = new RuleBuilder(); - Condition condition = rulesFileFormat.equalsIgnoreCase(SPEL.getValue()) - ? new RulesSPELCondition(rule.getCondition(), ignoreConditionErrors): new RulesMVELCondition(rule.getCondition(), ignoreConditionErrors); - ruleBuilder.name(rule.getName()) - .description(rule.getDescription()) - .priority(rule.getPriority()) - .when(condition); - for (Action action : rule.getActions()) { - ruleBuilder.then(facts -> { - actionHandler.execute(action, facts.asMap()); - }); - } - easyRules.register(ruleBuilder.build()); - }); - return easyRules; - } + private ComponentLog logger; + + EasyRulesListener(ComponentLog logger) { + this.logger = logger; + } - private class EasyRulesListener implements RuleListener { @Override public boolean beforeEvaluate(org.jeasy.rules.api.Rule rule, Facts facts) { return true; @@ -232,7 +81,6 @@ public class EasyRulesEngineService extends AbstractControllerService implement public void afterEvaluate(org.jeasy.rules.api.Rule rule, Facts facts, boolean b) { } - @Override public void beforeExecute(org.jeasy.rules.api.Rule rule, Facts facts) { @@ -240,13 +88,14 @@ public class EasyRulesEngineService extends AbstractControllerService implement @Override public void onSuccess(org.jeasy.rules.api.Rule rule, Facts facts) { - getLogger().debug("Rules was successfully processed for: {}",new Object[]{rule.getName()}); + logger.debug("Rules was successfully processed for: {}",new Object[]{rule.getName()}); } @Override public void onFailure(org.jeasy.rules.api.Rule rule, Facts facts, Exception e) { - getLogger().warn("Rule execution failed for: {}", new Object[]{rule.getName()}, e); + logger.warn("Rule execution failed for: {}", new Object[]{rule.getName()}, e); } } + } diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService index 5e5f394..3fa1fae 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService @@ -12,4 +12,5 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -org.apache.nifi.rules.engine.EasyRulesEngineService \ No newline at end of file +org.apache.nifi.rules.engine.EasyRulesEngineService +org.apache.nifi.rules.engine.EasyRulesEngineProvider \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/docs/org.apache.nifi.rules.engine.EasyRulesEngineService/additionalDetails.html b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/docs/org.apache.nifi.rules.engine.EasyRulesEngineService/additionalDetails.html index 48207df..eae2159 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/docs/org.apache.nifi.rules.engine.EasyRulesEngineService/additionalDetails.html +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/main/resources/docs/org.apache.nifi.rules.engine.EasyRulesEngineService/additionalDetails.html @@ -21,8 +21,9 @@ </head> <body> <h2>General</h2> -<p>The Easy Rules Engine Service supports execution of a centralized set of rules (stored as files or provided within the service configuration) against a provided set of data called facts. Facts sent to the service are processed against - the rules engine to determine what, if any, actions should be executed based on the conditions defined within the rules. The list of actions are returned to the caller to handle as needed. +<p>The Easy Rules Engine Service supports execution of a centralized set of rules (stored as files or provided within the service configuration) against a provided set of data called facts. It supports both the RulesEngineProvider + and RulesEngineService interfaces, allowing callers to send facts to the service to process against a centralized rules engine, or allowing them to retrieve an instance of a rules engine to process facts locally. + Upon execution, the rules engine will determine what rules have been met and return a list of actions that should be executed based on the conditions defined within the rules. </p> <p> Rules can be implemented in any of the following formats: diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/TestRulesCondition.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/TestRulesCondition.java new file mode 100644 index 0000000..d988a9a --- /dev/null +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/TestRulesCondition.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.rules; + +import org.jeasy.rules.api.Condition; +import org.jeasy.rules.api.Facts; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TestRulesCondition { + + @Test + public void testRulesMVELConditionPassed(){ + String expression = "predictedTimeToBytesBackpressureMillis <= 14400000"; + Facts facts = new Facts(); + facts.put("predictedTimeToBytesBackpressureMillis",13300000); + Condition condition = new RulesMVELCondition(expression, false); + long start = System.currentTimeMillis(); + boolean passed = condition.evaluate(facts); + long end = System.currentTimeMillis(); + System.out.println("Total Time: " + (end - start)); + assertTrue(passed); + } + + @Test + public void testRulesMVELConditionFailed(){ + String expression = "predictedQueuedCount > 50"; + Facts facts = new Facts(); + facts.put("predictedQueuedCount",49); + Condition condition = new RulesMVELCondition(expression, false); + assertFalse(condition.evaluate(facts)); + } + + @Test + public void testRulesMVELConditionError(){ + String expression = "predictedQueuedCount > 50"; + Facts facts = new Facts(); + facts.put("predictedQueued",100); + Condition condition = new RulesMVELCondition(expression, false); + try { + condition.evaluate(facts); + fail(); + }catch (Exception ignored){ + } + } + + @Test + public void testRulesSPELConditionPassed(){ + String expression = "#predictedQueuedCount > 50"; + Facts facts = new Facts(); + facts.put("predictedQueuedCount",100); + Condition condition = new RulesSPELCondition(expression, false); + long start = System.currentTimeMillis(); + boolean passed = condition.evaluate(facts); + long end = System.currentTimeMillis(); + System.out.println("Total Time: " + (end - start)); + assertTrue(passed); + } + + @Test + public void testRulesSPELConditionFailed(){ + String expression = "#predictedQueuedCount > 50"; + Facts facts = new Facts(); + facts.put("predictedQueuedCount",49); + Condition condition = new RulesSPELCondition(expression, false); + assertFalse(condition.evaluate(facts)); + } + + @Test + public void testRulesSPELConditionError(){ + String expression = "predictedQueuedCount > 50"; + Facts facts = new Facts(); + facts.put("predictedQueuedCount",100); + Condition condition = new RulesSPELCondition(expression, false); + try { + condition.evaluate(facts); + fail(); + }catch (Exception ignored){ + } + } + + +} diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngine.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngine.java new file mode 100644 index 0000000..092f63d --- /dev/null +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngine.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.rules.engine; +import org.apache.nifi.rules.Action; +import org.apache.nifi.rules.Rule; +import org.apache.nifi.rules.RulesFactory; +import org.junit.Test; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class TestEasyRulesEngine { + + @Test + public void testCheckRules() throws Exception { + String testYamlFile = "src/test/resources/test_nifi_rules.yml"; + List<Rule> rules = RulesFactory.createRulesFromFile(testYamlFile, "YAML", "NIFI"); + final EasyRulesEngine service = new EasyRulesEngine("NIFI",true,false, rules); + Map<String, Object> facts = new HashMap<>(); + facts.put("predictedQueuedCount",60); + facts.put("predictedTimeToBytesBackpressureMillis",311111); + Map<Rule, Boolean> checkedRules = service.checkRules(facts); + assertNotNull(checkedRules); + assertEquals(2,checkedRules.values().size()); + } + + @Test + public void testFireRules() throws Exception { + String testYamlFile = "src/test/resources/test_nifi_rules.yml"; + List<Rule> rules = RulesFactory.createRulesFromFile(testYamlFile, "YAML", "NIFI"); + final EasyRulesEngine service = new EasyRulesEngine("NIFI",true,false, rules); + Map<String, Object> facts = new HashMap<>(); + facts.put("predictedQueuedCount",60); + facts.put("predictedTimeToBytesBackpressureMillis",299999); + List<Action> actions = service.fireRules(facts); + assertNotNull(actions); + assertEquals(3,actions.size()); + } + + @Test + public void testIgnoreErrorConditions() throws Exception { + String testYamlFile = "src/test/resources/test_nifi_rules.yml"; + List<Rule> rules = RulesFactory.createRulesFromFile(testYamlFile, "YAML", "NIFI"); + final EasyRulesEngine service = new EasyRulesEngine("NIFI",false, false, rules); + Map<String, Object> facts = new HashMap<>(); + facts.put("predictedQueuedCount",60); + facts.put("predictedTimeToBytesBackpressure",311111); + try { + service.fireRules(facts); + fail("Error condition exception was not thrown"); + }catch (Exception ignored){ + } + + } + + @Test + public void testFilterRulesMissingFacts() throws Exception { + String testYamlFile = "src/test/resources/test_nifi_rules.yml"; + List<Rule> rules = RulesFactory.createRulesFromFile(testYamlFile, "YAML", "NIFI"); + final EasyRulesEngine service = new EasyRulesEngine("NIFI",false, true, rules); + Map<String, Object> facts = new HashMap<>(); + facts.put("predictedQueuedCount",60); + Map<Rule, Boolean> checkedRules = service.checkRules(facts); + assertEquals(1, checkedRules.size()); + } + +} diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineProvider.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineProvider.java new file mode 100644 index 0000000..538bb76 --- /dev/null +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineProvider.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.rules.engine; + +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.rules.Action; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TestEasyRulesEngineProvider { + + @Test + public void testGetRulesEngine() throws InitializationException, IOException { + final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class); + final RulesEngineProvider service = new MockEasyRulesEngineProvider(); + runner.addControllerService("easy-rules-engine-service-test",service); + runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_nifi_rules.yml"); + runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "YAML"); + runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "NIFI"); + runner.enableControllerService(service); + runner.assertValid(service); + Map<String, Object> facts = new HashMap<>(); + facts.put("predictedQueuedCount",60); + facts.put("predictedTimeToBytesBackpressureMillis",299999); + RulesEngine engine = service.getRulesEngine(); + assertNotNull(engine); + List<Action> actions = engine.fireRules(facts); + assertNotNull(actions); + assertEquals(actions.size(), 3); + + } + + private static class MockEasyRulesEngineProvider extends EasyRulesEngineProvider { + + } +} diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineService.java b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineService.java index bc7a785..5513212 100644 --- a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineService.java +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/java/org/apache/nifi/rules/engine/TestEasyRulesEngineService.java @@ -228,7 +228,7 @@ public class TestEasyRulesEngineService { try { service.fireRules(facts); fail("Expected exception to be thrown"); - }catch (PropertyAccessException pae){ + }catch (Exception pae){ assert true; } } @@ -273,7 +273,7 @@ public class TestEasyRulesEngineService { try { service.fireRules(facts); fail("Expected exception to be thrown"); - }catch (PropertyAccessException pae){ + }catch (Exception pae){ assert true; } } @@ -345,7 +345,26 @@ public class TestEasyRulesEngineService { fail(); } } - private class MockEasyRulesEngineService extends EasyRulesEngineService { + + @Test + public void testFilterRulesMissingFactsTrue() throws InitializationException, IOException { + final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class); + final RulesEngineService service = new MockEasyRulesEngineService(); + runner.addControllerService("easy-rules-engine-service-test",service); + runner.setProperty(service, EasyRulesEngineService.RULES_FILE_PATH, "src/test/resources/test_nifi_rules_filter.json"); + runner.setProperty(service,EasyRulesEngineService.RULES_FILE_TYPE, "JSON"); + runner.setProperty(service,EasyRulesEngineService.RULES_FILE_FORMAT, "NIFI"); + runner.setProperty(service,EasyRulesEngineService.FILTER_RULES_MISSING_FACTS, "true"); + runner.enableControllerService(service); + runner.assertValid(service); + Map<String, Object> facts = new HashMap<>(); + facts.put("predictedQueuedCount",60); + List<Action> actions = service.fireRules(facts); + assertNotNull(actions); + assertEquals(actions.size(), 1); + } + + private static class MockEasyRulesEngineService extends EasyRulesEngineService { } diff --git a/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/resources/test_nifi_rules_filter.json b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/resources/test_nifi_rules_filter.json new file mode 100644 index 0000000..b99fb62 --- /dev/null +++ b/nifi-nar-bundles/nifi-easyrules-bundle/nifi-easyrules-service/src/test/resources/test_nifi_rules_filter.json @@ -0,0 +1,34 @@ +[ + { + "name": "Queue Size", + "description": "Queue size check greater than 50", + "priority": 1, + "condition": "predictedQueuedCount > 50", + "actions": [ + { + "type": "LOG", + "attributes": { + "logLevel": "debug", + "message": "Queue Size Over 50 is detected!" + } + } + ], + "facts": ["predictedQueuedCount"] + }, + { + "name": "Time To Back Pressure", + "description": "Back pressure time less than 5 minutes", + "priority": 2, + "condition": "predictedTimeToBytesBackpressureMillis < 300000 && predictedTimeToBytesBackpressureMillis >= 0", + "actions": [ + { + "type": "LOG", + "attributes": { + "logLevel": "warn", + "message": "Back Pressure prediction less than 5 minutes!" + } + } + ], + "facts":["predictedTimeToBytesBackpressureMillis"] + } +] \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java index 434808f..b5f0829 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.rules; +import java.util.HashMap; import java.util.Map; /** @@ -23,7 +24,7 @@ import java.util.Map; * The type of action is dictated by the type field and attributes are used as parameters to configure * the Action's executor/handler */ -public class Action { +public class Action implements Cloneable{ private String type; private Map<String,String> attributes; @@ -50,4 +51,13 @@ public class Action { public void setAttributes(Map<String, String> attributes) { this.attributes = attributes; } + + public Action clone(){ + Action action = new Action(); + action.setType(type); + Map<String, String> attributeMap = new HashMap<>(attributes); + action.setAttributes(attributeMap); + return action; + } + } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Rule.java b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Rule.java index 8adbb21..d6f69d3 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Rule.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Rule.java @@ -16,6 +16,7 @@ */ package org.apache.nifi.rules; +import java.util.ArrayList; import java.util.List; /** @@ -23,7 +24,7 @@ import java.util.List; * one or more {@link Action} */ -public class Rule { +public class Rule implements Cloneable{ private String name; private String description; private Integer priority; @@ -90,4 +91,25 @@ public class Rule { public void setFacts(List<String> facts) { this.facts = facts; } + + @Override + public Rule clone(){ + Rule rule = new Rule(); + rule.setName(name); + rule.setDescription(description); + rule.setPriority(priority); + rule.setCondition(condition); + + if (actions != null) { + final List<Action> actionList = new ArrayList<>(); + rule.setActions(actionList); + actions.forEach(action -> actionList.add((Action)action.clone())); + } + if (facts != null){ + final List<String> factList = new ArrayList<>(); + rule.setFacts(factList); + factList.addAll(facts); + } + return rule; + } } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngine.java similarity index 50% copy from nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java copy to nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngine.java index 434808f..9c66c15 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/Action.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngine.java @@ -14,40 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.rules; +package org.apache.nifi.rules.engine; +import org.apache.nifi.rules.Action; +import org.apache.nifi.rules.Rule; + +import java.util.List; import java.util.Map; /** - * An Action that should be performed given a fact has met the condition specified in a Rule. - * The type of action is dictated by the type field and attributes are used as parameters to configure - * the Action's executor/handler + * <p> + * An instance of a RulesEngine which provides access to available rules + * </p> + * </p> */ -public class Action { - private String type; - private Map<String,String> attributes; - - public Action() { - } - - public Action(String type, Map<String, String> attributes) { - this.type = type; - this.attributes = attributes; - } - - public String getType() { - return type; - } +public interface RulesEngine { - public void setType(String type) { - this.type = type; - } - public Map<String, String> getAttributes() { - return attributes; - } + List<Action> fireRules(Map<String, Object> facts); + Map<Rule, Boolean> checkRules(Map<String, Object> facts); - public void setAttributes(Map<String, String> attributes) { - this.attributes = attributes; - } } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineProvider.java similarity index 53% copy from nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java copy to nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineProvider.java index f00e0ba..7f55360 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineProvider.java @@ -19,29 +19,20 @@ package org.apache.nifi.rules.engine; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.controller.ControllerService; -import org.apache.nifi.rules.Action; - -import java.util.List; -import java.util.Map; /** * <p> - * A Controller Service that is responsible for executing rules engine against provided facts. The subsequent - * actions can be executed either by the rules engine or a list of {@link Action} can be returned and interrogated/executed by - * the caller. + * A Controller Service that is responsible for providing an instance of a Rules Engine. * </p> * </p> */ @Tags({"rules", "rules-engine","facts","actions"}) -@CapabilityDescription("Specifies a Controller Service which hosts a rules engine, that can be used to determine actions that should be performed given provided facts.") -public interface RulesEngineService extends ControllerService { +@CapabilityDescription("Specifies a Controller Service which provides access to an instance of a Rules Engine.") +public interface RulesEngineProvider extends ControllerService { /** - * Returns the list of Actions that should be triggered as determined by the rules engine for the the given facts. - * Action in this case have not been executed by the engine. - * @param facts a Map of key and facts values, as objects, that should be evaluated by the rules engine - * @return a List of Actions that should be triggered + * Retrieve an instance of a rules engine + * @return RulesEngine instance */ - List<Action> fireRules(Map<String, Object> facts); - + RulesEngine getRulesEngine(); } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java index f00e0ba..45ab999 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-rules-engine-service-api/src/main/java/org/apache/nifi/rules/engine/RulesEngineService.java @@ -26,7 +26,7 @@ import java.util.Map; /** * <p> - * A Controller Service that is responsible for executing rules engine against provided facts. The subsequent + * A Controller Service that is responsible for executing a rules engine against provided facts. The subsequent * actions can be executed either by the rules engine or a list of {@link Action} can be returned and interrogated/executed by * the caller. * </p>