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