001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.model.constraint;
028
029import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
030import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
031import gov.nist.secauto.metaschema.core.metapath.ISequence;
032import gov.nist.secauto.metaschema.core.metapath.MetapathException;
033import gov.nist.secauto.metaschema.core.metapath.MetapathExpression;
034import gov.nist.secauto.metaschema.core.metapath.function.library.FnBoolean;
035import gov.nist.secauto.metaschema.core.metapath.function.library.FnData;
036import gov.nist.secauto.metaschema.core.metapath.item.node.AbstractNodeItemVisitor;
037import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
038import gov.nist.secauto.metaschema.core.metapath.item.node.IDefinitionNodeItem;
039import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
040import gov.nist.secauto.metaschema.core.metapath.item.node.IFieldNodeItem;
041import gov.nist.secauto.metaschema.core.metapath.item.node.IFlagNodeItem;
042import gov.nist.secauto.metaschema.core.metapath.item.node.IModuleNodeItem;
043import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
044import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
045import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
046import gov.nist.secauto.metaschema.core.model.IFlagDefinition;
047import gov.nist.secauto.metaschema.core.util.CollectionUtil;
048
049import org.apache.logging.log4j.LogManager;
050import org.apache.logging.log4j.Logger;
051
052import java.util.ArrayList;
053import java.util.LinkedHashMap;
054import java.util.LinkedList;
055import java.util.List;
056import java.util.Map;
057import java.util.concurrent.ConcurrentHashMap;
058import java.util.regex.Pattern;
059
060import edu.umd.cs.findbugs.annotations.NonNull;
061import edu.umd.cs.findbugs.annotations.Nullable;
062
063/**
064 * Used to perform constraint validation over one or more node items.
065 * <p>
066 * This class is not thread safe.
067 */
068public class DefaultConstraintValidator implements IConstraintValidator { // NOPMD - intentional
069  private static final Logger LOGGER = LogManager.getLogger(DefaultConstraintValidator.class);
070
071  @NonNull
072  private final Map<INodeItem, ValueStatus> valueMap = new LinkedHashMap<>(); // NOPMD - intentional
073  @NonNull
074  private final Map<String, IIndex> indexNameToIndexMap = new ConcurrentHashMap<>();
075  @NonNull
076  private final Map<String, List<KeyRef>> indexNameToKeyRefMap = new ConcurrentHashMap<>();
077  @NonNull
078  private final DynamicContext metapathContext;
079  @NonNull
080  private final IConstraintValidationHandler handler;
081
082  public DefaultConstraintValidator(
083      @NonNull DynamicContext metapathContext,
084      @NonNull IConstraintValidationHandler handler) {
085    this.metapathContext = metapathContext;
086    this.handler = handler;
087  }
088
089  @NonNull
090  public IConstraintValidationHandler getConstraintValidationHandler() {
091    return handler;
092  }
093
094  @NonNull
095  protected DynamicContext getMetapathContext() {
096    return metapathContext;
097  }
098
099  @Override
100  public void validate(@NonNull INodeItem item) {
101    item.accept(new Visitor(), null);
102  }
103
104  /**
105   * Validate the provided flag item against any associated constraints.
106   *
107   * @param item
108   *          the flag item to validate
109   * @throws MetapathException
110   *           if an error occurred while evaluating a Metapath used in a
111   *           constraint
112   */
113  protected void validateFlag(@NonNull IFlagNodeItem item) {
114    IFlagDefinition definition = item.getDefinition();
115
116    validateExpect(definition.getExpectConstraints(), item);
117    validateAllowedValues(definition.getAllowedValuesConstraints(), item);
118    validateIndexHasKey(definition.getIndexHasKeyConstraints(), item);
119    validateMatches(definition.getMatchesConstraints(), item);
120  }
121
122  /**
123   * Validate the provided field item against any associated constraints.
124   *
125   * @param item
126   *          the field item to validate
127   * @throws MetapathException
128   *           if an error occurred while evaluating a Metapath used in a
129   *           constraint
130   */
131  protected void validateField(@NonNull IFieldNodeItem item) {
132    IFieldDefinition definition = item.getDefinition();
133
134    validateExpect(definition.getExpectConstraints(), item);
135    validateAllowedValues(definition.getAllowedValuesConstraints(), item);
136    validateIndexHasKey(definition.getIndexHasKeyConstraints(), item);
137    validateMatches(definition.getMatchesConstraints(), item);
138  }
139
140  /**
141   * Validate the provided assembly item against any associated constraints.
142   *
143   * @param item
144   *          the assembly item to validate
145   * @throws MetapathException
146   *           if an error occurred while evaluating a Metapath used in a
147   *           constraint
148   */
149  protected void validateAssembly(@NonNull IAssemblyNodeItem item) {
150    IAssemblyDefinition definition = item.getDefinition();
151
152    validateExpect(definition.getExpectConstraints(), item);
153    validateAllowedValues(definition.getAllowedValuesConstraints(), item);
154    validateIndexHasKey(definition.getIndexHasKeyConstraints(), item);
155    validateMatches(definition.getMatchesConstraints(), item);
156    validateHasCardinality(definition.getHasCardinalityConstraints(), item);
157    validateIndex(definition.getIndexConstraints(), item);
158    validateUnique(definition.getUniqueConstraints(), item);
159  }
160
161  protected void validateHasCardinality(@NonNull List<? extends ICardinalityConstraint> constraints,
162      @NonNull List<? extends IAssemblyNodeItem> items) {
163
164    items.stream().forEachOrdered(item -> {
165      assert item != null;
166      validateHasCardinality(constraints, item);
167    });
168  }
169
170  protected void validateHasCardinality(@NonNull List<? extends ICardinalityConstraint> constraints,
171      @NonNull IAssemblyNodeItem item) {
172    for (ICardinalityConstraint constraint : constraints) {
173      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
174      try {
175        validateHasCardinality(constraint, item, targets);
176      } catch (MetapathException ex) {
177        rethrowConstraintError(constraint, item, ex);
178      }
179    }
180  }
181
182  protected void validateHasCardinality(@NonNull ICardinalityConstraint constraint, @NonNull IAssemblyNodeItem node,
183      ISequence<? extends INodeItem> targets) {
184    int itemCount = targets.size();
185
186    Integer minOccurs = constraint.getMinOccurs();
187    if (minOccurs != null && itemCount < minOccurs) {
188      getConstraintValidationHandler().handleCardinalityMinimumViolation(constraint, node, targets);
189    }
190
191    Integer maxOccurs = constraint.getMaxOccurs();
192    if (maxOccurs != null && itemCount > maxOccurs) {
193      getConstraintValidationHandler().handleCardinalityMaximumViolation(constraint, node, targets);
194    }
195  }
196
197  protected void validateIndex(@NonNull List<? extends IIndexConstraint> constraints,
198      @NonNull List<? extends IAssemblyNodeItem> items) {
199    items.stream().forEachOrdered(item -> {
200      assert item != null;
201      validateIndex(constraints, item);
202    });
203  }
204
205  protected void validateIndex(@NonNull List<? extends IIndexConstraint> constraints,
206      @NonNull IAssemblyNodeItem item) {
207    for (IIndexConstraint constraint : constraints) {
208      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
209      try {
210        validateIndex(constraint, item, targets);
211      } catch (MetapathException ex) {
212        rethrowConstraintError(constraint, item, ex);
213      }
214    }
215  }
216
217  protected void validateIndex(@NonNull IIndexConstraint constraint, @NonNull IAssemblyNodeItem node,
218      @NonNull ISequence<? extends INodeItem> targets) {
219    String indexName = constraint.getName();
220    if (indexNameToIndexMap.containsKey(indexName)) {
221      getConstraintValidationHandler().handleIndexDuplicateViolation(constraint, node);
222      return; // NOPMD - readability
223    }
224
225    IIndex index = IIndex.newInstance(constraint.getKeyFields());
226    targets.asStream()
227        .forEachOrdered(item -> {
228          assert item != null;
229          if (item.hasValue()) {
230            try {
231              INodeItem oldItem = index.put(item, metapathContext);
232              if (oldItem != null) {
233                getConstraintValidationHandler().handleIndexDuplicateKeyViolation(constraint, node, oldItem, item);
234              }
235            } catch (MetapathException ex) {
236              getConstraintValidationHandler().handleKeyMatchError(constraint, node, item, ex);
237            }
238          }
239        });
240    indexNameToIndexMap.put(indexName, index);
241  }
242
243  protected void validateUnique(@NonNull List<? extends IUniqueConstraint> constraints,
244      @NonNull List<? extends IAssemblyNodeItem> items) {
245
246    items.stream().forEachOrdered(item -> {
247      assert item != null;
248      validateUnique(constraints, item);
249    });
250  }
251
252  protected void validateUnique(@NonNull List<? extends IUniqueConstraint> constraints,
253      @NonNull IAssemblyNodeItem item) {
254    for (IUniqueConstraint constraint : constraints) {
255      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
256      try {
257        validateUnique(constraint, item, targets);
258      } catch (MetapathException ex) {
259        rethrowConstraintError(constraint, item, ex);
260      }
261    }
262  }
263
264  protected void validateUnique(@NonNull IUniqueConstraint constraint,
265      @NonNull IAssemblyNodeItem node, @NonNull ISequence<? extends INodeItem> targets) {
266    IIndex index = IIndex.newInstance(constraint.getKeyFields());
267    targets.asStream()
268        .forEachOrdered(item -> {
269          assert item != null;
270          if (item.hasValue()) {
271            try {
272              INodeItem oldItem = index.put(item, metapathContext);
273              if (oldItem != null) {
274                getConstraintValidationHandler().handleUniqueKeyViolation(constraint, node, oldItem, item);
275              }
276            } catch (MetapathException ex) {
277              getConstraintValidationHandler().handleKeyMatchError(constraint, node, item, ex);
278              throw ex;
279            }
280          }
281        });
282  }
283
284  protected void validateMatches(@NonNull List<? extends IMatchesConstraint> constraints,
285      @NonNull IDefinitionNodeItem<?, ?> item) {
286
287    for (IMatchesConstraint constraint : constraints) {
288      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
289      try {
290        validateMatches(constraint, item, targets);
291      } catch (MetapathException ex) {
292        rethrowConstraintError(constraint, item, ex);
293      }
294    }
295  }
296
297  protected void validateMatches(@NonNull IMatchesConstraint constraint, @NonNull INodeItem node,
298      ISequence<? extends INodeItem> targets) {
299    targets.asStream()
300        .forEachOrdered(item -> {
301          assert item != null;
302          if (item.hasValue()) {
303            String value = FnData.fnDataItem(item).asString();
304
305            Pattern pattern = constraint.getPattern();
306            if (pattern != null && !pattern.asMatchPredicate().test(value)) {
307              // failed pattern match
308              getConstraintValidationHandler().handleMatchPatternViolation(constraint, node, item, value);
309            }
310
311            IDataTypeAdapter<?> adapter = constraint.getDataType();
312            if (adapter != null) {
313              try {
314                adapter.parse(value);
315              } catch (IllegalArgumentException ex) {
316                getConstraintValidationHandler().handleMatchDatatypeViolation(constraint, node, item, value, ex);
317              }
318            }
319          }
320        });
321  }
322
323  protected void validateIndexHasKey(
324      @NonNull List<? extends IIndexHasKeyConstraint> constraints,
325      @NonNull IDefinitionNodeItem<?, ?> node) {
326
327    for (IIndexHasKeyConstraint constraint : constraints) {
328      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(node, getMetapathContext());
329      validateIndexHasKey(constraint, node, targets);
330    }
331  }
332
333  protected void validateIndexHasKey(
334      @NonNull IIndexHasKeyConstraint constraint,
335      @NonNull IDefinitionNodeItem<?, ?> node,
336      @NonNull ISequence<? extends INodeItem> targets) {
337    String indexName = constraint.getIndexName();
338
339    List<KeyRef> keyRefItems = indexNameToKeyRefMap.get(indexName);
340    if (keyRefItems == null) {
341      keyRefItems = new LinkedList<>();
342      indexNameToKeyRefMap.put(indexName, keyRefItems);
343    }
344
345    KeyRef keyRef = new KeyRef(constraint, node, new ArrayList<>(targets.asList()));
346    keyRefItems.add(keyRef);
347  }
348
349  protected void validateExpect(@NonNull List<? extends IExpectConstraint> constraints,
350      @NonNull IDefinitionNodeItem<?, ?> item) {
351    for (IExpectConstraint constraint : constraints) {
352      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
353      validateExpect(constraint, item, targets);
354    }
355  }
356
357  protected void validateExpect(@NonNull IExpectConstraint constraint, @NonNull INodeItem node,
358      @NonNull ISequence<? extends INodeItem> targets) {
359    targets.asStream()
360        .map(item -> (INodeItem) item)
361        .forEachOrdered(item -> {
362          assert item != null;
363          if (item.hasValue()) {
364            MetapathExpression metapath = constraint.getTest();
365            try {
366              ISequence<?> result = metapath.evaluate(item, getMetapathContext());
367              if (!FnBoolean.fnBoolean(result).toBoolean()) {
368                getConstraintValidationHandler().handleExpectViolation(constraint, node, item, getMetapathContext());
369              }
370            } catch (MetapathException ex) {
371              rethrowConstraintError(constraint, item, ex);
372            }
373          }
374        });
375  }
376
377  protected void validateAllowedValues(@NonNull List<? extends IAllowedValuesConstraint> constraints,
378      @NonNull IDefinitionNodeItem<?, ?> item) {
379    for (IAllowedValuesConstraint constraint : constraints) {
380      ISequence<? extends IDefinitionNodeItem<?, ?>> targets = constraint.matchTargets(item, getMetapathContext());
381      validateAllowedValues(constraint, targets);
382    }
383  }
384
385  protected void validateAllowedValues(@NonNull IAllowedValuesConstraint constraint,
386      ISequence<? extends IDefinitionNodeItem<?, ?>> targets) {
387    targets.asStream().forEachOrdered(item -> {
388      assert item != null;
389      if (item.hasValue()) {
390        try {
391          updateValueStatus(item, constraint);
392        } catch (MetapathException ex) {
393          rethrowConstraintError(constraint, item, ex);
394        }
395      }
396    });
397  }
398
399  private static void rethrowConstraintError(@NonNull IConstraint constraint, INodeItem item,
400      MetapathException ex) {
401    StringBuilder builder = new StringBuilder(128);
402    builder.append("A ")
403        .append(constraint.getClass().getName())
404        .append(" constraint");
405
406    String id = constraint.getId();
407    if (id == null) {
408      builder.append(" targeting the metapath '")
409          .append(constraint.getTarget().getPath())
410          .append('\'');
411    } else {
412      builder.append(" with id '")
413          .append(id)
414          .append('\'');
415    }
416
417    builder.append(", matching the item at path '")
418        .append(item.getMetapath())
419        .append("', resulted in an unexpected error. The error was: ")
420        .append(ex.getLocalizedMessage());
421
422    throw new MetapathException(builder.toString(), ex);
423  }
424
425  /**
426   * Add a new allowed value to the value status tracker.
427   *
428   * @param targetItem
429   *          the item whose value is targeted by the constraint
430   * @param allowedValues
431   *          the set of allowed values
432   */
433  protected void updateValueStatus(@NonNull INodeItem targetItem, @NonNull IAllowedValuesConstraint allowedValues) {
434    // constraint.getAllowedValues().containsKey(value)
435
436    @Nullable ValueStatus valueStatus = valueMap.get(targetItem);
437    if (valueStatus == null) {
438      valueStatus = new ValueStatus(targetItem);
439      valueMap.put(targetItem, valueStatus);
440    }
441
442    valueStatus.registerAllowedValue(allowedValues);
443  }
444
445  protected void handleAllowedValues(@NonNull INodeItem targetItem) {
446    ValueStatus valueStatus = valueMap.remove(targetItem);
447    if (valueStatus != null) {
448      valueStatus.validate();
449    }
450  }
451
452  @Override
453  public void finalizeValidation() {
454    // key references
455    for (Map.Entry<String, List<KeyRef>> entry : indexNameToKeyRefMap.entrySet()) {
456      String indexName = entry.getKey();
457      IIndex index = indexNameToIndexMap.get(indexName);
458
459      List<KeyRef> keyRefs = entry.getValue();
460
461      for (KeyRef keyRef : keyRefs) {
462        IIndexHasKeyConstraint constraint = keyRef.getConstraint();
463        for (INodeItem item : keyRef.getTargets()) {
464          assert item != null;
465
466          try {
467            List<String> key = IIndex.toKey(item, constraint.getKeyFields(), getMetapathContext());
468
469            INodeItem referencedItem = index.get(key);
470
471            if (referencedItem == null) {
472              getConstraintValidationHandler().handleIndexMiss(constraint, keyRef.getNode(), item, key);
473            }
474          } catch (MetapathException ex) {
475            getConstraintValidationHandler().handleKeyMatchError(constraint, keyRef.getNode(), item, ex);
476          }
477        }
478      }
479    }
480  }
481
482  private class ValueStatus {
483    @NonNull
484    private final List<IAllowedValuesConstraint> constraints = new LinkedList<>();
485    @NonNull
486    private final String value;
487    @NonNull
488    private final INodeItem item;
489    private boolean allowOthers = true;
490    @NonNull
491    private IAllowedValuesConstraint.Extensible extensible = IAllowedValuesConstraint.Extensible.EXTERNAL;
492
493    public ValueStatus(@NonNull INodeItem item) {
494      this.item = item;
495      this.value = FnData.fnDataItem(item).asString();
496    }
497
498    public void registerAllowedValue(@NonNull IAllowedValuesConstraint allowedValues) {
499      this.constraints.add(allowedValues);
500      if (!allowedValues.isAllowedOther()) {
501        // record the most restrictive value
502        allowOthers = false;
503      }
504
505      IAllowedValuesConstraint.Extensible newExtensible = allowedValues.getExtensible();
506      if (newExtensible.ordinal() > extensible.ordinal()) {
507        // record the most restrictive value
508        extensible = allowedValues.getExtensible();
509      } else if (IAllowedValuesConstraint.Extensible.NONE.equals(newExtensible)
510          && IAllowedValuesConstraint.Extensible.NONE.equals(extensible)) {
511        // this is an error, where there are two none constraints that conflict
512        throw new MetapathException(
513            String.format("Multiple constraints have extensibility scope=none at path '%s'", item.getMetapath()));
514      } else if (allowedValues.getExtensible().ordinal() < extensible.ordinal()) {
515        String msg = String.format(
516            "An allowed values constraint with an extensibility scope '%s'"
517                + " exceeds the allowed scope '%s' at path '%s'",
518            allowedValues.getExtensible().name(), extensible.name(), item.getMetapath());
519        LOGGER.atError().log(msg);
520        throw new MetapathException(msg);
521      }
522    }
523
524    public void validate() {
525      if (!constraints.isEmpty()) {
526        boolean match = false;
527        List<IAllowedValuesConstraint> failedConstraints = new LinkedList<>();
528        for (IAllowedValuesConstraint allowedValues : constraints) {
529          IAllowedValue matchingValue = allowedValues.getAllowedValue(value);
530          if (matchingValue != null) {
531            match = true;
532          } else if (IAllowedValuesConstraint.Extensible.NONE.equals(allowedValues.getExtensible())) {
533            // hard failure, since no other values can satisfy this constraint
534            failedConstraints = CollectionUtil.singletonList(allowedValues);
535            match = false;
536            break;
537          } else {
538            failedConstraints.add(allowedValues);
539          } // this constraint passes, but we need to make sure other constraints do as well
540        }
541
542        // it's not a failure if allow others is true
543        if (!match && !allowOthers) {
544          getConstraintValidationHandler().handleAllowedValuesViolation(failedConstraints, item);
545        }
546      }
547    }
548  }
549
550  class Visitor
551      extends AbstractNodeItemVisitor<Void, Void> {
552    @Override
553    public Void visitDocument(@NonNull IDocumentNodeItem item, Void context) {
554      return super.visitDocument(item, context);
555    }
556
557    @Override
558    public Void visitFlag(@NonNull IFlagNodeItem item, Void context) {
559      validateFlag(item);
560      super.visitFlag(item, context);
561      handleAllowedValues(item);
562      return null;
563    }
564
565    @Override
566    public Void visitField(@NonNull IFieldNodeItem item, Void context) {
567      validateField(item);
568      super.visitField(item, context);
569      handleAllowedValues(item);
570      return null;
571    }
572
573    @Override
574    public Void visitAssembly(@NonNull IAssemblyNodeItem item, Void context) {
575      validateAssembly(item);
576      super.visitAssembly(item, context);
577      return null;
578    }
579
580    @Override
581    public Void visitMetaschema(@NonNull IModuleNodeItem item, Void context) {
582      throw new UnsupportedOperationException("not needed");
583    }
584
585    @Override
586    protected Void defaultResult() {
587      // no result value
588      return null;
589    }
590  }
591
592  private static class KeyRef {
593    @NonNull
594    private final IIndexHasKeyConstraint constraint;
595    @NonNull
596    private final INodeItem node;
597    @NonNull
598    private final List<INodeItem> targets;
599
600    public KeyRef(
601        @NonNull IIndexHasKeyConstraint constraint,
602        @NonNull INodeItem node,
603        @NonNull List<INodeItem> targets) {
604      this.node = node;
605      this.constraint = constraint;
606      this.targets = targets;
607    }
608
609    @NonNull
610    public IIndexHasKeyConstraint getConstraint() {
611      return constraint;
612    }
613
614    @NonNull
615    protected INodeItem getNode() {
616      return node;
617    }
618
619    @NonNull
620    public List<INodeItem> getTargets() {
621      return targets;
622    }
623  }
624}