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 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 node
* the item the constraint targeted
* @param targets
* the targets matching the constraint
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newCardinalityMinimumViolationMessage(
@NonNull ICardinalityConstraint constraint,
@NonNull INodeItem node,
@NonNull ISequence<? extends INodeItem> targets) {
return String.format(
"The cardinality '%d' is below the required minimum '%d' for items matching '%s'.",
targets.size(),
constraint.getMinOccurs(),
constraint.getTarget());
}
/**
* 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 targets
* the targets matching the constraint
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newCardinalityMaximumViolationMessage(
@NonNull ICardinalityConstraint constraint,
@NonNull INodeItem node,
@NonNull ISequence<? extends INodeItem> targets) {
return String.format(
"The cardinality '%d' is greater than the required maximum '%d' at: %s.",
targets.size(),
constraint.getMinOccurs(),
targets.safeStream()
.map(item -> new StringBuilder(12)
.append('\'')
.append(toPath(item))
.append('\'')
.toString())
.collect(CustomCollectors.joiningWithOxfordComma("and")));
}
/**
* 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 newItem
* the new item matching the constraint
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newIndexDuplicateKeyViolationMessage(
@NonNull IIndexConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem oldItem,
@NonNull INodeItem newItem) {
// TODO: render the key paths
return String.format("Index '%s' has duplicate key for items at paths '%s' and '%s'",
constraint.getName(),
toPath(oldItem),
toPath(newItem));
}
/**
* 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 newItem
* the new item matching the constraint
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newUniqueKeyViolationMessage(
@NonNull IUniqueConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem oldItem,
@NonNull INodeItem newItem) {
return String.format("Unique constraint violation at paths '%s' and '%s'",
toPath(oldItem),
toPath(newItem));
}
/**
* 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
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newMatchPatternViolationMessage(
@NonNull IMatchesConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull String value,
@NonNull Pattern pattern) {
return String.format("Value '%s' did not match the pattern '%s' at path '%s'",
value,
pattern.pattern(),
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
* @param target
* the target matching the constraint
* @param value
* the target's value
* @param adapter
* the expected data type adapter
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newMatchDatatypeViolationMessage(
@NonNull IMatchesConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull String value,
@NonNull IDataTypeAdapter<?> adapter) {
return String.format("Value '%s' did not conform to the data type '%s' at path '%s'", value,
adapter.getPreferredName(), 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
* @param target
* the target matching the constraint
* @param dynamicContext
* the Metapath dynamic execution context to use for Metapath
* evaluation
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newExpectViolationMessage(
@NonNull IExpectConstraint constraint,
@SuppressWarnings("unused") @NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull DynamicContext dynamicContext) {
String message;
if (constraint.getMessage() != null) {
message = constraint.generateMessage(target, dynamicContext);
} else {
message = String.format("Expect constraint '%s' did not match the data at path '%s'",
constraint.getTest(),
toPath(target));
}
return message;
}
/**
* 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
*/
@SuppressWarnings("null")
@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 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
*/
@SuppressWarnings("null")
@NonNull
protected String newIndexDuplicateViolationMessage(
@NonNull IIndexConstraint constraint,
@NonNull INodeItem node) {
return 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
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newIndexMissMessage(
@NonNull IIndexHasKeyConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull List<String> key) {
String keyValues = key.stream()
.collect(Collectors.joining(","));
return String.format("Key reference [%s] not found in index '%s' for item at path '%s'",
keyValues,
constraint.getIndexName(),
target.getMetapath());
}
/**
* 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
* @return the new message
*/
@SuppressWarnings("null")
@NonNull
protected String newMissingIndexViolationMessage(
@NonNull IIndexHasKeyConstraint constraint,
@NonNull INodeItem node,
@NonNull INodeItem target,
@NonNull String message) {
return String.format("%s for constraint '%s' for item at path '%s'",
message,
Objects.requireNonNullElse(constraint.getId(), "?"),
target.getMetapath());
}
}