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