
com.imsweb.validation.ValidationEngine Maven / Gradle / Ivy
Show all versions of validation Show documentation
/*
* 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 Validator
s, keyed by validator ID
*/
protected Map _validators = new HashMap<>();
/**
* Map of Processor
s, 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 ExecutableRule
s, keyed by rule internal ID
*/
protected Map _executableRules = new HashMap<>();
/**
* Map of ExecutableCondition
s, 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 Validator
s 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 Validator
s 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:
*
* - Create an editable rule by using the EditableRule's default constructor.
* - Modify the editable rule (this would correspond to changes done in a GUI by a user)
* - Call this method using the editable rule
*
*
* 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:
*
* - Get the rule to update using the getRule() method
* - Wrap the rule into an editable rule by passing it to the EditableRule's constructor.
* - Modify the editable rule (this would correspond to changes done in a GUI by a user)
* - Call this method using the editable rule
*
* 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:
*
* - Get the rule to delete using the getRule() method
* - Wrap the rule into an editable rule by passing it to the EditableRule's constructor.
* - Call this method using the editable rule
*
*
* 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:
*
* - Create an editable condition by using the EditableCondition's default constructor.
* - Modify the editable condition (this would correspond to changes done in a GUI by a user)
* - Call this method using the editable condition
*
*
* 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:
*
* - Get the condition to update using the getCondition() method
* - Wrap the condition into an editable condition by passing it to the EditableCondition's constructor.
* - Modify the editable condition (this would correspond to changes done in a GUI by a user)
* - Call this method using the editable condition
*
* 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:
*
* - Get the condition to delete using the getCondition() method
* - Wrap the condition into an editable condition by passing it to the EditableCondition's constructor.
* - Call this method using the editable condition
*
*
* 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:
*
* - Create a new Validator object by loading it from XML using the ValidationXmlUtils, or create it programmatically
* - Wrap the validator into an editable validator by passing it to the EditableValidator's constructor.
* - Call this method using the editable validator
*
*
* 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:
*
* - Get the validator to update using the getValidator() method
* - Wrap the validator into an editable validator by passing it to the EditableValidator constructor.
* - Modify the editable validator (this would correspond to changes done in a GUI by a user)
* - Call this method using the editable validator
*
* 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:
*
* - Get the validator to delete using the getValidator() method
* - Wrap the validator into an editable validator by passing it to the editableValidator's constructor.
* - Call this method using the editable validator
*
*
* 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();
}
}