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