diff options
Diffstat (limited to 'src/main/java/org/onap/aai/validation/ruledriven')
14 files changed, 1985 insertions, 0 deletions
diff --git a/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java b/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java new file mode 100644 index 0000000..476c098 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/RuleDrivenValidator.java @@ -0,0 +1,278 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.onap.aai.validation.Validator; +import org.onap.aai.validation.config.RuleIndexingConfig; +import org.onap.aai.validation.exception.ValidationServiceError; +import org.onap.aai.validation.exception.ValidationServiceException; +import org.onap.aai.validation.logging.ApplicationMsgs; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.reader.EventReader; +import org.onap.aai.validation.reader.OxmReader; +import org.onap.aai.validation.reader.data.AttributeValues; +import org.onap.aai.validation.reader.data.Entity; +import org.onap.aai.validation.result.ValidationResult; +import org.onap.aai.validation.result.Violation; +import org.onap.aai.validation.result.Violation.ViolationType; +import org.onap.aai.validation.ruledriven.configuration.EntitySection; +import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; +import org.onap.aai.validation.ruledriven.configuration.RulesConfigurationLoader; +import org.onap.aai.validation.ruledriven.rule.Rule; + +/** + * Validator using explicit rules + * + */ +public class RuleDrivenValidator implements Validator { + + private static LogHelper applicationLogger = LogHelper.INSTANCE; + + private static final String RULES_CONFIG_FILE_SUFFIX = ".groovy"; + + private Path configurationPath; + private OxmReader oxmReader; + private EventReader eventReader; + private Optional<RuleIndexingConfig> ruleIndexingConfig; + // Map of event type name against RuleManager for that event type + private Map<String, RuleManager> ruleManagers; + + + /** + * Construct a Validator that is configured using rule files + * + * @param configurationPath path to the Groovy rules files + * @param oxmReader required for validating entity types + * @param eventReader a reader for extracting entities from each event to be validated + */ + public RuleDrivenValidator(final Path configurationPath, final OxmReader oxmReader, + final EventReader eventReader, final RuleIndexingConfig ruleIndexingConfig) { + this.configurationPath = configurationPath; + this.oxmReader = oxmReader; + this.eventReader = eventReader; + this.ruleIndexingConfig = Optional.ofNullable(ruleIndexingConfig); + this.ruleManagers = null; + } + + @Override + public void initialise() throws ValidationServiceException { + ruleManagers = new HashMap<>(); + for (String eventType : getSupportedEventTypes()) { + ruleManagers.put(eventType.toLowerCase(Locale.getDefault()), loadRulesConfiguration(eventType)); + } + validateRulesConfiguration(); + } + + private RuleManager loadRulesConfiguration(String eventType) throws ValidationServiceException { + StringBuilder rulesText = new StringBuilder(); + try (Stream<Path> paths = Files.find(configurationPath.resolve(eventType), 1, + (path, basicFileAttributes) -> path.toFile().getName().matches(".*\\" + RULES_CONFIG_FILE_SUFFIX));) { + paths.forEach(appendFileContent(rulesText)); + } catch (IOException e) { + throw new ValidationServiceException(ValidationServiceError.RULES_FILE_ERROR, + configurationPath.toAbsolutePath(), e); + } + + try { + return RulesConfigurationLoader.loadConfiguration(rulesText.toString()); + } catch (GroovyConfigurationException e) { + throw new ValidationServiceException(ValidationServiceError.RULES_FILE_ERROR, e, + configurationPath.toAbsolutePath() + File.separator + "*" + RULES_CONFIG_FILE_SUFFIX); + } + } + + private void validateRulesConfiguration() throws ValidationServiceException { + for (RuleManager ruleManager : ruleManagers.values()) { + for (EntitySection entity : ruleManager.getEntities()) { + if(ruleIndexingConfig.isPresent() && ruleIndexingConfig.get().skipOxmValidation(entity.getName())) { + continue; + } + if (oxmReader != null && oxmReader.getPrimaryKeys(entity.getName()).isEmpty()) { + throw new ValidationServiceException(ValidationServiceError.OXM_MISSING_KEY, + entity.getName() + " defined in " + configurationPath.toAbsolutePath() + File.separator + + "*" + RULES_CONFIG_FILE_SUFFIX); + } + } + } + } + + /** + * Helper method to expose the configured rules. This simplifies testing of the validator. + * + * @param entityType + * @param eventType + * @return the rules defined for this entityType + */ + public List<Rule> getRulesForEntity(String entityType, String eventType) { + return ruleManagers.get(eventType.toLowerCase(Locale.getDefault())).getRulesForEntity(entityType); + } + + /* + * (non-Javadoc) + * + * @see org.onap.aai.validation.Validator#validate(java.lang.String) + */ + @Override + public List<ValidationResult> validate(String event) throws ValidationServiceException { + List<ValidationResult> validationResults = new ArrayList<>(); + + Entity entity = getEventReader().getEntity(event); + Optional<String> eventType = eventReader.getEventType(event); + List<Rule> rules = getRulesToApply(entity, eventType).orElse(null); + if (rules == null) { + throw new ValidationServiceException(ValidationServiceError.RULES_NOT_DEFINED, eventType.orElse(null)); + } + ValidationResult validationResult = new ValidationResult(entity); + Violation.Builder builder = new Violation.Builder(entity); + + for (Rule rule : rules) { + AttributeValues attributeValues = entity.getAttributeValues(rule.getAttributePaths()); + + // Execute the rule for this particular set of attribute values. + boolean valid = false; + try { + valid = rule.execute(attributeValues); + } catch (IllegalArgumentException e) { + throw new ValidationServiceException(ValidationServiceError.RULE_EXECUTION_ERROR, e, rule, + attributeValues); + } + + applicationLogger.debug(String.format("%s|%s|\"%s\"|%s", entity.getType(), entity.getIds(), rule.getName(), + valid ? "pass" : "fail")); + + if (!valid) { + //@formatter:off + Violation violation = builder + .category(rule.getErrorCategory()) + .severity(rule.getSeverity()) + .violationType(ViolationType.RULE) + .validationRule(rule.getName()) + .violationDetails(attributeValues.generateReport()) + .errorMessage(rule.getErrorMessage()) + .build(); + //@formatter:on + + validationResult.addViolation(violation); + } + } + validationResults.add(validationResult); + + return validationResults; + } + + private Optional<List<Rule>> getRulesToApply(Entity entity, Optional<String> eventType) + throws ValidationServiceException { + Optional<List<Rule>> rules = Optional.empty(); + if (eventType.isPresent()) { + Optional<RuleManager> ruleManager = getRuleManager(eventType.get().toLowerCase(Locale.getDefault())); + if (ruleManager.isPresent()) { + if (ruleIndexingConfig.isPresent() && ruleIndexingConfig.get().getIndexedEvents() != null && + ruleIndexingConfig.get().getIndexedEvents().contains(eventType.get())) { + rules = getRulesByIndex(entity, eventType.get(), ruleManager.get()); + } else { + rules = Optional.of(ruleManager.get().getRulesForEntity(entity.getType())); + } + } + } + return rules; + } + + private Optional<List<Rule>> getRulesByIndex(Entity entity, String eventType, RuleManager ruleManager) { + String rulesKey = generateKey(entity, eventType); + applicationLogger.debug(String.format("Retrieving indexed rules for key '%s'", rulesKey)); + Optional<List<Rule>> rules = Optional.of(ruleManager.getRulesForEntity(rulesKey)); + if (rules.get().isEmpty() && ruleIndexingConfig.isPresent()) { + if (ruleIndexingConfig.get().getDefaultIndexKey() == null || ruleIndexingConfig.get().getDefaultIndexKey().isEmpty()) { + applicationLogger.debug("Default index value not configured, unable to get rules"); + applicationLogger.error(ApplicationMsgs.CANNOT_VALIDATE_ERROR, eventType); + return rules; + } + String defaultKey = RuleManager.generateKey(new String[] {ruleIndexingConfig.get().getDefaultIndexKey()}); + rules = Optional.of(ruleManager.getRulesForEntity(defaultKey)); + } + return rules; + } + + private String generateKey(Entity entity, String eventType) { + if (!ruleIndexingConfig.isPresent() || ruleIndexingConfig.get().getIndexAttributes() == null || + ruleIndexingConfig.get().getIndexAttributes().isEmpty()) { + applicationLogger.debug(String.format( + "Event '%s' is configured to use indexed rules but indexing attributes are not configured", eventType)); + return ""; + } + try { + AttributeValues attributeValues = entity.getAttributeValues(ruleIndexingConfig.get().getIndexAttributes()); + applicationLogger.debug("Generating index using attributes: " + attributeValues.generateReport().toString()); + Collection<Object> values = attributeValues.generateReport().values(); + return RuleManager.generateKey(values.stream().toArray(String[]::new)); + } catch (ValidationServiceException e) { + applicationLogger.debug("Failed to retrieve index key attributes from event"); + applicationLogger.error(ApplicationMsgs.CANNOT_VALIDATE_ERROR, eventType); + return ""; + } + } + + private EventReader getEventReader() { + return this.eventReader; + } + + private Optional<RuleManager> getRuleManager(String eventType) throws ValidationServiceException { + if (ruleManagers == null) { + initialise(); + } + return Optional.ofNullable(ruleManagers.get(eventType)); + } + + /** + * Read the text content of the specified Path and append this to the specified String + * + * @param sb StringBuilder for the rule configuration text + * @return a Consumer function that appends file content + */ + private Consumer<? super Path> appendFileContent(StringBuilder sb) { + return path -> { + try { + for (String line : Files.readAllLines(path)) { + sb.append(line).append("\n"); + } + } catch (IOException e) { + applicationLogger.error(ApplicationMsgs.READ_FILE_ERROR, e, path.toString()); + } + }; + } + + private Collection<String> getSupportedEventTypes() { + String[] list = configurationPath.toFile().list((current, name) -> new File(current, name).isDirectory()); + return list == null ? Collections.emptyList() : Arrays.asList(list); + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/RuleManager.java b/src/main/java/org/onap/aai/validation/ruledriven/RuleManager.java new file mode 100644 index 0000000..c21ce24 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/RuleManager.java @@ -0,0 +1,91 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import org.onap.aai.validation.ruledriven.configuration.EntitySection; +import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; +import org.onap.aai.validation.ruledriven.configuration.RuleSection; +import org.onap.aai.validation.ruledriven.rule.GroovyRule; +import org.onap.aai.validation.ruledriven.rule.Rule; + +/** + * Helper class storing the relationships from entity type to rules. This class constructs the actual rules from the + * supplied configuration. + * + */ +public class RuleManager { + + private Map<String, List<Rule>> rulesMap = new LinkedHashMap<>(); + private List<EntitySection> entities; + + /** + * Create the rules for each type of entity based on the supplied configuration + * + * @param entities configuration (all entities) + * @throws InstantiationException + * @throws IllegalAccessException + * @throws GroovyConfigurationException + * @throws IOException + */ + public RuleManager(List<EntitySection> entities) + throws InstantiationException, IllegalAccessException, GroovyConfigurationException, IOException { + this.entities = entities; + for (EntitySection entity : entities) { + List<Rule> rules = new ArrayList<>(); + for (RuleSection section : entity.getRules()) { + rules.add(new GroovyRule(section)); + } + rulesMap.put(entity.getType(), rules); + } + } + + public List<EntitySection> getEntities() { + return entities; + } + + /** + * @param entityType + * @return the rules configured for this entity type + */ + public List<Rule> getRulesForEntity(String entityType) { + List<Rule> rules = rulesMap.get(entityType); + return rules == null ? Collections.emptyList() : rules; + } + + public static String generateKey(String[] indices) { + SortedSet<String> sortedIndices = new TreeSet<>(); + Collections.addAll(sortedIndices, indices); + StringBuilder sb = new StringBuilder(); + Iterator<String> iterator = sortedIndices.iterator(); + while (iterator.hasNext()) { + sb.append("["); + sb.append(iterator.next()); + sb.append("]"); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/EntitySection.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/EntitySection.java new file mode 100644 index 0000000..357ed87 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/EntitySection.java @@ -0,0 +1,101 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import java.util.ArrayList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; +import org.onap.aai.validation.ruledriven.configuration.build.EntityBuilder; + +/** + * Rules Configuration: entity {} section. + */ +public class EntitySection { + + private String name; + private String type; + private final SortedSet<String> indices = new TreeSet<>(); + private final List<RuleSection> rules = new ArrayList<>(); + + /** + * Rules are specified within an entity section. + */ + public EntitySection() { + // Deliberately empty - invoked when an entity section is read from the rules DSL + } + + @Override + public String toString() { + return new EntityBuilder(this).toString(); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public void setName(String name) { + this.name = name; + } + + public void setType(String entityType) { + this.type = entityType; + } + + public void setIndices(List<String> indices) { + for(String index : indices) { + this.indices.add(index.trim()); + } + } + + public void setIndices(String[] indices) { + for(String index : indices) { + this.indices.add(index.trim()); + } + } + + public SortedSet<String> getIndices() { + return this.indices; + } + + /** + * Adds the rule. + * + * @param rule + * the rule + */ + public void addRule(RuleSection rule) { + this.rules.add(rule); + } + + /** + * <p> + * Getter for property {@link #rules}. + * </p> + * + * @return Value for property <tt>rules</tt>. + */ + public List<RuleSection> getRules() { + return this.rules; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/GroovyConfigurationException.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/GroovyConfigurationException.java new file mode 100644 index 0000000..3b1b98a --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/GroovyConfigurationException.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import groovy.lang.MissingMethodException; +import groovy.lang.MissingPropertyException; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.control.MultipleCompilationErrorsException; +import org.onap.aai.validation.exception.BaseValidationServiceException; +import org.onap.aai.validation.exception.ValidationServiceError; + +/** + * Configuration load/parse exceptions + * + */ +@SuppressWarnings("serial") +public class GroovyConfigurationException extends BaseValidationServiceException { + + private String invalidToken; // NOSONAR + private String configText; // NOSONAR + + /** + * @param text + */ + public GroovyConfigurationException(String text) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId() + ", " + text); + } + + /** + * @param text + * @param configuration + */ + public GroovyConfigurationException(String text, String configuration) { + super(text); + this.configText = configuration; + } + + /** + * @param e + * exception for a missing method + * @param configuration + */ + public GroovyConfigurationException(MissingMethodException e, String configuration) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), "Invalid keyword " + e.getMethod(), e); + setInvalidToken(e.getMethod()); + this.configText = configuration; + } + + /** + * @param e + * @param configuration + */ + public GroovyConfigurationException(MissingPropertyException e, String configuration) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), "Invalid keyword " + e.getProperty(), e); + setInvalidToken(e.getProperty()); + this.configText = configuration; + } + + /** + * @param e + */ + public GroovyConfigurationException(CompilationFailedException e) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId() + ", " + + ValidationServiceError.RULE_UNEXPECTED_TOKEN.getMessage() + "; Caused by: " + e.getMessage(), e); + } + + /** + * @param e + * @param configuration + */ + public GroovyConfigurationException(MultipleCompilationErrorsException e, String configuration) { + super(ValidationServiceError.RULE_UNEXPECTED_TOKEN.getId(), "", e); + this.configText = configuration; + } + + public String getInvalidToken() { + return invalidToken; + } + + public void setInvalidToken(String token) { + this.invalidToken = token; + } + + public String getConfigText() { + return configText; + } + +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/RuleSection.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RuleSection.java new file mode 100644 index 0000000..b03bcf3 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RuleSection.java @@ -0,0 +1,200 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.onap.aai.validation.ruledriven.configuration.build.RuleBuilder; + +/** + * Rules Configuration: rule {} section. + */ +public class RuleSection { + + private String name; + private boolean isGenericRule; + private String description; + private String category; + private String errorMessage; + private String type; + private String objectId; + private String severity; + private List<String> attributes = new ArrayList<>(); + private List<String> fields = new ArrayList<>(); + private String expression; + + /** + * Rules may be defined within an entity {} section. + */ + public RuleSection() { + isGenericRule = false; + } + + /** + * @param attribute + */ + public void addAttribute(String attribute) { + if (this.attributes == null) { + this.attributes = new ArrayList<>(); + } + this.attributes.add(attribute); + } + + private void addAttributeMapping(String field) { + if (this.fields == null) { + this.fields = new ArrayList<>(); + } + this.fields.add(field); + } + + private void addAttributeNames(List<String> fieldNames) { + for (String attribute : fieldNames) { + addAttributeMapping(attribute); + } + } + + private void addAttributes(List<String> attributes) { + for (String attribute : attributes) { + addAttribute(attribute); + } + } + + /** + * Make a copy of a generically defined rule so that we can tailor it for our specific entity. + * + * @param genericRule + */ + public void copyFrom(RuleSection genericRule) { + setCategory(genericRule.getCategory()); + setDescription(genericRule.getDescription()); + setSeverity(genericRule.getSeverity()); + setExpression(genericRule.getExpression()); + setErrorMessage(genericRule.getErrorMessage()); + if (genericRule.getAttributes() != null) { + if (getAttributes().isEmpty()) { + addAttributes(genericRule.getAttributes()); + } else { + // Map the attributes + addAttributeNames(genericRule.getAttributes()); + } + } + + if (getAttributes().isEmpty()) { + throw new IllegalArgumentException("No attributes defined"); + } + } + + public List<String> getAttributes() { + return new ArrayList<>(attributes); + } + + public String getCategory() { + return category; + } + + public String getDescription() { + return description; + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getExpression() { + return expression; + } + + public List<String> getExpressionFieldNames() { + if (fields.size() < attributes.size()) { + return attributes; + } else { + return fields; + } + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getObjectId() { + return objectId; + } + + public String getSeverity() { + return severity; + } + + public boolean isGeneric() { + return isGenericRule; + } + + public void setAttributes(List<String> attributes) { + this.attributes = attributes; + } + + public void setAttributes(String[] attributes) { + this.attributes = new ArrayList<>(Arrays.asList(attributes)); + } + + public void setCategory(String category) { + this.category = category; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public void setIsGeneric(boolean isGenericRule) { + this.isGenericRule = isGenericRule; + } + + public void setName(String name) { + this.name = name; + } + + public void setObject(String object) { + this.type = object; + } + + public void setObjectId(String objectId) { + this.objectId = objectId; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + @Override + public String toString() { + return new RuleBuilder(this, "").toString(); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/RulesConfigurationLoader.groovy b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RulesConfigurationLoader.groovy new file mode 100644 index 0000000..5fabde9 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/RulesConfigurationLoader.groovy @@ -0,0 +1,309 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration + +import groovy.lang.Closure +import groovy.lang.DelegatesTo +import groovy.lang.ExpandoMetaClass +import java.io.File +import java.util.List +import org.onap.aai.validation.ruledriven.RuleManager + +class RulesConfigurationLoader { + + static RuleManager loadConfiguration(File dsl) { + return loadConfiguration(dsl.text) + } + + static RuleManager loadConfiguration(String dsl) throws GroovyConfigurationException { + SettingsSection globalConfiguration = new SettingsSection() + def List<EntitySection> entities = [] + def List<RuleSection> rules = [] + Script dslScript + + try { + dslScript = new GroovyShell().parse(dsl) + } catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e) { + throw new GroovyConfigurationException(e, dsl) + } + + dslScript.metaClass = createEMC(dslScript.class, { ExpandoMetaClass emc -> + + emc.settings = { Closure cl -> + cl.delegate = new SettingsDelegate(globalConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + emc.entity = { Closure cl -> + EntitySection entityConfiguration = new EntitySection() + cl.delegate = new EntityDelegate(entityConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + entities.add(entityConfiguration) + } + + emc.rule = { Closure cl -> + RuleSection ruleConfiguration = new RuleSection() + cl.delegate = new RuleDelegate(ruleConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + rules.add(ruleConfiguration) + } + }) + + try { + dslScript.run() + } catch (MissingMethodException | MissingPropertyException e) { + throw new GroovyConfigurationException(e, dsl) + } + + loadGenericRules(entities, rules) + checkForDuplicateRules(rules) + + return new RuleManager(entities) + } + + static void loadGenericRules(List<EntitySection> entities, List<RuleSection> rules) { + for (entity in entities) { + for (rule in entity.getRules()) { + if (rule.isGeneric()) { + def namedRule = rules.find() { item -> + item.getName() == rule.getName() + } + if (namedRule == null) { + throw new GroovyConfigurationException("rule '" + rule.getName() + "' is not defined") + } + try { + rule.copyFrom(namedRule) + } catch (IllegalArgumentException e) { + throw new GroovyConfigurationException("rule '" + rule.getName() + "' has no attributes defined, referenced by entity '" + entity.getName() + "'") + } + } + if (rule.getExpression() == null) { + throw new GroovyConfigurationException("rule '" + rule.getName() + "' does not have an expression defined") + } + } + } + } + + static void checkForDuplicateRules(List<RuleSection> rules) { + def duplicates = rules.countBy{ rule -> rule.name }.grep{ it.value > 1 }.collect{ it.key } + + rules.each { rule -> + if (rule.name in duplicates) { + throw new GroovyConfigurationException("Generic rule '" + rule.name + "' is duplicated") + } + } + } + + static ExpandoMetaClass createEMC(Class scriptClass, Closure cl) { + ExpandoMetaClass emc = new ExpandoMetaClass(scriptClass, false) + cl(emc) + emc.initialize() + return emc + } +} + +// Parse the settings {} block +class SettingsDelegate { + private SettingsSection configuration + + SettingsDelegate(SettingsSection configuration) { + this.configuration = configuration + } + + void environment(String environment) { + this.configuration.setEnvironment environment + } +} + +// Parse an entity {} block +class EntityDelegate { + private EntitySection configuration + + EntityDelegate(EntitySection configuration) { + this.configuration = configuration + } + + void name(String name) { + this.configuration.setName name + } + + void type(String name) { + if (!configuration.name) configuration.name = name + configuration.type = name + configuration.getRules().each { rule -> + rule.setObject configuration.type + } + } + + def indexing(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=IndexingDelegate) Closure cl) { + cl.delegate = new IndexingDelegate(configuration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + def validation(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=ValidationDelegate) Closure cl) { + cl.delegate = new ValidationDelegate(configuration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} + + +// Parse an indexing {} block within an entity block +class IndexingDelegate { + private EntitySection configuration + + IndexingDelegate(EntitySection configuration) { + this.configuration = configuration + } + + void indices(String... indices) { + this.configuration.setIndices indices + def index = RuleManager.generateKey(indices) + this.configuration.type = index + this.configuration.getRules().each { rule -> + rule.setObject configuration.type + } + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} + +// Parse a validation {} block within an entity block +class ValidationDelegate { + private EntitySection configuration + private String id + + ValidationDelegate(EntitySection configuration) { + this.configuration = configuration + } + + void useRule(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=UseRuleDelegate) Closure cl) { + cl.delegate = new UseRuleDelegate(configuration, configuration.type, id) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + } + + void rule(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=RuleDelegate) Closure cl) { + RuleSection ruleConfiguration = new RuleSection() + ruleConfiguration.setObject configuration.type + ruleConfiguration.setObjectId this.id + cl.delegate = new RuleDelegate(ruleConfiguration) + cl.resolveStrategy = Closure.DELEGATE_FIRST + cl() + configuration.addRule(ruleConfiguration) + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } +} + +// Parse a rule {} block +class RuleDelegate { + private RuleSection configuration + + RuleDelegate(RuleSection configuration) { + this.configuration = configuration + } + + void name(String name) { + this.configuration.setName name + } + + void category(String category) { + this.configuration.setCategory category + } + + void description(String description) { + this.configuration.setDescription description + } + + void errorText(String text) { + this.configuration.setErrorMessage text + } + + void severity(String severity) { + this.configuration.setSeverity severity + } + + void attributes(String... attributesList) { + this.configuration.setAttributes attributesList + } + + void validate(String validate) { + this.configuration.setExpression validate + } + + void methodMissing(String name, Object args) { + throw new MissingMethodException(name, this.class, args as Object[]) + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} + +class UseRuleDelegate { + private EntitySection configuration + private String objectName + private String id + private RuleSection ruleConfig + + UseRuleDelegate(EntitySection configuration, String objectName, String id) { + this.configuration = configuration + this.objectName = objectName + this.id = id + } + + void name(String name) { + ruleConfig = new RuleSection() + ruleConfig.setIsGeneric true; + ruleConfig.setName name; + ruleConfig.setObject this.objectName + ruleConfig.setObjectId this.id + this.configuration.addRule ruleConfig + } + + void attributes(String[] attributes) { + if (ruleConfig.attributes.empty) { + attributes.each {ruleConfig.addAttribute it} + } + } + + def propertyMissing(String name) { + throw new MissingMethodException(name, this.class) + } +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/SettingsSection.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/SettingsSection.java new file mode 100644 index 0000000..d59d07d --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/SettingsSection.java @@ -0,0 +1,48 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration; + +import org.onap.aai.validation.ruledriven.configuration.build.EntityBuilder; + +/** + * Parse the settings configuration block. + */ +public class SettingsSection { + + private String environment; + + public SettingsSection() { + // Deliberately empty + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(new EntityBuilder()); + return sb.toString(); + } + + public String getEnvironment() { + return environment; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ContentBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ContentBuilder.java new file mode 100644 index 0000000..320e29e --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ContentBuilder.java @@ -0,0 +1,198 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + + +/** + * Helper class for building internal configuration settings. + */ +public class ContentBuilder { + + private final String contentName; + protected List<ContentBuilder> items = new ArrayList<>(); + private StringBuilder content = new StringBuilder(); + protected String indent = ""; + + /** + * Construct an empty section. + */ + public ContentBuilder() { + this.contentName = null; + } + + /** + * Instantiates a new content builder. + * + * @param contentName + * the content name + */ + public ContentBuilder(String contentName) { + this.contentName = contentName; + } + + @Override + public String toString() { + return build(); + } + + /** + * Builds the configuration section + * + * @return the configuration as a string + */ + public String build() { + StringBuilder sb = new StringBuilder(); + appendPrefix(sb); + // Build child items + for (ContentBuilder item : items) { + sb.append(item.build()); + } + // Any ad-hoc content goes here + sb.append(content); + appendSuffix(sb); + return sb.toString(); + } + + /** + * Adds the content. + * + * @param item + * the item + */ + public void addContent(ContentBuilder item) { + items.add(item); + } + + /** + * Adds the content. + * + * @param stringContent + * the string content + */ + public void addContent(String stringContent) { + content.append(stringContent); + } + + /** + * Append content as a new line. + * + * @param stringContent + * the string + */ + public void appendLine(String stringContent) { + content.append(indent).append(stringContent).append(System.lineSeparator()); + } + + /** + * Append properties. + * + * @param props + * the props + */ + public void appendProperties(Properties props) { + @SuppressWarnings("unchecked") + Enumeration<String> e = (Enumeration<String>) props.propertyNames(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + appendValue(key, props.get(key)); + } + } + + /** + * Append value. + * + * @param key + * the key + * @param value + * the value + * @return + */ + public ContentBuilder appendValue(String key, Object value) { + if (value == null) { + return this; + } + + addContent(indent + "\t" + key + " "); + + if (value instanceof String) { + addStringValue((String) value); + } else if (value instanceof Number) { + addStringValue(value.toString()); + } else if (value instanceof List<?>) { + boolean first = true; + for (Object element : (List<?>) value) { + if (!first) { + addContent(", "); + } + addStringValue((String) element); + first = false; + } + } else if (value instanceof Properties) { + appendLine("{"); + appendProperties((Properties) value); + appendLine("}"); + } else { + throw new IllegalArgumentException(key); + } + addContent(System.lineSeparator()); + + return this; + } + + /** + * Adds the string value. + * + * @param value + * the value + */ + private void addStringValue(String value) { + addContent("'" + value + "'"); + } + + /** + * Append suffix. + * + * @param sb + * the sb + */ + private void appendSuffix(StringBuilder sb) { + if (contentName != null) { + sb.append(indent).append("}").append(System.lineSeparator()); + } + } + + /** + * Append prefix. + * + * @param sb + * the sb + */ + private void appendPrefix(StringBuilder sb) { + if (contentName != null) { + sb.append(indent); + sb.append(contentName); + sb.append(" {").append(System.lineSeparator()); + } + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/EntityBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/EntityBuilder.java new file mode 100644 index 0000000..166ae50 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/EntityBuilder.java @@ -0,0 +1,70 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import java.util.Properties; +import org.onap.aai.validation.ruledriven.configuration.EntitySection; +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Builder for entity config section + * + */ +public class EntityBuilder extends ContentBuilder { + + /** + * Create an empty entity section + */ + public EntityBuilder() { + super("entity"); + } + + /** + * @param auditConfiguration + */ + public EntityBuilder(EntitySection auditConfiguration) { + this(); + appendValue("name", auditConfiguration.getName()); + + for (RuleSection rule : auditConfiguration.getRules()) { + addContent(new ValidationBuilder(rule).toString()); + } + } + + /** + * Add an empty validation section to this entity + * + * @return + */ + public ValidationBuilder validation() { + ValidationBuilder item = new ValidationBuilder(); + addContent(item); + return item; + } + + /** + * @param props + * @return + */ + public ValidationBuilder validation(Properties props) { + ValidationBuilder item = validation(); + item.appendProperties(props); + return item; + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/RuleBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/RuleBuilder.java new file mode 100644 index 0000000..07fb5b4 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/RuleBuilder.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Builder for rule config section. + */ +public class RuleBuilder extends ContentBuilder { + + /** + * Instantiates a new rule builder. + */ + public RuleBuilder() { + super("rule"); + } + + /** + * Instantiates a new rule builder using the existing rule configuration + * + * @param ruleConfig + * the rule configuration to clone + * @param indent + * the indent/prefix for the section + */ + public RuleBuilder(RuleSection ruleConfig, String indent) { + this(); + this.indent = indent; + if (ruleConfig.isGeneric()) { + appendLine(indent + "\t// Generic Rule"); + } + appendValue("name", ruleConfig.getName()); + appendValue("category", ruleConfig.getCategory()); + appendValue("description", ruleConfig.getDescription()); + appendValue("severity", ruleConfig.getSeverity()); + if (ruleConfig.isGeneric()) { + appendLine(indent + "\t// Passing " + ruleConfig.getAttributes()); + } + appendValue("attributes", ruleConfig.getExpressionFieldNames()); + appendValue("validate", ruleConfig.getExpression()); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/UseRuleBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/UseRuleBuilder.java new file mode 100644 index 0000000..fe72bac --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/UseRuleBuilder.java @@ -0,0 +1,46 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Content Builder for a useRule section + * + */ +public class UseRuleBuilder extends ContentBuilder { + + /** + * Build a useRule section + */ + public UseRuleBuilder() { + super("useRule"); + } + + /** + * @param ruleConfig + * @param indent + */ + public UseRuleBuilder(RuleSection ruleConfig, String indent) { + this(); + this.indent = indent; + appendValue("name", ruleConfig.getName()); + appendValue("attributes", ruleConfig.getAttributes()); + } + +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ValidationBuilder.java b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ValidationBuilder.java new file mode 100644 index 0000000..44f3c4c --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/configuration/build/ValidationBuilder.java @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.configuration.build; + +import org.onap.aai.validation.ruledriven.configuration.RuleSection; + +/** + * Builder for validation config section + * + */ +public class ValidationBuilder extends ContentBuilder { + /** + * Create an empty validation section + */ + public ValidationBuilder() { + super("validation"); + indent = "\t"; + } + + /** + * Create a validation section using the existing rule configuration + * + * @param ruleConfig + */ + public ValidationBuilder(RuleSection ruleConfig) { + this(); + appendValue("object", ruleConfig.getType()); + appendValue("objectId", ruleConfig.getObjectId()); + if (ruleConfig.isGeneric()) { + addContent(new UseRuleBuilder(ruleConfig, indent + "\t").toString()); + } else { + addContent(new RuleBuilder(ruleConfig, indent + "\t").toString()); + } + } + + /** + * Add a useRule section + * + * @param name + * @return the new useRule section + */ + public UseRuleBuilder useRule(String name) { + UseRuleBuilder item = new UseRuleBuilder(); + item.indent = "\t\t"; + item.appendValue("name", name); + addContent(item); + return item; + } + + /** + * Add a rule section comprising the specified name item + * + * @param name + * @return + */ + public RuleBuilder rule(String name) { + RuleBuilder item = new RuleBuilder(); + item.indent = "\t\t"; + item.appendValue("name", name); + addContent(item); + return item; + } +}
\ No newline at end of file diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java new file mode 100644 index 0000000..b151f6b --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/GroovyRule.java @@ -0,0 +1,323 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.rule; + +import groovy.lang.GroovyClassLoader; +import groovy.lang.GroovyObject; +import groovy.lang.MetaMethod; +import groovy.lang.MissingMethodException; +import groovy.lang.MissingPropertyException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; +import org.codehaus.groovy.control.CompilationFailedException; +import org.codehaus.groovy.runtime.InvokerInvocationException; +import org.onap.aai.cl.api.Logger; +import org.onap.aai.validation.logging.LogHelper; +import org.onap.aai.validation.reader.data.AttributeValues; +import org.onap.aai.validation.ruledriven.configuration.GroovyConfigurationException; +import org.onap.aai.validation.ruledriven.configuration.RuleSection; +import org.onap.aai.validation.util.StringUtils; + +/** + * Rule based on a Groovy script + * + */ +public class GroovyRule implements Rule { + + private static final Logger applicationLogger = LogHelper.INSTANCE; + + /** + * Do not allow special characters (e.g. ?) in an attribute name. Hyphens and underscores are accepted. + */ + private static final Pattern ATTRIBUTE_NAME_WHITELIST = Pattern.compile("^[a-zA-Z0-9-_.\\*\\[\\]]*$"); + private static final Pattern ATTRIBUTE_NAME_BLACKLIST = Pattern.compile("^(|null)$"); + + private String errorCategory; + private String errorMessage; + private String severity; + private List<String> attributes; + private List<String> attributePaths; // where in the JSON entity to read the attributes from + + private String methodName; + private GroovyObject groovyObject; + private List<String> originalFields; + private String originalExpression; + private String groovyExpression; // NOSONAR stored for debugging purposes + private boolean ruleIsValid = true; + private String name; + + + /** + * @param ruleConfig + * @throws InstantiationException + * @throws IllegalAccessException + * @throws IOException + * @throws GroovyConfigurationException + */ + public GroovyRule(RuleSection ruleConfig) + throws InstantiationException, IllegalAccessException, IOException, GroovyConfigurationException { + setName(ruleConfig.getName()); + setErrorCategory(ruleConfig.getCategory()); + setErrorMessage(ruleConfig.getErrorMessage()); + setSeverity(ruleConfig.getSeverity()); + setAttributes(ruleConfig.getAttributes()); + setAttributePaths(ruleConfig.getAttributes()); + + this.originalFields = new ArrayList<>(); + for (String field : ruleConfig.getExpressionFieldNames()) { + originalFields.add(StringUtils.stripPrefix(field, ".")); + } + + Class<?> groovyClass = createRule(ruleConfig.getExpressionFieldNames(), ruleConfig.getExpression()); + + if (groovyClass != null) { + groovyObject = (GroovyObject) groovyClass.newInstance(); + + for (MetaMethod method : groovyObject.getMetaClass().getMethods()) { + if (method.getName().startsWith("rule")) { + methodName = method.getName(); + } + } + + try { + executeWithSampleData(); + } catch (IllegalArgumentException e) { // NOSONAR + if (e.getCause() instanceof InvokerInvocationException + && e.getCause().getCause() instanceof MissingMethodException) { + applicationLogger + .debug("WARNING: Rule \"" + getName() + "\" does not accept \"1\" for all input values"); + } else { + ruleIsValid = false; + } + } + } else { + ruleIsValid = false; + } + } + + public boolean isValid() { + return ruleIsValid; + } + + /** + * Run the rule expression on the specified attribute values + * + * @param attributeValues + * @return + */ + @Override + public Boolean execute(AttributeValues attributeValues) { + // Obtain the values of each of the attributes to pass into the rule + List<Object> valueList = new ArrayList<>(); + for (String attrName : this.attributePaths) { + valueList.add(attributeValues.get(attrName)); + } + Object[] attrValuesArray = valueList.toArray(); + return execute(attrValuesArray); + } + + /** + * Apply the rule to some attribute(s) + * + * @param values + * + * @param groovyObject an instance/object of a Groovy class that implements one or more rule methods + * @return the Boolean result of evaluating the expression + */ + @Override + public Boolean execute(Object... values) { + Object result = null; + try { + result = groovyObject.invokeMethod(getRuleMethod(), values); + } catch (MissingPropertyException | MissingMethodException | InvokerInvocationException e) { + throw new IllegalArgumentException(e); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Argument is null", e); + } + + if (result instanceof Number) { + return !result.equals(0); + } else { + return (Boolean) result; + } + } + + @Override + public String toString() { + return "GroovyRule \"" + name + "\" " + attributePaths + " -> " + originalFields + " {" + originalExpression + + "}"; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getErrorCategory() { + return errorCategory; + } + + public void setErrorCategory(String errorCategory) { + this.errorCategory = errorCategory; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + @Override + public List<String> getAttributePaths() { + return attributePaths; + } + + private String getRuleMethod() { + return methodName; + } + + private void setAttributePaths(List<String> attributePaths) { + this.attributePaths = attributePaths; + } + + private void setAttributes(List<String> attributes) { + this.attributes = new ArrayList<>(); + for (String attribute : attributes) { + // Strip any prefixes containing the . character + this.attributes.add(attribute.substring(attribute.lastIndexOf('.') + 1)); + } + } + + /** + * @param fields + * @param expression + * @return + * @throws IOException + * @throws GroovyConfigurationException + */ + private Class<?> createRule(List<String> fields, String expression) + throws IOException, GroovyConfigurationException { + originalExpression = expression; + groovyExpression = expression; + String methodParams = ""; + + int i = 1; + for (String attribute : fields) { + if (isValidAttributeName(attribute)) { + String fieldName = "field" + i++; + methodParams = appendParameter(methodParams, fieldName); + // Strip any prefixes from the attribute name in case of JayWay expression attributes. + attribute = StringUtils.stripPrefix(attribute, "."); + String regex = "\\b" + attribute + "\\b(?!\\()"; + groovyExpression = groovyExpression.replaceAll(regex, fieldName); + } else { + ruleIsValid = false; + return null; + } + } + + return loadGroovyClass("def rule(" + methodParams + ") {" + groovyExpression + "}"); + } + + private String appendParameter(String methodParams, String fieldName) { + StringBuilder newParams = new StringBuilder(); + if (methodParams.length() > 0) { + newParams.append(methodParams).append(", "); + } + newParams.append("Object ").append(fieldName); + return newParams.toString(); + } + + private boolean isValidAttributeName(String attributeName) { + if (ATTRIBUTE_NAME_BLACKLIST.matcher(attributeName).matches()) { + return false; + } + + if (!ATTRIBUTE_NAME_WHITELIST.matcher(attributeName).matches()) { + return false; + } + + // Make sure that the attribute name is NOT purely a number + try (Scanner sc = new Scanner(attributeName.trim())) { + if (sc.hasNextInt()) { + sc.nextInt(); // Consume the integer + return sc.hasNext(); // Name is valid if there is more content + } + } + + return true; // Not an integer + } + + private void executeWithSampleData() { + Object[] values = new Object[attributes.size()]; + int i = 0; + for (String attribute : attributes) { + if (attribute.contains("[*]")) { + values[i++] = Arrays.asList("{}"); + } else { + values[i++] = "1"; + } + } + execute(values); + } + + /** + * Load and parse a Groovy script to create an anonymous class + * + * @param script a file containing the Groovy scripting language + * @return the Java Class for accessing the Groovy methods + * @throws IOException + * @throws GroovyConfigurationException + */ + @SuppressWarnings("rawtypes") + private static Class loadGroovyClass(String expression) throws IOException, GroovyConfigurationException { + ClassLoader parent = GroovyRule.class.getClassLoader(); + GroovyClassLoader loader = new GroovyClassLoader(parent); + Class groovyClass; + try { + groovyClass = loader.parseClass(expression); + } catch (CompilationFailedException e) { + throw new GroovyConfigurationException(e); + } finally { + loader.close(); + } + return groovyClass; + } + +} diff --git a/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java b/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java new file mode 100644 index 0000000..1196db0 --- /dev/null +++ b/src/main/java/org/onap/aai/validation/ruledriven/rule/Rule.java @@ -0,0 +1,81 @@ +/* + * ============LICENSE_START=================================================== + * Copyright (c) 2018 Amdocs + * ============================================================================ + * Licensed 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. + * ============LICENSE_END===================================================== + */ +package org.onap.aai.validation.ruledriven.rule; + +import java.util.List; +import org.onap.aai.validation.reader.data.AttributeValues; + +/** + * A rule that accepts one or more attributes and returns a Boolean result (when executed) + */ +public interface Rule { + + /** + * Gets the name of the rule + * + * @return the name + */ + String getName(); + + /** + * Gets the error message. + * + * @return the error message + */ + String getErrorMessage(); + + /** + * Gets the error category. + * + * @return the error category + */ + String getErrorCategory(); + + /** + * Gets the severity. + * + * @return the severity + */ + String getSeverity(); + + /** + * Gets the paths to the attributes to pass to the rule + * + * @return the attribute paths + */ + List<String> getAttributePaths(); + + /** + * Execute the rule. + * + * @param values + * the attribute values to pass to the rule + * @return a boolean representing the rule evaluation (meaning success/failure) + */ + Boolean execute(AttributeValues values); + + /** + * Execute the rule. + * + * @param values + * the attribute values to pass to the rule + * @return a boolean representing the rule evaluation (meaning success/failure) + */ + Boolean execute(Object... values); + +} |