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