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