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