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