DefaultConstraintValidator.java

/*
 * Portions of this software was developed by employees of the National Institute
 * of Standards and Technology (NIST), an agency of the Federal Government and is
 * being made available as a public service. Pursuant to title 17 United States
 * Code Section 105, works of NIST employees are not subject to copyright
 * protection in the United States. This software may be subject to foreign
 * copyright. Permission in the United States and in foreign countries, to the
 * extent that NIST may hold copyright, to use, copy, modify, create derivative
 * works, and distribute this software and its documentation without fee is hereby
 * granted on a non-exclusive basis, provided that this notice and disclaimer
 * of warranty appears in all copies.
 *
 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
 */

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

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.IDocumentNodeItem;
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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.ArrayList;
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 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.
 */
public class DefaultConstraintValidator implements IConstraintValidator { // 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 DynamicContext metapathContext;
  @NonNull
  private final IConstraintValidationHandler handler;

  public DefaultConstraintValidator(
      @NonNull DynamicContext metapathContext,
      @NonNull IConstraintValidationHandler handler) {
    this.metapathContext = metapathContext;
    this.handler = handler;
  }

  @NonNull
  public IConstraintValidationHandler getConstraintValidationHandler() {
    return handler;
  }

  @NonNull
  protected DynamicContext getMetapathContext() {
    return metapathContext;
  }

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

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

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

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

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

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

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

  protected void validateHasCardinality(@NonNull List<? extends ICardinalityConstraint> constraints,
      @NonNull List<? extends IAssemblyNodeItem> items) {

    items.stream().forEachOrdered(item -> {
      assert item != null;
      validateHasCardinality(constraints, item);
    });
  }

  protected void validateHasCardinality(@NonNull List<? extends ICardinalityConstraint> constraints,
      @NonNull IAssemblyNodeItem item) {
    for (ICardinalityConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
      try {
        validateHasCardinality(constraint, item, targets);
      } catch (MetapathException ex) {
        rethrowConstraintError(constraint, item, ex);
      }
    }
  }

  protected void validateHasCardinality(@NonNull ICardinalityConstraint constraint, @NonNull IAssemblyNodeItem node,
      ISequence<? extends INodeItem> targets) {
    int itemCount = targets.size();

    Integer minOccurs = constraint.getMinOccurs();
    if (minOccurs != null && itemCount < minOccurs) {
      getConstraintValidationHandler().handleCardinalityMinimumViolation(constraint, node, targets);
    }

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

  protected void validateIndex(@NonNull List<? extends IIndexConstraint> constraints,
      @NonNull List<? extends IAssemblyNodeItem> items) {
    items.stream().forEachOrdered(item -> {
      assert item != null;
      validateIndex(constraints, item);
    });
  }

  protected void validateIndex(@NonNull List<? extends IIndexConstraint> constraints,
      @NonNull IAssemblyNodeItem item) {
    for (IIndexConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
      try {
        validateIndex(constraint, item, targets);
      } catch (MetapathException ex) {
        rethrowConstraintError(constraint, item, ex);
      }
    }
  }

  protected void validateIndex(@NonNull IIndexConstraint constraint, @NonNull IAssemblyNodeItem node,
      @NonNull ISequence<? extends INodeItem> targets) {
    String indexName = constraint.getName();
    if (indexNameToIndexMap.containsKey(indexName)) {
      getConstraintValidationHandler().handleIndexDuplicateViolation(constraint, node);
      return; // NOPMD - readability
    }

    IIndex index = IIndex.newInstance(constraint.getKeyFields());
    targets.asStream()
        .forEachOrdered(item -> {
          assert item != null;
          if (item.hasValue()) {
            try {
              INodeItem oldItem = index.put(item, metapathContext);
              if (oldItem != null) {
                getConstraintValidationHandler().handleIndexDuplicateKeyViolation(constraint, node, oldItem, item);
              }
            } catch (MetapathException ex) {
              getConstraintValidationHandler().handleKeyMatchError(constraint, node, item, ex);
            }
          }
        });
    indexNameToIndexMap.put(indexName, index);
  }

  protected void validateUnique(@NonNull List<? extends IUniqueConstraint> constraints,
      @NonNull List<? extends IAssemblyNodeItem> items) {

    items.stream().forEachOrdered(item -> {
      assert item != null;
      validateUnique(constraints, item);
    });
  }

  protected void validateUnique(@NonNull List<? extends IUniqueConstraint> constraints,
      @NonNull IAssemblyNodeItem item) {
    for (IUniqueConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
      try {
        validateUnique(constraint, item, targets);
      } catch (MetapathException ex) {
        rethrowConstraintError(constraint, item, ex);
      }
    }
  }

  protected void validateUnique(@NonNull IUniqueConstraint constraint,
      @NonNull IAssemblyNodeItem node, @NonNull ISequence<? extends INodeItem> targets) {
    IIndex index = IIndex.newInstance(constraint.getKeyFields());
    targets.asStream()
        .forEachOrdered(item -> {
          assert item != null;
          if (item.hasValue()) {
            try {
              INodeItem oldItem = index.put(item, metapathContext);
              if (oldItem != null) {
                getConstraintValidationHandler().handleUniqueKeyViolation(constraint, node, oldItem, item);
              }
            } catch (MetapathException ex) {
              getConstraintValidationHandler().handleKeyMatchError(constraint, node, item, ex);
              throw ex;
            }
          }
        });
  }

  protected void validateMatches(@NonNull List<? extends IMatchesConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item) {

    for (IMatchesConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
      try {
        validateMatches(constraint, item, targets);
      } catch (MetapathException ex) {
        rethrowConstraintError(constraint, item, ex);
      }
    }
  }

  protected void validateMatches(@NonNull IMatchesConstraint constraint, @NonNull INodeItem node,
      ISequence<? extends INodeItem> targets) {
    targets.asStream()
        .forEachOrdered(item -> {
          assert item != null;
          if (item.hasValue()) {
            String value = FnData.fnDataItem(item).asString();

            Pattern pattern = constraint.getPattern();
            if (pattern != null && !pattern.asMatchPredicate().test(value)) {
              // failed pattern match
              getConstraintValidationHandler().handleMatchPatternViolation(constraint, node, item, value);
            }

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

  protected void validateIndexHasKey(
      @NonNull List<? extends IIndexHasKeyConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> node) {

    for (IIndexHasKeyConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(node, getMetapathContext());
      validateIndexHasKey(constraint, node, targets);
    }
  }

  protected 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.asList()));
    keyRefItems.add(keyRef);
  }

  protected void validateExpect(@NonNull List<? extends IExpectConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item) {
    for (IExpectConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
      validateExpect(constraint, item, targets);
    }
  }

  protected void validateExpect(@NonNull IExpectConstraint constraint, @NonNull INodeItem node,
      @NonNull ISequence<? extends INodeItem> targets) {
    targets.asStream()
        .map(item -> (INodeItem) item)
        .forEachOrdered(item -> {
          assert item != null;
          if (item.hasValue()) {
            MetapathExpression metapath = constraint.getTest();
            try {
              ISequence<?> result = metapath.evaluate(item, getMetapathContext());
              if (!FnBoolean.fnBoolean(result).toBoolean()) {
                getConstraintValidationHandler().handleExpectViolation(constraint, node, item, getMetapathContext());
              }
            } catch (MetapathException ex) {
              rethrowConstraintError(constraint, item, ex);
            }
          }
        });
  }

  protected void validateAllowedValues(@NonNull List<? extends IAllowedValuesConstraint> constraints,
      @NonNull IDefinitionNodeItem<?, ?> item) {
    for (IAllowedValuesConstraint constraint : constraints) {
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
      validateAllowedValues(constraint, targets);
    }
  }

  protected void validateAllowedValues(@NonNull IAllowedValuesConstraint constraint,
      ISequence<? extends IDefinitionNodeItem<?, ?>> targets) {
    targets.asStream().forEachOrdered(item -> {
      assert item != null;
      if (item.hasValue()) {
        try {
          updateValueStatus(item, constraint);
        } catch (MetapathException ex) {
          rethrowConstraintError(constraint, item, ex);
        }
      }
    });
  }

  private static void rethrowConstraintError(@NonNull IConstraint constraint, INodeItem item,
      MetapathException 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().getPath())
          .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());

    throw new MetapathException(builder.toString(), ex);
  }

  /**
   * Add a new allowed value to the value status tracker.
   *
   * @param targetItem
   *          the item whose value is targeted by the constraint
   * @param allowedValues
   *          the set of allowed values
   */
  protected void updateValueStatus(@NonNull INodeItem targetItem, @NonNull IAllowedValuesConstraint allowedValues) {
    // constraint.getAllowedValues().containsKey(value)

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

    valueStatus.registerAllowedValue(allowedValues);
  }

  protected void handleAllowedValues(@NonNull INodeItem targetItem) {
    ValueStatus valueStatus = valueMap.remove(targetItem);
    if (valueStatus != null) {
      valueStatus.validate();
    }
  }

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

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

      for (KeyRef keyRef : keyRefs) {
        IIndexHasKeyConstraint constraint = keyRef.getConstraint();
        for (INodeItem item : keyRef.getTargets()) {
          assert item != null;

          try {
            List<String> key = IIndex.toKey(item, constraint.getKeyFields(), getMetapathContext());

            INodeItem referencedItem = index.get(key);

            if (referencedItem == null) {
              getConstraintValidationHandler().handleIndexMiss(constraint, keyRef.getNode(), item, key);
            }
          } catch (MetapathException ex) {
            getConstraintValidationHandler().handleKeyMatchError(constraint, keyRef.getNode(), item, ex);
          }
        }
      }
    }
  }

  private class ValueStatus {
    @NonNull
    private final List<IAllowedValuesConstraint> 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) {
      this.constraints.add(allowedValues);
      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() {
      if (!constraints.isEmpty()) {
        boolean match = false;
        List<IAllowedValuesConstraint> failedConstraints = new LinkedList<>();
        for (IAllowedValuesConstraint allowedValues : constraints) {
          IAllowedValue matchingValue = allowedValues.getAllowedValue(value);
          if (matchingValue != null) {
            match = true;
          } 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) {
          getConstraintValidationHandler().handleAllowedValuesViolation(failedConstraints, item);
        }
      }
    }
  }

  class Visitor
      extends AbstractNodeItemVisitor<Void, Void> {
    @Override
    public Void visitDocument(@NonNull IDocumentNodeItem item, Void context) {
      return super.visitDocument(item, context);
    }

    @Override
    public Void visitFlag(@NonNull IFlagNodeItem item, Void context) {
      validateFlag(item);
      super.visitFlag(item, context);
      handleAllowedValues(item);
      return null;
    }

    @Override
    public Void visitField(@NonNull IFieldNodeItem item, Void context) {
      validateField(item);
      super.visitField(item, context);
      handleAllowedValues(item);
      return null;
    }

    @Override
    public Void visitAssembly(@NonNull IAssemblyNodeItem item, Void context) {
      validateAssembly(item);
      super.visitAssembly(item, context);
      return null;
    }

    @Override
    public Void visitMetaschema(@NonNull IModuleNodeItem item, Void 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;
    }
  }
}