Invert SchemaInferenceStack.deque
[yangtools.git] / model / yang-model-util / src / main / java / org / opendaylight / yangtools / yang / model / util / SchemaInferenceStack.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.model.util;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import static com.google.common.base.Verify.verify;
13 import static com.google.common.base.Verify.verifyNotNull;
14 import static java.util.Objects.requireNonNull;
15
16 import com.google.common.annotations.Beta;
17 import com.google.common.annotations.VisibleForTesting;
18 import com.google.common.base.MoreObjects;
19 import com.google.common.base.VerifyException;
20 import com.google.common.collect.Collections2;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.Iterables;
23 import com.google.common.collect.Iterators;
24 import java.util.ArrayDeque;
25 import java.util.Collection;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.NoSuchElementException;
29 import java.util.Optional;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.opendaylight.yangtools.concepts.Mutable;
33 import org.opendaylight.yangtools.rfc8040.model.api.YangDataEffectiveStatement;
34 import org.opendaylight.yangtools.yang.common.AbstractQName;
35 import org.opendaylight.yangtools.yang.common.QName;
36 import org.opendaylight.yangtools.yang.common.QNameModule;
37 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
38 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
39 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextProvider;
40 import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
41 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.PathExpression;
43 import org.opendaylight.yangtools.yang.model.api.PathExpression.DerefSteps;
44 import org.opendaylight.yangtools.yang.model.api.PathExpression.LocationPathSteps;
45 import org.opendaylight.yangtools.yang.model.api.PathExpression.Steps;
46 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
47 import org.opendaylight.yangtools.yang.model.api.SchemaTreeInference;
48 import org.opendaylight.yangtools.yang.model.api.TypeAware;
49 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
52 import org.opendaylight.yangtools.yang.model.api.stmt.CaseEffectiveStatement;
53 import org.opendaylight.yangtools.yang.model.api.stmt.ChoiceEffectiveStatement;
54 import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeAwareEffectiveStatement;
55 import org.opendaylight.yangtools.yang.model.api.stmt.DataTreeEffectiveStatement;
56 import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement;
57 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
58 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
59 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
60 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeAwareEffectiveStatement;
61 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
62 import org.opendaylight.yangtools.yang.model.api.stmt.TypedefEffectiveStatement;
63 import org.opendaylight.yangtools.yang.model.api.stmt.TypedefNamespace;
64 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.spi.AbstractEffectiveStatementInference;
67 import org.opendaylight.yangtools.yang.model.spi.DefaultSchemaTreeInference;
68 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath;
69 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.AxisStep;
70 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.QNameStep;
71 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.Step;
72 import org.opendaylight.yangtools.yang.xpath.api.YangXPathAxis;
73 import org.slf4j.LoggerFactory;
74
75 /**
76  * A state tracking utility for walking {@link EffectiveModelContext}'s contents along schema/grouping namespaces. This
77  * is conceptually a stack, tracking {@link EffectiveStatement}s encountered along traversal.
78  *
79  * <p>
80  * This is meant to be a replacement concept for the use of {@link SchemaPath} in various places, notably
81  * in {@link SchemaContextUtil} methods.
82  *
83  * <p>
84  * This class is designed for single-threaded uses and does not make any guarantees around concurrent access.
85  */
86 @Beta
87 public final class SchemaInferenceStack implements Mutable, EffectiveModelContextProvider, LeafrefResolver {
88     /**
89      * Semantic binding of {@link EffectiveStatementInference} produced by {@link SchemaInferenceStack}. Sequence of
90      * {@link #statementPath()} is implementation-specific.
91      */
92     @Beta
93     public static final class Inference extends AbstractEffectiveStatementInference<EffectiveStatement<?, ?>> {
94         private final ArrayDeque<EffectiveStatement<?, ?>> deque;
95         private final ModuleEffectiveStatement currentModule;
96         private final int groupingDepth;
97         private final boolean clean;
98
99         Inference(final @NonNull EffectiveModelContext modelContext, final ArrayDeque<EffectiveStatement<?, ?>> deque,
100                 final ModuleEffectiveStatement currentModule, final int groupingDepth, final boolean clean) {
101             super(modelContext);
102             this.deque = requireNonNull(deque);
103             this.currentModule = currentModule;
104             this.groupingDepth = groupingDepth;
105             this.clean = clean;
106         }
107
108         /**
109          * Create a new stack backed by an effective model and set up to point and specified data tree node.
110          *
111          * @param effectiveModel EffectiveModelContext to which this stack is attached
112          * @param qnames Data tree path qnames
113          * @return A new stack
114          * @throws NullPointerException if any argument is null or path contains a null element
115          * @throws IllegalArgumentException if a path element cannot be found
116          */
117         public static @NonNull Inference ofDataTreePath(final EffectiveModelContext effectiveModel,
118                 final QName... qnames) {
119             return SchemaInferenceStack.ofDataTreePath(effectiveModel, qnames).toInference();
120         }
121
122         @Override
123         public List<EffectiveStatement<?, ?>> statementPath() {
124             return ImmutableList.copyOf(deque);
125         }
126
127         /**
128          * Convert this inference into a {@link SchemaInferenceStack}.
129          *
130          * @return A new stack
131          */
132         public @NonNull SchemaInferenceStack toSchemaInferenceStack() {
133             return new SchemaInferenceStack(getEffectiveModelContext(), deque, currentModule, groupingDepth, clean);
134         }
135     }
136
137     private static final String VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP =
138         "org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.verifyDefaultSchemaTreeInference";
139     private static final boolean VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE =
140         Boolean.getBoolean(VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP);
141
142     static {
143         if (VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE) {
144             LoggerFactory.getLogger(SchemaInferenceStack.class)
145                 .info("SchemaTreeStack.ofInference(DefaultSchemaTreeInference) argument is being verified");
146         }
147     }
148
149     private final @NonNull EffectiveModelContext effectiveModel;
150     private final ArrayDeque<EffectiveStatement<?, ?>> deque;
151
152     private @Nullable ModuleEffectiveStatement currentModule;
153     private int groupingDepth;
154
155     // True if there were only steps along grouping and schema tree, hence it is consistent with SchemaNodeIdentifier
156     // False if we have evidence of a data tree lookup succeeding
157     private boolean clean;
158
159     private SchemaInferenceStack(final EffectiveModelContext effectiveModel, final int expectedSize) {
160         deque = new ArrayDeque<>(expectedSize);
161         this.effectiveModel = requireNonNull(effectiveModel);
162         clean = true;
163     }
164
165     private SchemaInferenceStack(final SchemaInferenceStack source) {
166         deque = source.deque.clone();
167         effectiveModel = source.effectiveModel;
168         currentModule = source.currentModule;
169         groupingDepth = source.groupingDepth;
170         clean = source.clean;
171     }
172
173     private SchemaInferenceStack(final EffectiveModelContext effectiveModel,
174             final ArrayDeque<EffectiveStatement<?, ?>> deque, final ModuleEffectiveStatement currentModule,
175             final int groupingDepth, final boolean clean) {
176         this.effectiveModel = requireNonNull(effectiveModel);
177         this.deque = deque.clone();
178         this.currentModule = currentModule;
179         this.groupingDepth = groupingDepth;
180         this.clean = clean;
181     }
182
183     private SchemaInferenceStack(final EffectiveModelContext effectiveModel) {
184         this.effectiveModel = requireNonNull(effectiveModel);
185         deque = new ArrayDeque<>();
186         clean = true;
187     }
188
189     /**
190      * Create a new empty stack backed by an effective model.
191      *
192      * @param effectiveModel EffectiveModelContext to which this stack is attached
193      * @return A new stack
194      * @throws NullPointerException if {@code effectiveModel} is null
195      */
196     public static @NonNull SchemaInferenceStack of(final EffectiveModelContext effectiveModel) {
197         return new SchemaInferenceStack(effectiveModel);
198     }
199
200     /**
201      * Create a new stack backed by an effective model, pointing to specified schema node identified by
202      * {@link Absolute}.
203      *
204      * @param effectiveModel EffectiveModelContext to which this stack is attached
205      * @return A new stack
206      * @throws NullPointerException if {@code effectiveModel} is null
207      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model
208      */
209     public static @NonNull SchemaInferenceStack of(final EffectiveModelContext effectiveModel, final Absolute path) {
210         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
211         path.getNodeIdentifiers().forEach(ret::enterSchemaTree);
212         return ret;
213     }
214
215     /**
216      * Create a new stack from an {@link EffectiveStatementInference}.
217      *
218      * @param inference Inference to use for initialization
219      * @return A new stack
220      * @throws NullPointerException if {@code inference} is null
221      * @throws IllegalArgumentException if {@code inference} implementation is not supported
222      */
223     public static @NonNull SchemaInferenceStack ofInference(final EffectiveStatementInference inference) {
224         if (inference.statementPath().isEmpty()) {
225             return new SchemaInferenceStack(inference.getEffectiveModelContext());
226         } else if (inference instanceof SchemaTreeInference) {
227             return ofInference((SchemaTreeInference) inference);
228         } else if (inference instanceof Inference) {
229             return ((Inference) inference).toSchemaInferenceStack();
230         } else {
231             throw new IllegalArgumentException("Unsupported Inference " + inference);
232         }
233     }
234
235     /**
236      * Create a new stack from an {@link SchemaTreeInference}.
237      *
238      * @param inference SchemaTreeInference to use for initialization
239      * @return A new stack
240      * @throws NullPointerException if {@code inference} is null
241      * @throws IllegalArgumentException if {@code inference} cannot be resolved to a valid stack
242      */
243     public static @NonNull SchemaInferenceStack ofInference(final SchemaTreeInference inference) {
244         return inference instanceof DefaultSchemaTreeInference ? ofInference((DefaultSchemaTreeInference) inference)
245             : of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier());
246     }
247
248     /**
249      * Create a new stack from an {@link DefaultSchemaTreeInference}. The argument is nominally trusted to be an
250      * accurate representation of the schema tree.
251      *
252      * <p>
253      * Run-time verification of {@code inference} can be enabled by setting the
254      * {@value #VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP} system property to {@code true}.
255      *
256      * @param inference DefaultSchemaTreeInference to use for initialization
257      * @return A new stack
258      * @throws NullPointerException if {@code inference} is null
259      * @throws IllegalArgumentException if {@code inference} refers to a missing module or when verification is enabled
260      *                                  and it does not match its context's scheam tree
261      */
262     public static @NonNull SchemaInferenceStack ofInference(final DefaultSchemaTreeInference inference) {
263         return VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE ? ofUntrusted(inference) : ofTrusted(inference);
264     }
265
266     private static @NonNull SchemaInferenceStack ofTrusted(final DefaultSchemaTreeInference inference) {
267         final var path = inference.statementPath();
268         final var ret = new SchemaInferenceStack(inference.getEffectiveModelContext(), path.size());
269         ret.currentModule = ret.getModule(path.get(0).argument());
270         ret.deque.addAll(path);
271         return ret;
272     }
273
274     @VisibleForTesting
275     static @NonNull SchemaInferenceStack ofUntrusted(final DefaultSchemaTreeInference inference) {
276         final var ret = of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier());
277         if (!Iterables.elementsEqual(ret.deque, inference.statementPath())) {
278             throw new IllegalArgumentException("Provided " + inference + " is not consistent with resolved path "
279                 + ret.toSchemaTreeInference());
280         }
281         return ret;
282     }
283
284     /**
285      * Create a new stack backed by an effective model and set up to point and specified data tree node.
286      *
287      * @param effectiveModel EffectiveModelContext to which this stack is attached
288      * @return A new stack
289      * @throws NullPointerException if any argument is null or path contains a null element
290      * @throws IllegalArgumentException if a path element cannot be found
291      */
292     public static @NonNull SchemaInferenceStack ofDataTreePath(final EffectiveModelContext effectiveModel,
293             final QName... path) {
294         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
295         for (QName qname : path) {
296             ret.enterDataTree(qname);
297         }
298         return ret;
299     }
300
301     /**
302      * Create a new stack backed by an effective model, pointing to specified schema node identified by an absolute
303      * {@link SchemaPath} and its {@link SchemaPath#getPathFromRoot()} interpreted as a schema node identifier.
304      *
305      * @param effectiveModel EffectiveModelContext to which this stack is attached
306      * @return A new stack
307      * @throws NullPointerException {@code effectiveModel} is null
308      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model or if it is not an
309      *                                  absolute path.
310      */
311     @Deprecated
312     public static @NonNull SchemaInferenceStack ofInstantiatedPath(final EffectiveModelContext effectiveModel,
313             final SchemaPath path) {
314         checkArgument(path.isAbsolute(), "Cannot operate on relative path %s", path);
315         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
316         path.getPathFromRoot().forEach(ret::enterSchemaTree);
317         return ret;
318     }
319
320     /**
321      * Create a new stack backed by an effective model, pointing to specified schema node identified by an absolute
322      * {@link SchemaPath} and its {@link SchemaPath#getPathFromRoot()}, interpreted as a series of steps along primarily
323      * schema tree, with grouping namespace being the alternative lookup.
324      *
325      * @param effectiveModel EffectiveModelContext to which this stack is attached
326      * @return A new stack
327      * @throws NullPointerException {@code effectiveModel} is null
328      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model or if it is not an
329      *                                  absolute path.
330      */
331     @Deprecated
332     public static @NonNull SchemaInferenceStack ofSchemaPath(final EffectiveModelContext effectiveModel,
333             final SchemaPath path) {
334         checkArgument(path.isAbsolute(), "Cannot operate on relative path %s", path);
335         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
336
337         for (QName step : path.getPathFromRoot()) {
338             try {
339                 ret.enterSchemaTree(step);
340             } catch (IllegalArgumentException schemaEx) {
341                 try {
342                     ret.enterGrouping(step);
343                 } catch (IllegalArgumentException ex) {
344                     ex.addSuppressed(schemaEx);
345                     throw ex;
346                 }
347             }
348         }
349
350         return ret;
351     }
352
353     @Override
354     public EffectiveModelContext getEffectiveModelContext() {
355         return effectiveModel;
356     }
357
358     /**
359      * Create a deep copy of this object.
360      *
361      * @return An isolated copy of this object
362      */
363     public @NonNull SchemaInferenceStack copy() {
364         return new SchemaInferenceStack(this);
365     }
366
367     /**
368      * Check if this stack is empty.
369      *
370      * @return True if this stack has not entered any node.
371      */
372     public boolean isEmpty() {
373         return deque.isEmpty();
374     }
375
376     /**
377      * Return the statement at the top of the stack.
378      *
379      * @return Top statement
380      * @throws IllegalStateException if the stack is empty
381      */
382     public @NonNull EffectiveStatement<?, ?> currentStatement() {
383         return checkNonNullState(deque.peekLast());
384     }
385
386     /**
387      * Return current module the stack has entered.
388      *
389      * @return Current module
390      * @throws IllegalStateException if the stack is empty
391      */
392     public @NonNull ModuleEffectiveStatement currentModule() {
393         return checkNonNullState(currentModule);
394     }
395
396     /**
397      * Check if the stack is in instantiated context. This indicates the stack is non-empty and there are only schema
398      * tree statements in the stack.
399      *
400      * @return False if the stack is empty or contains a statement which is not a {@link SchemaTreeEffectiveStatement},
401      *         true otherwise.
402      */
403     public boolean inInstantiatedContext() {
404         return groupingDepth == 0 && !deque.isEmpty()
405             && deque.stream().allMatch(SchemaTreeEffectiveStatement.class::isInstance);
406     }
407
408     /**
409      * Check if the stack is in a {@code grouping} context.
410      *
411      * @return False if the stack contains a grouping.
412      */
413     public boolean inGrouping() {
414         return groupingDepth != 0;
415     }
416
417     /**
418      * Reset this stack to empty state.
419      */
420     public void clear() {
421         deque.clear();
422         currentModule = null;
423         groupingDepth = 0;
424         clean = true;
425     }
426
427     /**
428      * Lookup a {@code choice} by its node identifier and push it to the stack. This step is very similar to
429      * {@link #enterSchemaTree(QName)}, except it handles the use case where traversal ignores actual {@code case}
430      * intermediate schema tree children.
431      *
432      * @param nodeIdentifier Node identifier of the choice to enter
433      * @return Resolved choice
434      * @throws NullPointerException if {@code nodeIdentifier} is null
435      * @throws IllegalArgumentException if the corresponding choice cannot be found
436      */
437     public @NonNull ChoiceEffectiveStatement enterChoice(final QName nodeIdentifier) {
438         final QName nodeId = requireNonNull(nodeIdentifier);
439         final EffectiveStatement<?, ?> parent = deque.peekLast();
440         if (parent instanceof ChoiceEffectiveStatement) {
441             return enterChoice((ChoiceEffectiveStatement) parent, nodeId);
442         }
443
444         // Fall back to schema tree lookup. Note if it results in non-choice, we rewind before reporting an error
445         final SchemaTreeEffectiveStatement<?> result = enterSchemaTree(nodeId);
446         if (result instanceof ChoiceEffectiveStatement) {
447             return (ChoiceEffectiveStatement) result;
448         }
449         exit();
450
451         if (parent != null) {
452             throw notPresent(parent, "Choice", nodeId);
453         }
454         throw new IllegalArgumentException("Choice " + nodeId + " not present");
455     }
456
457     // choice -> choice transition, we have to deal with intermediate case nodes
458     private @NonNull ChoiceEffectiveStatement enterChoice(final @NonNull ChoiceEffectiveStatement parent,
459             final QName nodeIdentifier) {
460         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
461             if (stmt instanceof CaseEffectiveStatement) {
462                 final Optional<ChoiceEffectiveStatement> optMatch = ((CaseEffectiveStatement) stmt)
463                     .findSchemaTreeNode(nodeIdentifier)
464                     .filter(ChoiceEffectiveStatement.class::isInstance)
465                     .map(ChoiceEffectiveStatement.class::cast);
466                 if (optMatch.isPresent()) {
467                     final SchemaTreeEffectiveStatement<?> match = optMatch.orElseThrow();
468                     deque.addLast(match);
469                     clean = false;
470                     return (ChoiceEffectiveStatement) match;
471                 }
472             }
473         }
474         throw notPresent(parent, "Choice", nodeIdentifier);
475     }
476
477     /**
478      * Lookup a {@code grouping} by its node identifier and push it to the stack.
479      *
480      * @param nodeIdentifier Node identifier of the grouping to enter
481      * @return Resolved grouping
482      * @throws NullPointerException if {@code nodeIdentifier} is null
483      * @throws IllegalArgumentException if the corresponding grouping cannot be found
484      */
485     public @NonNull GroupingEffectiveStatement enterGrouping(final QName nodeIdentifier) {
486         return pushGrouping(requireNonNull(nodeIdentifier));
487     }
488
489     /**
490      * Lookup a {@code schema tree} child by its node identifier and push it to the stack.
491      *
492      * @param nodeIdentifier Node identifier of the schema tree child to enter
493      * @return Resolved schema tree child
494      * @throws NullPointerException if {@code nodeIdentifier} is null
495      * @throws IllegalArgumentException if the corresponding child cannot be found
496      */
497     public @NonNull SchemaTreeEffectiveStatement<?> enterSchemaTree(final QName nodeIdentifier) {
498         return pushSchema(requireNonNull(nodeIdentifier));
499     }
500
501     /**
502      * Lookup a {@code schema tree} node by its schema node identifier and push it to the stack.
503      *
504      * @param nodeIdentifier Schema node identifier of the schema tree node to enter
505      * @return Resolved schema tree node
506      * @throws NullPointerException if {@code nodeIdentifier} is null
507      * @throws IllegalArgumentException if the corresponding node cannot be found
508      */
509     public @NonNull SchemaTreeEffectiveStatement<?> enterSchemaTree(final SchemaNodeIdentifier nodeIdentifier) {
510         if (nodeIdentifier instanceof Absolute) {
511             clear();
512         }
513
514         final Iterator<QName> it = nodeIdentifier.getNodeIdentifiers().iterator();
515         SchemaTreeEffectiveStatement<?> ret;
516         do {
517             ret = enterSchemaTree(it.next());
518         } while (it.hasNext());
519
520         return ret;
521     }
522
523     /**
524      * Lookup a {@code schema tree} child by its node identifier and push it to the stack.
525      *
526      * @param nodeIdentifier Node identifier of the date tree child to enter
527      * @return Resolved date tree child
528      * @throws NullPointerException if {@code nodeIdentifier} is null
529      * @throws IllegalArgumentException if the corresponding child cannot be found
530      */
531     public @NonNull DataTreeEffectiveStatement<?> enterDataTree(final QName nodeIdentifier) {
532         return pushData(requireNonNull(nodeIdentifier));
533     }
534
535     /**
536      * Lookup a {@code typedef} by its node identifier and push it to the stack.
537      *
538      * @param nodeIdentifier Node identifier of the typedef to enter
539      * @return Resolved typedef
540      * @throws NullPointerException if {@code nodeIdentifier} is null
541      * @throws IllegalArgumentException if the corresponding typedef cannot be found
542      */
543     public @NonNull TypedefEffectiveStatement enterTypedef(final QName nodeIdentifier) {
544         return pushTypedef(requireNonNull(nodeIdentifier));
545     }
546
547     /**
548      * Lookup a {@code rc:yang-data} by the module namespace where it is defined and its template name.
549      *
550      * @param namespace Module namespace in which to lookup the template
551      * @param name Template name
552      * @return Resolved yang-data
553      * @throws NullPointerException if any argument is null
554      * @throws IllegalArgumentException if the corresponding yang-data cannot be found
555      * @throws IllegalStateException if this stack is not empty
556      */
557     public @NonNull YangDataEffectiveStatement enterYangData(final QNameModule namespace, final String name) {
558         final EffectiveStatement<?, ?> parent = deque.peekLast();
559         checkState(parent == null, "Cannot lookup yang-data in a non-empty stack");
560
561         final String templateName = requireNonNull(name);
562         final ModuleEffectiveStatement module = effectiveModel.getModuleStatements().get(requireNonNull(namespace));
563         checkArgument(module != null, "Module for %s not found", namespace);
564
565         final YangDataEffectiveStatement ret = module.streamEffectiveSubstatements(YangDataEffectiveStatement.class)
566             .filter(stmt -> templateName.equals(stmt.argument()))
567             .findFirst()
568             .orElseThrow(
569                 () -> new IllegalArgumentException("yang-data " + templateName + " not present in " + namespace));
570         deque.addLast(ret);
571         currentModule = module;
572         return ret;
573     }
574
575     /**
576      * Pop the current statement from the stack.
577      *
578      * @return Previous statement
579      * @throws NoSuchElementException if this stack is empty
580      */
581     public @NonNull EffectiveStatement<?, ?> exit() {
582         final EffectiveStatement<?, ?> prev = deque.removeLast();
583         if (prev instanceof GroupingEffectiveStatement) {
584             --groupingDepth;
585         }
586         if (deque.isEmpty()) {
587             currentModule = null;
588             clean = true;
589         }
590         return prev;
591     }
592
593     /**
594      * Pop the current statement from the stack, asserting it is a {@link DataTreeEffectiveStatement} and that
595      * subsequent {@link #enterDataTree(QName)} will find it again.
596      *
597      * @return Previous statement
598      * @throws NoSuchElementException if this stack is empty
599      * @throws IllegalStateException if current statement is not a DataTreeEffectiveStatement or if its parent is not
600      *                               a {@link DataTreeAwareEffectiveStatement}
601      */
602     public @NonNull DataTreeEffectiveStatement<?> exitToDataTree() {
603         final EffectiveStatement<?, ?> child = exit();
604         checkState(child instanceof DataTreeEffectiveStatement, "Unexpected current %s", child);
605         EffectiveStatement<?, ?> parent = deque.peekLast();
606         while (parent instanceof ChoiceEffectiveStatement || parent instanceof CaseEffectiveStatement) {
607             deque.pollLast();
608             parent = deque.peekLast();
609         }
610
611         checkState(parent == null || parent instanceof DataTreeAwareEffectiveStatement, "Unexpected parent %s", parent);
612         return (DataTreeEffectiveStatement<?>) child;
613     }
614
615     @Override
616     public TypeDefinition<?> resolveLeafref(final LeafrefTypeDefinition type) {
617         final SchemaInferenceStack tmp = copy();
618
619         LeafrefTypeDefinition current = type;
620         while (true) {
621             final EffectiveStatement<?, ?> resolved = tmp.resolvePathExpression(current.getPathStatement());
622             checkState(resolved instanceof TypeAware, "Unexpected result %s resultion of %s", resolved, type);
623             final TypeDefinition<?> result = ((TypedDataSchemaNode) resolved).getType();
624             if (result instanceof LeafrefTypeDefinition) {
625                 checkArgument(result != type, "Resolution of %s loops back onto itself via %s", type, current);
626                 current = (LeafrefTypeDefinition) result;
627             } else {
628                 return result;
629             }
630         }
631     }
632
633     /**
634      * Resolve a {@link PathExpression}.
635      *
636      * <p>
637      * Note if this method throws, this stack may be in an undefined state.
638      *
639      * @param path Requested path
640      * @return Resolved schema tree child
641      * @throws NullPointerException if {@code path} is null
642      * @throws IllegalArgumentException if the target node cannot be found
643      * @throws VerifyException if path expression is invalid
644      */
645     public @NonNull EffectiveStatement<?, ?> resolvePathExpression(final PathExpression path) {
646         final Steps steps = path.getSteps();
647         if (steps instanceof LocationPathSteps) {
648             return resolveLocationPath(((LocationPathSteps) steps).getLocationPath());
649         } else if (steps instanceof DerefSteps) {
650             return resolveDeref((DerefSteps) steps);
651         } else {
652             throw new VerifyException("Unhandled steps " + steps);
653         }
654     }
655
656     private @NonNull EffectiveStatement<?, ?> resolveDeref(final DerefSteps deref) {
657         final EffectiveStatement<?, ?> leafRefSchemaNode = currentStatement();
658         final YangLocationPath.Relative derefArg = deref.getDerefArgument();
659         final EffectiveStatement<?, ?> derefStmt = resolveLocationPath(derefArg);
660         checkArgument(derefStmt != null, "Cannot find deref(%s) target node %s in context of %s",
661                 derefArg, leafRefSchemaNode);
662         checkArgument(derefStmt instanceof TypedDataSchemaNode, "deref(%s) resolved to non-typed %s", derefArg,
663                 derefStmt);
664
665         // We have a deref() target, decide what to do about it
666         final TypeDefinition<?> targetType = ((TypedDataSchemaNode) derefStmt).getType();
667         if (targetType instanceof InstanceIdentifierTypeDefinition) {
668             // Static inference breaks down, we cannot determine where this points to
669             // FIXME: dedicated exception, users can recover from it, derive from IAE
670             throw new UnsupportedOperationException("Cannot infer instance-identifier reference " + targetType);
671         }
672
673         // deref() is defined only for instance-identifier and leafref types, handle the latter
674         checkArgument(targetType instanceof LeafrefTypeDefinition, "Illegal target type %s", targetType);
675
676         final PathExpression dereferencedLeafRefPath = ((LeafrefTypeDefinition) targetType).getPathStatement();
677         EffectiveStatement<?, ?> derefNode = resolvePathExpression(dereferencedLeafRefPath);
678         checkArgument(derefStmt != null, "Can not find target node of dereferenced node %s", derefStmt);
679         checkArgument(derefNode instanceof LeafSchemaNode, "Unexpected %s reference in %s", deref,
680                 dereferencedLeafRefPath);
681         return resolveLocationPath(deref.getRelativePath());
682     }
683
684     private @NonNull EffectiveStatement<?, ?> resolveLocationPath(final YangLocationPath path) {
685         // get the default namespace before we clear and loose our deque
686         final QNameModule defaultNamespace = deque.isEmpty() ? null : ((QName) deque.peekLast().argument()).getModule();
687         if (path.isAbsolute()) {
688             clear();
689         }
690
691         EffectiveStatement<?, ?> current = null;
692         for (Step step : path.getSteps()) {
693             final YangXPathAxis axis = step.getAxis();
694             switch (axis) {
695                 case PARENT:
696                     verify(step instanceof AxisStep, "Unexpected parent step %s", step);
697                     try {
698                         current = exitToDataTree();
699                     } catch (IllegalStateException | NoSuchElementException e) {
700                         throw new IllegalArgumentException("Illegal parent access in " + path, e);
701                     }
702                     break;
703                 case CHILD:
704                     verify(step instanceof QNameStep, "Unexpected child step %s", step);
705                     current = enterChild((QNameStep) step, defaultNamespace);
706                     break;
707                 default:
708                     throw new VerifyException("Unexpected step " + step);
709             }
710         }
711
712         return verifyNotNull(current);
713     }
714
715     private @NonNull EffectiveStatement<?, ?> enterChild(final QNameStep step, final QNameModule defaultNamespace) {
716         final AbstractQName toResolve = step.getQName();
717         final QName qname;
718         if (toResolve instanceof QName) {
719             qname = (QName) toResolve;
720         } else if (toResolve instanceof Unqualified) {
721             checkArgument(defaultNamespace != null, "Can not find target module of step %s", step);
722             qname = ((Unqualified) toResolve).bindTo(defaultNamespace);
723         } else {
724             throw new VerifyException("Unexpected child step QName " + toResolve);
725         }
726         return enterDataTree(qname);
727     }
728
729     /**
730      * Return an {@link Inference} equivalent of current state.
731      *
732      * @return An {@link Inference}
733      */
734     public @NonNull Inference toInference() {
735         return new Inference(effectiveModel, deque.clone(), currentModule, groupingDepth, clean);
736     }
737
738     /**
739      * Return an {@link SchemaTreeInference} equivalent of current state.
740      *
741      * @return An {@link SchemaTreeInference}
742      * @throws IllegalStateException if current state cannot be converted to a {@link SchemaTreeInference}
743      */
744     public @NonNull SchemaTreeInference toSchemaTreeInference() {
745         checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this);
746         final var cleanDeque = clean ? deque : reconstructSchemaInferenceStack().deque;
747         return DefaultSchemaTreeInference.unsafeOf(getEffectiveModelContext(), cleanDeque.stream()
748             .map(stmt -> (SchemaTreeEffectiveStatement<?>) stmt)
749             .collect(ImmutableList.toImmutableList()));
750     }
751
752     /**
753      * Convert current state into an absolute schema node identifier.
754      *
755      * @return Absolute schema node identifier representing current state
756      * @throws IllegalStateException if current state is not instantiated
757      */
758     public @NonNull Absolute toSchemaNodeIdentifier() {
759         checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this);
760         return Absolute.of(simplePathFromRoot());
761     }
762
763     /**
764      * Convert current state into a SchemaPath.
765      *
766      * @return Absolute SchemaPath representing current state
767      * @throws IllegalStateException if current state is not instantiated
768      * @deprecated This method is meant only for interoperation with SchemaPath-based APIs.
769      */
770     @Deprecated
771     public @NonNull SchemaPath toSchemaPath() {
772         return SchemaPath.create(simplePathFromRoot(), true);
773     }
774
775     /**
776      * Return an iterator along {@link SchemaPath#getPathFromRoot()}. This method is a faster equivalent of
777      * {@code toSchemaPath().getPathFromRoot().iterator()}.
778      *
779      * @return An unmodifiable iterator
780      */
781     @Deprecated
782     public @NonNull Iterator<QName> schemaPathIterator() {
783         return Iterators.unmodifiableIterator(simplePathFromRoot().iterator());
784     }
785
786     @Override
787     public String toString() {
788         return MoreObjects.toStringHelper(this).add("path", deque).toString();
789     }
790
791     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull QName nodeIdentifier) {
792         final EffectiveStatement<?, ?> parent = deque.peekLast();
793         return parent != null ? pushGrouping(parent, nodeIdentifier) : pushFirstGrouping(nodeIdentifier);
794     }
795
796     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull EffectiveStatement<?, ?> parent,
797             final @NonNull QName nodeIdentifier) {
798         final GroupingEffectiveStatement ret = parent.streamEffectiveSubstatements(GroupingEffectiveStatement.class)
799             .filter(stmt -> nodeIdentifier.equals(stmt.argument()))
800             .findFirst()
801             .orElseThrow(() -> notPresent(parent, "Grouping", nodeIdentifier));
802         deque.addLast(ret);
803         ++groupingDepth;
804         return ret;
805     }
806
807     private @NonNull GroupingEffectiveStatement pushFirstGrouping(final @NonNull QName nodeIdentifier) {
808         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
809         final GroupingEffectiveStatement ret = pushGrouping(module, nodeIdentifier);
810         currentModule = module;
811         return ret;
812     }
813
814     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final @NonNull QName nodeIdentifier) {
815         final EffectiveStatement<?, ?> parent = deque.peekLast();
816         return parent != null ? pushSchema(parent, nodeIdentifier) : pushFirstSchema(nodeIdentifier);
817     }
818
819     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final EffectiveStatement<?, ?> parent,
820             final @NonNull QName nodeIdentifier) {
821         checkState(parent instanceof SchemaTreeAwareEffectiveStatement, "Cannot descend schema tree at %s", parent);
822         return pushSchema((SchemaTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
823     }
824
825     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(
826             final @NonNull SchemaTreeAwareEffectiveStatement<?, ?> parent, final @NonNull QName nodeIdentifier) {
827         final SchemaTreeEffectiveStatement<?> ret = parent.findSchemaTreeNode(nodeIdentifier)
828             .orElseThrow(() -> notPresent(parent, "Schema tree child ", nodeIdentifier));
829         deque.addLast(ret);
830         return ret;
831     }
832
833     private @NonNull SchemaTreeEffectiveStatement<?> pushFirstSchema(final @NonNull QName nodeIdentifier) {
834         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
835         final SchemaTreeEffectiveStatement<?> ret = pushSchema(module, nodeIdentifier);
836         currentModule = module;
837         return ret;
838     }
839
840     private @NonNull DataTreeEffectiveStatement<?> pushData(final @NonNull QName nodeIdentifier) {
841         final EffectiveStatement<?, ?> parent = deque.peekLast();
842         return parent != null ? pushData(parent, nodeIdentifier) : pushFirstData(nodeIdentifier);
843     }
844
845     private @NonNull DataTreeEffectiveStatement<?> pushData(final EffectiveStatement<?, ?> parent,
846             final @NonNull QName nodeIdentifier) {
847         checkState(parent instanceof DataTreeAwareEffectiveStatement, "Cannot descend data tree at %s", parent);
848         return pushData((DataTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
849     }
850
851     private @NonNull DataTreeEffectiveStatement<?> pushData(final @NonNull DataTreeAwareEffectiveStatement<?, ?> parent,
852             final @NonNull QName nodeIdentifier) {
853         final DataTreeEffectiveStatement<?> ret = parent.findDataTreeNode(nodeIdentifier)
854             .orElseThrow(() -> notPresent(parent, "Data tree child", nodeIdentifier));
855         deque.addLast(ret);
856         clean = false;
857         return ret;
858     }
859
860     private @NonNull DataTreeEffectiveStatement<?> pushFirstData(final @NonNull QName nodeIdentifier) {
861         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
862         final DataTreeEffectiveStatement<?> ret = pushData(module, nodeIdentifier);
863         currentModule = module;
864         return ret;
865     }
866
867     private @NonNull TypedefEffectiveStatement pushTypedef(final @NonNull QName nodeIdentifier) {
868         final EffectiveStatement<?, ?> parent = deque.peekLast();
869         return parent != null ? pushTypedef(parent, nodeIdentifier) : pushFirstTypedef(nodeIdentifier);
870     }
871
872     private @NonNull TypedefEffectiveStatement pushTypedef(final @NonNull EffectiveStatement<?, ?> parent,
873             final @NonNull QName nodeIdentifier) {
874         final TypedefEffectiveStatement ret = parent.get(TypedefNamespace.class, nodeIdentifier)
875             .orElseThrow(() -> notPresent(parent, "Typedef", nodeIdentifier));
876         deque.addLast(ret);
877         return ret;
878     }
879
880     private @NonNull TypedefEffectiveStatement pushFirstTypedef(final @NonNull QName nodeIdentifier) {
881         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
882         final TypedefEffectiveStatement ret = pushTypedef(module, nodeIdentifier);
883         currentModule = module;
884         return ret;
885     }
886
887     private @NonNull ModuleEffectiveStatement getModule(final @NonNull QName nodeIdentifier) {
888         final ModuleEffectiveStatement module = effectiveModel.getModuleStatements().get(nodeIdentifier.getModule());
889         checkArgument(module != null, "Module for %s not found", nodeIdentifier);
890         return module;
891     }
892
893     // Unified access to queue iteration for addressing purposes. Since we keep 'logical' steps as executed by user
894     // at this point, conversion to SchemaNodeIdentifier may be needed. We dispatch based on 'clean'.
895     private Collection<QName> simplePathFromRoot() {
896         return clean ? qnames() : reconstructQNames();
897     }
898
899     private Collection<QName> qnames() {
900         return Collections2.transform(deque, stmt -> {
901             final Object argument = stmt.argument();
902             verify(argument instanceof QName, "Unexpected statement %s", stmt);
903             return (QName) argument;
904         });
905     }
906
907     // So there are some data tree steps in the stack... we essentially need to convert a data tree item into a series
908     // of schema tree items. This means at least N searches, but after they are done, we get an opportunity to set the
909     // clean flag.
910     private Collection<QName> reconstructQNames() {
911         return reconstructSchemaInferenceStack().qnames();
912     }
913
914     private SchemaInferenceStack reconstructSchemaInferenceStack() {
915         // Let's walk all statements and decipher them into a temporary stack
916         final SchemaInferenceStack tmp = new SchemaInferenceStack(effectiveModel, deque.size());
917         final Iterator<EffectiveStatement<?, ?>> it = deque.iterator();
918         while (it.hasNext()) {
919             final EffectiveStatement<?, ?> stmt = it.next();
920             // Order of checks is significant
921             if (stmt instanceof DataTreeEffectiveStatement) {
922                 tmp.resolveDataTreeSteps(((DataTreeEffectiveStatement<?>) stmt).argument());
923             } else if (stmt instanceof ChoiceEffectiveStatement) {
924                 tmp.resolveChoiceSteps(((ChoiceEffectiveStatement) stmt).argument());
925             } else if (stmt instanceof SchemaTreeEffectiveStatement) {
926                 tmp.enterSchemaTree(((SchemaTreeEffectiveStatement<?> )stmt).argument());
927             } else if (stmt instanceof GroupingEffectiveStatement) {
928                 tmp.enterGrouping(((GroupingEffectiveStatement) stmt).argument());
929             } else if (stmt instanceof TypedefEffectiveStatement) {
930                 tmp.enterTypedef(((TypedefEffectiveStatement) stmt).argument());
931             } else {
932                 throw new VerifyException("Unexpected statement " + stmt);
933             }
934         }
935
936         // if the sizes match, we did not jump through hoops. let's remember that for future.
937         if (deque.size() == tmp.deque.size()) {
938             clean = true;
939             return this;
940         }
941         return tmp;
942     }
943
944     private void resolveChoiceSteps(final @NonNull QName nodeIdentifier) {
945         final EffectiveStatement<?, ?> parent = deque.peekLast();
946         if (parent instanceof ChoiceEffectiveStatement) {
947             resolveChoiceSteps((ChoiceEffectiveStatement) parent, nodeIdentifier);
948         } else {
949             enterSchemaTree(nodeIdentifier);
950         }
951     }
952
953     private void resolveChoiceSteps(final @NonNull ChoiceEffectiveStatement parent,
954             final @NonNull QName nodeIdentifier) {
955         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
956             if (stmt instanceof CaseEffectiveStatement) {
957                 final CaseEffectiveStatement caze = (CaseEffectiveStatement) stmt;
958                 final SchemaTreeEffectiveStatement<?> found = caze.findSchemaTreeNode(nodeIdentifier).orElse(null);
959                 if (found instanceof ChoiceEffectiveStatement) {
960                     deque.addLast(caze);
961                     deque.addLast(found);
962                     return;
963                 }
964             }
965         }
966         throw new VerifyException("Failed to resolve " + nodeIdentifier + " in " + parent);
967     }
968
969     private void resolveDataTreeSteps(final @NonNull QName nodeIdentifier) {
970         final EffectiveStatement<?, ?> parent = deque.peekLast();
971         if (parent != null) {
972             verify(parent instanceof SchemaTreeAwareEffectiveStatement, "Unexpected parent %s", parent);
973             resolveDataTreeSteps((SchemaTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
974             return;
975         }
976
977         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
978         resolveDataTreeSteps(module, nodeIdentifier);
979         currentModule = module;
980     }
981
982     private void resolveDataTreeSteps(final @NonNull SchemaTreeAwareEffectiveStatement<?, ?> parent,
983             final @NonNull QName nodeIdentifier) {
984         // The algebra of identifiers in 'schema tree versus data tree':
985         // - data tree parents are always schema tree parents
986         // - data tree children are always schema tree children
987
988         // that implies that a data tree parent must satisfy schema tree queries with data tree children,
989         // so a successful lookup of 'data tree parent -> child' and 'schema tree parent -> child' has to be the same
990         // for a direct lookup.
991         final SchemaTreeEffectiveStatement<?> found = parent.findSchemaTreeNode(nodeIdentifier).orElse(null);
992         if (found instanceof DataTreeEffectiveStatement) {
993             // ... and it did, we are done
994             deque.addLast(found);
995             return;
996         }
997
998         // Alright, so now it's down to filtering choice/case statements. For that we keep some globally-reused state
999         // and employ a recursive match.
1000         final var match = new ArrayDeque<EffectiveStatement<QName, ?>>();
1001         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
1002             if (stmt instanceof ChoiceEffectiveStatement
1003                 && searchChoice(match, (ChoiceEffectiveStatement) stmt, nodeIdentifier)) {
1004                 deque.addAll(match);
1005                 return;
1006             }
1007         }
1008
1009         throw new VerifyException("Failed to resolve " + nodeIdentifier + " in " + parent);
1010     }
1011
1012     private static boolean searchCase(final @NonNull ArrayDeque<EffectiveStatement<QName, ?>> result,
1013             final @NonNull CaseEffectiveStatement parent, final @NonNull QName nodeIdentifier) {
1014         result.addLast(parent);
1015         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
1016             if (stmt instanceof DataTreeEffectiveStatement && nodeIdentifier.equals(stmt.argument())) {
1017                 result.addLast((DataTreeEffectiveStatement<?>) stmt);
1018                 return true;
1019             }
1020             if (stmt instanceof ChoiceEffectiveStatement
1021                 && searchChoice(result, (ChoiceEffectiveStatement) stmt, nodeIdentifier)) {
1022                 return true;
1023             }
1024         }
1025         result.removeLast();
1026         return false;
1027     }
1028
1029     private static boolean searchChoice(final @NonNull ArrayDeque<EffectiveStatement<QName, ?>> result,
1030             final @NonNull ChoiceEffectiveStatement parent, final @NonNull QName nodeIdentifier) {
1031         result.addLast(parent);
1032         for (EffectiveStatement<?, ?> stmt : parent.effectiveSubstatements()) {
1033             if (stmt instanceof CaseEffectiveStatement
1034                 && searchCase(result, (CaseEffectiveStatement) stmt, nodeIdentifier)) {
1035                 return true;
1036             }
1037         }
1038         result.removeLast();
1039         return false;
1040     }
1041
1042     private static <T> @NonNull T checkNonNullState(final @Nullable T obj) {
1043         if (obj == null) {
1044             throw new IllegalStateException("Cannot execute on empty stack");
1045         }
1046         return obj;
1047     }
1048
1049     private static @NonNull IllegalArgumentException notPresent(final @NonNull EffectiveStatement<?, ?> parent,
1050             final @NonNull String name, final QName nodeIdentifier) {
1051         return new IllegalArgumentException(name + " " + nodeIdentifier + " not present in " + describeParent(parent));
1052     }
1053
1054     private static @NonNull String describeParent(final @NonNull EffectiveStatement<?, ?> parent) {
1055         // Add just enough information to be useful without being overly-verbose. Note we want to expose namespace
1056         // information, so that we understand what revisions we are dealing with
1057         if (parent instanceof SchemaTreeEffectiveStatement) {
1058             return "schema parent " + parent.argument();
1059         } else if (parent instanceof GroupingEffectiveStatement) {
1060             return "grouping " + parent.argument();
1061         } else if (parent instanceof ModuleEffectiveStatement) {
1062             final var module = (ModuleEffectiveStatement) parent;
1063             return "module " + module.argument().bindTo(module.localQNameModule());
1064         } else {
1065             // Shorthand for QNames, should provide enough context
1066             final Object arg = parent.argument();
1067             return "parent " + (arg instanceof QName ? arg : parent);
1068         }
1069     }
1070 }