AbstractConstraintValidationHandler.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
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.format.IPathFormatter;
import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
import gov.nist.secauto.metaschema.core.util.CustomCollectors;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import edu.umd.cs.findbugs.annotations.NonNull;
/**
* Provides messaging for constraint violations.
*/
public abstract class AbstractConstraintValidationHandler implements IConstraintValidationHandler {
@NonNull
private IPathFormatter pathFormatter = IPathFormatter.METAPATH_PATH_FORMATER;
/**
* Get the formatter used to generate content paths for validation issue
* locations.
*
* @return the formatter
*/
@NonNull
public IPathFormatter getPathFormatter() {
return pathFormatter;
}
/**
* Set the path formatter to use when generating contextual paths in validation
* messages.
*
* @param formatter
* the path formatter to use
*/
public void setPathFormatter(@NonNull IPathFormatter formatter) {
this.pathFormatter = Objects.requireNonNull(formatter, "pathFormatter");
}
/**
* Get the path of the provided item using the configured path formatter.
*
* @param item
* the node item to generate the path for
* @return the path
* @see #getPathFormatter()
*/
protected String toPath(@NonNull INodeItem item) {
return item.toPath(getPathFormatter());
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param target
* the item the constraint targeted
* @param testedItems
* the items tested by the constraint
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newCardinalityMinimumViolationMessage(
@NonNull ICardinalityConstraint constraint,
@NonNull INodeItem target,
@NonNull ISequence<? extends INodeItem> testedItems,
@NonNull DynamicContext dynamicContext) {
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format(
"The cardinality '%d' is below the required minimum '%d' for items matching '%s'.",
testedItems.size(),
constraint.getMinOccurs(),
constraint.getTarget()))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param target
* the item the constraint targeted
* @param testedItems
* the items tested by the constraint
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newCardinalityMaximumViolationMessage(
@NonNull ICardinalityConstraint constraint,
@NonNull INodeItem target,
@NonNull ISequence<? extends INodeItem> testedItems,
@NonNull DynamicContext dynamicContext) {
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format(
"The cardinality '%d' is greater than the required maximum '%d' at: %s.",
testedItems.size(),
constraint.getMinOccurs(),
testedItems.safeStream()
.map(item -> new StringBuilder(12)
.append('\'')
.append(toPath(ObjectUtils.notNull(item)))
.append('\'')
.toString())
.collect(CustomCollectors.joiningWithOxfordComma("and"))))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param oldItem
* the original item matching the constraint
* @param target
* the new item matching the constraint
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newIndexDuplicateKeyViolationMessage(
@NonNull IIndexConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem oldItem,
@NonNull INodeItem target,
@NonNull DynamicContext dynamicContext) {
// TODO: render the key paths
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format("Index '%s' has duplicate key for items at paths '%s' and '%s'",
constraint.getName(),
toPath(oldItem),
toPath(target)))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param oldItem
* the original item matching the constraint
* @param target
* the new item matching the constraint
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newUniqueKeyViolationMessage(
@NonNull IUniqueConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem oldItem,
@NonNull INodeItem target,
@NonNull DynamicContext dynamicContext) {
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format("Unique constraint violation at paths '%s' and '%s'",
toPath(oldItem),
toPath(target)))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param target
* the target matching the constraint
* @param value
* the target's value
* @param pattern
* the expected pattern
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newMatchPatternViolationMessage(
@NonNull IMatchesConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull String value,
@NonNull Pattern pattern,
@NonNull DynamicContext dynamicContext) {
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format("Value '%s' did not match the pattern '%s' at path '%s'",
value,
pattern.pattern(),
toPath(target)))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param target
* the target matching the constraint
* @param value
* the target's value
* @param adapter
* the expected data type adapter
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newMatchDatatypeViolationMessage(
@NonNull IMatchesConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull String value,
@NonNull IDataTypeAdapter<?> adapter,
@NonNull DynamicContext dynamicContext) {
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format("Value '%s' did not conform to the data type '%s' at path '%s'",
value,
adapter.getPreferredName(),
toPath(target)))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param target
* the target matching the constraint
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newExpectViolationMessage(
@NonNull IExpectConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull DynamicContext dynamicContext) {
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format("Expect constraint '%s' did not match the data at path '%s'",
constraint.getTest(),
toPath(target)))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraints
* the constraints the requested message pertains to
* @param target
* the target matching the constraint
* @return the new message
*/
@NonNull
protected String newAllowedValuesViolationMessage(
@NonNull List<IAllowedValuesConstraint> constraints,
@NonNull INodeItem target) {
String allowedValues = constraints.stream()
.flatMap(constraint -> constraint.getAllowedValues().values().stream())
.map(IAllowedValue::getValue)
.sorted()
.distinct()
.collect(CustomCollectors.joiningWithOxfordComma("or"));
return ObjectUtils.notNull(String.format("Value '%s' doesn't match one of '%s' at path '%s'",
FnData.fnDataItem(target).asString(),
allowedValues,
toPath(target)));
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @return the new message
*/
@NonNull
protected String newIndexDuplicateViolationMessage(
@NonNull IIndexConstraint constraint,
@NonNull INodeItem node) {
return ObjectUtils.notNull(String.format("Duplicate index named '%s' found at path '%s'",
constraint.getName(),
node.getMetapath()));
}
/**
* Construct a new violation message for the provided {@code constraint} applied
* to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param target
* the target matching the constraint
* @param key
* the key derived from the target that failed to be found in the index
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@NonNull
protected String newIndexMissMessage(
@NonNull IIndexHasKeyConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull List<String> key,
@NonNull DynamicContext dynamicContext) {
String keyValues = key.stream()
.collect(Collectors.joining(","));
return constraint.getMessage() == null
? ObjectUtils.notNull(String.format("Key reference [%s] not found in index '%s' for item at path '%s'",
keyValues,
constraint.getIndexName(),
target.getMetapath()))
: constraint.generateMessage(target, dynamicContext);
}
/**
* Construct a new generic violation message for the provided {@code constraint}
* applied to the {@code node}.
*
* @param constraint
* the constraint the requested message pertains to
* @param node
* the item the constraint targeted
* @param target
* the target matching the constraint
* @param message
* the message to be added before information about the target path
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newMissingIndexViolationMessage(
@NonNull IIndexHasKeyConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull String message,
@NonNull DynamicContext dynamicContext) {
return String.format("%s for constraint '%s' for item at path '%s'",
message,
Objects.requireNonNullElse(constraint.getId(), "?"),
target.getMetapath());
}
}