All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.imsweb.validation.ValidationEngine Maven / Gradle / Ivy

There is a newer version: 021-11
Show newest version
/*
 * Copyright (C) 2004 Information Management Services, Inc.
 */
package com.imsweb.validation;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;

import com.imsweb.validation.entities.Category;
import com.imsweb.validation.entities.Condition;
import com.imsweb.validation.entities.ContextEntry;
import com.imsweb.validation.entities.EditableCondition;
import com.imsweb.validation.entities.EditableRule;
import com.imsweb.validation.entities.EditableValidator;
import com.imsweb.validation.entities.EmbeddedSet;
import com.imsweb.validation.entities.Rule;
import com.imsweb.validation.entities.RuleFailure;
import com.imsweb.validation.entities.RuleHistory;
import com.imsweb.validation.entities.Validatable;
import com.imsweb.validation.entities.Validator;
import com.imsweb.validation.internal.ExecutableCondition;
import com.imsweb.validation.internal.ExecutableRule;
import com.imsweb.validation.internal.IterativeProcessor;
import com.imsweb.validation.internal.Processor;
import com.imsweb.validation.internal.ValidatingProcessor;
import com.imsweb.validation.internal.callable.RuleCompilingCallable;
import com.imsweb.validation.runtime.CompiledRules;
import com.imsweb.validation.runtime.RuntimeUtils;

/**
 * This class is responsible for running loaded rules (edits) on {@link Validatable} objects and returning a collection of {@link RuleFailure} objects.
 * 

* The first thing that needs to happen before using the engine for validation is to initialize the services and the context methods. This is accomplished * by calling the initialize method of the {@link ValidationServices} and {@link ValidationContextFunctions} classes. That method takes as an argument * an instance of those classes; to use the default implementation, you can instanciate those classes themselves: *
 *     ValidationServices.initialize(new ValidationServices());
 *     ValidationContextFunctions.initialize(new ValidationContextFunctions());
 * 
* But that is not required since those classes will be automatically initialized with those default classes the first time the getInstance() method is called * (if they have not been explicitly initialized yet). *

* More complex applications might need more customized services and extra context functions available to the Groovy edits; in that case those classes * should be extended and initialized with the customized versions (before any code tries to get their instance): *
 *     ValidationServices.initialize(new MyCustomValidatorServices());
 *     ValidationContextFunctions.initialize(new MyCustomValidatorContextFunctions());
 * 
* The second thing to do is to initialize the engine using one of its initialize() methods. Those methods take as argument an optional options object and one * or several {@link Validator} objects, which represent a logical group of {@link Rule} (edits); usually in a file. The {@link Validator} object can be built * programmatically or parsed from XML using the utility methods from the {@link ValidationXmlUtils} class. *

* Prior to version 2.0 of the library, the engine was a singleton class with static methods. As of 2.0, the engine is not static anymore and can be created via its * public constructor. A big advantages of that approach is to allow multiple engines to run concurrently in a given application. To be compatible with prior versions * and because most applications only need one instance of an engine, this class has a cached static engine available. It can always be called via the getInstance() * method and that instance is never null (but it does need to be initialized like any other instance). *

* After initializing the engine, the validate() methods can be called. They take as argument a {@link Validatable} object. Those objects are * application-dependent; the validation module only defines an interface (and a few simple implementations, like {@link com.imsweb.validation.entities.SimpleMapValidatable} * and {@link com.imsweb.validation.entities.SimpleNaaccrLinesValidatable}), it is the caller's responsibility to define the {@link Validatable} corresponding to their need * (for example one application might validate NAACCR records, but another one might validate tumor objects). *

* This class also contains several methods to allow updating the state of the engine dynamically (for example adding/removing rules, etc...). This is an * advanced feature; most applications only need to execute edits, not update them... *

* Created on Apr 26, 2008 by Fabian Depry */ public class ValidationEngine { /** * Engine version (used to check compatibility with the edits) */ private static final String _ENGINE_VERSION = "6.6"; /** * The different context types supported by the engine */ public static final String CONTEXT_TYPE_GROOVY = "groovy"; public static final String CONTEXT_TYPE_JAVA = "java"; public static final String CONTEXT_TYPE_TABLE = "table"; public static final String CONTEXT_TYPE_TABLE_INDEX_DEF = "table-index-def"; /** * Context key for the helper functions - Functions. */ public static final String VALIDATOR_FUNCTIONS_KEY = "Functions"; /** * Context key for the context objects (new context notation, introduced in version 4.3) - Context. */ public static final String VALIDATOR_CONTEXT_KEY = "Context"; /** * Context key for the testing helper functions - Testing. */ public static final String VALIDATOR_TESTING_FUNCTIONS_KEY = "Testing"; /** * Context key for the force-failure-on-entity mechanism (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_FORCE_FAILURE_ENTITY_KEY = "__force_failure_on_entity_key"; /** * Context key for the force-failure-on-entity mechanism (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_FORCE_FAILURE_PROPERTY_KEY = "__force_failure_on_property_key"; /** * Context key for the force-failure-on-entity mechanism (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_IGNORE_FAILURE_PROPERTY_KEY = "__ignore_failure_on_property_key"; /** * Context key to provide extra error messages (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_ERROR_MESSAGE = "__error_message"; /** * Context key to provide extra error messages (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_EXTRA_ERROR_MESSAGES = "__extra_error_messages"; /** * Context key to provide warning messages (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_INFORMATION_MESSAGES = "__information_messages"; /** * Context key to set a flag indicating an edit failed (used internally for the Genedits translated edits only). */ public static final String VALIDATOR_FAILING_FLAG = "__failing_flag"; /** * The true result returned by the translated edits (it's possible for an edit to fail because of a set flag but still return true...) */ public static final String VALIDATOR_ORIGINAL_RESULT = "__original_result"; /** * Message used when an exception happened while executing a rule. */ public static final String EXCEPTION_MSG = "Edit failed with exception."; /** * Message used when a rule doesn't define an error message. */ @SuppressWarnings("unused") public static final String NO_MESSAGE_DEFINED_MSG = "No default error message defined."; /** * Cached instance of an engine; most applications should use this instance but some advance use of this framework might require multiple engine to run concurrently... */ private static final ValidationEngine _INSTANCE = new ValidationEngine(); /** * Returns the cached engine. */ public static ValidationEngine getInstance() { return _INSTANCE; } /** * Map of Validators, keyed by validator ID */ protected Map _validators = new HashMap<>(); /** * Map of Processors, keyed by java-path root */ protected Map _processors = new HashMap<>(); /** * Currently used processor roots (for SEER, that would be "lines", for DMS it would be "patient", etc...); values are number of edits under that root */ protected Map _processorRoots = new HashMap<>(); /** * Map of ExecutableRules, keyed by rule internal ID */ protected Map _executableRules = new HashMap<>(); /** * Map of ExecutableConditions, keyed by condition internal ID */ protected Map _executableConditions = new HashMap<>(); /** * Compiled contexts, keyed by validator internal ID and context ID */ protected Map> _contexts = new HashMap<>(); /** * Possible statuses for the engine */ private enum ValidationEngineStatus { /** * Validation engine status */ NOT_INITIALIZED, /** * Validation engine status */ INITIALIZING, /** * Validation engine status */ INITIALIZED } /** * Current engine status */ private ValidationEngineStatus _status = ValidationEngineStatus.NOT_INITIALIZED; /** * Initialization options */ protected InitializationOptions _options; /** * Private lock controlling access to the state of the engine; all methods using the state of the engine (including the validate methods) need to acquire a read lock; * all methods changing the state of the engine need to acquire a write lock. */ private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock(); /** * The edits statistics gathered so far by the engine (the collection will be empty if the statistics are disabled in the initialization options) */ protected Map _editsStats = new HashMap<>(); /** * Whether or not the edits statistics should be computed (initial value is based on the initialization options, but can be changed later) */ protected boolean _computeEditsStats = false; /** * Private lock controlling access to the stats; those are "written" every time new stats are reported (which is constantly), and so using a global engine lock is not good enough. */ private final ReentrantReadWriteLock _statsLock = new ReentrantReadWriteLock(); // ******************************************************************************** // INITIALIZATION METHOD (require the write lock) // ******************************************************************************** /** * Returns true if the engine has been initialized, false otherwise. *

* Created on Aug 12, 2009 by depryf * @return true if the engine has been initialized, false otherwise. */ public boolean isInitialized() { return _status == ValidationEngineStatus.INITIALIZED; } /** * Initializes this validation engine. *

* Created on Mar 6, 2008 by depryf */ public void initialize() { try { initialize(new InitializationOptions(), Collections.emptyList()); } catch (ConstructionException e) { throw new RuntimeException(e); // should never happen since we are not really initializing anything... } } /** * Initializes this validation engine. *

* Created on Mar 6, 2008 by depryf * @param options initialization options * @return initialization statistics */ public InitializationStats initialize(InitializationOptions options) { try { return initialize(options, Collections.emptyList()); } catch (ConstructionException e) { throw new RuntimeException(e); // should never happen since we are not really initializing anything... } } /** * Initializes this validation engine using the provided Validator. *

* Created on Mar 6, 2008 by depryf * @param validator Validator to load * @return initialization statistics * @throws ConstructionException if a ... construction exception happens... */ public InitializationStats initialize(Validator validator) throws ConstructionException { return initialize(new InitializationOptions(), Collections.singletonList(validator)); } /** * Initializes this validation engine using the provided Validator. *

* Created on Mar 6, 2008 by depryf * @param options initialization options * @param validator Validator to load * @return initialization statistics * @throws ConstructionException if a ... construction exception happens... */ public InitializationStats initialize(InitializationOptions options, Validator validator) throws ConstructionException { return initialize(options, Collections.singletonList(validator)); } /** * Initializes this validation engine using the provided list of Validator. *

* Created on Mar 6, 2008 by depryf * @param validators list of Validator to load * @return initialization statistics * @throws ConstructionException if a ... construction exception happens... */ public InitializationStats initialize(List validators) throws ConstructionException { return initialize(new InitializationOptions(), validators); } /** * Initializes this validation engine using the provided list of Validator. *

* Created on Mar 6, 2008 by depryf * @param options initialization options * @param validators list of Validator to load * @return initialization statistics * @throws ConstructionException if a ... construction exception happens... */ public InitializationStats initialize(InitializationOptions options, List validators) throws ConstructionException { _status = ValidationEngineStatus.INITIALIZING; InitializationStats stats = new InitializationStats(); long start = System.currentTimeMillis(); _lock.writeLock().lock(); try { uninitialize(); _options = options == null ? new InitializationOptions() : options; _computeEditsStats = _options.isEngineStatsEnabled(); if (validators != null) { checkValidatorConstraints(validators); Map rules = new ConcurrentHashMap<>(); Map conditions = new ConcurrentHashMap<>(); Map> allContexts = new ConcurrentHashMap<>(); // internalize the validators (that will compile any Groovy, which could through a construction exception) for (Validator v : validators) { Map contexts = new HashMap<>(); internalizeValidator(v, conditions, rules, contexts, stats); allContexts.put(v.getValidatorId(), contexts); } // sort the rules by dependencies (this could throw a dependency exception) List sortedRules = getRulesSortedByDependencies(rules, conditions); // at this point we checked everything, so let's update the internal state of the engine _executableConditions.putAll(conditions); _executableRules.putAll(rules); _contexts.putAll(allContexts); populateProcessors(sortedRules); // update the raw structure only if the state was successfully updated... for (Validator v : validators) _validators.put(v.getId(), v); } else populateProcessors(null); } finally { _lock.writeLock().unlock(); } _status = ValidationEngineStatus.INITIALIZED; stats.setInitializationDuration(System.currentTimeMillis() - start); return stats; } /** * Un-initializes the engine; the engine can't be used after this call, unless it is re-initialized. *

* Created on Apr 15, 2010 by depryf */ public void uninitialize() { _status = ValidationEngineStatus.NOT_INITIALIZED; _lock.writeLock().lock(); try { _validators.clear(); _processors.clear(); _processorRoots.clear(); _executableRules.clear(); _executableConditions.clear(); _contexts.clear(); } finally { _lock.writeLock().unlock(); } } // ******************************************************************************** // GET METHODS (require the read lock) // ******************************************************************************** /** * Returns all the Validators contained in the engine, keyed by their ID. *

* ATTENTION: any entities returned by this method should not be modified outside of the engine! *

* Created on Jun 29, 2011 by depryf * @return all the Validators contained in the engine, keyed by their ID; maybe empty but never null */ public Map getValidators() { _lock.readLock().lock(); try { return Collections.unmodifiableMap(_validators); } finally { _lock.readLock().unlock(); } } /** * Returns the Validator for the requested ID. *

* ATTENTION: any entities returned by this method should not be modified outside of the engine! *

* Created on Jun 29, 2011 by depryf * @param validatorId validator ID * @return the Validator for the requested ID, null if not found */ public Validator getValidator(String validatorId) { if (validatorId == null) return null; _lock.readLock().lock(); try { return _validators.get(validatorId); } finally { _lock.readLock().unlock(); } } /** * Returns the Condition for the requested ID. *

* Created on Apr 5, 2011 by depryf * @param conditionId condition ID * @return requested Condition, null if not found */ public Condition getCondition(String conditionId) { return getCondition(conditionId, null); } /** * Returns the Condition for the requested ID, in the requested validator. *

* Created on Apr 5, 2011 by depryf * @param conditionId condition ID (if null, then null will be returned) * @param validatorId validator ID (if null then the condition will be searched among all the available validators) * @return requested Condition, null if not found */ public Condition getCondition(String conditionId, String validatorId) { if (conditionId == null) return null; _lock.readLock().lock(); try { if (validatorId != null) { Validator v = _validators.get(validatorId); if (v == null) return null; return v.getCondition(conditionId); } for (Validator v : _validators.values()) { Condition c = v.getCondition(conditionId); if (c != null) return c; } return null; } finally { _lock.readLock().unlock(); } } /** * Returns the Category for the requested ID. *

* Created on Apr 5, 2011 by depryf * @param categoryId category ID * @return requested Category, null if not found */ public Category getCategory(String categoryId) { return getCategory(categoryId, null); } /** * Returns the Category for the requested ID, in the requested validator. *

* Created on Apr 5, 2011 by depryf * @param categoryId category ID (if null, then null will be returned) * @param validatorId validator ID (if null then the category will be searched among all the available validators) * @return requested Category, null if not found */ public Category getCategory(String categoryId, String validatorId) { if (categoryId == null) return null; _lock.readLock().lock(); try { if (validatorId != null) { Validator v = _validators.get(validatorId); if (v == null) return null; return v.getCategory(categoryId); } for (Validator v : _validators.values()) { Category c = v.getCategory(categoryId); if (c != null) return c; } return null; } finally { _lock.readLock().unlock(); } } /** * Returns the Rule for the requested ID. *

* Created on Apr 5, 2011 by depryf * @param ruleId rule ID * @return requested Rule, null if not found */ public Rule getRule(String ruleId) { return getRule(ruleId, null); } /** * Returns the Rule for the requested ID, in the requested validator. *

* Created on Apr 5, 2011 by depryf * @param ruleId rule ID (if null, then null will be returned) * @param validatorId validator ID (if null then the rule will be searched among all the available validators) * @return requested Rule, null if not found */ public Rule getRule(String ruleId, String validatorId) { if (ruleId == null) return null; _lock.readLock().lock(); try { if (validatorId != null) { Validator v = _validators.get(validatorId); if (v == null) return null; return v.getRule(ruleId); } for (Validator v : _validators.values()) { Rule r = v.getRule(ruleId); if (r != null) return r; } return null; } finally { _lock.readLock().unlock(); } } /** * Returns the compiled context for the requested key. *

* Created on Jul 7, 2011 by depryf * @param contextKey context key (if null, then null will be returned) * @return requested compiled context, null if not found */ public Object getContext(String contextKey) { return getContext(contextKey, null); } /** * Returns the compiled context for the requested key, in the requested validator. *

* Created on Jul 7, 2011 by depryf * @param contextKey context key (if null, then null will be returned) * @param validatorId validator ID (if null then the context will be searched among all the available validators) * @return requested compiled context, null if not found */ public Object getContext(String contextKey, String validatorId) { if (contextKey == null) return null; _lock.readLock().lock(); try { if (validatorId != null) { Validator v = _validators.get(validatorId); if (v == null) return null; return _contexts.get(v.getValidatorId()).get(contextKey); } for (Map context : _contexts.values()) { Object c = context.get(contextKey); if (c != null) return c; } return null; } finally { _lock.readLock().unlock(); } } // ******************************************************************************** // VALIDATE METHODS (require the read lock) // ******************************************************************************** /** * Validates the provided Validatable object using all the rules loaded in the engine. *

* Note that a rule object itself can be flagged as ignored, in which case it will not run when this method is invoked. *

* Created on Mar 6, 2008 by depryf * @param validatable a Validatable, cannot be null * @return a collection of RuleFailure, maybe empty but not null * @throws ValidationException if anything goes wrong during the validation */ public Collection validate(Validatable validatable) throws ValidationException { _lock.readLock().lock(); try { ValidatingContext vContext = new ValidatingContext(); vContext.setComputeEditsStats(_computeEditsStats); return internalValidate(validatable, vContext); } finally { _lock.readLock().unlock(); } } /** * Validates the provided Validatable object, ignoring the provided rule IDs. *

* If a rule depends on an ignored rule, it will also be ignored. *

* Note that a rule object itself can be flagged as ignored, in which case it will not run when this method is invoked. *

* This method has been kept for compatibility, the preferred way is to use the one taking both a collection of rule IDs to * ignore and a collection of rule IDs to execute (optionally using null for one of them). *

* Created on Mar 6, 2008 by depryf * @param validatable a Validatable, cannot be null * @param ruleIdsToIgnore rule IDs that need to be ignored * @return a collection of RuleFailure, maybe empty but not null * @throws ValidationException if anything goes wrong during the validation */ public Collection validate(Validatable validatable, Collection ruleIdsToIgnore) throws ValidationException { _lock.readLock().lock(); try { ValidatingContext vContext = new ValidatingContext(); vContext.setToIgnore(ruleIdsToIgnore); vContext.setComputeEditsStats(_computeEditsStats); return internalValidate(validatable, vContext); } finally { _lock.readLock().unlock(); } } /** * Validates the provided Validatable object, executing the provided rule IDs or ignoring them. *

* If a rule depends on an ignored rule, it will be ignored (in other words, the list of execute/ignored IDs doesn't influence the dependency mechanism). *

* Note that a rule object itself can be flagged as ignored, in which case it will not run when this method is invoked (in other words, the list of execute/ignored IDs * doesn't influence the ignored flag of the individual rules). *

* Either collection of IDs can be null (they can also be both null). If they are both non-null, only the execute one will be used. *

* Created on Mar 6, 2008 by depryf * @param validatable a Validatable, cannot be null * @param ruleIdsToIgnore rule IDs that need to be ignored * @param ruleIdsToExecute rule IDs that need to be executed * @return a collection of RuleFailure, maybe empty but not null * @throws ValidationException if anything goes wrong during the validation */ public Collection validate(Validatable validatable, Collection ruleIdsToIgnore, Collection ruleIdsToExecute) throws ValidationException { _lock.readLock().lock(); try { ValidatingContext vContext = new ValidatingContext(); vContext.setToIgnore(ruleIdsToIgnore); vContext.setToExecute(ruleIdsToExecute); vContext.setComputeEditsStats(_computeEditsStats); return internalValidate(validatable, vContext); } finally { _lock.readLock().unlock(); } } /** * Validates the provided Validatable object, running only the single provided rule ID, which must be an exising rule within the engine. *

* Note that a rule object itself can be flagged as ignored, but if a rule is forced to run through this method, its ignore flag won't be used. Similarly, * the condition referenced by the rule (if one is defined) won't be executed. A typical situation for using this method would be for testing a particular rule, * independently of the ignore flags, dependencies or condition. *

* Because this method uses an existing rule, the engine won't have to create a runtime version of the rule (it will use the one already exisitng); * therefore this method is the one to use when doing batch testing (running many tests on many rules). *

* Created on Mar 6, 2008 by depryf * @param validatable a Validatable, cannot be null * @param ruleId ID of the rule that needs to be executed, cannot be null, must be a rule existing in the engine * @return a collection of RuleFailure, maybe empty but not null * @throws ValidationException if anything goes wrong during the validation */ public Collection validate(Validatable validatable, String ruleId) throws ValidationException { _lock.readLock().lock(); try { Rule rule = getRule(ruleId); if (rule == null) throw new RuntimeException("Unknown rule ID: " + ruleId); ValidatingContext vContext = new ValidatingContext(); vContext.setToForce(rule); vContext.setComputeEditsStats(_computeEditsStats); return internalValidate(validatable, vContext); } finally { _lock.readLock().unlock(); } } /** * Validates the provided Validatable object, running only the single provided rule. The rule must have a valid java path set. *

* Note that a rule object itself can be flagged as ignored, but if a rule is forced to run through this method, its ignore flag won't be used. Similarly, * the condition referenced by the rule (if one is defined) won't be executed. A typical situation for using this method would be for testing a particular rule, * independently of the ignore flags, dependencies or condition. *

* Whether the provided rule already exists in the engine or not, this method will force the engine to create a runtime version of the rule. This is useful * to run a test on a modified version of an existing rule, or on a rule that doesn't exist in the engine yet. The downside is that this is an expensive * operation that will be very inefficient for doing batch testing (running many tests on many rules). *

* Created on Mar 6, 2008 by depryf * @param validatable a Validatable, cannot be null * @param rule a Rule object, cannot be null, it doesn't have to exist in the validation engine * @return a collection of RuleFailure, maybe empty but not null * @throws ValidationException if anything goes wrong during the validation */ public Collection validate(Validatable validatable, Rule rule) throws ValidationException { _lock.readLock().lock(); try { if (rule == null) throw new RuntimeException("This method requires a non-null rule!"); if (rule.getJavaPath() == null) throw new RuntimeException("The provided rule must have a java-path!"); ValidatingContext vContext = new ValidatingContext(); vContext.setToForce(rule); vContext.setComputeEditsStats(_computeEditsStats); return internalValidate(validatable, vContext); } finally { _lock.readLock().unlock(); } } /** * Validates the provided Validatable object using all the rules loaded in the engine. *

* Note that a rule object itself can be flagged as ignored, in which case it will not run when this method is invoked. *

* Created on Mar 6, 2008 by depryf * @param validatable a Validatable, cannot be null * @param vContext a ValidatingContext, cannot be null. All the other validate methods are convenience methods, this one is the main one. * Using a validating context allows the caller to have access to some information that is gathered during the validation; for example, * all the failed rules and failed conditions can be accessed through the context once the method returns... * @return a collection of RuleFailure, maybe empty but not null * @throws ValidationException if anything goes wrong during the validation */ public Collection validate(Validatable validatable, ValidatingContext vContext) throws ValidationException { _lock.readLock().lock(); try { vContext.setComputeEditsStats(_computeEditsStats); return internalValidate(validatable, vContext); } finally { _lock.readLock().unlock(); } } // ******************************************************************************** // ADD/DELETE/UPDATE METHODS (require the write lock // ******************************************************************************** /** * Updates an existing rule in the engine. *

* The following steps should be performed: *

    *
  1. Create an editable rule by using the EditableRule's default constructor.
  2. *
  3. Modify the editable rule (this would correspond to changes done in a GUI by a user)
  4. *
  5. Call this method using the editable rule
  6. *
*

* Once this method returns (and if no exception were thrown), the new rule will be accessible by using the getRule() method. *

* Created on Jun 29, 2011 by depryf * @param editableRule EditableRule, cannot be null * @return the created Rule * @throws ConstructionException if the rule contains an error */ public Rule addRule(EditableRule editableRule) throws ConstructionException { _lock.writeLock().lock(); try { if (editableRule == null) throw new ConstructionException("An editable rule is required for adding a new edit"); if (editableRule.getId() == null) throw new ConstructionException("An edit ID is required when adding a new edit"); if (editableRule.getJavaPath() == null) throw new ConstructionException("A java-path is required when adding a new edit"); if (editableRule.getValidatorId() == null) throw new ConstructionException("A group is required when adding a new edit"); if (editableRule.getMessage() == null) throw new ConstructionException("A message is required when adding a new edit"); if (getRule(editableRule.getId()) != null) throw new ConstructionException("Edit IDs must be unique within the edits engine, cannot add '" + editableRule.getId() + "'"); if (!_validators.containsKey(editableRule.getValidatorId())) throw new ConstructionException("Unknown group: " + editableRule.getValidatorId()); if (!ValidationServices.getInstance().getAllJavaPaths().containsKey(editableRule.getJavaPath())) throw new ConstructionException("Unknown java-path: " + editableRule.getJavaPath()); // verify the condition exists if provided if (editableRule.getConditions() != null) { for (String conditionId : editableRule.getConditions()) { Condition condition = getCondition(conditionId, null); // passing null for the validator ID to allow cross-validator conditions (used in SEER*DMS) if (condition == null) throw new ConstructionException("Unknown condition: " + conditionId); } } // verify the category exists if provided if (editableRule.getCategory() != null) { Category category = getCategory(editableRule.getCategory(), null); // passing null for the validator ID to allow cross-validator conditions (used in SEER*DMS) if (category == null) throw new ConstructionException("Unknown category: " + editableRule.getCategory()); } // create the rule to add Rule rule = new Rule(); rule.setId(editableRule.getId()); rule.setRuleId(editableRule.getRuleId()); if (rule.getRuleId() == null) rule.setRuleId(ValidationServices.getInstance().getNextRuleSequence()); rule.setName(editableRule.getName()); rule.setJavaPath(editableRule.getJavaPath()); rule.setExpression(editableRule.getExpression()); rule.setMessage(editableRule.getMessage()); if (editableRule.getIgnored() != null) rule.setIgnored(editableRule.getIgnored()); if (editableRule.getSeverity() != null) rule.setSeverity(editableRule.getSeverity()); rule.setConditions(editableRule.getConditions()); rule.setUseAndForConditions(editableRule.getUseAndForConditions()); rule.setCategory(editableRule.getCategory()); rule.setDescription(editableRule.getDescription()); rule.setDependencies(editableRule.getDependencies()); rule.setHistories(editableRule.getHistories()); rule.setValidator(_validators.get(editableRule.getValidatorId())); // create an executable rule from it ExecutableRule execRule = new ExecutableRule(rule); // update the dependencies; make sure we don't leave the internal structures in a bad state if something goes wrong... Map rules = new HashMap<>(_executableRules); rules.put(execRule.getInternalId(), execRule); List sortedRules = getRulesSortedByDependencies(rules, _executableConditions); // this will validate the rule dependencies... _executableRules.put(execRule.getInternalId(), execRule); // update the processors after re-evaluating the rules order (if the new java path doesn't exist, re-populate all the processors) if (!_processors.containsKey(editableRule.getJavaPath())) populateProcessors(sortedRules); else updateProcessorsRules(sortedRules); // this is way less expensive than re-populating the processors... // update raw data _validators.get(editableRule.getValidatorId()).getRules().add(rule); // update the inverted dependencies in the raw data if (editableRule.getDependencies() != null && !editableRule.getDependencies().isEmpty()) for (Rule r : rule.getValidator().getRules()) if (rule.getDependencies().contains(r.getId())) r.getInvertedDependencies().add(rule.getId()); return rule; } finally { _lock.writeLock().unlock(); } } /** * Updates an existing rule in the engine. *

* The following steps should be performed: *

    *
  1. Get the rule to update using the getRule() method
  2. *
  3. Wrap the rule into an editable rule by passing it to the EditableRule's constructor.
  4. *
  5. Modify the editable rule (this would correspond to changes done in a GUI by a user)
  6. *
  7. Call this method using the editable rule
  8. *
* No modification should be done on the Rule object itself, only the editable rule should be modified! *

* Once this method returns (and if no exception were thrown), the rule will be accessible by using the getRule() method and * all the requested modifications will have been applied to it. *

* Note that the ruleId property should not be modified under any circumstances since it is how the engine knows * which edit needs to be updated. It is an internal identifier. *

* Created on Jun 29, 2011 by depryf * @param editableRule EditableRule, cannot be null * @throws ConstructionException if the rule contains an error */ public void updateRule(EditableRule editableRule) throws ConstructionException { _lock.writeLock().lock(); try { if (editableRule == null) throw new ConstructionException("An editable rule is required for modifying an edit"); if (editableRule.getRuleId() == null) throw new ConstructionException("An internal ID is required when modifying an edit"); if (editableRule.getId() == null) throw new ConstructionException("An edit ID is required when modifying an edit"); if (editableRule.getValidatorId() == null) throw new ConstructionException("A group is required when modifying an edit"); if (editableRule.getMessage() == null) throw new ConstructionException("A message is required when modifying an edit"); if (!_validators.containsKey(editableRule.getValidatorId())) throw new ConstructionException("Unknown group: " + editableRule.getValidatorId()); if (!ValidationServices.getInstance().getAllJavaPaths().containsKey(editableRule.getJavaPath())) throw new ConstructionException("Unknown java-path: " + editableRule.getJavaPath()); // get original executable rule ExecutableRule originalExecRule = _executableRules.get(editableRule.getRuleId()); if (originalExecRule == null) throw new ConstructionException("Validation Engine does not contain requested edit"); // get the rule to update Rule rule = getRule(originalExecRule.getId()); if (rule == null) throw new ConstructionException("Validation Engine does not contain requested edit"); // verify the condition exists if provided if (editableRule.getConditions() != null) { for (String conditionId : editableRule.getConditions()) { Condition condition = getCondition(conditionId, null); // passing null for the validator ID to allow cross-validator conditions (used in SEER*DMS) if (condition == null) throw new ConstructionException("Unknown condition: " + conditionId); } } // verify the category exists if provided if (editableRule.getCategory() != null) { Category category = getCategory(editableRule.getCategory(), null); // passing null for the validator ID to allow cross-validator conditions (used in SEER*DMS) if (category == null) throw new ConstructionException("Unknown category: " + editableRule.getCategory()); } // check ID unicity if (!editableRule.getId().equals(rule.getId())) if (getRule(editableRule.getId()) != null) throw new ConstructionException("Edit IDs must be unique within the edits engine, cannot add '" + editableRule.getId() + "'"); boolean idUpdated = !editableRule.getId().equals(rule.getId()); boolean expressionUpdated = editableRule.getExpression() == null || !editableRule.getExpression().equals(rule.getExpression()); boolean dependenciesUpdated = editableRule.getDependencies() == null || !editableRule.getDependencies().equals(rule.getDependencies()); boolean historiesUpdated = editableRule.getHistories() == null || !editableRule.getHistories().equals(rule.getHistories()); // create an executable rule and update the requested properties (the cheap one are always updated, other ones have a pre-condition) ExecutableRule execRule = new ExecutableRule(originalExecRule); if (idUpdated) execRule.setId(editableRule.getId()); if (expressionUpdated) execRule.setExpression(editableRule.getExpression()); execRule.setMessage(editableRule.getMessage()); execRule.setIgnored(editableRule.getIgnored() == null ? Boolean.FALSE : editableRule.getIgnored()); if (dependenciesUpdated) execRule.setDependencies(editableRule.getDependencies() == null ? Collections.emptySet() : editableRule.getDependencies()); execRule.setConditions(editableRule.getConditions()); execRule.setUseAndForConditions(editableRule.getUseAndForConditions()); execRule.setJavaPath(editableRule.getJavaPath()); // update the dependencies; make sure we don't leave the internal structures in a bad state if something goes wrong... Map rules = new HashMap<>(_executableRules); rules.put(execRule.getInternalId(), execRule); List sortedRules = getRulesSortedByDependencies(rules, _executableConditions); // this will validate the rule dependencies... _executableRules.put(execRule.getInternalId(), execRule); // update the processors after re-evaluating the rules order (if the new java path doesn't exist, re-populate all the processors) if (!_processors.containsKey(editableRule.getJavaPath())) populateProcessors(sortedRules); else updateProcessorsRules(sortedRules); // this is way less expensive than re-populating the processors... // update the raw data rule.setId(editableRule.getId()); rule.setName(editableRule.getName()); rule.setExpression(editableRule.getExpression()); rule.setMessage(editableRule.getMessage()); rule.setIgnored(editableRule.getIgnored() == null ? Boolean.FALSE : editableRule.getIgnored()); rule.setDescription(editableRule.getDescription()); rule.setJavaPath(editableRule.getJavaPath()); rule.setConditions(editableRule.getConditions()); rule.setUseAndForConditions(editableRule.getUseAndForConditions()); rule.setCategory(editableRule.getCategory()); if (editableRule.getSeverity() != null) rule.setSeverity(editableRule.getSeverity()); if (dependenciesUpdated) { Set dependencies = new HashSet<>(); if (editableRule.getDependencies() != null) dependencies.addAll(editableRule.getDependencies()); rule.setDependencies(new HashSet<>(dependencies)); } if (historiesUpdated) { Set histories = new HashSet<>(); if (editableRule.getHistories() != null) { for (RuleHistory hist : editableRule.getHistories()) { hist.setRule(rule); histories.add(hist); } } rule.setHistories(histories); } // update the inverted dependencies in the raw data if (dependenciesUpdated) { for (Rule r : rule.getValidator().getRules()) { if (rule.getDependencies().contains(r.getId())) r.getInvertedDependencies().add(rule.getId()); else r.getInvertedDependencies().remove(rule.getId()); } } } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing rule from the engine. *

* This is a convenient method that only takes the rule ID as a parameter. *

* Created on Jul 7, 2011 by depryf * @param ruleId rule ID to delete * @throws ConstructionException if the rule cannot be found */ public void deleteRule(String ruleId) throws ConstructionException { _lock.writeLock().lock(); try { Rule r = getRule(ruleId); if (r == null) throw new ConstructionException("Unknown edit: " + ruleId); deleteRule(new EditableRule(r)); } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing rule from the engine. *

* The following steps should be performed: *

    *
  1. Get the rule to delete using the getRule() method
  2. *
  3. Wrap the rule into an editable rule by passing it to the EditableRule's constructor.
  4. *
  5. Call this method using the editable rule
  6. *
*

* Created on Jun 29, 2011 by depryf * @param editableRule EditableRule, cannot be null */ public void deleteRule(EditableRule editableRule) throws ConstructionException { _lock.writeLock().lock(); try { if (editableRule == null) throw new ConstructionException("An editable rule is required for deleting an edit"); if (editableRule.getRuleId() == null) throw new ConstructionException("An internal edit ID is required when deleting an edit"); if (editableRule.getId() == null) throw new ConstructionException("An edit ID is required when deleting an edit"); if (editableRule.getValidatorId() == null) throw new ConstructionException("A group is required when deleting an edit"); if (!_validators.containsKey(editableRule.getValidatorId())) throw new ConstructionException("Unknown group: " + editableRule.getValidatorId()); for (Rule r : _validators.get(editableRule.getValidatorId()).getRules()) if (r.getDependencies().contains(editableRule.getId())) throw new ConstructionException(editableRule.getId() + " cannot be deleted, " + r.getId() + " depends on it"); // get the rule Rule rule = getRule(editableRule.getId(), editableRule.getValidatorId()); if (rule == null) throw new ConstructionException("Validation Engine does not contain requested edit"); // update the executable rule _executableRules.remove(rule.getRuleId()); // update the processors after re-evaluating the rules order updateProcessorsRules(getRulesSortedByDependencies(_executableRules, _executableConditions)); // update raw data _validators.get(editableRule.getValidatorId()).getRules().remove(rule); // update the inverted dependencies in the raw data for (Rule r : rule.getValidator().getRules()) if (rule.getDependencies().contains(r.getId())) r.getInvertedDependencies().remove(rule.getId()); } finally { _lock.writeLock().unlock(); } } /** * Adds a new condition in the engine. *

* The following steps should be performed: *

    *
  1. Create an editable condition by using the EditableCondition's default constructor.
  2. *
  3. Modify the editable condition (this would correspond to changes done in a GUI by a user)
  4. *
  5. Call this method using the editable condition
  6. *
*

* Once this method returns (and if no exception were thrown), the new condition will be accessible by using the getCondition() method. *

* Created on Jun 29, 2011 by depryf * @param editableCondition EditableCondition, cannot be null * @return the created Condition * @throws ConstructionException if the condition contains an error */ public Condition addCondition(EditableCondition editableCondition) throws ConstructionException { _lock.writeLock().lock(); try { if (editableCondition == null) throw new ConstructionException("An editable condition is required for adding a new condition"); if (editableCondition.getId() == null) throw new ConstructionException("A condition ID is required when adding a new condition"); if (editableCondition.getValidatorId() == null) throw new ConstructionException("A group is required when adding a new condition"); if (editableCondition.getJavaPath() == null) throw new ConstructionException("A java-path is required when adding a new condition"); if (getCondition(editableCondition.getId()) != null) throw new ConstructionException("Condition IDs must be unique within the edits engine, cannot add '" + editableCondition.getId() + "'"); if (!_validators.containsKey(editableCondition.getValidatorId())) throw new ConstructionException("Unknown group: " + editableCondition.getValidatorId()); if (!ValidationServices.getInstance().getAllJavaPaths().containsKey(editableCondition.getJavaPath())) throw new ConstructionException("Unknown java-path: " + editableCondition.getJavaPath()); // create the condition to add Condition condition = new Condition(); condition.setId(editableCondition.getId()); condition.setConditionId(editableCondition.getConditionId()); if (condition.getConditionId() == null) condition.setConditionId(ValidationServices.getInstance().getNextConditionSequence()); condition.setId(editableCondition.getId()); condition.setName(editableCondition.getName()); condition.setDescription(editableCondition.getDescription()); condition.setJavaPath(editableCondition.getJavaPath()); condition.setExpression(editableCondition.getExpression()); condition.setValidator(_validators.get(editableCondition.getValidatorId())); // create the executable condition ExecutableCondition execCondition = new ExecutableCondition(condition); // update internal state _executableConditions.put(execCondition.getInternalId(), execCondition); // update the processors (if the new java path doesn't exist, re-populate all the processors) if (!_processors.containsKey(editableCondition.getJavaPath())) populateProcessors(getRulesSortedByDependencies(_executableRules, _executableConditions)); else updateProcessorsConditions(_executableConditions.values()); // this is way less expensive than re-populating the processors... // update the raw structure only if the state was successfully updated... _validators.get(editableCondition.getValidatorId()).getConditions().add(condition); return condition; } finally { _lock.writeLock().unlock(); } } /** * Updates an existing condition in the engine. *

* The following steps should be performed: *

    *
  1. Get the condition to update using the getCondition() method
  2. *
  3. Wrap the condition into an editable condition by passing it to the EditableCondition's constructor.
  4. *
  5. Modify the editable condition (this would correspond to changes done in a GUI by a user)
  6. *
  7. Call this method using the editable condition
  8. *
* No modification should be done on the condition object itself, only the editable condition should be modified! *

* Once this method returns (and if no exception were thrown), the condition will be accessible by using the getCondition() method and * all the requested modifications will have been applied to it. *

* Created on Jun 29, 2011 by depryf * @param editableCondition EditableCondition, cannot be null * @throws ConstructionException if the condition contains an error */ public void updateCondition(EditableCondition editableCondition) throws ConstructionException { _lock.writeLock().lock(); try { if (editableCondition == null) throw new ConstructionException("An editable condition is required for modifying an condition"); if (editableCondition.getConditionId() == null) throw new ConstructionException("An internal ID is required when modifying a condition"); if (editableCondition.getId() == null) throw new ConstructionException("A category ID is required when modifying a condition"); if (editableCondition.getValidatorId() == null) throw new ConstructionException("A group is required when modifying a condition"); if (editableCondition.getJavaPath() == null) throw new ConstructionException("A java-path is required when adding a condition"); if (!_processorRoots.containsKey(StringUtils.split(editableCondition.getJavaPath(), '.')[0])) throw new ConstructionException("Invalid java-path"); if (!_validators.containsKey(editableCondition.getValidatorId())) throw new ConstructionException("Unknown group: " + editableCondition.getValidatorId()); if (!ValidationServices.getInstance().getAllJavaPaths().containsKey(editableCondition.getJavaPath())) throw new ConstructionException("Unknown java-path: " + editableCondition.getJavaPath()); // get original executable condition ExecutableCondition originalExecCondition = _executableConditions.get(editableCondition.getConditionId()); if (originalExecCondition == null) throw new ConstructionException("Unknown condition: " + editableCondition.getId()); // get the condition Condition condition = getCondition(originalExecCondition.getId()); if (condition == null) throw new ConstructionException("Unknown condition: " + editableCondition.getId()); // check condition unicity if (!condition.getId().equals(editableCondition.getId())) if (getCondition(editableCondition.getId()) != null) throw new ConstructionException("Condition IDs must be unique within the edits engine, cannot update ID to '" + editableCondition.getId() + "'"); // create the executable condition ExecutableCondition execCondition = new ExecutableCondition(originalExecCondition); execCondition.setId(editableCondition.getId()); execCondition.setJavaPath(editableCondition.getJavaPath()); if ((condition.getExpression() == null && editableCondition.getExpression() != null) || (condition.getExpression() != null && !condition.getExpression().equals( editableCondition.getExpression()))) execCondition.setExpression(editableCondition.getExpression()); // update internal state _executableConditions.put(execCondition.getInternalId(), execCondition); // update the processors (if the new java path doesn't exist, re-populate all the processors) if (!_processors.containsKey(editableCondition.getJavaPath())) populateProcessors(getRulesSortedByDependencies(_executableRules, _executableConditions)); else updateProcessorsConditions(_executableConditions.values()); // this is way less expensive than re-populating the processors... // update the raw structure only if the state was successfully updated... condition.setId(editableCondition.getId()); condition.setValidator(_validators.get(editableCondition.getValidatorId())); condition.setName(editableCondition.getName()); condition.setDescription(editableCondition.getDescription()); condition.setJavaPath(editableCondition.getJavaPath()); condition.setExpression(editableCondition.getExpression()); } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing condition from the engine. *

* This is a convenient method that only takes the rule ID as a parameter. *

* Created on Jul 7, 2011 by depryf * @param conditionId condition ID to delete * @throws ConstructionException if the condition cannot be found */ public void deleteCondition(String conditionId) throws ConstructionException { _lock.writeLock().lock(); try { Condition condition = getCondition(conditionId); if (condition == null) throw new ConstructionException("Unknown condition: " + conditionId); deleteCondition(new EditableCondition(condition)); } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing condition from the engine. *

* The following steps should be performed: *

    *
  1. Get the condition to delete using the getCondition() method
  2. *
  3. Wrap the condition into an editable condition by passing it to the EditableCondition's constructor.
  4. *
  5. Call this method using the editable condition
  6. *
*

* Created on Jun 29, 2011 by depryf * @param editableCondition EditableCondition, cannot be null */ public void deleteCondition(EditableCondition editableCondition) throws ConstructionException { _lock.writeLock().lock(); try { // get the condition Condition condition = getCondition(editableCondition.getId()); if (condition == null) throw new ConstructionException("Unknown condition: " + editableCondition.getId()); // update internal state _executableConditions.remove(editableCondition.getConditionId()); updateProcessorsConditions(_executableConditions.values()); // update the raw structure only if the state was successfully updated... _validators.get(editableCondition.getValidatorId()).getConditions().remove(condition); } finally { _lock.writeLock().unlock(); } } /** * Adds a new validator in the engine. *

* The following steps should be performed: *

    *
  1. Create a new Validator object by loading it from XML using the ValidationXmlUtils, or create it programmatically
  2. *
  3. Wrap the validator into an editable validator by passing it to the EditableValidator's constructor.
  4. *
  5. Call this method using the editable validator
  6. *
*

* Once this method returns (and if no exception were thrown), the new validator will be accessible by using the getValidator() method. *

* Created on Jun 29, 2011 by depryf * @param editableValidator EditableValidator, cannot be null * @return the created Validator * @throws ConstructionException if the validator contains an error */ public Validator addValidator(EditableValidator editableValidator) throws ConstructionException { _lock.writeLock().lock(); try { if (getValidator(editableValidator.getId()) != null) throw new ConstructionException("Group IDs must be unique within the edits engine, cannot add '" + editableValidator.getId() + "'"); // create the validator to add Validator v = new Validator(); v.setValidatorId(editableValidator.getValidatorId()); if (v.getValidatorId() == null) v.setValidatorId(ValidationServices.getInstance().getNextValidatorSequence()); v.setId(editableValidator.getId()); v.setName(editableValidator.getName()); v.setReleases(editableValidator.getReleases()); v.setVersion(v.getReleases() == null || v.getReleases().isEmpty() ? null : v.getReleases().last().getVersion().getRawString()); v.setHash(v.getHash()); v.setRawContext(editableValidator.getRawContext()); v.setCategories(editableValidator.getCategories()); v.setConditions(editableValidator.getConditions()); v.setRules(editableValidator.getRules()); // internalize the validators (that will compile any Groovy, which could through a construction exception) Map conditions = new ConcurrentHashMap<>(); Map rules = new ConcurrentHashMap<>(); Map contexts = new ConcurrentHashMap<>(); internalizeValidator(v, conditions, rules, contexts, null); // add the existing rules and conditions conditions.putAll(_executableConditions); rules.putAll(_executableRules); // sort the rules by dependencies (this could though a dependency exception) List sortedRules = getRulesSortedByDependencies(rules, conditions); // at this point we checked everything, so let's update the internal state of the engine _executableConditions.putAll(conditions); _executableRules.putAll(rules); _contexts.put(v.getValidatorId(), contexts); populateProcessors(sortedRules); // update the raw structure only if the state was successfully updated... _validators.put(v.getId(), v); return v; } finally { _lock.writeLock().unlock(); } } /** * Updates an existing validator in the engine. *

* The following steps should be performed: *

    *
  1. Get the validator to update using the getValidator() method
  2. *
  3. Wrap the validator into an editable validator by passing it to the EditableValidator constructor.
  4. *
  5. Modify the editable validator (this would correspond to changes done in a GUI by a user)
  6. *
  7. Call this method using the editable validator
  8. *
* No modification should be done on the Validator object itself, only the editable validator should be modified! *

* Once this method returns (and if no exception were thrown), the validator will be accessible by using the getValidator() method and * all the requested modifications will have been applied to it. *

* Created on Jun 29, 2011 by depryf * @param editableValidator EditableValidator, cannot be null * @throws ConstructionException if the validator contains an error */ public void updateValidator(EditableValidator editableValidator) throws ConstructionException { _lock.writeLock().lock(); try { // get the validator Validator v = null; for (Validator val : _validators.values()) if (val.getValidatorId().equals(editableValidator.getValidatorId())) v = val; if (v == null) throw new ConstructionException("Unknown group: " + editableValidator.getId()); // this is a very lazy way of doing it; if it becomes an issue, we can be smarter and do an actual update... deleteValidator(v.getId()); addValidator(editableValidator); } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing validator from the engine. *

* This is a convenient method that only takes the validator ID as a parameter. *

* Created on Jul 7, 2011 by depryf * @param validatorId validator ID to delete * @throws ConstructionException if the validator cannot be found */ public void deleteValidator(String validatorId) throws ConstructionException { _lock.writeLock().lock(); try { Validator v = getValidator(validatorId); if (v == null) throw new ConstructionException("Unknown group: " + validatorId); deleteValidator(new EditableValidator(v)); } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing validator from the engine. *

* The following steps should be performed: *

    *
  1. Get the validator to delete using the getValidator() method
  2. *
  3. Wrap the validator into an editable validator by passing it to the editableValidator's constructor.
  4. *
  5. Call this method using the editable validator
  6. *
*

* Created on Jun 29, 2011 by depryf * @param editableValidator EditableValidator, cannot be null * @throws ConstructionException if the validator contains an error */ public void deleteValidator(EditableValidator editableValidator) throws ConstructionException { _lock.writeLock().lock(); try { // get the validator Validator v = getValidator(editableValidator.getId()); if (v == null) throw new ConstructionException("Unknown group: " + editableValidator.getId()); for (Condition condition : v.getConditions()) _executableConditions.remove(condition.getConditionId()); for (Rule r : v.getRules()) _executableRules.remove(r.getRuleId()); _contexts.remove(editableValidator.getValidatorId()); // sort the rules by dependencies (this could though a dependency exception) List sortedRules = getRulesSortedByDependencies(_executableRules, _executableConditions); // update the internal state of the engine populateProcessors(sortedRules); // update the raw structure only if the state was successfully updated... _validators.remove(editableValidator.getId()); } finally { _lock.writeLock().unlock(); } } /** * Adds a new context entry for the provided validator ID. *

* Created on Jul 7, 2011 by depryf * @param contextEntryId internal ID for the new context, if null a new ID will be generated using the ValidationServices.getNextContextEntrySequence() * @param contextKey new context key * @param validatorId validator ID * @param expression raw expression * @param type type ("java", "groovy", "table", etc...) * @return the created ContextEntry * @throws ConstructionException if the context contains an error */ public ContextEntry addContext(Long contextEntryId, String contextKey, String validatorId, String expression, String type) throws ConstructionException { _lock.writeLock().lock(); try { Validator v = getValidator(validatorId); if (v == null) throw new ConstructionException("Invalid group: " + validatorId); // check unicity if (_contexts.get(v.getValidatorId()).containsKey(contextKey)) throw new ConstructionException("Context key '" + contextKey + "' already exists; context keys must be unique within a group"); Map contexts = _contexts.get(v.getValidatorId()); if (contexts == null) throw new ConstructionException("Invalid group: " + validatorId); ValidationServices.getInstance().addContextExpression(expression, contexts, contextKey, type); updateProcessorsContexts(_contexts); ContextEntry entry = new ContextEntry(); entry.setContextEntryId(contextEntryId); entry.setKey(contextKey); entry.setExpression(expression); entry.setType(type); v.getRawContext().add(entry); return entry; } finally { _lock.writeLock().unlock(); } } /** * Updates an existing context entry for the provided validator ID. *

* Created on Jul 7, 2011 by depryf * @param contextKey new context key * @param validatorId validator ID * @param expression raw expression * @param type type ("java", "groovy", "table", etc...) * @throws ConstructionException if the context contains an error or is not found */ public void updateContext(String contextKey, String validatorId, String expression, String type) throws ConstructionException { _lock.writeLock().lock(); try { Validator v = getValidator(validatorId); if (v == null) throw new ConstructionException("Invalid group: " + validatorId); ContextEntry entry = v.getRawContext(contextKey); if (entry == null) throw new ConstructionException("Invalid key: " + contextKey); Map contexts = _contexts.get(v.getValidatorId()); if (contexts == null) throw new ConstructionException("Invalid group: " + validatorId); if (!contexts.containsKey(contextKey)) throw new ConstructionException("Group " + validatorId + " does not contain a context for key " + contextKey); ValidationServices.getInstance().addContextExpression(expression, contexts, contextKey, type); updateProcessorsContexts(_contexts); entry.setExpression(expression); entry.setType(type); } finally { _lock.writeLock().unlock(); } } /** * Deletes an existing context entry for the provided validator ID. *

* Created on Jul 7, 2011 by depryf * @param contextKey new context key * @param validatorId validator ID * @throws ConstructionException if the context is not found */ public void deleteContext(String contextKey, String validatorId) throws ConstructionException { _lock.writeLock().lock(); try { Validator v = getValidator(validatorId); if (v == null) throw new ConstructionException("Invalid group: " + validatorId); ContextEntry entry = v.getRawContext(contextKey); if (entry == null) throw new ConstructionException("Invalid key: " + contextKey); Map contexts = _contexts.get(v.getValidatorId()); if (contexts == null) throw new ConstructionException("Invalid group: " + validatorId); if (!contexts.containsKey(contextKey)) throw new ConstructionException("Group " + validatorId + " does not contain a context for key " + contextKey); contexts.remove(contextKey); updateProcessorsContexts(_contexts); v.getRawContext().remove(entry); } finally { _lock.writeLock().unlock(); } } /** * Utility method that allows to update the ignore flags on many edits very efficiently; the alternative is to call the * updateRule method once per edit, but that will be MUCH slower. *

* If a rule ID is not present in any of the provided collections, then its ignore flag won't be modified. *

* Created on Oct 6, 2011 by depryf * @param idsToIgnore a collection of rule IDs that must be ignored, no rule will be set to ignore if the collection is null (or empty) * @param idsToStopIgnoring a collection of rule IDs that must not be ignored anymore, no rule will be set to not-ignore if the collection is null (or empty) */ public void massUpdateIgnoreFlags(Collection idsToIgnore, Collection idsToStopIgnoring) { _lock.writeLock().lock(); try { // update the executable rules for (ExecutableRule execRule : _executableRules.values()) { String id = execRule.getId(); if (idsToIgnore != null && idsToIgnore.contains(id)) execRule.setIgnored(Boolean.TRUE); else if (idsToStopIgnoring != null && idsToStopIgnoring.contains(id)) execRule.setIgnored(Boolean.FALSE); } // update the processors after re-evaluating the rules order try { updateProcessorsRules(getRulesSortedByDependencies(_executableRules, _executableConditions)); } catch (ConstructionException e) { throw new RuntimeException("Internal state has not changed, this exception should not happen!", e); } // update the raw data for (Validator v : _validators.values()) { for (Rule r : v.getRules()) { String id = r.getId(); if (idsToIgnore != null && idsToIgnore.contains(id)) r.setIgnored(Boolean.TRUE); else if (idsToStopIgnoring != null && idsToStopIgnoring.contains(id)) r.setIgnored(Boolean.FALSE); } } } finally { _lock.writeLock().unlock(); } } /** * Enables the requested embedded set. * @param validatorId validator ID * @param setId set ID * @throws ConstructionException if the set is not found */ @SuppressWarnings("unused") public void enableEmbeddedSet(String validatorId, String setId) throws ConstructionException { _lock.writeLock().lock(); try { Validator v = getValidator(validatorId); if (v == null) throw new ConstructionException("Invalid group: " + validatorId); EmbeddedSet s = v.getSet(setId); if (s == null) throw new ConstructionException("Invalid set: " + setId); // the sets are not used in the internal state of the engine; so all we have to do is to update the raw data... s.setIgnored(false); } finally { _lock.writeLock().unlock(); } } /** * Disables the requested embedded set. * @param validatorId validator ID * @param setId set ID * @throws ConstructionException if the set is not found */ @SuppressWarnings("unused") public void disableEmbeddedSet(String validatorId, String setId) throws ConstructionException { _lock.writeLock().lock(); try { Validator v = getValidator(validatorId); if (v == null) throw new ConstructionException("Invalid group: " + validatorId); EmbeddedSet s = v.getSet(setId); if (s == null) throw new ConstructionException("Invalid set: " + setId); // the sets are not used in the internal state of the engine; so all we have to do is to update the raw data... s.setIgnored(true); } finally { _lock.writeLock().unlock(); } } // ******************************************************************************** // OTHER PUBLIC METHODS (some lock required, depends on the method) // ******************************************************************************** public String getEngineVersion() { return _ENGINE_VERSION; } /** * Returns the root (first element) of the supported java-path. *

* Created on Sep 30, 2010 by depryf * @return the root (first element) of the supported java-path */ public Set getSupportedJavaPathRoots() { return getSupportedJavaPathRoots(false); } /** * Returns the root (first element) of the supported java-path. *

* Created on Sep 30, 2010 by depryf * @param filterEmptyPaths if set to true then a root java path that doesn't have any edit under it will be excluded * @return the root (first element) of the supported java-path */ public Set getSupportedJavaPathRoots(boolean filterEmptyPaths) { _lock.readLock().lock(); try { if (filterEmptyPaths) return _processorRoots.entrySet().stream().filter(e -> e.getValue().get() > 0).map(Entry::getKey).collect(Collectors.toSet()); else return Collections.unmodifiableSet(_processorRoots.keySet()); } finally { _lock.readLock().unlock(); } } /** * Returns the statistics gathered so far... *

* Created on Nov 30, 2007 by depryf * @return a collection of StatsDTO object, possibly empty */ public Map getStats() { _statsLock.readLock().lock(); try { return _editsStats; } finally { _statsLock.readLock().unlock(); } } /** * Resets the statistics gathered so far... *

* Created on Jun 29, 2009 by depryf */ public void resetStats() { _statsLock.writeLock().lock(); try { _editsStats.clear(); } finally { _statsLock.writeLock().unlock(); } } /** * Dynamically enables/disabled computing the edits statistics on this engine. */ public void setEditsStatsEnabled(boolean enabled) { _computeEditsStats = enabled; } /** * Returns true if the edits statistics are on (that can be done via the initialization or dynamically via the engine itself). */ public boolean isEditsStatsEnabled() { return _computeEditsStats; } /** * Returns a string representation of the engine's internal state. *

* Note that this string can't be used as a mechanism to persist a state and re-initialize the engine from it, the returned * String does not contain enough information for that. *

* Created on Jan 14, 2008 by depryf * @return a string representation of the engine's internal state */ public String dumpInternalState() { _lock.readLock().lock(); try { StringBuilder result = new StringBuilder(); for (String key : new TreeSet<>(_processors.keySet())) // let's display the processors from smallest java path to biggest one... _processors.get(key).dumpCache(result, key); return result.toString(); } finally { _lock.readLock().unlock(); } } // ******************************************************************************** // INTERNAL METHODS (no lock required) // ******************************************************************************** private void internalizeValidator(Validator validator, Map conditions, Map rules, Map contexts, InitializationStats stats) throws ConstructionException { if (validator.getValidatorId() == null) validator.setValidatorId(ValidationServices.getInstance().getNextValidatorSequence()); if (validator.getValidatorId() == null) throw new ConstructionException("Validator must have a non-null internal ID to be registered in the engine"); // get pre-compiled rules if we have to CompiledRules compiledRules = null; if (_options.isPreCompiledEditsEnabled()) compiledRules = RuntimeUtils.findCompileRules(validator, stats); else if (stats != null) stats.setReasonNotPreCompiled(validator.getId(), InitializationStats.REASON_DISABLED); // internalize the rules ExecutorService service = Executors.newFixedThreadPool(_options.getNumCompilationThreads()); List> results = new ArrayList<>(validator.getRules().size()); if (validator.getRules() != null) { for (Rule r : validator.getRules()) { if (r.getRuleId() == null) r.setRuleId(ValidationServices.getInstance().getNextRuleSequence()); if (r.getRuleId() == null) throw new ConstructionException("Edits must have a non-null internal ID to be registered in the engine"); results.add(service.submit(new RuleCompilingCallable(r, rules, compiledRules, stats))); } validator.setRules(new HashSet<>(validator.getRules())); // since internal IDs might have changed } // internalize the conditions if (validator.getConditions() != null) { for (Condition c : validator.getConditions()) { if (c.getConditionId() == null) c.setConditionId(ValidationServices.getInstance().getNextConditionSequence()); if (c.getConditionId() == null) throw new ConstructionException("Conditions must have a non-null internal ID to be registered in the engine"); conditions.put(c.getConditionId(), new ExecutableCondition(c)); } validator.setConditions(new HashSet<>(validator.getConditions())); // since internal IDs might have changed } // we don't internalize the categories because they are not used at runtime, but let's still assign a unique ID to them... if (validator.getCategories() != null) { for (Category c : validator.getCategories()) if (c.getCategoryId() == null) c.setCategoryId(ValidationServices.getInstance().getNextCategorySequence()); validator.setCategories(new HashSet<>(validator.getCategories())); // since internal IDs might have changed } // for any context entry that threw an exception, re-try it a second time, hopefully the dependency exception will be resolved... if (validator.getRawContext() != null) { Set reRun = new HashSet<>(); for (ContextEntry entry : validator.getRawContext()) { if (entry.getContextEntryId() == null) entry.setContextEntryId(ValidationServices.getInstance().getNextContextEntrySequence()); try { // this is really not great, a better way would be to fully parse the context expressions, but that will do for now... if (entry.getExpression().contains(VALIDATOR_CONTEXT_KEY + ".")) reRun.add(entry); else ValidationServices.getInstance().addContextExpression(entry.getExpression(), contexts, entry.getKey(), entry.getType()); } catch (ConstructionException e) { reRun.add(entry); } } for (ContextEntry entry : reRun) ValidationServices.getInstance().addContextExpression(entry.getExpression(), contexts, entry.getKey(), entry.getType()); validator.setRawContext(new HashSet<>(validator.getRawContext())); // since internal IDs might have changed } // we don't internalize the sets because they are not used at runtime, but let's still assign a unique ID to them... if (validator.getSets() != null) { for (EmbeddedSet s : validator.getSets()) if (s.getSetId() == null) s.setSetId(ValidationServices.getInstance().getNextSetSequence()); validator.setSets(new HashSet<>(validator.getSets())); // since internal IDs might have changed } // we won't be submitting new work anymore service.shutdown(); // this is important to detect any exception in the background threads for (Future result : results) { try { result.get(); } catch (InterruptedException e) { // ignore this one } catch (ExecutionException e) { if (e.getCause() instanceof ConstructionException) throw (ConstructionException)e.getCause(); throw new RuntimeException(e); } } // the work should be done by now because we call get(), which is a blocking call; but better safe than sorry... try { service.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException e) { // ignore this one... } } private void checkValidatorConstraints(List validators) throws ConstructionException { Set validatorIds = new HashSet<>(), conditionIds = new HashSet<>(), categoryIds = new HashSet<>(), ruleIds = new HashSet<>(); for (Validator v : validators) { if (validatorIds.contains(v.getId())) throw new ConstructionException("Group ID '" + v.getId() + "' is not unique"); if (v.getMinEngineVersion() != null && ValidationServices.getInstance().compareEngineVersions(v.getMinEngineVersion(), _ENGINE_VERSION) > 0) throw new ConstructionException("Group ID '" + v.getId() + "' requires version " + v.getMinEngineVersion() + "; current version is " + _ENGINE_VERSION); validatorIds.add(v.getId()); if (v.getConditions() != null) { for (Condition c : v.getConditions()) { if (conditionIds.contains(c.getId())) throw new ConstructionException("Condition ID '" + c.getId() + "' (from group '" + v.getId() + "') is not unique across all groups"); conditionIds.add(c.getId()); } } if (v.getCategories() != null) { for (Category c : v.getCategories()) { if (categoryIds.contains(c.getId())) throw new ConstructionException("Category ID '" + c.getId() + "' (from group '" + v.getId() + "') is not unique across all groups"); categoryIds.add(c.getId()); } } for (Rule r : v.getRules()) { if (ruleIds.contains(r.getId())) throw new ConstructionException("Edit ID '" + r.getId() + "' (from group '" + v.getId() + "') is not unique across all groups"); ruleIds.add(r.getId()); } } } private Collection internalValidate(Validatable validatable, ValidatingContext vContext) throws ValidationException { // pre-condition: engine must be initialized if (_status == ValidationEngineStatus.NOT_INITIALIZED) return new HashSet<>(); // pre-condition: there must be a root processor for this validatable Processor processor = _processors.get(validatable.getRootLevel()); if (processor == null) return new HashSet<>(); // pre-condition: if a forced rule is provided, it must have a known java path if (vContext.getToForce() != null && !ValidationServices.getInstance().getAllJavaPaths().containsKey(vContext.getToForce().getJavaPath())) throw new ValidationException("Unknown java path for forced edit: " + vContext.getToForce().getJavaPath()); // process the validatable Collection failures = processor.process(validatable, vContext); // report the stats if we have to if (_computeEditsStats) { _statsLock.writeLock().lock(); try { for (Entry entry : vContext.getEditDurations().entrySet()) _editsStats.computeIfAbsent(entry.getKey(), EngineStats::new).reportStat(entry.getValue()); } finally { _statsLock.writeLock().unlock(); } } return failures; } private void populateProcessors(List sortedRules) { _processors.clear(); _processorRoots.clear(); // go through each java path and create/get the corresponding processors for (String javaPath : ValidationServices.getInstance().getAllJavaPaths().keySet()) { String[] parts = StringUtils.split(javaPath, '.'); // keep track of the roots (I couldn't find a concurrent implementation of a set, so I am using a map with dummy objects) _processorRoots.put(parts[0], new AtomicInteger()); // keep track of the current partial path StringBuilder partialPath = new StringBuilder(parts[0]); // first part correspond to a validating processor, the rest of the parts correspond to iterative processors... ValidatingProcessor current = _processors.computeIfAbsent(partialPath.toString(), k -> new ValidatingProcessor(partialPath.toString())); for (int i = 1; i < parts.length; i++) { partialPath.append(".").append(parts[i]); ValidatingProcessor vProcessor = _processors.get(partialPath.toString()); if (vProcessor == null) { vProcessor = new ValidatingProcessor(partialPath.toString()); IterativeProcessor iProcessor = new IterativeProcessor(vProcessor, parts[i]); _processors.put(partialPath.toString(), vProcessor); current.addNested(iProcessor); } current = vProcessor; } } // update the processors if (sortedRules != null) updateProcessorsRules(sortedRules); updateProcessorsConditions(_executableConditions.values()); updateProcessorsContexts(_contexts); } private void updateProcessorsRules(List sortedRules) { // get the sorted rules by java-path Map> rules = new HashMap<>(); for (ExecutableRule rule : sortedRules) rules.computeIfAbsent(rule.getJavaPath(), k -> new ArrayList<>()).add(rule); // since we are about to reset all the rules in every processor, let's reset the rule counts as well _processorRoots.values().forEach(i -> i.set(0)); // update all the processors for (ValidatingProcessor p : _processors.values()) { List rulesForCurrentProcessor = rules.getOrDefault(p.getJavaPath(), Collections.emptyList()); p.setRules(rulesForCurrentProcessor); _processorRoots.get(StringUtils.split(p.getJavaPath(), '.')[0]).addAndGet(rulesForCurrentProcessor.size()); } } private void updateProcessorsConditions(Collection allConditions) { // get the conditions by java-path (there is no order needed for conditions) Map> conditions = new HashMap<>(); for (ExecutableCondition condition : allConditions) conditions.computeIfAbsent(condition.getJavaPath(), k -> new ArrayList<>()).add(condition); // update all the processors for (ValidatingProcessor p : _processors.values()) p.setConditions(conditions.getOrDefault(p.getJavaPath(), Collections.emptyList())); } private void updateProcessorsContexts(Map> allContexts) { // this code used to be smart about which validator was used at which java-path, and provide only the contexts for that particular // java-path to the processor; but that doesn't work in SEER*DMS where some edits are persisted but not registered to the engine! for (ValidatingProcessor p : _processors.values()) p.setContexts(allContexts); } private List getRulesSortedByDependencies(Map rules, Map conditions) throws ConstructionException { List rulesQueue = new ArrayList<>(); // cache all of our rules Map ruleCache = new HashMap<>(rules.size() + 1); // rule-id -> rule object, modified as the process goes on Map pathCache = new HashMap<>(rules.size() + 1); // rule-id -> rule-set-java-path, unmodified Map validatorCache = new HashMap<>(rules.size() + 1); // rule-id -> validator-id, unmodified // build a map of condition ID -> java path Map conditionPaths = new HashMap<>(); for (ExecutableCondition condition : conditions.values()) conditionPaths.put(condition.getId(), condition.getJavaPath()); // gather and validate all the rules for (ExecutableRule rule : rules.values()) { addToRuleCache(pathCache, validatorCache, rule, ruleCache); // validate the referenced condition(s): rule must be at the same level, or lower if (rule.getConditions() != null) { for (String conditionId : rule.getConditions()) { String conditionPath = conditionPaths.get(conditionId); if (conditionPath == null) throw new ConstructionException("Edit '" + rule.getId() + "' references unknown condition: " + conditionId, rule.getId()); if (conditionPath.startsWith(rule.getJavaPath()) && !conditionPath.equals(rule.getJavaPath())) throw new ConstructionException("Edit '" + rule.getId() + "' references condition '" + conditionId + "' which is defined lower in the data structure tree.", rule.getId()); } } } // cache of IDs, used to identify circular dependencies Set currents = new HashSet<>(); // populate the queue while (!ruleCache.isEmpty()) addToRuleQueue(ruleCache.remove(ruleCache.keySet().iterator().next()), ruleCache, currents, rulesQueue, pathCache, validatorCache); return rulesQueue; } private void addToRuleCache(Map pathCache, Map validatorCache, ExecutableRule rule, Map ruleCache) throws ConstructionException { String ruleId = rule.getId(); pathCache.put(ruleId, rule.getJavaPath()); validatorCache.put(ruleId, rule.getRule().getValidator().getId()); // check self dependency if (rule.getDependencies().contains(ruleId)) throw new ConstructionException("Edit '" + ruleId + "' cannot depend on itself", ruleId); ruleCache.put(ruleId, rule); } private void addToRuleQueue(ExecutableRule rule, Map cache, Set currents, List queue, Map pathCache, Map validatorCache) throws ConstructionException { if (rule.getDependencies() != null && !rule.getDependencies().isEmpty()) { String rId = rule.getId(), rPath = rule.getJavaPath(), vId = rule.getRule().getValidator().getId(); for (String depId : rule.getDependencies()) { String validatorCachedId = validatorCache.get(depId); if (validatorCachedId == null) throw new ConstructionException("Unable to resolve dependency '" + depId + "' for edit '" + rId + "' (" + vId + ")"); // check that dependencies go bottom-up in the patient set structure String depPath = pathCache.get(depId); if (rPath == null) throw new ConstructionException("Got a null java-path for edit '" + rId + "' (" + vId + ")"); if (depPath == null) throw new ConstructionException("Got a null java-path for edit '" + depId + "' (on which '" + rId + "' depends)"); if (!rPath.startsWith(depPath) || rPath.length() < depPath.length()) throw new ConstructionException("Edit '" + rId + "' cannot depend on '" + depId + "' which is lower in the data structure tree."); // check that dependencies do not cross validators if (!vId.equals(validatorCachedId)) throw new ConstructionException("No cross-group dependency is allowed, edit '" + rId + "' (" + vId + ") cannot depend on '" + depId + "' (" + validatorCachedId + ")", rId); // check for circular dependencies if (currents.contains(depId)) throw new ConstructionException("Circular dependency detected between '" + depId + "' and '" + rId + "'", depId, rId); if (cache.containsKey(depId)) { currents.add(rule.getId()); addToRuleQueue(cache.remove(depId), cache, currents, queue, pathCache, validatorCache); } } } queue.add(rule); currents.clear(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy