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 java.util.ArrayDeque;
18 import java.util.Iterator;
19 import java.util.NoSuchElementException;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.opendaylight.yangtools.concepts.Mutable;
23 import org.opendaylight.yangtools.yang.common.QName;
24 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
25 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextProvider;
26 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
27 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
28 import org.opendaylight.yangtools.yang.model.api.stmt.GroupingEffectiveStatement;
29 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
30 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
31 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeAwareEffectiveStatement;
32 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
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     @Override
72     public EffectiveModelContext getEffectiveModelContext() {
73         return effectiveModel;
74     }
75
76     /**
77      * Create a deep copy of this object.
78      *
79      * @return An isolated copy of this object
80      */
81     public @NonNull SchemaInferenceStack copy() {
82         return new SchemaInferenceStack(this);
83     }
84
85     /**
86      * Check if this stack is empty.
87      *
88      * @return True if this stack has not entered any node.
89      */
90     public boolean isEmpty() {
91         return deque.isEmpty();
92     }
93
94     /**
95      * Return the statement at the top of the stack.
96      *
97      * @return Top statement
98      * @throws IllegalStateException if the stack is empty
99      */
100     public @NonNull EffectiveStatement<QName, ?> currentStatement() {
101         return checkNonNullState(deque.peekFirst());
102     }
103
104     /**
105      * Return current module the stack has entered.
106      *
107      * @return Current module
108      * @throws IllegalStateException if the stack is empty
109      */
110     public @NonNull ModuleEffectiveStatement currentModule() {
111         return checkNonNullState(currentModule);
112     }
113
114     /**
115      * Check if the stack is in instantiated context. This indicates the stack is non-empty and there is no grouping
116      * (or similar construct) present in the stack.
117      *
118      * @return False if the stack is empty or contains a grouping, true otherwise.
119      */
120     public boolean inInstantiatedContext() {
121         return groupingDepth == 0 && !deque.isEmpty();
122     }
123
124     /**
125      * Reset this stack to empty state.
126      */
127     public void clear() {
128         deque.clear();
129         currentModule = null;
130         groupingDepth = 0;
131     }
132
133     /**
134      * Lookup a {@code grouping} by its node identifier and push it to the stack.
135      *
136      * @param nodeIdentifier Node identifier of the grouping to enter
137      * @return Resolved grouping
138      * @throws NullPointerException if {@code nodeIdentifier} is null
139      * @throws IllegalArgumentException if the corresponding grouping cannot be found
140      */
141     public @NonNull GroupingEffectiveStatement enterGrouping(final QName nodeIdentifier) {
142         return pushGrouping(requireNonNull(nodeIdentifier));
143     }
144
145     /**
146      * Lookup a {@code schema tree} child by its node identifier and push it to the stack.
147      *
148      * @param nodeIdentifier Node identifier of the schema tree child to enter
149      * @return Resolved schema tree child
150      * @throws NullPointerException if {@code nodeIdentifier} is null
151      * @throws IllegalArgumentException if the corresponding child cannot be found
152      */
153     public @NonNull SchemaTreeEffectiveStatement<?> enterSchemaTree(final QName nodeIdentifier) {
154         return pushSchema(requireNonNull(nodeIdentifier));
155     }
156
157     /**
158      * Pop the current statement from the stack.
159      *
160      * @return Previous statement
161      * @throws NoSuchElementException if this stack is empty
162      */
163     public @NonNull EffectiveStatement<QName, ?> exit() {
164         final EffectiveStatement<QName, ?> prev = deque.pop();
165         if (prev instanceof GroupingEffectiveStatement) {
166             --groupingDepth;
167         }
168         if (deque.isEmpty()) {
169             currentModule = null;
170         }
171         return prev;
172     }
173
174     /**
175      * Convert current state into an absolute schema node identifier.
176      *
177      * @return Absolute schema node identifier representing current state
178      * @throws IllegalStateException if current state is not instantiated
179      */
180     public @NonNull Absolute toSchemaNodeIdentifier() {
181         checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this);
182         final ImmutableList.Builder<QName> builder = ImmutableList.builderWithExpectedSize(deque.size());
183         deque.descendingIterator().forEachRemaining(stmt -> builder.add(stmt.argument()));
184         return Absolute.of(builder.build());
185     }
186
187     /**
188      * Convert current state into a SchemaPath.
189      *
190      * @return Absolute SchemaPath representing current state
191      * @throws IllegalStateException if current state is not instantiated
192      * @deprecated This method is meant only for interoperation with SchemaPath-based APIs.
193      */
194     @Deprecated
195     public @NonNull SchemaPath toSchemaPath() {
196         SchemaPath ret = SchemaPath.ROOT;
197         final Iterator<EffectiveStatement<QName, ?>> it = deque.descendingIterator();
198         while (it.hasNext()) {
199             ret = ret.createChild(it.next().argument());
200         }
201         return ret;
202     }
203
204     @Override
205     public String toString() {
206         return MoreObjects.toStringHelper(this).add("stack", deque).toString();
207     }
208
209     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull QName nodeIdentifier) {
210         final EffectiveStatement<QName, ?> parent = deque.peekFirst();
211         return parent != null ? pushGrouping(parent, nodeIdentifier) : pushFirstGrouping(nodeIdentifier);
212     }
213
214     private @NonNull GroupingEffectiveStatement pushGrouping(final @NonNull EffectiveStatement<?, ?> parent,
215             final @NonNull QName nodeIdentifier) {
216         final GroupingEffectiveStatement ret = parent.streamEffectiveSubstatements(GroupingEffectiveStatement.class)
217             .filter(stmt -> nodeIdentifier.equals(stmt.argument()))
218             .findFirst()
219             .orElseThrow(() -> new IllegalArgumentException("Grouping " + nodeIdentifier + " not present"));
220         deque.push(ret);
221         ++groupingDepth;
222         return ret;
223     }
224
225     private @NonNull GroupingEffectiveStatement pushFirstGrouping(final @NonNull QName nodeIdentifier) {
226         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
227         final GroupingEffectiveStatement ret = pushGrouping(module, nodeIdentifier);
228         currentModule = module;
229         return ret;
230     }
231
232     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final @NonNull QName nodeIdentifier) {
233         final EffectiveStatement<QName, ?> parent = deque.peekFirst();
234         return parent != null ? pushSchema(parent, nodeIdentifier) : pushFirstSchema(nodeIdentifier);
235     }
236
237     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(final EffectiveStatement<QName, ?> parent,
238             final @NonNull QName nodeIdentifier) {
239         checkState(parent instanceof SchemaTreeAwareEffectiveStatement, "Cannot descend schema tree at %s", parent);
240         return pushSchema((SchemaTreeAwareEffectiveStatement<?, ?>) parent, nodeIdentifier);
241     }
242
243     private @NonNull SchemaTreeEffectiveStatement<?> pushSchema(
244             final @NonNull SchemaTreeAwareEffectiveStatement<?, ?> parent, final @NonNull QName nodeIdentifier) {
245         final SchemaTreeEffectiveStatement<?> ret = parent.findSchemaTreeNode(nodeIdentifier).orElseThrow(
246             () -> new IllegalArgumentException("Schema tree child " + nodeIdentifier + " not present"));
247         deque.push(ret);
248         return ret;
249     }
250
251     private @NonNull SchemaTreeEffectiveStatement<?> pushFirstSchema(final @NonNull QName nodeIdentifier) {
252         final ModuleEffectiveStatement module = getModule(nodeIdentifier);
253         final SchemaTreeEffectiveStatement<?> ret = pushSchema(module, nodeIdentifier);
254         currentModule = module;
255         return ret;
256     }
257
258     private @NonNull ModuleEffectiveStatement getModule(final @NonNull QName nodeIdentifier) {
259         final ModuleEffectiveStatement module = effectiveModel.getModuleStatements().get(nodeIdentifier.getModule());
260         checkArgument(module != null, "Module for %s not found", nodeIdentifier);
261         return module;
262     }
263
264     private static <T> @NonNull T checkNonNullState(final @Nullable T obj) {
265         if (obj == null) {
266             throw new IllegalStateException("Cannot execute on empty stack");
267         }
268         return obj;
269     }
270 }