Improve SchemaInferenceStack.enterSchemaTree()
[yangtools.git] / yang / 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 java.util.Objects.requireNonNull;
13
14 import com.google.common.annotations.Beta;
15 import com.google.common.base.MoreObjects;
16 import com.google.common.collect.ImmutableList;
17 import com.google.common.collect.Iterators;
18 import java.util.ArrayDeque;
19 import java.util.Iterator;
20 import java.util.NoSuchElementException;
21 import org.eclipse.jdt.annotation.NonNull;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.opendaylight.yangtools.concepts.Mutable;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
26 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextProvider;
27 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
28 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
29 import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement;
30 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
31 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
32 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeAwareEffectiveStatement;
33 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
34
35 /**
36  * A state tracking utility for walking {@link EffectiveModelContext}'s contents along schema/grouping namespaces. This
37  * is conceptually a stack, tracking {@link EffectiveStatement}s encountered along traversal.
38  *
39  * <p>
40  * This is meant to be a replacement concept for the use of {@link SchemaPath} in various places, notably
41  * in {@link SchemaContextUtil} methods.
42  *
43  * <p>
44  * This class is designed for single-threaded uses and does not make any guarantees around concurrent access.
45  */
46 @Beta
47 public final class SchemaInferenceStack implements Mutable, EffectiveModelContextProvider {
48     private final ArrayDeque<EffectiveStatement<QName, ?>> deque;
49     private final @NonNull EffectiveModelContext effectiveModel;
50
51     private @Nullable ModuleEffectiveStatement currentModule;
52     private int groupingDepth;
53
54     private SchemaInferenceStack(final SchemaInferenceStack source) {
55         this.deque = source.deque.clone();
56         this.effectiveModel = source.effectiveModel;
57         this.currentModule = source.currentModule;
58         this.groupingDepth = source.groupingDepth;
59     }
60
61     /**
62      * Create a new empty stack backed by an effective model.
63      *
64      * @param effectiveModel EffectiveModelContext to which this stack is attached
65      * @throws NullPointerException {@code effectiveModel} is null
66      */
67     public SchemaInferenceStack(final EffectiveModelContext effectiveModel) {
68         this.deque = new ArrayDeque<>();
69         this.effectiveModel = requireNonNull(effectiveModel);
70     }
71
72     /**
73      * Create a new stack backed by an effective model, pointing to specified schema node identified by
74      * {@link Absolute}.
75      *
76      * @param effectiveModel EffectiveModelContext to which this stack is attached
77      * @throws NullPointerException {@code effectiveModel} is null
78      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model
79      */
80     public static @NonNull SchemaInferenceStack of(final EffectiveModelContext effectiveModel, final Absolute path) {
81         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
82         path.getNodeIdentifiers().forEach(ret::enterSchemaTree);
83         return ret;
84     }
85
86     /**
87      * Create a new stack backed by an effective model, pointing to specified schema node identified by an absolute
88      * {@link SchemaPath} and its {@link SchemaPath#getPathFromRoot()}.
89      *
90      * @param effectiveModel EffectiveModelContext to which this stack is attached
91      * @throws NullPointerException {@code effectiveModel} is null
92      * @throws IllegalArgumentException if {@code path} cannot be resolved in the effective model or if it is not an
93      *                                  absolute path.
94      */
95     // FIXME: 7.0.0: consider deprecating this method
96     public static @NonNull SchemaInferenceStack ofInstantiatedPath(final EffectiveModelContext effectiveModel,
97             final SchemaPath path) {
98         checkArgument(path.isAbsolute(), "Cannot operate on relative path %s", path);
99         final SchemaInferenceStack ret = new SchemaInferenceStack(effectiveModel);
100         path.getPathFromRoot().forEach(ret::enterSchemaTree);
101         return ret;
102     }
103
104     @Override
105     public EffectiveModelContext getEffectiveModelContext() {
106         return effectiveModel;
107     }
108
109     /**
110      * Create a deep copy of this object.
111      *
112      * @return An isolated copy of this object
113      */
114     public @NonNull SchemaInferenceStack copy() {
115         return new SchemaInferenceStack(this);
116     }
117
118     /**
119      * Check if this stack is empty.
120      *
121      * @return True if this stack has not entered any node.
122      */
123     public boolean isEmpty() {
124         return deque.isEmpty();
125     }
126
127     /**
128      * Return the statement at the top of the stack.
129      *
130      * @return Top statement
131      * @throws IllegalStateException if the stack is empty
132      */
133     public @NonNull EffectiveStatement<QName, ?> currentStatement() {
134         return checkNonNullState(deque.peekFirst());
135     }
136
137     /**
138      * Return current module the stack has entered.
139      *
140      * @return Current module
141      * @throws IllegalStateException if the stack is empty
142      */
143     public @NonNull ModuleEffectiveStatement currentModule() {
144         return checkNonNullState(currentModule);
145     }
146
147     /**
148      * Check if the stack is in instantiated context. This indicates the stack is non-empty and there is no grouping
149      * (or similar construct) present in the stack.
150      *
151      * @return False if the stack is empty or contains a grouping, true otherwise.
152      */
153     public boolean inInstantiatedContext() {
154         return groupingDepth == 0 && !deque.isEmpty();
155     }
156
157     /**
158      * Reset this stack to empty state.
159      */
160     public void clear() {
161         deque.clear();
162         currentModule = null;
163         groupingDepth = 0;
164     }
165
166     /**
167      * Lookup a {@code grouping} by its node identifier and push it to the stack.
168      *
169      * @param nodeIdentifier Node identifier of the grouping to enter
170      * @return Resolved grouping
171      * @throws NullPointerException if {@code nodeIdentifier} is null
172      * @throws IllegalArgumentException if the corresponding grouping cannot be found
173      */
174     public @NonNull GroupingEffectiveStatement enterGrouping(final QName nodeIdentifier) {
175         return pushGrouping(requireNonNull(nodeIdentifier));
176     }
177
178     /**
179      * Lookup a {@code schema tree} child by its node identifier and push it to the stack.
180      *
181      * @param nodeIdentifier Node identifier of the schema tree child to enter
182      * @return Resolved schema tree child
183      * @throws NullPointerException if {@code nodeIdentifier} is null
184      * @throws IllegalArgumentException if the corresponding child cannot be found
185      */
186     public @NonNull SchemaTreeEffectiveStatement<?> enterSchemaTree(final QName nodeIdentifier) {
187         return pushSchema(requireNonNull(nodeIdentifier));
188     }
189
190     /**
191      * Pop the current statement from the stack.
192      *
193      * @return Previous statement
194      * @throws NoSuchElementException if this stack is empty
195      */
196     public @NonNull EffectiveStatement<QName, ?> exit() {
197         final EffectiveStatement<QName, ?> prev = deque.pop();
198         if (prev instanceof GroupingEffectiveStatement) {
199             --groupingDepth;
200         }
201         if (deque.isEmpty()) {
202             currentModule = null;
203         }
204         return prev;
205     }
206
207     /**
208      * Convert current state into an absolute schema node identifier.
209      *
210      * @return Absolute schema node identifier representing current state
211      * @throws IllegalStateException if current state is not instantiated
212      */
213     public @NonNull Absolute toSchemaNodeIdentifier() {
214         checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this);
215         final ImmutableList.Builder<QName> builder = ImmutableList.builderWithExpectedSize(deque.size());
216         deque.descendingIterator().forEachRemaining(stmt -> builder.add(stmt.argument()));
217         return Absolute.of(builder.build());
218     }
219
220     /**
221      * Convert current state into a SchemaPath.
222      *
223      * @return Absolute SchemaPath representing current state
224      * @throws IllegalStateException if current state is not instantiated
225      * @deprecated This method is meant only for interoperation with SchemaPath-based APIs.
226      */
227     @Deprecated
228     public @NonNull SchemaPath toSchemaPath() {
229         SchemaPath ret = SchemaPath.ROOT;
230         final Iterator<EffectiveStatement<QName, ?>> it = deque.descendingIterator();
231         while (it.hasNext()) {
232             ret = ret.createChild(it.next().argument());
233         }
234         return ret;
235     }
236
237     /**
238      * Return an iterator along {@link SchemaPath#getPathFromRoot()}. This method is a faster equivalent of
239      * {@code toSchemaPath().getPathFromRoot().iterator()}.
240      *
241      * @return An unmodifiable iterator
242      */
243     @Deprecated
244     public @NonNull Iterator<QName> schemaPathIterator() {
245         return Iterators.unmodifiableIterator(Iterators.transform(deque.descendingIterator(),
246             EffectiveStatement::argument));
247     }
248
249     @Override
250     public String toString() {
251         return MoreObjects.toStringHelper(this).add("stack", deque).toString();
252     }
253
254     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull QName nodeIdentifier) {
255         final EffectiveStatement<QName, ?> parent = deque.peekFirst();
256         return parent != null ? pushGrouping(parent, nodeIdentifier) : pushFirstGrouping(nodeIdentifier);
257     }
258
259     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull EffectiveStatement<?, ?> parent,
260             final @NonNull QName nodeIdentifier) {
261         final GroupingEffectiveStatement ret = parent.streamEffectiveSubstatements(GroupingEffectiveStatement.class)
262             .filter(stmt -> nodeIdentifier.equals(stmt.argument()))
263             .findFirst()
264             .orElseThrow(() -> new IllegalArgumentException("Grouping " + nodeIdentifier + " not present"));
265         deque.push(ret);
266         ++groupingDepth;
267         return ret;
268     }
269
270     private @NonNull GroupingEffectiveStatement pushFirstGrouping(final @NonNull QName nodeIdentifier) {
271         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
272         final GroupingEffectiveStatement ret = pushGrouping(module, nodeIdentifier);
273         currentModule = module;
274         return ret;
275     }
276
277     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final @NonNull QName nodeIdentifier) {
278         final EffectiveStatement<QName, ?> parent = deque.peekFirst();
279         return parent != null ? pushSchema(parent, nodeIdentifier) : pushFirstSchema(nodeIdentifier);
280     }
281
282     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final EffectiveStatement<QName, ?> parent,
283             final @NonNull QName nodeIdentifier) {
284         checkState(parent instanceof SchemaTreeAwareEffectiveStatement, "Cannot descend schema tree at %s", parent);
285         return pushSchema((SchemaTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
286     }
287
288     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(
289             final @NonNull SchemaTreeAwareEffectiveStatement<?, ?> parent, final @NonNull QName nodeIdentifier) {
290         final SchemaTreeEffectiveStatement<?> ret = parent.findSchemaTreeNode(nodeIdentifier).orElseThrow(
291             () -> new IllegalArgumentException("Schema tree child " + nodeIdentifier + " not present"));
292         deque.push(ret);
293         return ret;
294     }
295
296     private @NonNull SchemaTreeEffectiveStatement<?> pushFirstSchema(final @NonNull QName nodeIdentifier) {
297         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
298         final SchemaTreeEffectiveStatement<?> ret = pushSchema(module, nodeIdentifier);
299         currentModule = module;
300         return ret;
301     }
302
303     private @NonNull ModuleEffectiveStatement getModule(final @NonNull QName nodeIdentifier) {
304         final ModuleEffectiveStatement module = effectiveModel.getModuleStatements().get(nodeIdentifier.getModule());
305         checkArgument(module != null, "Module for %s not found", nodeIdentifier);
306         return module;
307     }
308
309     private static <T> @NonNull T checkNonNullState(final @Nullable T obj) {
310         if (obj == null) {
311             throw new IllegalStateException("Cannot execute on empty stack");
312         }
313         return obj;
314     }
315 }