DefaultConstraintValidator.java

/*
 * SPDX-FileCopyrightText: none
 * SPDX-License-Identifier: CC0-1.0
 */

package gov.nist.secauto.metaschema.core.model.constraint;

import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
import gov.nist.secauto.metaschema.core.configuration.IConfiguration;
import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
import gov.nist.secauto.metaschema.core.metapath.ISequence;
import gov.nist.secauto.metaschema.core.metapath.MetapathException;
import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
import gov.nist.secauto.metaschema.core.metapath.function.library.FnBoolean;
import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
import gov.nist.secauto.metaschema.core.metapath.item.node.AbstractNodeItemVisitor;
import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IFieldNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IFlagNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IModuleNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
import gov.nist.secauto.metaschema.core.model.IFlagDefinition;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import javax.xml.namespace.QName;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
 * Used to perform constraint validation over one or more node items.
 * <p>
 * This class is not thread safe.
 */
@SuppressWarnings({
    "PMD.CouplingBetweenObjects",
    "PMD.GodClass" // provides validators for all types
})
public class DefaultConstraintValidator
    implements IConstraintValidator, IMutableConfiguration<ValidationFeature<?>> { // NOPMD - intentional
  private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class);

  @NonNull
  private final Map<INodeItem, ValueStatus> valueMap = new LinkedHashMap<>(); // NOPMD - intentional
  @NonNull
  private final Map<String, IIndex> indexNameToIndexMap = new ConcurrentHashMap<>();
  @NonNull
  private final Map<String, List<KeyRef>> indexNameToKeyRefMap = new ConcurrentHashMap<>();
  @NonNull
  private final IConstraintValidationHandler handler;
  @NonNull
  private final IMutableConfiguration<ValidationFeature<?>> configuration;

  /**
   * Construct a new constraint validator instance.
   *
   * @param handler
   *          the validation handler to use for handling constraint violations
   */
  public DefaultConstraintValidator(
      @NonNull IConstraintValidationHandler handler) {
    this.handler = handler;
    this.configuration = new DefaultConfiguration<>();
  }

  /**
   * Get the current configuration of the serializer/deserializer.
   *
   * @return the configuration
   */
  @NonNull
  protected IMutableConfiguration<ValidationFeature<?>> getConfiguration() {
    return configuration;
  }

  @Override
  public DefaultConstraintValidator enableFeature(ValidationFeature<?> feature) {
    return set(feature, true);
  }

  @Override
  public DefaultConstraintValidator disableFeature(ValidationFeature<?> feature) {
    return set(feature, false);
  }

  @Override
  public DefaultConstraintValidator applyConfiguration(
      @NonNull IConfiguration<ValidationFeature<?>> other) {
    getConfiguration().applyConfiguration(other);
    return this;
  }

  @Override
  public DefaultConstraintValidator set(ValidationFeature<?> feature, Object value) {
    getConfiguration().set(feature, value);
    return this;
  }

  @Override
  public boolean isFeatureEnabled(ValidationFeature<?> feature) {
    return getConfiguration().isFeatureEnabled(feature);
  }

  @Override
  public Map<ValidationFeature<?>, Object> getFeatureValues() {
    return getConfiguration().getFeatureValues();
  }

  /**
   * Get the validation handler to use for handling constraint violations.
   *
   * @return the handler
   */
  @NonNull
  protected IConstraintValidationHandler getConstraintValidationHandler() {
    return handler;
  }

  @Override
  public void validate(
      @NonNull INodeItem item,
      @NonNull DynamicContext dynamicContext) {
    item.accept(new Visitor(), dynamicContext);
  }

  /**
   * Validate the provided flag item against any associated constraints.
   *
   * @param item
   *          the flag item to validate
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   * @throws MetapathException
   *           if an error occurred while evaluating a Metapath used in a
   *           constraint
   */
  protected void validateFlag(
      @NonNull IFlagNodeItem item,
      @NonNull DynamicContext dynamicContext) {
    IFlagDefinition definition = item.getDefinition();

    validateExpect(definition.getExpectConstraints(), item, dynamicContext);
    validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext);
    validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext);
    validateMatches(definition.getMatchesConstraints(), item, dynamicContext);
  }

  /**
   * Validate the provided field item against any associated constraints.
   *
   * @param item
   *          the field item to validate
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   * @throws MetapathException
   *           if an error occurred while evaluating a Metapath used in a
   *           constraint
   */
  protected void validateField(
      @NonNull IFieldNodeItem item,
      @NonNull DynamicContext dynamicContext) {
    IFieldDefinition definition = item.getDefinition();

    validateExpect(definition.getExpectConstraints(), item, dynamicContext);
    validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext);
    validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext);
    validateMatches(definition.getMatchesConstraints(), item, dynamicContext);
  }

  /**
   * Validate the provided assembly item against any associated constraints.
   *
   * @param item
   *          the assembly item to validate
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   * @throws MetapathException
   *           if an error occurred while evaluating a Metapath used in a
   *           constraint
   */
  protected void validateAssembly(
      @NonNull IAssemblyNodeItem item,
      @NonNull DynamicContext dynamicContext) {
    IAssemblyDefinition definition = item.getDefinition();

    validateExpect(definition.getExpectConstraints(), item, dynamicContext);
    validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext);
    validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext);
    validateMatches(definition.getMatchesConstraints(), item, dynamicContext);
    validateHasCardinality(definition.getHasCardinalityConstraints(), item, dynamicContext);
    validateIndex(definition.getIndexConstraints(), item, dynamicContext);
    validateUnique(definition.getUniqueConstraints(), item, dynamicContext);
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateHasCardinality( // NOPMD false positive
      @NonNull List<? extends ICardinalityConstraint> constraints,
      @NonNull IAssemblyNodeItem item,
      @NonNull DynamicContext dynamicContext) {
    for (ICardinalityConstraint constraint : constraints) {
      assert constraint != null;

      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateHasCardinality(constraint, item, targets, dynamicContext);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   */
  private void validateHasCardinality(
      @NonNull ICardinalityConstraint constraint,
      @NonNull IAssemblyNodeItem node,
      @NonNull ISequence<? extends INodeItem> targets,
      @NonNull DynamicContext dynamicContext) {
    int itemCount = targets.size();

    IConstraintValidationHandler handler = getConstraintValidationHandler();

    boolean violation = false;
    Integer minOccurs = constraint.getMinOccurs();
    if (minOccurs != null && itemCount < minOccurs) {
      handler.handleCardinalityMinimumViolation(constraint, node, targets, dynamicContext);
      violation = true;
    }

    Integer maxOccurs = constraint.getMaxOccurs();
    if (maxOccurs != null && itemCount > maxOccurs) {
      handler.handleCardinalityMaximumViolation(constraint, node, targets, dynamicContext);
      violation = true;
    }

    if (!violation) {
      handlePass(constraint, node, node, dynamicContext);
    }
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateIndex(
      @NonNull List<? extends IIndexConstraint> constraints,
      @NonNull IAssemblyNodeItem item,
      @NonNull DynamicContext dynamicContext) {
    for (IIndexConstraint constraint : constraints) {
      assert constraint != null;

      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateIndex(constraint, item, targets, dynamicContext);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  private void validateIndex(
      @NonNull IIndexConstraint constraint,
      @NonNull IAssemblyNodeItem node,
      @NonNull ISequence<? extends INodeItem> targets,
      @NonNull DynamicContext dynamicContext) {
    String indexName = constraint.getName();

    IConstraintValidationHandler handler = getConstraintValidationHandler();
    if (indexNameToIndexMap.containsKey(indexName)) {
      handler.handleIndexDuplicateViolation(constraint, node, dynamicContext);
    } else {
      IIndex index = IIndex.newInstance(constraint.getKeyFields());
      targets.stream()
          .forEachOrdered(item -> {
            assert item != null;
            if (item.hasValue()) {
              try {
                INodeItem oldItem = index.put(item, dynamicContext);
                if (oldItem == null) {
                  handlePass(constraint, node, item, dynamicContext);
                } else {
                  handler.handleIndexDuplicateKeyViolation(constraint, node, oldItem, item, dynamicContext);
                }
              } catch (MetapathException ex) {
                handler.handleKeyMatchError(constraint, node, item, ex, dynamicContext);
              }
            }
          });
      indexNameToIndexMap.put(indexName, index);
    }
  }

  private void handlePass(
      @NonNull IConstraint constraint,
      @NonNull INodeItem node,
      @NonNull INodeItem item,
      @NonNull DynamicContext dynamicContext) {
    if (isFeatureEnabled(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS)) {
      getConstraintValidationHandler().handlePass(constraint, node, item, dynamicContext);
    }
  }

  private void handleError(
      @NonNull IConstraint constraint,
      @NonNull INodeItem node,
      @NonNull Throwable ex,
      @NonNull DynamicContext dynamicContext) {
    getConstraintValidationHandler()
        .handleError(constraint, node, toErrorMessage(constraint, node, ex), ex, dynamicContext);
  }

  @NonNull
  private static String toErrorMessage(
      @NonNull IConstraint constraint,
      @NonNull INodeItem item,
      @NonNull Throwable ex) {
    StringBuilder builder = new StringBuilder(128);
    builder.append("A ")
        .append(constraint.getClass().getName())
        .append(" constraint");

    String id = constraint.getId();
    if (id == null) {
      builder.append(" targeting the metapath '")
          .append(constraint.getTarget())
          .append('\'');
    } else {
      builder.append(" with id '")
          .append(id)
          .append('\'');
    }

    builder.append(", matching the item at path '")
        .append(item.getMetapath())
        .append("', resulted in an unexpected error. The error was: ")
        .append(ex.getLocalizedMessage());
    return ObjectUtils.notNull(builder.toString());
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateUnique(
      @NonNull List<? extends IUniqueConstraint> constraints,
      @NonNull IAssemblyNodeItem item,
      @NonNull DynamicContext dynamicContext) {
    for (IUniqueConstraint constraint : constraints) {
      assert constraint != null;

      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateUnique(constraint, item, targets, dynamicContext);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  private void validateUnique(
      @NonNull IUniqueConstraint constraint,
      @NonNull IAssemblyNodeItem node,
      @NonNull ISequence<? extends INodeItem> targets,
      @NonNull DynamicContext dynamicContext) {

    IConstraintValidationHandler handler = getConstraintValidationHandler();
    IIndex index = IIndex.newInstance(constraint.getKeyFields());
    targets.stream()
        .forEachOrdered(item -> {
          assert item != null;
          if (item.hasValue()) {
            try {
              INodeItem oldItem = index.put(item, dynamicContext);
              if (oldItem == null) {
                handlePass(constraint, node, item, dynamicContext);
              } else {
                handler.handleUniqueKeyViolation(constraint, node, oldItem, item, dynamicContext);
              }
            } catch (MetapathException ex) {
              handler.handleKeyMatchError(constraint, node, item, ex, dynamicContext);
              throw ex;
            }
          }
        });
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateMatches( // NOPMD false positive
      @NonNull List<? extends IMatchesConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item,
      @NonNull DynamicContext dynamicContext) {

    for (IMatchesConstraint constraint : constraints) {
      assert constraint != null;

      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateMatches(constraint, item, targets, dynamicContext);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   */
  private void validateMatches(
      @NonNull IMatchesConstraint constraint,
      @NonNull INodeItem node,
      @NonNull ISequence<? extends INodeItem> targets,
      @NonNull DynamicContext dynamicContext) {
    targets.stream()
        .forEachOrdered(item -> {
          assert item != null;
          if (item.hasValue()) {
            validateMatchesItem(constraint, node, item, dynamicContext);
          }
        });
  }

  private void validateMatchesItem(
      @NonNull IMatchesConstraint constraint,
      @NonNull INodeItem node,
      @NonNull INodeItem item,
      @NonNull DynamicContext dynamicContext) {
    String value = FnData.fnDataItem(item).asString();

    IConstraintValidationHandler handler = getConstraintValidationHandler();
    boolean valid = true;
    Pattern pattern = constraint.getPattern();
    if (pattern != null && !pattern.asMatchPredicate().test(value)) {
      // failed pattern match
      handler.handleMatchPatternViolation(constraint, node, item, value, pattern, dynamicContext);
      valid = false;
    }

    IDataTypeAdapter<?> adapter = constraint.getDataType();
    if (adapter != null) {
      try {
        adapter.parse(value);
      } catch (IllegalArgumentException ex) {
        handler.handleMatchDatatypeViolation(constraint, node, item, value, adapter, ex, dynamicContext);
        valid = false;
      }
    }

    if (valid) {
      handlePass(constraint, node, item, dynamicContext);
    }
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateIndexHasKey( // NOPMD false positive
      @NonNull List<? extends IIndexHasKeyConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item,
      @NonNull DynamicContext dynamicContext) {

    for (IIndexHasKeyConstraint constraint : constraints) {
      assert constraint != null;

      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateIndexHasKey(constraint, item, targets);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   */
  private void validateIndexHasKey(
      @NonNull IIndexHasKeyConstraint constraint,
      @NonNull IDefinitionNodeItem<?, ?> node,
      @NonNull ISequence<? extends INodeItem> targets) {
    String indexName = constraint.getIndexName();

    List<KeyRef> keyRefItems = indexNameToKeyRefMap.get(indexName);
    if (keyRefItems == null) {
      keyRefItems = new LinkedList<>();
      indexNameToKeyRefMap.put(indexName, keyRefItems);
    }

    KeyRef keyRef = new KeyRef(constraint, node, new ArrayList<>(targets.getValue()));
    keyRefItems.add(keyRef);
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateExpect(
      @NonNull List<? extends IExpectConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item,
      @NonNull DynamicContext dynamicContext) {
    for (IExpectConstraint constraint : constraints) {
      assert constraint != null;

      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateExpect(constraint, item, targets, dynamicContext);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  private void validateExpect(
      @NonNull IExpectConstraint constraint,
      @NonNull INodeItem node,
      @NonNull ISequence<? extends INodeItem> targets,
      @NonNull DynamicContext dynamicContext) {
    MetapathExpression metapath = MetapathExpression.compile(
        constraint.getTest(),
        dynamicContext.getStaticContext());

    IConstraintValidationHandler handler = getConstraintValidationHandler();
    targets.stream()
        .forEachOrdered(item -> {
          assert item != null;

          if (item.hasValue()) {
            try {
              ISequence<?> result = metapath.evaluate(item, dynamicContext);
              if (FnBoolean.fnBoolean(result).toBoolean()) {
                handlePass(constraint, node, item, dynamicContext);
              } else {
                handler.handleExpectViolation(constraint, node, item, dynamicContext);
              }
            } catch (MetapathException ex) {
              handleError(constraint, item, ex, dynamicContext);
            }
          }
        });
  }

  /**
   * Evaluates the provided collection of {@code constraints} in the context of
   * the {@code item}.
   *
   * @param constraints
   *          the constraints to execute
   * @param item
   *          the focus of Metapath evaluation
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateAllowedValues(
      @NonNull List<? extends IAllowedValuesConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item,
      @NonNull DynamicContext dynamicContext) {
    for (IAllowedValuesConstraint constraint : constraints) {
      assert constraint != null;
      try {
        ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
        validateAllowedValues(constraint, item, targets, dynamicContext);
      } catch (RuntimeException ex) {
        handleError(constraint, item, ex, dynamicContext);
      }
    }
  }

  /**
   * Evaluates the provided {@code constraint} against each of the
   * {@code targets}.
   *
   * @param constraint
   *          the constraint to execute
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   * @param targets
   *          the focus of Metapath evaluation for evaluating any constraint
   *          Metapath clauses
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  private void validateAllowedValues(
      @NonNull IAllowedValuesConstraint constraint,
      @NonNull IDefinitionNodeItem<?, ?> node,
      @NonNull ISequence<? extends IDefinitionNodeItem<?, ?>> targets,
      @NonNull DynamicContext dynamicContext) {
    targets.stream().forEachOrdered(item -> {
      assert item != null;
      if (item.hasValue()) {
        try {
          updateValueStatus(item, constraint, node);
        } catch (RuntimeException ex) {
          handleError(constraint, item, ex, dynamicContext);
        }
      }
    });
  }

  /**
   * Add a new allowed value to the value status tracker.
   *
   * @param targetItem
   *          the item whose value is targeted by the constraint
   * @param allowedValues
   *          the allowed values constraint
   * @param node
   *          the original focus of Metapath evaluation for identifying the
   *          targets
   */
  protected void updateValueStatus(
      @NonNull INodeItem targetItem,
      @NonNull IAllowedValuesConstraint allowedValues,
      @NonNull IDefinitionNodeItem<?, ?> node) {
    // constraint.getAllowedValues().containsKey(value)

    @Nullable
    ValueStatus valueStatus = valueMap.get(targetItem);
    if (valueStatus == null) {
      valueStatus = new ValueStatus(targetItem);
      valueMap.put(targetItem, valueStatus);
    }

    valueStatus.registerAllowedValue(allowedValues, node);
  }

  /**
   * Evaluate the value associated with the {@code targetItem} and update the
   * status tracker.
   *
   * @param targetItem
   *          the item whose value will be validated
   * @param dynamicContext
   *          the Metapath dynamic execution context to use for Metapath
   *          evaluation
   */
  protected void handleAllowedValues(
      @NonNull INodeItem targetItem,
      @NonNull DynamicContext dynamicContext) {
    ValueStatus valueStatus = valueMap.remove(targetItem);
    if (valueStatus != null) {
      valueStatus.validate(dynamicContext);
    }
  }

  @SuppressWarnings("PMD.AvoidCatchingGenericException")
  @Override
  public void finalizeValidation(DynamicContext dynamicContext) {
    // key references
    for (Map.Entry<String, List<KeyRef>> entry : indexNameToKeyRefMap.entrySet()) {
      String indexName = ObjectUtils.notNull(entry.getKey());
      IIndex index = indexNameToIndexMap.get(indexName);

      List<KeyRef> keyRefs = entry.getValue();

      for (KeyRef keyRef : keyRefs) {
        IIndexHasKeyConstraint constraint = keyRef.getConstraint();

        INodeItem node = keyRef.getNode();
        List<INodeItem> targets = keyRef.getTargets();
        for (INodeItem item : targets) {
          assert item != null;
          try {
            validateKeyRef(constraint, node, item, indexName, index, dynamicContext);
          } catch (RuntimeException ex) {
            handleError(constraint, item, ex, dynamicContext);
          }
        }
      }
    }
  }

  private void validateKeyRef(
      @NonNull IIndexHasKeyConstraint constraint,
      @NonNull INodeItem contextNode,
      @NonNull INodeItem item,
      @NonNull String indexName,
      @Nullable IIndex index,
      @NonNull DynamicContext dynamicContext) {
    IConstraintValidationHandler handler = getConstraintValidationHandler();
    try {
      List<String> key = IIndex.toKey(item, constraint.getKeyFields(), dynamicContext);

      if (index == null) {
        handler.handleMissingIndexViolation(
            constraint,
            contextNode,
            item,
            ObjectUtils.notNull(String.format("Key reference to undefined index with name '%s'",
                indexName)),
            dynamicContext);
      } else {
        INodeItem referencedItem = index.get(key);

        if (referencedItem == null) {
          handler.handleIndexMiss(constraint, contextNode, item, key, dynamicContext);
        } else {
          handlePass(constraint, contextNode, item, dynamicContext);
        }
      }
    } catch (MetapathException ex) {
      handler.handleKeyMatchError(constraint, contextNode, item, ex, dynamicContext);
    }
  }

  private class ValueStatus {
    @NonNull
    private final List<Pair<IAllowedValuesConstraint, IDefinitionNodeItem<?, ?>>> constraints = new LinkedList<>();
    @NonNull
    private final String value;
    @NonNull
    private final INodeItem item;
    private boolean allowOthers = true;
    @NonNull
    private IAllowedValuesConstraint.Extensible extensible = IAllowedValuesConstraint.Extensible.EXTERNAL;

    public ValueStatus(@NonNull INodeItem item) {
      this.item = item;
      this.value = FnData.fnDataItem(item).asString();
    }

    public void registerAllowedValue(
        @NonNull IAllowedValuesConstraint allowedValues,
        @NonNull IDefinitionNodeItem<?, ?> node) {
      this.constraints.add(Pair.of(allowedValues, node));
      if (!allowedValues.isAllowedOther()) {
        // record the most restrictive value
        allowOthers = false;
      }

      IAllowedValuesConstraint.Extensible newExtensible = allowedValues.getExtensible();
      if (newExtensible.ordinal() > extensible.ordinal()) {
        // record the most restrictive value
        extensible = allowedValues.getExtensible();
      } else if (IAllowedValuesConstraint.Extensible.NONE.equals(newExtensible)
          && IAllowedValuesConstraint.Extensible.NONE.equals(extensible)) {
        // this is an error, where there are two none constraints that conflict
        throw new MetapathException(
            String.format("Multiple constraints have extensibility scope=none at path '%s'", item.getMetapath()));
      } else if (allowedValues.getExtensible().ordinal() < extensible.ordinal()) {
        String msg = String.format(
            "An allowed values constraint with an extensibility scope '%s'"
                + " exceeds the allowed scope '%s' at path '%s'",
            allowedValues.getExtensible().name(), extensible.name(), item.getMetapath());
        LOGGER.atError().log(msg);
        throw new MetapathException(msg);
      }
    }

    public void validate(@NonNull DynamicContext dynamicContext) {
      if (!constraints.isEmpty()) {
        boolean match = false;
        List<IAllowedValuesConstraint> failedConstraints = new LinkedList<>();
        IConstraintValidationHandler handler = getConstraintValidationHandler();
        for (Pair<IAllowedValuesConstraint, IDefinitionNodeItem<?, ?>> pair : constraints) {
          IAllowedValuesConstraint allowedValues = pair.getLeft();
          IDefinitionNodeItem<?, ?> node = ObjectUtils.notNull(pair.getRight());
          IAllowedValue matchingValue = allowedValues.getAllowedValue(value);
          if (matchingValue != null) {
            match = true;
            handlePass(allowedValues, node, item, dynamicContext);
          } else if (IAllowedValuesConstraint.Extensible.NONE.equals(allowedValues.getExtensible())) {
            // hard failure, since no other values can satisfy this constraint
            failedConstraints = CollectionUtil.singletonList(allowedValues);
            match = false;
            break;
          } else {
            failedConstraints.add(allowedValues);
          } // this constraint passes, but we need to make sure other constraints do as well
        }

        // it's not a failure if allow others is true
        if (!match && !allowOthers) {
          handler.handleAllowedValuesViolation(failedConstraints, item, dynamicContext);
        }
      }
    }
  }

  class Visitor
      extends AbstractNodeItemVisitor<DynamicContext, Void> {

    @NonNull
    private DynamicContext handleLetStatements(
        @NonNull INodeItem focus,
        @NonNull Map<QName, ILet> letExpressions,
        @NonNull DynamicContext dynamicContext) {

      DynamicContext retval;
      Collection<ILet> lets = letExpressions.values();
      if (lets.isEmpty()) {
        retval = dynamicContext;
      } else {
        final DynamicContext subContext = dynamicContext.subContext();

        for (ILet let : lets) {
          QName name = let.getName();
          ISequence<?> result = let.getValueExpression().evaluate(focus, subContext);

          // ensure the sequence is list backed
          result.getValue();

          subContext.bindVariableValue(name, result);
        }
        retval = subContext;
      }
      return retval;
    }

    @Override
    public Void visitFlag(@NonNull IFlagNodeItem item, DynamicContext context) {
      assert context != null;

      IFlagDefinition definition = item.getDefinition();
      DynamicContext effectiveContext = handleLetStatements(item, definition.getLetExpressions(), context);

      validateFlag(item, effectiveContext);
      super.visitFlag(item, effectiveContext);
      handleAllowedValues(item, context);
      return null;
    }

    @Override
    public Void visitField(@NonNull IFieldNodeItem item, DynamicContext context) {
      assert context != null;

      IFieldDefinition definition = item.getDefinition();
      DynamicContext effectiveContext = handleLetStatements(item, definition.getLetExpressions(), context);

      validateField(item, effectiveContext);
      super.visitField(item, effectiveContext);
      handleAllowedValues(item, context);
      return null;
    }

    @Override
    public Void visitAssembly(@NonNull IAssemblyNodeItem item, DynamicContext context) {
      assert context != null;

      IAssemblyDefinition definition = item.getDefinition();
      DynamicContext effectiveContext = handleLetStatements(item, definition.getLetExpressions(), context);

      validateAssembly(item, effectiveContext);
      super.visitAssembly(item, effectiveContext);
      return null;
    }

    @Override
    public Void visitMetaschema(@NonNull IModuleNodeItem item, DynamicContext context) {
      throw new UnsupportedOperationException("not needed");
    }

    @Override
    protected Void defaultResult() {
      // no result value
      return null;
    }
  }

  private static class KeyRef {
    @NonNull
    private final IIndexHasKeyConstraint constraint;
    @NonNull
    private final INodeItem node;
    @NonNull
    private final List<INodeItem> targets;

    public KeyRef(
        @NonNull IIndexHasKeyConstraint constraint,
        @NonNull INodeItem node,
        @NonNull List<INodeItem> targets) {
      this.node = node;
      this.constraint = constraint;
      this.targets = targets;
    }

    @NonNull
    public IIndexHasKeyConstraint getConstraint() {
      return constraint;
    }

    @NonNull
    protected INodeItem getNode() {
      return node;
    }

    @NonNull
    public List<INodeItem> getTargets() {
      return targets;
    }
  }
}