1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.model.constraint;
7   
8   import org.apache.commons.lang3.tuple.Pair;
9   import org.apache.logging.log4j.LogManager;
10  import org.apache.logging.log4j.Logger;
11  import org.eclipse.jdt.annotation.Owning;
12  
13  import java.util.ArrayList;
14  import java.util.Collection;
15  import java.util.Collections;
16  import java.util.List;
17  import java.util.Map;
18  import java.util.concurrent.ConcurrentHashMap;
19  import java.util.concurrent.ExecutionException;
20  import java.util.concurrent.ExecutorService;
21  import java.util.concurrent.Future;
22  import java.util.regex.Pattern;
23  import java.util.stream.Collectors;
24  import java.util.stream.Stream;
25  
26  import dev.metaschema.core.configuration.DefaultConfiguration;
27  import dev.metaschema.core.configuration.IConfiguration;
28  import dev.metaschema.core.configuration.IMutableConfiguration;
29  import dev.metaschema.core.datatype.IDataTypeAdapter;
30  import dev.metaschema.core.metapath.DynamicContext;
31  import dev.metaschema.core.metapath.IMetapathExpression;
32  import dev.metaschema.core.metapath.MetapathException;
33  import dev.metaschema.core.metapath.function.library.FnBoolean;
34  import dev.metaschema.core.metapath.item.ISequence;
35  import dev.metaschema.core.metapath.item.node.AbstractNodeItemVisitor;
36  import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
37  import dev.metaschema.core.metapath.item.node.IDefinitionNodeItem;
38  import dev.metaschema.core.metapath.item.node.IFieldNodeItem;
39  import dev.metaschema.core.metapath.item.node.IFlagNodeItem;
40  import dev.metaschema.core.metapath.item.node.IModelNodeItem;
41  import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
42  import dev.metaschema.core.metapath.item.node.INodeItem;
43  import dev.metaschema.core.model.IAssemblyDefinition;
44  import dev.metaschema.core.model.IFieldDefinition;
45  import dev.metaschema.core.model.IFlagDefinition;
46  import dev.metaschema.core.qname.IEnhancedQName;
47  import dev.metaschema.core.util.CollectionUtil;
48  import dev.metaschema.core.util.ExceptionUtils;
49  import dev.metaschema.core.util.ExceptionUtils.WrappedException;
50  import dev.metaschema.core.util.ObjectUtils;
51  import edu.umd.cs.findbugs.annotations.NonNull;
52  import edu.umd.cs.findbugs.annotations.Nullable;
53  
54  /**
55   * Used to perform constraint validation over one or more node items.
56   * <p>
57   * This class is thread-safe and can be used with parallel constraint
58   * validation.
59   */
60  @SuppressWarnings({
61      "PMD.CouplingBetweenObjects",
62      "PMD.GodClass" // provides validators for all types
63  })
64  public class DefaultConstraintValidator
65      implements IConstraintValidator, IMutableConfiguration<ValidationFeature<?>> {
66    private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class);
67  
68    @NonNull
69    private final Map<INodeItem, ValueStatus> valueMap = new ConcurrentHashMap<>();
70    @NonNull
71    private final Map<String, IIndex> indexNameToIndexMap = new ConcurrentHashMap<>();
72    @NonNull
73    private final Map<String, List<KeyRef>> indexNameToKeyRefMap = new ConcurrentHashMap<>();
74    @NonNull
75    private final IConstraintValidationHandler handler;
76    @NonNull
77    private final IMutableConfiguration<ValidationFeature<?>> configuration;
78    @NonNull
79    @Owning
80    private final ParallelValidationConfig parallelConfig;
81  
82    /**
83     * Construct a new constraint validation instance with sequential execution.
84     *
85     * @param handler
86     *          the validation handler to use for handling constraint violations
87     */
88    public DefaultConstraintValidator(
89        @NonNull IConstraintValidationHandler handler) {
90      this(handler, ParallelValidationConfig.SEQUENTIAL);
91    }
92  
93    /**
94     * Construct a new constraint validation instance with configurable parallelism.
95     *
96     * @param handler
97     *          the validation handler to use for handling constraint violations
98     * @param parallelConfig
99     *          the parallel execution configuration
100    */
101   public DefaultConstraintValidator(
102       @NonNull IConstraintValidationHandler handler,
103       @NonNull ParallelValidationConfig parallelConfig) {
104     this.handler = handler;
105     this.configuration = new DefaultConfiguration<>();
106     this.parallelConfig = parallelConfig;
107   }
108 
109   /**
110    * Get the current configuration of the serializer/deserializer.
111    *
112    * @return the configuration
113    */
114   @NonNull
115   protected IMutableConfiguration<ValidationFeature<?>> getConfiguration() {
116     return configuration;
117   }
118 
119   @Override
120   @Owning
121   public DefaultConstraintValidator enableFeature(ValidationFeature<?> feature) {
122     return set(feature, true);
123   }
124 
125   @Override
126   @Owning
127   public DefaultConstraintValidator disableFeature(ValidationFeature<?> feature) {
128     return set(feature, false);
129   }
130 
131   @Override
132   @Owning
133   public DefaultConstraintValidator applyConfiguration(
134       @NonNull IConfiguration<ValidationFeature<?>> other) {
135     getConfiguration().applyConfiguration(other);
136     return this;
137   }
138 
139   @Override
140   @Owning
141   public DefaultConstraintValidator set(ValidationFeature<?> feature, Object value) {
142     getConfiguration().set(feature, value);
143     return this;
144   }
145 
146   @Override
147   public boolean isFeatureEnabled(ValidationFeature<?> feature) {
148     return getConfiguration().isFeatureEnabled(feature);
149   }
150 
151   @Override
152   public Map<ValidationFeature<?>, Object> getFeatureValues() {
153     return getConfiguration().getFeatureValues();
154   }
155 
156   /**
157    * Get the validation handler to use for handling constraint violations.
158    *
159    * @return the handler
160    */
161   @NonNull
162   protected IConstraintValidationHandler getConstraintValidationHandler() {
163     return handler;
164   }
165 
166   @Override
167   public void close() {
168     parallelConfig.close();
169   }
170 
171   @Override
172   public void validate(
173       @NonNull INodeItem item,
174       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
175     try {
176       item.accept(new Visitor(), dynamicContext);
177     } catch (WrappedException ex) {
178       ex.unwrapAndThrow(ConstraintValidationException.class);
179     }
180   }
181 
182   /**
183    * Validate the provided flag item against any associated constraints.
184    *
185    * @param item
186    *          the flag item to validate
187    * @param dynamicContext
188    *          the Metapath dynamic execution context to use for Metapath
189    *          evaluation
190    * @throws MetapathException
191    *           if an error occurred while evaluating a Metapath used in a
192    *           constraint
193    */
194   protected void validateFlag(
195       @NonNull IFlagNodeItem item,
196       @NonNull DynamicContext dynamicContext) {
197     IFlagDefinition definition = item.getDefinition();
198 
199     try {
200       validateExpect(definition.getExpectConstraints(), item, dynamicContext);
201       validateReport(definition.getReportConstraints(), item, dynamicContext);
202       validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext);
203       validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext);
204       validateMatches(definition.getMatchesConstraints(), item, dynamicContext);
205     } catch (ConstraintValidationException ex) {
206       throw ExceptionUtils.wrap(ex);
207     }
208   }
209 
210   /**
211    * Validate the provided field item against any associated constraints.
212    *
213    * @param item
214    *          the field item to validate
215    * @param dynamicContext
216    *          the Metapath dynamic execution context to use for Metapath
217    *          evaluation
218    * @throws MetapathException
219    *           if an error occurred while evaluating a Metapath used in a
220    *           constraint
221    */
222   protected void validateField(
223       @NonNull IFieldNodeItem item,
224       @NonNull DynamicContext dynamicContext) {
225     IFieldDefinition definition = item.getDefinition();
226 
227     try {
228       validateExpect(definition.getExpectConstraints(), item, dynamicContext);
229       validateReport(definition.getReportConstraints(), item, dynamicContext);
230       validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext);
231       validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext);
232       validateMatches(definition.getMatchesConstraints(), item, dynamicContext);
233     } catch (ConstraintValidationException ex) {
234       throw ExceptionUtils.wrap(ex);
235     }
236   }
237 
238   /**
239    * Validate the provided assembly item against any associated constraints.
240    *
241    * @param item
242    *          the assembly item to validate
243    * @param dynamicContext
244    *          the Metapath dynamic execution context to use for Metapath
245    *          evaluation
246    * @throws ConstraintValidationException
247    *           if an unexpected error occurred while validating a constraint
248    */
249   protected void validateAssembly(
250       @NonNull IAssemblyNodeItem item,
251       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
252     IAssemblyDefinition definition = item.getDefinition();
253 
254     try {
255       validateExpect(definition.getExpectConstraints(), item, dynamicContext);
256       validateReport(definition.getReportConstraints(), item, dynamicContext);
257       validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext);
258       validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext);
259       validateMatches(definition.getMatchesConstraints(), item, dynamicContext);
260       validateHasCardinality(definition.getHasCardinalityConstraints(), item, dynamicContext);
261       validateIndex(definition.getIndexConstraints(), item, dynamicContext);
262       validateUnique(definition.getUniqueConstraints(), item, dynamicContext);
263     } catch (ConstraintValidationException ex) {
264       throw ExceptionUtils.wrap(ex);
265     }
266   }
267 
268   /**
269    * Evaluates the provided collection of {@code constraints} in the context of
270    * the {@code item}.
271    *
272    * @param constraints
273    *          the constraints to execute
274    * @param item
275    *          the focus of Metapath evaluation
276    * @param dynamicContext
277    *          the Metapath dynamic execution context to use for Metapath
278    *          evaluation
279    * @throws ConstraintValidationException
280    *           if an unexpected error occurred while validating a constraint
281    */
282   private void validateHasCardinality(
283       @NonNull List<? extends ICardinalityConstraint> constraints,
284       @NonNull IAssemblyNodeItem item,
285       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
286     for (ICardinalityConstraint constraint : constraints) {
287       assert constraint != null;
288       try {
289         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
290         validateHasCardinality(constraint, item, targets, dynamicContext);
291       } catch (MetapathException ex) {
292         handleError(constraint, item, ex, dynamicContext);
293       }
294     }
295   }
296 
297   /**
298    * Evaluates the provided {@code constraint} against each of the
299    * {@code targets}.
300    *
301    * @param constraint
302    *          the constraint to execute
303    * @param node
304    *          the original focus of Metapath evaluation for identifying the
305    *          targets
306    * @param targets
307    *          the focus of Metapath evaluation for evaluating any constraint
308    *          Metapath clauses
309    * @throws ConstraintValidationException
310    *           if an unexpected error occurred while validating a constraint
311    */
312   private void validateHasCardinality(
313       @NonNull ICardinalityConstraint constraint,
314       @NonNull IAssemblyNodeItem node,
315       @NonNull ISequence<? extends INodeItem> targets,
316       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
317     int itemCount = targets.size();
318 
319     IConstraintValidationHandler handler = getConstraintValidationHandler();
320 
321     boolean violation = false;
322     Integer minOccurs = constraint.getMinOccurs();
323     if (minOccurs != null && itemCount < minOccurs) {
324       handler.handleCardinalityMinimumViolation(constraint, node, targets, dynamicContext);
325       violation = true;
326     }
327 
328     Integer maxOccurs = constraint.getMaxOccurs();
329     if (maxOccurs != null && itemCount > maxOccurs) {
330       handler.handleCardinalityMaximumViolation(constraint, node, targets, dynamicContext);
331       violation = true;
332     }
333 
334     if (!violation) {
335       handlePass(constraint, node, node, dynamicContext);
336     }
337   }
338 
339   /**
340    * Evaluates the provided collection of {@code constraints} in the context of
341    * the {@code item}.
342    *
343    * @param constraints
344    *          the constraints to execute
345    * @param item
346    *          the focus of Metapath evaluation
347    * @param dynamicContext
348    *          the Metapath dynamic execution context to use for Metapath
349    *          evaluation
350    * @throws ConstraintValidationException
351    *           if an unexpected error occurred while validating a constraint
352    */
353   private void validateIndex(
354       @NonNull List<? extends IIndexConstraint> constraints,
355       @NonNull IAssemblyNodeItem item,
356       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
357     for (IIndexConstraint constraint : constraints) {
358       assert constraint != null;
359 
360       try {
361         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
362         validateIndex(constraint, item, targets, dynamicContext);
363       } catch (MetapathException ex) {
364         handleError(constraint, item, ex, dynamicContext);
365       }
366     }
367   }
368 
369   /**
370    * Evaluates the provided {@code constraint} against each of the
371    * {@code targets}.
372    *
373    * @param constraint
374    *          the constraint to execute
375    * @param node
376    *          the original focus of Metapath evaluation for identifying the
377    *          targets
378    * @param targets
379    *          the focus of Metapath evaluation for evaluating any constraint
380    *          Metapath clauses
381    * @param dynamicContext
382    *          the Metapath dynamic execution context to use for Metapath
383    *          evaluation
384    * @throws ConstraintValidationException
385    *           if an unexpected error occurred while validating a constraint
386    */
387   private void validateIndex(
388       @NonNull IIndexConstraint constraint,
389       @NonNull IAssemblyNodeItem node,
390       @NonNull ISequence<? extends INodeItem> targets,
391       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
392     IConstraintValidationHandler handler = getConstraintValidationHandler();
393     if (indexNameToIndexMap.containsKey(constraint.getName())) {
394       handler.handleIndexDuplicateViolation(constraint, node, dynamicContext);
395     } else {
396       validateIndexEntries(constraint, node, targets, dynamicContext);
397     }
398   }
399 
400   private void validateIndexEntries(
401       @NonNull IIndexConstraint constraint,
402       @NonNull IAssemblyNodeItem node,
403       @NonNull ISequence<? extends INodeItem> targets,
404       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
405     IIndex index = IIndex.newInstance(constraint.getKeyFields());
406     for (INodeItem item : targets) {
407       assert item != null;
408       if (item.hasValue()) {
409         INodeItem oldItem = null;
410         try {
411           oldItem = index.put(item, IIndex.toKey(item, index.getKeyFields(), dynamicContext));
412         } catch (IllegalArgumentException ex) {
413           // throw by IIndex.toKey
414           handleError(constraint, item, ex, dynamicContext);
415         }
416         try {
417           if (oldItem == null) {
418             handlePass(constraint, node, item, dynamicContext);
419           } else {
420             handler.handleIndexDuplicateKeyViolation(constraint, node, oldItem, item, dynamicContext);
421           }
422         } catch (MetapathException ex) {
423           handler.handleKeyMatchError(constraint, node, item, ex, dynamicContext);
424         }
425       }
426     }
427     indexNameToIndexMap.put(constraint.getName(), index);
428   }
429 
430   private void handlePass(
431       @NonNull IConstraint constraint,
432       @NonNull INodeItem node,
433       @NonNull INodeItem item,
434       @NonNull DynamicContext dynamicContext) {
435     if (isFeatureEnabled(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS)) {
436       getConstraintValidationHandler().handlePass(constraint, node, item, dynamicContext);
437     }
438   }
439 
440   private void handleError(
441       @NonNull IConstraint constraint,
442       @NonNull INodeItem node,
443       @NonNull Throwable ex,
444       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
445     if (isFeatureEnabled(ValidationFeature.THROW_EXCEPTION_ON_ERROR)) {
446       if (ex instanceof ConstraintValidationException) {
447         throw (ConstraintValidationException) ex;
448       }
449       throw new ConstraintValidationException(ex);
450     }
451     getConstraintValidationHandler()
452         .handleError(constraint, node, toErrorMessage(constraint, node, ex), ex, dynamicContext);
453   }
454 
455   @NonNull
456   private static String toErrorMessage(
457       @NonNull IConstraint constraint,
458       @NonNull INodeItem item,
459       @NonNull Throwable ex) {
460     StringBuilder builder = new StringBuilder(128);
461     builder.append("A ")
462         .append(constraint.getType().getName())
463         .append(" constraint");
464 
465     String id = constraint.getId();
466     if (id == null) {
467       builder.append(" targeting the metapath '")
468           .append(constraint.getTarget().getPath())
469           .append('\'');
470     } else {
471       builder.append(" with id '")
472           .append(id)
473           .append('\'');
474     }
475 
476     builder.append(", matching the item at path '")
477         .append(item.getMetapath())
478         .append("', resulted in an unexpected error. ")
479         .append(ex.getLocalizedMessage());
480     return ObjectUtils.notNull(builder.toString());
481   }
482 
483   /**
484    * Evaluates the provided collection of {@code constraints} in the context of
485    * the {@code item}.
486    *
487    * @param constraints
488    *          the constraints to execute
489    * @param item
490    *          the focus of Metapath evaluation
491    * @param dynamicContext
492    *          the Metapath dynamic execution context to use for Metapath
493    *          evaluation
494    * @throws ConstraintValidationException
495    *           if an unexpected error occurred while validating a constraint
496    */
497   private void validateUnique(
498       @NonNull List<? extends IUniqueConstraint> constraints,
499       @NonNull IAssemblyNodeItem item,
500       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
501     for (IUniqueConstraint constraint : constraints) {
502       assert constraint != null;
503 
504       try {
505         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
506         validateUnique(constraint, item, targets, dynamicContext);
507       } catch (MetapathException ex) {
508         handleError(constraint, item, ex, dynamicContext);
509       }
510     }
511   }
512 
513   /**
514    * Evaluates the provided {@code constraint} against each of the
515    * {@code targets}.
516    *
517    * @param constraint
518    *          the constraint to execute
519    * @param node
520    *          the original focus of Metapath evaluation for identifying the
521    *          targets
522    * @param targets
523    *          the focus of Metapath evaluation for evaluating any constraint
524    *          Metapath clauses
525    * @param dynamicContext
526    *          the Metapath dynamic execution context to use for Metapath
527    *          evaluation
528    * @throws ConstraintValidationException
529    *           if an unexpected error occurred while validating a constraint
530    */
531   private void validateUnique(
532       @NonNull IUniqueConstraint constraint,
533       @NonNull IAssemblyNodeItem node,
534       @NonNull ISequence<? extends INodeItem> targets,
535       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
536 
537     IConstraintValidationHandler handler = getConstraintValidationHandler();
538     IIndex index = IIndex.newInstance(constraint.getKeyFields());
539 
540     for (INodeItem item : targets) {
541       assert item != null;
542       if (item.hasValue()) {
543         INodeItem oldItem = null;
544         try {
545           oldItem = index.put(item, IIndex.toKey(item, index.getKeyFields(), dynamicContext));
546         } catch (IllegalArgumentException ex) {
547           // raised by IIndex.toKey
548           handleError(constraint, item, ex, dynamicContext);
549         }
550 
551         try {
552           if (oldItem == null) {
553             handlePass(constraint, node, item, dynamicContext);
554           } else {
555             handler.handleUniqueKeyViolation(constraint, node, oldItem, item, dynamicContext);
556           }
557         } catch (MetapathException ex) {
558           handleError(constraint, item, ex, dynamicContext);
559         }
560       }
561     }
562   }
563 
564   /**
565    * Evaluates the provided collection of {@code constraints} in the context of
566    * the {@code item}.
567    *
568    * @param constraints
569    *          the constraints to execute
570    * @param item
571    *          the focus of Metapath evaluation
572    * @param dynamicContext
573    *          the Metapath dynamic execution context to use for Metapath
574    *          evaluation
575    * @throws ConstraintValidationException
576    *           if an unexpected error occurred while validating a constraint
577    */
578   private void validateMatches(
579       @NonNull List<? extends IMatchesConstraint> constraints,
580       @NonNull IDefinitionNodeItem<?, ?> item,
581       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
582 
583     for (IMatchesConstraint constraint : constraints) {
584       assert constraint != null;
585 
586       try {
587         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
588         validateMatches(constraint, item, targets, dynamicContext);
589       } catch (MetapathException ex) {
590         handleError(constraint, item, ex, dynamicContext);
591       }
592     }
593   }
594 
595   /**
596    * Evaluates the provided {@code constraint} against each of the
597    * {@code targets}.
598    *
599    * @param constraint
600    *          the constraint to execute
601    * @param node
602    *          the original focus of Metapath evaluation for identifying the
603    *          targets
604    * @param targets
605    *          the focus of Metapath evaluation for evaluating any constraint
606    *          Metapath clauses
607    * @throws ConstraintValidationException
608    *           if an unexpected error occurred while validating a constraint
609    */
610   private void validateMatches(
611       @NonNull IMatchesConstraint constraint,
612       @NonNull INodeItem node,
613       @NonNull ISequence<? extends INodeItem> targets,
614       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
615     for (INodeItem item : targets) {
616       assert item != null;
617       if (item.hasValue()) {
618         validateMatchesItem(constraint, node, item, dynamicContext);
619       }
620     }
621   }
622 
623   private void validateMatchesItem(
624       @NonNull IMatchesConstraint constraint,
625       @NonNull INodeItem node,
626       @NonNull INodeItem item,
627       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
628     String value = item.toAtomicItem().asString();
629 
630     IConstraintValidationHandler handler = getConstraintValidationHandler();
631     boolean valid = true;
632     Pattern pattern = constraint.getPattern();
633     if (pattern != null && !pattern.asMatchPredicate().test(value)) {
634       // failed pattern match
635       handler.handleMatchPatternViolation(constraint, node, item, value, pattern, dynamicContext);
636       valid = false;
637     }
638 
639     IDataTypeAdapter<?> adapter = constraint.getDataType();
640     if (adapter != null) {
641       try {
642         adapter.parse(value);
643       } catch (IllegalArgumentException ex) {
644         handler.handleMatchDatatypeViolation(constraint, node, item, value, adapter, ex, dynamicContext);
645         valid = false;
646       }
647     }
648 
649     if (valid) {
650       handlePass(constraint, node, item, dynamicContext);
651     }
652   }
653 
654   /**
655    * Evaluates the provided collection of {@code constraints} in the context of
656    * the {@code item}.
657    *
658    * @param constraints
659    *          the constraints to execute
660    * @param item
661    *          the focus of Metapath evaluation
662    * @param dynamicContext
663    *          the Metapath dynamic execution context to use for Metapath
664    *          evaluation
665    * @throws ConstraintValidationException
666    *           if an unexpected error occurred while validating a constraint
667    */
668   private void validateIndexHasKey(
669       @NonNull List<? extends IIndexHasKeyConstraint> constraints,
670       @NonNull IDefinitionNodeItem<?, ?> item,
671       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
672 
673     for (IIndexHasKeyConstraint constraint : constraints) {
674       assert constraint != null;
675 
676       try {
677         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
678         validateIndexHasKey(constraint, item, targets);
679       } catch (MetapathException ex) {
680         handleError(constraint, item, ex, dynamicContext);
681       }
682     }
683   }
684 
685   /**
686    * Evaluates the provided {@code constraint} against each of the
687    * {@code targets}.
688    *
689    * @param constraint
690    *          the constraint to execute
691    * @param node
692    *          the original focus of Metapath evaluation for identifying the
693    *          targets
694    * @param targets
695    *          the focus of Metapath evaluation for evaluating any constraint
696    *          Metapath clauses
697    */
698   private void validateIndexHasKey(
699       @NonNull IIndexHasKeyConstraint constraint,
700       @NonNull IDefinitionNodeItem<?, ?> node,
701       @NonNull ISequence<? extends INodeItem> targets) {
702     String indexName = constraint.getIndexName();
703 
704     // Use computeIfAbsent for thread-safe lazy initialization
705     // The list is wrapped in synchronizedList to ensure thread-safe add operations
706     List<KeyRef> keyRefItems = indexNameToKeyRefMap.computeIfAbsent(
707         indexName,
708         k -> Collections.synchronizedList(new ArrayList<>()));
709 
710     keyRefItems.add(new KeyRef(constraint, node, new ArrayList<>(targets)));
711   }
712 
713   /**
714    * Evaluates the provided collection of {@code constraints} in the context of
715    * the {@code item}.
716    *
717    * @param constraints
718    *          the constraints to execute
719    * @param item
720    *          the focus of Metapath evaluation
721    * @param dynamicContext
722    *          the Metapath dynamic execution context to use for Metapath
723    *          evaluation
724    * @throws ConstraintValidationException
725    *           if an unexpected error occurred while validating a constraint
726    */
727   private void validateExpect(
728       @NonNull List<? extends IExpectConstraint> constraints,
729       @NonNull IDefinitionNodeItem<?, ?> item,
730       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
731     for (IExpectConstraint constraint : constraints) {
732       assert constraint != null;
733 
734       try {
735         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
736         validateExpect(constraint, item, targets, dynamicContext);
737       } catch (MetapathException ex) {
738         handleError(constraint, item, ex, dynamicContext);
739       }
740     }
741   }
742 
743   /**
744    * Evaluates the provided {@code constraint} against each of the
745    * {@code targets}.
746    *
747    * @param constraint
748    *          the constraint to execute
749    * @param node
750    *          the original focus of Metapath evaluation for identifying the
751    *          targets
752    * @param targets
753    *          the focus of Metapath evaluation for evaluating any constraint
754    *          Metapath clauses
755    * @param dynamicContext
756    *          the Metapath dynamic execution context to use for Metapath
757    *          evaluation
758    * @throws ConstraintValidationException
759    *           if an unexpected error occurred while validating a constraint
760    */
761   private void validateExpect(
762       @NonNull IExpectConstraint constraint,
763       @NonNull INodeItem node,
764       @NonNull ISequence<? extends INodeItem> targets,
765       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
766     IMetapathExpression test = constraint.getTest();
767     IConstraintValidationHandler handler = getConstraintValidationHandler();
768     for (INodeItem item : targets) {
769       assert item != null;
770 
771       if (item.hasValue()) {
772         try {
773           ISequence<?> result = test.evaluate(item, dynamicContext);
774           if (FnBoolean.fnBoolean(result).toBoolean()) {
775             handlePass(constraint, node, item, dynamicContext);
776           } else {
777             handler.handleExpectViolation(constraint, node, item, dynamicContext);
778           }
779         } catch (MetapathException ex) {
780           handleError(constraint, item, ex, dynamicContext);
781         }
782       }
783     }
784   }
785 
786   /**
787    * Evaluates the provided collection of report {@code constraints} in the
788    * context of the {@code item}.
789    * <p>
790    * Report constraints generate findings when their test expression evaluates to
791    * {@code true}, which is the opposite of expect constraints.
792    *
793    * @param constraints
794    *          the constraints to execute
795    * @param item
796    *          the focus of Metapath evaluation
797    * @param dynamicContext
798    *          the Metapath dynamic execution context to use for Metapath
799    *          evaluation
800    * @throws ConstraintValidationException
801    *           if an unexpected error occurred while validating a constraint
802    */
803   private void validateReport(
804       @NonNull List<? extends IReportConstraint> constraints,
805       @NonNull IDefinitionNodeItem<?, ?> item,
806       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
807     for (IReportConstraint constraint : constraints) {
808       assert constraint != null;
809 
810       try {
811         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
812         validateReport(constraint, item, targets, dynamicContext);
813       } catch (MetapathException ex) {
814         handleError(constraint, item, ex, dynamicContext);
815       }
816     }
817   }
818 
819   /**
820    * Evaluates the provided report {@code constraint} against each of the
821    * {@code targets}.
822    * <p>
823    * Report constraints generate findings when their test expression evaluates to
824    * {@code true}, which is the opposite of expect constraints.
825    *
826    * @param constraint
827    *          the constraint to execute
828    * @param node
829    *          the original focus of Metapath evaluation for identifying the
830    *          targets
831    * @param targets
832    *          the focus of Metapath evaluation for evaluating any constraint
833    *          Metapath clauses
834    * @param dynamicContext
835    *          the Metapath dynamic execution context to use for Metapath
836    *          evaluation
837    * @throws ConstraintValidationException
838    *           if an unexpected error occurred while validating a constraint
839    */
840   private void validateReport(
841       @NonNull IReportConstraint constraint,
842       @NonNull INodeItem node,
843       @NonNull ISequence<? extends INodeItem> targets,
844       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
845     IMetapathExpression test = constraint.getTest();
846     IConstraintValidationHandler handler = getConstraintValidationHandler();
847     for (INodeItem item : targets) {
848       assert item != null;
849 
850       if (item.hasValue()) {
851         try {
852           ISequence<?> result = test.evaluate(item, dynamicContext);
853           // Report constraints fire when test is TRUE (opposite of expect)
854           if (FnBoolean.fnBoolean(result).toBoolean()) {
855             handler.handleReportViolation(constraint, node, item, dynamicContext);
856           } else {
857             handlePass(constraint, node, item, dynamicContext);
858           }
859         } catch (MetapathException ex) {
860           handleError(constraint, item, ex, dynamicContext);
861         }
862       }
863     }
864   }
865 
866   /**
867    * Evaluates the provided collection of {@code constraints} in the context of
868    * the {@code item}.
869    *
870    * @param constraints
871    *          the constraints to execute
872    * @param item
873    *          the focus of Metapath evaluation
874    * @param dynamicContext
875    *          the Metapath dynamic execution context to use for Metapath
876    *          evaluation
877    * @throws ConstraintValidationException
878    *           if an unexpected error occurred while validating a constraint
879    */
880   private void validateAllowedValues(
881       @NonNull List<? extends IAllowedValuesConstraint> constraints,
882       @NonNull IDefinitionNodeItem<?, ?> item,
883       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
884     for (IAllowedValuesConstraint constraint : constraints) {
885       assert constraint != null;
886       try {
887         ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, dynamicContext);
888         validateAllowedValues(constraint, item, targets, dynamicContext);
889       } catch (MetapathException ex) {
890         handleError(constraint, item, ex, dynamicContext);
891       }
892     }
893   }
894 
895   /**
896    * Evaluates the provided {@code constraint} against each of the
897    * {@code targets}.
898    *
899    * @param constraint
900    *          the constraint to execute
901    * @param node
902    *          the original focus of Metapath evaluation for identifying the
903    *          targets
904    * @param targets
905    *          the focus of Metapath evaluation for evaluating any constraint
906    *          Metapath clauses
907    * @param dynamicContext
908    *          the Metapath dynamic execution context to use for Metapath
909    *          evaluation
910    * @throws ConstraintValidationException
911    *           if an unexpected error occurred while validating a constraint
912    */
913   private void validateAllowedValues(
914       @NonNull IAllowedValuesConstraint constraint,
915       @NonNull IDefinitionNodeItem<?, ?> node,
916       @NonNull ISequence<? extends IDefinitionNodeItem<?, ?>> targets,
917       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
918     for (INodeItem item : targets) {
919       assert item != null;
920       if (item.hasValue()) {
921         try {
922           updateValueStatus(item, constraint, node);
923         } catch (ConstraintValidationException ex) {
924           handleError(constraint, item, ex, dynamicContext);
925         }
926       }
927     }
928   }
929 
930   /**
931    * Add a new allowed value to the value status tracker.
932    *
933    * @param targetItem
934    *          the item whose value is targeted by the constraint
935    * @param allowedValues
936    *          the allowed values constraint
937    * @param node
938    *          the original focus of Metapath evaluation for identifying the
939    *          targets
940    * @throws ConstraintValidationException
941    *           if an unexpected error occurred while registering the allowed
942    *           values
943    */
944   protected void updateValueStatus(
945       @NonNull INodeItem targetItem,
946       @NonNull IAllowedValuesConstraint allowedValues,
947       @NonNull IDefinitionNodeItem<?, ?> node) throws ConstraintValidationException {
948     // Use computeIfAbsent for thread-safe lazy initialization
949     ValueStatus valueStatus = valueMap.computeIfAbsent(targetItem, ValueStatus::new);
950 
951     valueStatus.registerAllowedValues(allowedValues, node);
952   }
953 
954   /**
955    * Evaluate the value associated with the {@code targetItem} and update the
956    * status tracker.
957    *
958    * @param targetItem
959    *          the item whose value will be validated
960    * @param dynamicContext
961    *          the Metapath dynamic execution context to use for Metapath
962    *          evaluation
963    */
964   protected void handleAllowedValues(
965       @NonNull INodeItem targetItem,
966       @NonNull DynamicContext dynamicContext) {
967     ValueStatus valueStatus = valueMap.remove(targetItem);
968     if (valueStatus != null) {
969       valueStatus.validate(dynamicContext);
970     }
971   }
972 
973   @Override
974   public void finalizeValidation(DynamicContext dynamicContext) throws ConstraintValidationException {
975     // key references
976     for (Map.Entry<String, List<KeyRef>> entry : indexNameToKeyRefMap.entrySet()) {
977       String indexName = ObjectUtils.notNull(entry.getKey());
978       IIndex index = indexNameToIndexMap.get(indexName);
979 
980       List<KeyRef> keyRefs = entry.getValue();
981 
982       for (KeyRef keyRef : keyRefs) {
983         IIndexHasKeyConstraint constraint = keyRef.getConstraint();
984 
985         INodeItem node = keyRef.getNode();
986         List<INodeItem> targets = keyRef.getTargets();
987         for (INodeItem item : targets) {
988           assert item != null;
989           try {
990             validateKeyRef(constraint, node, item, indexName, index, dynamicContext);
991           } catch (MetapathException ex) {
992             handleError(constraint, item, ex, dynamicContext);
993           }
994         }
995       }
996     }
997   }
998 
999   private void validateKeyRef(
1000       @NonNull IIndexHasKeyConstraint constraint,
1001       @NonNull INodeItem contextNode,
1002       @NonNull INodeItem item,
1003       @NonNull String indexName,
1004       @Nullable IIndex index,
1005       @NonNull DynamicContext dynamicContext) throws ConstraintValidationException {
1006     IConstraintValidationHandler handler = getConstraintValidationHandler();
1007     try {
1008       List<String> key;
1009       try {
1010         key = IIndex.toKey(item, constraint.getKeyFields(), dynamicContext);
1011       } catch (IllegalArgumentException ex) {
1012         handler.handleError(constraint, item, toErrorMessage(constraint, item, ex), ex, dynamicContext);
1013         throw ex;
1014       }
1015 
1016       if (index == null) {
1017         handler.handleMissingIndexViolation(
1018             constraint,
1019             contextNode,
1020             item,
1021             ObjectUtils.notNull(String.format("Key reference to undefined index with name '%s'",
1022                 indexName)),
1023             dynamicContext);
1024       } else {
1025         INodeItem referencedItem = index.get(key);
1026 
1027         if (referencedItem == null) {
1028           handler.handleIndexMiss(constraint, contextNode, item, key, dynamicContext);
1029         } else {
1030           handlePass(constraint, contextNode, item, dynamicContext);
1031         }
1032       }
1033     } catch (MetapathException ex) {
1034       handler.handleKeyMatchError(constraint, contextNode, item, ex, dynamicContext);
1035     }
1036   }
1037 
1038   @SuppressWarnings("PMD.AvoidUsingVolatile") // Required for thread-safe visibility across threads
1039   private class ValueStatus {
1040     @NonNull
1041     private final List<Pair<IAllowedValuesConstraint, IDefinitionNodeItem<?, ?>>> constraints
1042         = Collections.synchronizedList(new ArrayList<>());
1043     @NonNull
1044     private final String value;
1045     @NonNull
1046     private final INodeItem item;
1047     private volatile boolean allowOthers = true;
1048     @NonNull
1049     private volatile IAllowedValuesConstraint.Extensible extensible = IAllowedValuesConstraint.Extensible.EXTERNAL;
1050 
1051     public ValueStatus(@NonNull INodeItem item) {
1052       this.item = item;
1053       this.value = item.toAtomicItem().asString();
1054     }
1055 
1056     /**
1057      * Register allowed values constraint for this item.
1058      * <p>
1059      * This method is synchronized to ensure thread-safe updates to the
1060      * extensibility and allowOthers state.
1061      *
1062      * @param allowedValues
1063      *          the allowed values constraint
1064      * @param node
1065      *          the definition node
1066      * @throws ConstraintValidationException
1067      *           if constraint registration fails
1068      */
1069     public synchronized void registerAllowedValues(
1070         @NonNull IAllowedValuesConstraint allowedValues,
1071         @NonNull IDefinitionNodeItem<?, ?> node) throws ConstraintValidationException {
1072       IAllowedValuesConstraint.Extensible newExtensible = allowedValues.getExtensible();
1073       if (newExtensible.ordinal() > extensible.ordinal()) {
1074         // record the most restrictive value
1075         extensible = allowedValues.getExtensible();
1076       } else if (newExtensible == IAllowedValuesConstraint.Extensible.NONE
1077           && extensible == IAllowedValuesConstraint.Extensible.NONE) {
1078         // this is an error, where there are two none constraints that conflict
1079         // TODO: find a different exception type to use
1080         throw new ConstraintValidationException(
1081             ObjectUtils.notNull(String.format(
1082                 "Multiple constraints matching path '%s' have scope='none', which prevents extension. Involved" +
1083                     " constraints are: %s",
1084                 Stream.concat(
1085                     Stream.of(allowedValues),
1086                     constraints.stream()
1087                         .map(Pair::getLeft)
1088                         .filter(
1089                             constraint -> constraint.getExtensible() == IAllowedValuesConstraint.Extensible.NONE))
1090                     .map(IConstraint::getConstraintIdentity)
1091                     .collect(Collectors.joining(", ", "{", "}")),
1092                 item.getMetapath())));
1093       } else if (allowedValues.getExtensible().ordinal() < extensible.ordinal()) {
1094         String msg = ObjectUtils.notNull(String.format(
1095             "An allowed values constraint with an extensibility scope '%s'"
1096                 + " exceeds the allowed scope '%s' at path '%s'",
1097             allowedValues.getExtensible().name(), extensible.name(), item.getMetapath()));
1098         LOGGER.atError().log(msg);
1099         throw new ConstraintValidationException(msg);
1100       }
1101       this.constraints.add(Pair.of(allowedValues, node));
1102       if (!allowedValues.isAllowedOther()) {
1103         // record the most restrictive value
1104         allowOthers = false;
1105       }
1106     }
1107 
1108     public void validate(@NonNull DynamicContext dynamicContext) {
1109       // Take a snapshot of the state for thread-safe validation
1110       final boolean localAllowOthers;
1111       final List<Pair<IAllowedValuesConstraint, IDefinitionNodeItem<?, ?>>> localConstraints;
1112       synchronized (this) {
1113         localAllowOthers = this.allowOthers;
1114         localConstraints = new ArrayList<>(this.constraints);
1115       }
1116 
1117       if (!localConstraints.isEmpty()) {
1118         boolean match = false;
1119         List<IAllowedValuesConstraint> failedConstraints = new ArrayList<>();
1120         IConstraintValidationHandler handler = getConstraintValidationHandler();
1121         for (Pair<IAllowedValuesConstraint, IDefinitionNodeItem<?, ?>> pair : localConstraints) {
1122           IAllowedValuesConstraint allowedValues = pair.getLeft();
1123           IDefinitionNodeItem<?, ?> node = ObjectUtils.notNull(pair.getRight());
1124           IAllowedValue matchingValue = allowedValues.getAllowedValue(value);
1125           if (matchingValue != null) {
1126             match = true;
1127             handlePass(allowedValues, node, item, dynamicContext);
1128           } else if (allowedValues.getExtensible() == IAllowedValuesConstraint.Extensible.NONE) {
1129             // hard failure, since no other values can satisfy this constraint
1130             failedConstraints = CollectionUtil.singletonList(allowedValues);
1131             match = false;
1132             break;
1133           } else {
1134             failedConstraints.add(allowedValues);
1135           } // this constraint passes, but we need to make sure other constraints do as well
1136         }
1137 
1138         // it's not a failure if allow others is true
1139         if (!match && !localAllowOthers) {
1140           handler.handleAllowedValuesViolation(failedConstraints, item, dynamicContext);
1141         }
1142       }
1143     }
1144   }
1145 
1146   class Visitor
1147       extends AbstractNodeItemVisitor<DynamicContext, Void> {
1148 
1149     /**
1150      * Minimum number of model children required to enable parallel traversal.
1151      */
1152     private static final int PARALLEL_THRESHOLD = 4;
1153 
1154     @NonNull
1155     private DynamicContext handleLetStatements(
1156         @NonNull INodeItem focus,
1157         @NonNull Map<IEnhancedQName, ILet> letExpressions,
1158         @NonNull DynamicContext dynamicContext) {
1159 
1160       DynamicContext retval;
1161       Collection<ILet> lets = letExpressions.values();
1162       if (lets.isEmpty()) {
1163         retval = dynamicContext;
1164       } else {
1165         final DynamicContext subContext = dynamicContext.subContext();
1166 
1167         for (ILet let : lets) {
1168           IEnhancedQName name = let.getName();
1169           ISequence<?> result = let.getValueExpression().evaluate(focus, subContext);
1170           subContext.bindVariableValue(
1171               name,
1172               // ensure the sequence is list backed
1173               result.reusable());
1174         }
1175         retval = subContext;
1176       }
1177       return retval;
1178     }
1179 
1180     @Override
1181     public Void visitFlag(@NonNull IFlagNodeItem item, DynamicContext context) {
1182       assert context != null;
1183 
1184       IFlagDefinition definition = item.getDefinition();
1185       DynamicContext effectiveContext = handleLetStatements(item, definition.getLetExpressions(), context);
1186 
1187       validateFlag(item, effectiveContext);
1188       super.visitFlag(item, effectiveContext);
1189       handleAllowedValues(item, context);
1190       return null;
1191     }
1192 
1193     @Override
1194     public Void visitField(@NonNull IFieldNodeItem item, DynamicContext context) {
1195       assert context != null;
1196 
1197       IFieldDefinition definition = item.getDefinition();
1198       DynamicContext effectiveContext = handleLetStatements(item, definition.getLetExpressions(), context);
1199 
1200       validateField(item, effectiveContext);
1201       super.visitField(item, effectiveContext);
1202       handleAllowedValues(item, context);
1203       return null;
1204     }
1205 
1206     @Override
1207     public Void visitAssembly(@NonNull IAssemblyNodeItem item, DynamicContext context) {
1208       assert context != null;
1209 
1210       IAssemblyDefinition definition = item.getDefinition();
1211       DynamicContext effectiveContext = handleLetStatements(item, definition.getLetExpressions(), context);
1212 
1213       try {
1214         validateAssembly(item, effectiveContext);
1215       } catch (ConstraintValidationException ex) {
1216         throw ExceptionUtils.wrap(ex);
1217       }
1218 
1219       // Parallel or sequential child traversal
1220       if (parallelConfig.isParallel() && shouldParallelize(item)) {
1221         visitFlags(item, effectiveContext);
1222         visitChildrenParallel(item, effectiveContext);
1223       } else {
1224         super.visitAssembly(item, effectiveContext);
1225       }
1226 
1227       return null;
1228     }
1229 
1230     /**
1231      * Check if the item has enough children to benefit from parallel traversal.
1232      *
1233      * @param item
1234      *          the assembly item to check
1235      * @return true if the item has at least PARALLEL_THRESHOLD model children
1236      */
1237     private boolean shouldParallelize(@NonNull IAssemblyNodeItem item) {
1238       return item.modelItems().count() >= PARALLEL_THRESHOLD;
1239     }
1240 
1241     /**
1242      * Visit model children in parallel using the configured executor.
1243      *
1244      * @param item
1245      *          the parent assembly item
1246      * @param context
1247      *          the dynamic context
1248      */
1249     private void visitChildrenParallel(
1250         @NonNull IAssemblyNodeItem item,
1251         @NonNull DynamicContext context) {
1252 
1253       ExecutorService executor = parallelConfig.getExecutor();
1254       List<? extends IModelNodeItem<?, ?>> children = item.modelItems()
1255           .collect(Collectors.toList());
1256 
1257       List<Future<?>> futures = new ArrayList<>(children.size());
1258       for (IModelNodeItem<?, ?> child : children) {
1259         futures.add(executor.submit(() -> {
1260           // Each parallel task gets its own subContext for isolated execution stack
1261           DynamicContext childContext = context.subContext();
1262           child.accept(this, childContext);
1263           return null;
1264         }));
1265       }
1266 
1267       // Wait for all children and propagate exceptions
1268       try {
1269         for (Future<?> future : futures) {
1270           future.get();
1271         }
1272       } catch (ExecutionException e) {
1273         cancelRemainingFutures(futures);
1274         Throwable cause = e.getCause();
1275         if (cause instanceof RuntimeException) {
1276           throw (RuntimeException) cause;
1277         }
1278         throw ExceptionUtils.wrap(new ConstraintValidationException("Error during parallel validation", cause));
1279       } catch (InterruptedException e) {
1280         cancelRemainingFutures(futures);
1281         Thread.currentThread().interrupt();
1282         throw ExceptionUtils.wrap(new ConstraintValidationException("Validation interrupted", e));
1283       }
1284     }
1285 
1286     /**
1287      * Cancel any futures that are still running.
1288      *
1289      * @param futures
1290      *          the list of futures to cancel
1291      */
1292     private void cancelRemainingFutures(@NonNull List<Future<?>> futures) {
1293       for (Future<?> future : futures) {
1294         if (!future.isDone()) {
1295           future.cancel(true);
1296         }
1297       }
1298     }
1299 
1300     @Override
1301     public Void visitMetaschema(@NonNull IModuleNodeItem item, DynamicContext context) {
1302       throw new UnsupportedOperationException("Method not used.");
1303     }
1304 
1305     @Override
1306     protected Void defaultResult() {
1307       // no result value
1308       return null;
1309     }
1310   }
1311 
1312   private static class KeyRef {
1313     @NonNull
1314     private final IIndexHasKeyConstraint constraint;
1315     @NonNull
1316     private final INodeItem node;
1317     @NonNull
1318     private final List<INodeItem> targets;
1319 
1320     public KeyRef(
1321         @NonNull IIndexHasKeyConstraint constraint,
1322         @NonNull INodeItem node,
1323         @NonNull List<INodeItem> targets) {
1324       this.node = node;
1325       this.constraint = constraint;
1326       this.targets = targets;
1327     }
1328 
1329     @NonNull
1330     public IIndexHasKeyConstraint getConstraint() {
1331       return constraint;
1332     }
1333 
1334     @NonNull
1335     protected INodeItem getNode() {
1336       return node;
1337     }
1338 
1339     @NonNull
1340     public List<INodeItem> getTargets() {
1341       return targets;
1342     }
1343   }
1344 }