Fix nested augmentations
[yangtools.git] / data / yang-data-api / src / main / java / org / opendaylight / yangtools / yang / data / api / schema / stream / YangInstanceIdentifierWriter.java
1 /*
2  * Copyright (c) 2022 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.data.api.schema.stream;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.Collections2;
13 import com.google.common.collect.ImmutableList;
14 import com.google.common.collect.ImmutableMap;
15 import com.google.common.collect.ImmutableSet;
16 import com.google.common.collect.Iterables;
17 import java.io.IOException;
18 import java.util.List;
19 import javax.xml.transform.dom.DOMSource;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.opendaylight.yangtools.util.ImmutableOffsetMap;
22 import org.opendaylight.yangtools.yang.common.QName;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
28 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedAnydata;
29 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
33 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
35 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
36 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
40 import org.opendaylight.yangtools.yang.model.util.EffectiveAugmentationSchema;
41
42 /**
43  * Utility for emitting a {@link YangInstanceIdentifier} into a {@link NormalizedNodeStreamWriter} as a set of
44  * {@code startXXXNode} events. An example of usage would be something along the lines of:
45  * <pre>
46  *   <code>
47  *       YangModelContext
48  *       YangInstanceIdentifier id;
49  *       var result = new NormalizedNodeResult();
50  *       try (var writer = ImmutableNormalizedNodeStreamWriter.from(result)) {
51  *           try (var iidWriter = YangInstanceIdentifierWriter.open(writer, ctx, id)) {
52  *               // Here the state of 'writer' reflects the nodes in 'id'
53  *           }
54  *           // Here the writer is back to its initial state
55  *       }
56  *
57  *       // NormalizedNode result, including the structure created from YangInstanceIdentifier
58  *       var node = result.getResult();
59  *   </code>
60  * </pre>
61  */
62 public final class YangInstanceIdentifierWriter implements AutoCloseable {
63     private NormalizedNodeStreamWriter writer;
64     private final int endNodeCount;
65
66     private YangInstanceIdentifierWriter(final NormalizedNodeStreamWriter writer, final int endNodeCount) {
67         this.writer = requireNonNull(writer);
68         this.endNodeCount = endNodeCount;
69     }
70
71     /**
72      * Open a writer, emitting events in target {@link NormalizedNodeStreamWriter}.
73      *
74      * @param writer Writer to enter
75      * @param root Root container
76      * @param path Path to enter
77      * @return A writer instance
78      * @throws IOException if the path cannot be entered
79      */
80     public static @NonNull YangInstanceIdentifierWriter open(final NormalizedNodeStreamWriter writer,
81             final DataNodeContainer root, final YangInstanceIdentifier path) throws IOException {
82         final var it = path.getPathArguments().iterator();
83         if (!it.hasNext()) {
84             return new YangInstanceIdentifierWriter(writer, 0);
85         }
86
87         // State tracking
88         int endNodes = 0;
89         Object parent = root;
90         boolean reuse = false;
91         boolean terminal = false;
92
93         do {
94             if (terminal) {
95                 throw new IOException(parent + " is a terminal node, cannot resolve " + ImmutableList.copyOf(it));
96             }
97
98             final var arg = it.next();
99             if (arg instanceof AugmentationIdentifier) {
100                 if (!(parent instanceof AugmentationTarget)) {
101                     throw new IOException(parent + " does not support augmentations, cannot resolve " + arg);
102                 }
103                 if (reuse) {
104                     throw new IOException(parent + " is expecting a nested item, cannot resolve " + arg);
105                 }
106
107                 final var augId = (AugmentationIdentifier) arg;
108                 if (parent instanceof DataNodeContainer) {
109                     parent = new EffectiveAugmentationSchema(enterAugmentation((AugmentationTarget) parent, augId),
110                         (DataNodeContainer) parent);
111                 } else if (parent instanceof ChoiceSchemaNode) {
112                     throw new IOException(parent + " should not use addressing through " + arg);
113                 } else {
114                     throw new IOException("Unhandled parent " + parent + " while resolving " + arg);
115                 }
116                 writer.startAugmentationNode(augId);
117             } else if (arg instanceof NodeWithValue) {
118                 if (!(parent instanceof LeafListSchemaNode)) {
119                     throw new IOException(parent + " does not support leaf-list entry " + arg);
120                 }
121                 if (!reuse) {
122                     throw new IOException(parent + " is already at its entry, cannot enter " + arg);
123                 }
124
125                 reuse = false;
126                 terminal = true;
127                 writer.startLeafSetEntryNode((NodeWithValue<?>) arg);
128             } else if (arg instanceof NodeIdentifierWithPredicates) {
129                 if (!(parent instanceof ListSchemaNode)) {
130                     throw new IOException(parent + " does not support map entry " + arg);
131                 }
132                 if (!reuse) {
133                     throw new IOException(parent + " is already at its entry, cannot enter " + arg);
134                 }
135
136                 final var nodeId = (NodeIdentifierWithPredicates) arg;
137                 final var list = (ListSchemaNode) parent;
138                 if (!list.getQName().equals(nodeId.getNodeType())) {
139                     throw new IOException(parent + " expects a matching map entry, cannot enter " + arg);
140                 }
141
142                 final var key = list.getKeyDefinition();
143                 if (key.isEmpty()) {
144                     throw new IOException(parent + " does not expect map entry " + arg);
145                 }
146                 if (key.size() != nodeId.size()) {
147                     throw new IOException(parent + " expects " + key.size() + " predicates, cannot use " + arg);
148                 }
149
150                 reuse = false;
151                 writer.startMapEntryNode(normalizePredicates(nodeId, key), 1);
152             } else if (arg instanceof NodeIdentifier) {
153                 final var nodeId = (NodeIdentifier) arg;
154
155                 if (reuse) {
156                     if (!(parent instanceof ListSchemaNode)) {
157                         throw new IOException(parent + " expects an identifiable entry, cannot enter " + arg);
158                     }
159
160                     final var list = (ListSchemaNode) parent;
161                     if (!list.getKeyDefinition().isEmpty()) {
162                         throw new IOException(parent + " expects a map entry, cannot enter " + arg);
163                     }
164                     if (!list.getQName().equals(nodeId.getNodeType())) {
165                         throw new IOException(parent + " expects a matching entry, cannot enter " + arg);
166                     }
167
168                     reuse = false;
169                     writer.startUnkeyedListItem(nodeId, 1);
170                     endNodes++;
171                     continue;
172                 }
173
174                 final DataSchemaNode child;
175                 if (parent instanceof DataNodeContainer) {
176                     child = ((DataNodeContainer) parent).dataChildByName(nodeId.getNodeType());
177                 } else if (parent instanceof ChoiceSchemaNode) {
178                     child = ((ChoiceSchemaNode) parent).findDataSchemaChild(nodeId.getNodeType()).orElse(null);
179                 } else {
180                     throw new IOException("Unhandled parent " + parent + " when looking up " + arg);
181                 }
182
183                 if (child == null) {
184                     throw new IOException("Failed to find child " + arg + " in parent " + parent);
185                 }
186
187                 // FIXME: check & repair augmentations (brr!)
188
189                 if (child instanceof ContainerLike) {
190                     parent = child;
191                     writer.startContainerNode(nodeId, 1);
192                 } else if (child instanceof ListSchemaNode) {
193                     parent = child;
194                     reuse = true;
195                     final var list = (ListSchemaNode) child;
196                     if (list.getKeyDefinition().isEmpty()) {
197                         writer.startUnkeyedList(nodeId, 1);
198                     } else if (list.isUserOrdered()) {
199                         writer.startOrderedMapNode(nodeId, 1);
200                     } else {
201                         writer.startMapNode(nodeId, 1);
202                     }
203                 } else if (child instanceof LeafSchemaNode) {
204                     parent = child;
205                     terminal = true;
206                     writer.startLeafNode(nodeId);
207                 } else if (child instanceof ChoiceSchemaNode) {
208                     parent = child;
209                     writer.startChoiceNode(nodeId, 1);
210                 } else if (child instanceof LeafListSchemaNode) {
211                     parent = child;
212                     reuse = true;
213                     if (((LeafListSchemaNode) child).isUserOrdered()) {
214                         writer.startOrderedLeafSet(nodeId, 1);
215                     } else {
216                         writer.startLeafSet(nodeId, 1);
217                     }
218                 } else if (child instanceof AnydataSchemaNode) {
219                     parent = child;
220                     terminal = true;
221                     writer.startAnydataNode(nodeId, NormalizedAnydata.class);
222                 } else if (child instanceof AnyxmlSchemaNode) {
223                     parent = child;
224                     terminal = true;
225                     writer.startAnyxmlNode(nodeId, DOMSource.class);
226                 } else {
227                     throw new IOException("Unhandled child " + child);
228                 }
229             } else {
230                 throw new IOException("Unhandled argument " + arg);
231             }
232
233             endNodes++;
234         } while (it.hasNext());
235
236         return new YangInstanceIdentifierWriter(writer, endNodes);
237     }
238
239     @Override
240     public void close() throws IOException {
241         if (writer != null) {
242             for (int i = 0; i < endNodeCount; ++i) {
243                 writer.endNode();
244             }
245             writer = null;
246         }
247     }
248
249     private static NodeIdentifierWithPredicates normalizePredicates(final NodeIdentifierWithPredicates input,
250             final List<QName> key) throws IOException {
251         if (Iterables.elementsEqual(input.keySet(), key)) {
252             return input;
253         }
254
255         final var builder = ImmutableMap.<QName, Object>builderWithExpectedSize(key.size());
256         for (var qname : key) {
257             final var value = input.getValue(qname);
258             if (value == null) {
259                 throw new IOException("Cannot normalize " + input + " to " + key + ", missing value for " + qname);
260             }
261             builder.put(qname, value);
262         }
263
264         return NodeIdentifierWithPredicates.of(input.getNodeType(), ImmutableOffsetMap.orderedCopyOf(builder.build()));
265     }
266
267     private static AugmentationSchemaNode enterAugmentation(final AugmentationTarget target,
268             final AugmentationIdentifier id) throws IOException {
269         final var augs = target.getAvailableAugmentations();
270         for (var augment : augs) {
271             if (id.equals(augmentationIdentifierFrom(augment))) {
272                 return augment;
273             }
274         }
275         throw new IOException("Cannot find augmentation " + id + " in " + target + ", available: "
276             + Collections2.transform(augs, YangInstanceIdentifierWriter::augmentationIdentifierFrom));
277     }
278
279     // FIXME: duplicate of data.util.DataSchemaContextNode.augmentationIdentifierFrom()
280     static @NonNull AugmentationIdentifier augmentationIdentifierFrom(final AugmentationSchemaNode schema) {
281         return new AugmentationIdentifier(
282             schema.getChildNodes().stream().map(DataSchemaNode::getQName).collect(ImmutableSet.toImmutableSet()));
283     }
284 }