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;
- }
- }
- }