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 }