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 <mattyb149@apache.org>

This closes #4081
This commit is contained in:
Yolanda M. Davis 2020-02-24 15:27:39 -05:00 committed by Matthew Burgess
parent 778012412a
commit abf223d574
No known key found for this signature in database
GPG Key ID: 05D3DEB8126DAD24
19 changed files with 846 additions and 186 deletions

View File

@ -49,6 +49,11 @@
<artifactId>easy-rules-mvel</artifactId>
<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>
@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,184 @@
/*
* 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.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.reporting.InitializationException;
import org.apache.nifi.rules.Rule;
import org.apache.nifi.rules.RulesFactory;
import org.apache.nifi.util.StringUtils;
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;
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.");
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();
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 List<Rule> rules;
protected volatile String rulesFileFormat;
protected boolean ignoreConditionErrors;
protected boolean filterRules;
@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);
properties.add(FILTER_RULES_MISSING_FACTS);
this.properties = Collections.unmodifiableList(properties);
}
@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();
filterRules = context.getProperty(FILTER_RULES_MISSING_FACTS).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;
}
protected RulesEngine getRulesEngine() {
List<Rule> rulesCopy = new ArrayList<>();
rules.forEach(rule -> {
rulesCopy.add(rule.clone());
});
return new EasyRulesEngine(rulesFileFormat, ignoreConditionErrors, filterRules, Collections.unmodifiableList(rulesCopy));
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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 {
public class EasyRulesEngineService extends EasyRulesEngineProvider 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;
private volatile RulesEngine rulesEngine;
@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);
}
@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);
}
super.onEnabled(context);
EasyRulesEngine easyRulesEngine = (EasyRulesEngine) getRulesEngine();
List<RuleListener> ruleListeners = new ArrayList<>();
ruleListeners.add(new EasyRulesListener(getLogger()));
easyRulesEngine.setRuleListeners(ruleListeners);
rulesEngine = easyRulesEngine;
}
/**
* 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;
}
/**
* 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 {
private ComponentLog logger;
EasyRulesListener(ComponentLog logger) {
this.logger = logger;
}
}
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 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);
}
}
}

View File

@ -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
org.apache.nifi.rules.engine.EasyRulesEngineService
org.apache.nifi.rules.engine.EasyRulesEngineProvider

View File

@ -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:

View File

@ -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){
}
}
}

View File

@ -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());
}
}

View File

@ -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 {
}
}

View File

@ -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 {
}

View File

@ -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"]
}
]

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.rules.Action;
import org.apache.nifi.rules.Rule;
import java.util.List;
import java.util.Map;
/**
* <p>
* An instance of a RulesEngine which provides access to available rules
* </p>
* </p>
*/
public interface RulesEngine {
List<Action> fireRules(Map<String, Object> facts);
Map<Rule, Boolean> checkRules(Map<String, Object> facts);
}

View File

@ -0,0 +1,38 @@
/*
* 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;
import org.apache.nifi.controller.ControllerService;
/**
* <p>
* 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 provides access to an instance of a Rules Engine.")
public interface RulesEngineProvider extends ControllerService {
/**
* Retrieve an instance of a rules engine
* @return RulesEngine instance
*/
RulesEngine getRulesEngine();
}

View File

@ -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>