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.format.IPathFormatter;
11  import gov.nist.secauto.metaschema.core.metapath.item.ISequence;
12  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
13  import gov.nist.secauto.metaschema.core.util.CustomCollectors;
14  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
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 target
71     *          the item the constraint targeted
72     * @param testedItems
73     *          the items tested by the constraint
74     * @param dynamicContext
75     *          the Metapath dynamic execution context to use for Metapath
76     *          evaluation
77     * @return the new message
78     * @throws ConstraintValidationException
79     *           if the custom message contains a Metapath expression that is
80     *           invalid or if the expression failed to evaluate
81     */
82    @NonNull
83    protected String newCardinalityMinimumViolationMessage(
84        @NonNull ICardinalityConstraint constraint,
85        @NonNull INodeItem target,
86        @NonNull ISequence<? extends INodeItem> testedItems,
87        @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
88      return constraint.getMessage() == null
89          ? ObjectUtils.notNull(String.format(
90              "The cardinality '%d' is below the required minimum '%d' for items matching '%s'.",
91              testedItems.size(),
92              constraint.getMinOccurs(),
93              constraint.getTarget().getPath()))
94          : constraint.generateMessage(target, dynamicContext);
95    }
96  
97    /**
98     * Construct a new violation message for the provided {@code constraint} applied
99     * to the {@code node}.
100    *
101    * @param constraint
102    *          the constraint the requested message pertains to
103    * @param target
104    *          the item the constraint targeted
105    * @param testedItems
106    *          the items tested by the constraint
107    * @param dynamicContext
108    *          the Metapath dynamic execution context to use for Metapath
109    *          evaluation
110    * @return the new message
111    * @throws ConstraintValidationException
112    *           if the custom message contains a Metapath expression that is
113    *           invalid or if the expression failed to evaluate
114    */
115   @NonNull
116   protected String newCardinalityMaximumViolationMessage(
117       @NonNull ICardinalityConstraint constraint,
118       @NonNull INodeItem target,
119       @NonNull ISequence<? extends INodeItem> testedItems,
120       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
121     return constraint.getMessage() == null
122         ? ObjectUtils.notNull(String.format(
123             "The cardinality '%d' is greater than the required maximum '%d' at: %s.",
124             testedItems.size(),
125             constraint.getMinOccurs(),
126             testedItems.safeStream()
127                 .map(item -> new StringBuilder(12)
128                     .append('\'')
129                     .append(toPath(ObjectUtils.notNull(item)))
130                     .append('\'')
131                     .toString())
132                 .collect(CustomCollectors.joiningWithOxfordComma("and"))))
133         : constraint.generateMessage(target, dynamicContext);
134   }
135 
136   /**
137    * Construct a new violation message for the provided {@code constraint} applied
138    * to the {@code node}.
139    *
140    * @param constraint
141    *          the constraint the requested message pertains to
142    * @param node
143    *          the item the constraint targeted
144    * @param oldItem
145    *          the original item matching the constraint
146    * @param target
147    *          the new item matching the constraint
148    * @param dynamicContext
149    *          the Metapath dynamic execution context to use for Metapath
150    *          evaluation
151    * @return the new message
152    * @throws ConstraintValidationException
153    *           if the custom message contains a Metapath expression that is
154    *           invalid or if the expression failed to evaluate
155    */
156   @NonNull
157   protected String newIndexDuplicateKeyViolationMessage(
158       @NonNull IIndexConstraint constraint,
159       @NonNull INodeItem node,
160       @NonNull INodeItem oldItem,
161       @NonNull INodeItem target,
162       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
163     // TODO: render the key paths
164     return constraint.getMessage() == null
165         ? ObjectUtils.notNull(String.format("Index '%s' has duplicate key for items at paths '%s' and '%s'",
166             constraint.getName(),
167             toPath(oldItem),
168             toPath(target)))
169         : constraint.generateMessage(target, dynamicContext);
170   }
171 
172   /**
173    * Construct a new violation message for the provided {@code constraint} applied
174    * to the {@code node}.
175    *
176    * @param constraint
177    *          the constraint the requested message pertains to
178    * @param node
179    *          the item the constraint targeted
180    * @param oldItem
181    *          the original item matching the constraint
182    * @param target
183    *          the new item matching the constraint
184    * @param dynamicContext
185    *          the Metapath dynamic execution context to use for Metapath
186    *          evaluation
187    * @return the new message
188    * @throws ConstraintValidationException
189    *           if the custom message contains a Metapath expression that is
190    *           invalid or if the expression failed to evaluate
191    */
192   @NonNull
193   protected String newUniqueKeyViolationMessage(
194       @NonNull IUniqueConstraint constraint,
195       @NonNull INodeItem node,
196       @NonNull INodeItem oldItem,
197       @NonNull INodeItem target,
198       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
199     return constraint.getMessage() == null
200         ? ObjectUtils.notNull(String.format("Unique constraint violation at paths '%s' and '%s'",
201             toPath(oldItem),
202             toPath(target)))
203         : constraint.generateMessage(target, dynamicContext);
204   }
205 
206   /**
207    * Construct a new violation message for the provided {@code constraint} applied
208    * to the {@code node}.
209    *
210    * @param constraint
211    *          the constraint the requested message pertains to
212    * @param node
213    *          the item the constraint targeted
214    * @param target
215    *          the target matching the constraint
216    * @param value
217    *          the target's value
218    * @param pattern
219    *          the expected pattern
220    * @param dynamicContext
221    *          the Metapath dynamic execution context to use for Metapath
222    *          evaluation
223    * @return the new message
224    * @throws ConstraintValidationException
225    *           if the custom message contains a Metapath expression that is
226    *           invalid or if the expression failed to evaluate
227    */
228   @NonNull
229   protected String newMatchPatternViolationMessage(
230       @NonNull IMatchesConstraint constraint,
231       @NonNull INodeItem node,
232       @NonNull INodeItem target,
233       @NonNull String value,
234       @NonNull Pattern pattern,
235       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
236     return constraint.getMessage() == null
237         ? ObjectUtils.notNull(String.format("Value '%s' did not match the pattern '%s' at path '%s'",
238             value,
239             pattern.pattern(),
240             toPath(target)))
241         : constraint.generateMessage(target, dynamicContext);
242   }
243 
244   /**
245    * Construct a new violation message for the provided {@code constraint} applied
246    * to the {@code node}.
247    *
248    * @param constraint
249    *          the constraint the requested message pertains to
250    * @param node
251    *          the item the constraint targeted
252    * @param target
253    *          the target matching the constraint
254    * @param value
255    *          the target's value
256    * @param adapter
257    *          the expected data type adapter
258    * @param dynamicContext
259    *          the Metapath dynamic execution context to use for Metapath
260    *          evaluation
261    * @return the new message
262    * @throws ConstraintValidationException
263    *           if the custom message contains a Metapath expression that is
264    *           invalid or if the expression failed to evaluate
265    */
266   @NonNull
267   protected String newMatchDatatypeViolationMessage(
268       @NonNull IMatchesConstraint constraint,
269       @NonNull INodeItem node,
270       @NonNull INodeItem target,
271       @NonNull String value,
272       @NonNull IDataTypeAdapter<?> adapter,
273       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
274     return constraint.getMessage() == null
275         ? ObjectUtils.notNull(String.format("Value '%s' did not conform to the data type '%s' at path '%s'",
276             value,
277             adapter.getPreferredName(),
278             toPath(target)))
279         : constraint.generateMessage(target, dynamicContext);
280   }
281 
282   /**
283    * Construct a new violation message for the provided {@code constraint} applied
284    * to the {@code node}.
285    *
286    * @param constraint
287    *          the constraint the requested message pertains to
288    * @param node
289    *          the item the constraint targeted
290    * @param target
291    *          the target matching the constraint
292    * @param dynamicContext
293    *          the Metapath dynamic execution context to use for Metapath
294    *          evaluation
295    * @return the new message
296    * @throws ConstraintValidationException
297    *           if the custom message contains a Metapath expression that is
298    *           invalid or if the expression failed to evaluate
299    */
300   @NonNull
301   protected String newExpectViolationMessage(
302       @NonNull IExpectConstraint constraint,
303       @NonNull INodeItem node,
304       @NonNull INodeItem target,
305       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
306     return constraint.getMessage() == null
307         ? ObjectUtils.notNull(String.format("Expect constraint '%s' did not match the data at path '%s'",
308             constraint.getTest().getPath(),
309             toPath(target)))
310         : constraint.generateMessage(target, dynamicContext);
311   }
312 
313   /**
314    * Construct a new violation message for the provided {@code constraint} applied
315    * to the {@code node}.
316    *
317    * @param constraints
318    *          the constraints the requested message pertains to
319    * @param target
320    *          the target matching the constraint
321    * @return the new message
322    */
323   @NonNull
324   protected String newAllowedValuesViolationMessage(
325       @NonNull List<IAllowedValuesConstraint> constraints,
326       @NonNull INodeItem target) {
327     String allowedValues = constraints.stream()
328         .flatMap(constraint -> constraint.getAllowedValues().values().stream())
329         .map(IAllowedValue::getValue)
330         .sorted()
331         .distinct()
332         .collect(CustomCollectors.joiningWithOxfordComma("or"));
333 
334     return ObjectUtils.notNull(String.format("Value '%s' doesn't match one of '%s' at path '%s'",
335         target.toAtomicItem().asString(),
336         allowedValues,
337         toPath(target)));
338   }
339 
340   /**
341    * Construct a new violation message for the provided {@code constraint} applied
342    * to the {@code node}.
343    *
344    * @param constraint
345    *          the constraint the requested message pertains to
346    * @param node
347    *          the item the constraint targeted
348    * @return the new message
349    */
350   @NonNull
351   protected String newIndexDuplicateViolationMessage(
352       @NonNull IIndexConstraint constraint,
353       @NonNull INodeItem node) {
354     return ObjectUtils.notNull(String.format("Duplicate index named '%s' found at path '%s'",
355         constraint.getName(),
356         node.getMetapath()));
357   }
358 
359   /**
360    * Construct a new violation message for the provided {@code constraint} applied
361    * to the {@code node}.
362    *
363    * @param constraint
364    *          the constraint the requested message pertains to
365    * @param node
366    *          the item the constraint targeted
367    * @param target
368    *          the target matching the constraint
369    * @param key
370    *          the key derived from the target that failed to be found in the index
371    * @param dynamicContext
372    *          the Metapath dynamic execution context to use for Metapath
373    *          evaluation
374    * @return the new message
375    * @throws ConstraintValidationException
376    *           if the custom message contains a Metapath expression that is
377    *           invalid or if the expression failed to evaluate
378    */
379   @NonNull
380   protected String newIndexMissMessage(
381       @NonNull IIndexHasKeyConstraint constraint,
382       @NonNull INodeItem node,
383       @NonNull INodeItem target,
384       @NonNull List<String> key,
385       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
386     String keyValues = key.stream()
387         .collect(Collectors.joining(","));
388 
389     return constraint.getMessage() == null
390         ? ObjectUtils.notNull(String.format("Key reference [%s] not found in index '%s' for item at path '%s'",
391             keyValues,
392             constraint.getIndexName(),
393             target.getMetapath()))
394         : constraint.generateMessage(target, dynamicContext);
395   }
396 
397   /**
398    * Construct a new generic violation message for the provided {@code constraint}
399    * applied to the {@code node}.
400    *
401    * @param constraint
402    *          the constraint the requested message pertains to
403    * @param node
404    *          the item the constraint targeted
405    * @param target
406    *          the target matching the constraint
407    * @param message
408    *          the message to be added before information about the target path
409    * @param dynamicContext
410    *          the Metapath dynamic execution context to use for Metapath
411    *          evaluation
412    * @return the new message
413    */
414   @SuppressWarnings("null")
415   @NonNull
416   protected String newMissingIndexViolationMessage(
417       @NonNull IIndexHasKeyConstraint constraint,
418       @NonNull INodeItem node,
419       @NonNull INodeItem target,
420       @NonNull String message,
421       @NonNull DynamicContext dynamicContext) {
422     return String.format("%s for constraint '%s' for item at path '%s'",
423         message,
424         Objects.requireNonNullElse(constraint.getId(), "?"),
425         target.getMetapath());
426   }
427 }