1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.model.constraint;
7   
8   import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
9   import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
10  import gov.nist.secauto.metaschema.core.metapath.ISequence;
11  import gov.nist.secauto.metaschema.core.metapath.format.IPathFormatter;
12  import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
13  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
14  import gov.nist.secauto.metaschema.core.util.CustomCollectors;
15  
16  import java.util.List;
17  import java.util.Objects;
18  import java.util.regex.Pattern;
19  import java.util.stream.Collectors;
20  
21  import edu.umd.cs.findbugs.annotations.NonNull;
22  
23  /**
24   * Provides messaging for constraint violations.
25   */
26  public abstract class AbstractConstraintValidationHandler implements IConstraintValidationHandler {
27    @NonNull
28    private IPathFormatter pathFormatter = IPathFormatter.METAPATH_PATH_FORMATER;
29  
30    /**
31     * Get the formatter used to generate content paths for validation issue
32     * locations.
33     *
34     * @return the formatter
35     */
36    @NonNull
37    public IPathFormatter getPathFormatter() {
38      return pathFormatter;
39    }
40  
41    /**
42     * Set the path formatter to use when generating contextual paths in validation
43     * messages.
44     *
45     * @param formatter
46     *          the path formatter to use
47     */
48    public void setPathFormatter(@NonNull IPathFormatter formatter) {
49      this.pathFormatter = Objects.requireNonNull(formatter, "pathFormatter");
50    }
51  
52    /**
53     * Get the path of the provided item using the configured path formatter.
54     *
55     * @param item
56     *          the node item to generate the path for
57     * @return the path
58     * @see #getPathFormatter()
59     */
60    protected String toPath(@NonNull INodeItem item) {
61      return item.toPath(getPathFormatter());
62    }
63  
64    /**
65     * Construct a new violation message for the provided {@code constraint} applied
66     * to the {@code node}.
67     *
68     * @param constraint
69     *          the constraint the requested message pertains to
70     * @param node
71     *          the item the constraint targeted
72     * @param targets
73     *          the targets matching the constraint
74     * @return the new message
75     */
76    @SuppressWarnings("null")
77    @NonNull
78    protected String newCardinalityMinimumViolationMessage(
79        @NonNull ICardinalityConstraint constraint,
80        @NonNull INodeItem node,
81        @NonNull ISequence<? extends INodeItem> targets) {
82      return String.format(
83          "The cardinality '%d' is below the required minimum '%d' for items matching '%s'.",
84          targets.size(),
85          constraint.getMinOccurs(),
86          constraint.getTarget());
87    }
88  
89    /**
90     * Construct a new violation message for the provided {@code constraint} applied
91     * to the {@code node}.
92     *
93     * @param constraint
94     *          the constraint the requested message pertains to
95     * @param node
96     *          the item the constraint targeted
97     * @param targets
98     *          the targets matching the constraint
99     * @return the new message
100    */
101   @SuppressWarnings("null")
102   @NonNull
103   protected String newCardinalityMaximumViolationMessage(
104       @NonNull ICardinalityConstraint constraint,
105       @NonNull INodeItem node,
106       @NonNull ISequence<? extends INodeItem> targets) {
107     return String.format(
108         "The cardinality '%d' is greater than the required maximum '%d' at: %s.",
109         targets.size(),
110         constraint.getMinOccurs(),
111         targets.safeStream()
112             .map(item -> new StringBuilder(12)
113                 .append('\'')
114                 .append(toPath(item))
115                 .append('\'')
116                 .toString())
117             .collect(CustomCollectors.joiningWithOxfordComma("and")));
118   }
119 
120   /**
121    * Construct a new violation message for the provided {@code constraint} applied
122    * to the {@code node}.
123    *
124    * @param constraint
125    *          the constraint the requested message pertains to
126    * @param node
127    *          the item the constraint targeted
128    * @param oldItem
129    *          the original item matching the constraint
130    * @param newItem
131    *          the new item matching the constraint
132    * @return the new message
133    */
134   @SuppressWarnings("null")
135   @NonNull
136   protected String newIndexDuplicateKeyViolationMessage(
137       @NonNull IIndexConstraint constraint,
138       @NonNull INodeItem node,
139       @NonNull INodeItem oldItem,
140       @NonNull INodeItem newItem) {
141     // TODO: render the key paths
142     return String.format("Index '%s' has duplicate key for items at paths '%s' and '%s'",
143         constraint.getName(),
144         toPath(oldItem),
145         toPath(newItem));
146   }
147 
148   /**
149    * Construct a new violation message for the provided {@code constraint} applied
150    * to the {@code node}.
151    *
152    * @param constraint
153    *          the constraint the requested message pertains to
154    * @param node
155    *          the item the constraint targeted
156    * @param oldItem
157    *          the original item matching the constraint
158    * @param newItem
159    *          the new item matching the constraint
160    * @return the new message
161    */
162   @SuppressWarnings("null")
163   @NonNull
164   protected String newUniqueKeyViolationMessage(
165       @NonNull IUniqueConstraint constraint,
166       @NonNull INodeItem node,
167       @NonNull INodeItem oldItem,
168       @NonNull INodeItem newItem) {
169     return String.format("Unique constraint violation at paths '%s' and '%s'",
170         toPath(oldItem),
171         toPath(newItem));
172   }
173 
174   /**
175    * Construct a new violation message for the provided {@code constraint} applied
176    * to the {@code node}.
177    *
178    * @param constraint
179    *          the constraint the requested message pertains to
180    * @param node
181    *          the item the constraint targeted
182    * @param target
183    *          the target matching the constraint
184    * @param value
185    *          the target's value
186    * @param pattern
187    *          the expected pattern
188    * @return the new message
189    */
190   @SuppressWarnings("null")
191   @NonNull
192   protected String newMatchPatternViolationMessage(
193       @NonNull IMatchesConstraint constraint,
194       @NonNull INodeItem node,
195       @NonNull INodeItem target,
196       @NonNull String value,
197       @NonNull Pattern pattern) {
198     return String.format("Value '%s' did not match the pattern '%s' at path '%s'",
199         value,
200         pattern.pattern(),
201         toPath(target));
202   }
203 
204   /**
205    * Construct a new violation message for the provided {@code constraint} applied
206    * to the {@code node}.
207    *
208    * @param constraint
209    *          the constraint the requested message pertains to
210    * @param node
211    *          the item the constraint targeted
212    * @param target
213    *          the target matching the constraint
214    * @param value
215    *          the target's value
216    * @param adapter
217    *          the expected data type adapter
218    * @return the new message
219    */
220   @SuppressWarnings("null")
221   @NonNull
222   protected String newMatchDatatypeViolationMessage(
223       @NonNull IMatchesConstraint constraint,
224       @NonNull INodeItem node,
225       @NonNull INodeItem target,
226       @NonNull String value,
227       @NonNull IDataTypeAdapter<?> adapter) {
228     return String.format("Value '%s' did not conform to the data type '%s' at path '%s'", value,
229         adapter.getPreferredName(), toPath(target));
230   }
231 
232   /**
233    * Construct a new violation message for the provided {@code constraint} applied
234    * to the {@code node}.
235    *
236    * @param constraint
237    *          the constraint the requested message pertains to
238    * @param node
239    *          the item the constraint targeted
240    * @param target
241    *          the target matching the constraint
242    * @param dynamicContext
243    *          the Metapath dynamic execution context to use for Metapath
244    *          evaluation
245    * @return the new message
246    */
247   @SuppressWarnings("null")
248   @NonNull
249   protected String newExpectViolationMessage(
250       @NonNull IExpectConstraint constraint,
251       @SuppressWarnings("unused") @NonNull INodeItem node,
252       @NonNull INodeItem target,
253       @NonNull DynamicContext dynamicContext) {
254     String message;
255     if (constraint.getMessage() != null) {
256       message = constraint.generateMessage(target, dynamicContext);
257     } else {
258       message = String.format("Expect constraint '%s' did not match the data at path '%s'",
259           constraint.getTest(),
260           toPath(target));
261     }
262     return message;
263   }
264 
265   /**
266    * Construct a new violation message for the provided {@code constraint} applied
267    * to the {@code node}.
268    *
269    * @param constraints
270    *          the constraints the requested message pertains to
271    * @param target
272    *          the target matching the constraint
273    * @return the new message
274    */
275   @SuppressWarnings("null")
276   @NonNull
277   protected String newAllowedValuesViolationMessage(
278       @NonNull List<IAllowedValuesConstraint> constraints,
279       @NonNull INodeItem target) {
280 
281     String allowedValues = constraints.stream()
282         .flatMap(constraint -> constraint.getAllowedValues().values().stream())
283         .map(IAllowedValue::getValue)
284         .sorted()
285         .distinct()
286         .collect(CustomCollectors.joiningWithOxfordComma("or"));
287 
288     return String.format("Value '%s' doesn't match one of '%s' at path '%s'",
289         FnData.fnDataItem(target).asString(),
290         allowedValues,
291         toPath(target));
292   }
293 
294   /**
295    * Construct a new violation message for the provided {@code constraint} applied
296    * to the {@code node}.
297    *
298    * @param constraint
299    *          the constraint the requested message pertains to
300    * @param node
301    *          the item the constraint targeted
302    * @return the new message
303    */
304   @SuppressWarnings("null")
305   @NonNull
306   protected String newIndexDuplicateViolationMessage(
307       @NonNull IIndexConstraint constraint,
308       @NonNull INodeItem node) {
309     return String.format("Duplicate index named '%s' found at path '%s'",
310         constraint.getName(),
311         node.getMetapath());
312   }
313 
314   /**
315    * Construct a new violation message for the provided {@code constraint} applied
316    * to the {@code node}.
317    *
318    * @param constraint
319    *          the constraint the requested message pertains to
320    * @param node
321    *          the item the constraint targeted
322    * @param target
323    *          the target matching the constraint
324    * @param key
325    *          the key derived from the target that failed to be found in the index
326    * @return the new message
327    */
328   @SuppressWarnings("null")
329   @NonNull
330   protected String newIndexMissMessage(
331       @NonNull IIndexHasKeyConstraint constraint,
332       @NonNull INodeItem node,
333       @NonNull INodeItem target,
334       @NonNull List<String> key) {
335     String keyValues = key.stream()
336         .collect(Collectors.joining(","));
337 
338     return String.format("Key reference [%s] not found in index '%s' for item at path '%s'",
339         keyValues,
340         constraint.getIndexName(),
341         target.getMetapath());
342   }
343 
344   /**
345    * Construct a new generic violation message for the provided {@code constraint}
346    * applied to the {@code node}.
347    *
348    * @param constraint
349    *          the constraint the requested message pertains to
350    * @param node
351    *          the item the constraint targeted
352    * @param target
353    *          the target matching the constraint
354    * @param message
355    *          the message to be added before information about the target path
356    * @return the new message
357    */
358   @SuppressWarnings("null")
359   @NonNull
360   protected String newMissingIndexViolationMessage(
361       @NonNull IIndexHasKeyConstraint constraint,
362       @NonNull INodeItem node,
363       @NonNull INodeItem target,
364       @NonNull String message) {
365     return String.format("%s for constraint '%s' for item at path '%s'",
366         message,
367         Objects.requireNonNullElse(constraint.getId(), "?"),
368         target.getMetapath());
369   }
370 }