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