Use switch with patterns in YangInstanceIdentifierWriter
[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.Iterables;
15 import java.io.IOException;
16 import java.util.List;
17 import javax.xml.transform.dom.DOMSource;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.opendaylight.yangtools.util.ImmutableOffsetMap;
20 import org.opendaylight.yangtools.yang.common.QName;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
25 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedAnydata;
26 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
27 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
29 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
30 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
31 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
34
35 /**
36  * Utility for emitting a {@link YangInstanceIdentifier} into a {@link NormalizedNodeStreamWriter} as a set of
37  * {@code startXXXNode} events. An example of usage would be something along the lines of:
38  * <pre>
39  *   <code>
40  *       YangModelContext
41  *       YangInstanceIdentifier id;
42  *       var result = new NormalizedNodeResult();
43  *       try (var writer = ImmutableNormalizedNodeStreamWriter.from(result)) {
44  *           try (var iidWriter = YangInstanceIdentifierWriter.open(writer, ctx, id)) {
45  *               // Here the state of 'writer' reflects the nodes in 'id'
46  *           }
47  *           // Here the writer is back to its initial state
48  *       }
49  *
50  *       // NormalizedNode result, including the structure created from YangInstanceIdentifier
51  *       var node = result.getResult();
52  *   </code>
53  * </pre>
54  */
55 public final class YangInstanceIdentifierWriter implements AutoCloseable {
56     private NormalizedNodeStreamWriter writer;
57     private final int endNodeCount;
58
59     private YangInstanceIdentifierWriter(final NormalizedNodeStreamWriter writer, final int endNodeCount) {
60         this.writer = requireNonNull(writer);
61         this.endNodeCount = endNodeCount;
62     }
63
64     /**
65      * Open a writer, emitting events in target {@link NormalizedNodeStreamWriter}.
66      *
67      * @param writer Writer to enter
68      * @param root Root container
69      * @param path Path to enter
70      * @return A writer instance
71      * @throws IOException if the path cannot be entered
72      */
73     public static @NonNull YangInstanceIdentifierWriter open(final NormalizedNodeStreamWriter writer,
74             final DataNodeContainer root, final YangInstanceIdentifier path) throws IOException {
75         final var it = path.getPathArguments().iterator();
76         if (!it.hasNext()) {
77             return new YangInstanceIdentifierWriter(writer, 0);
78         }
79
80         // State tracking
81         int endNodes = 0;
82         Object parent = root;
83         boolean reuse = false;
84         boolean terminal = false;
85
86         do {
87             if (terminal) {
88                 throw new IOException(parent + " is a terminal node, cannot resolve " + ImmutableList.copyOf(it));
89             }
90
91             final var arg = it.next();
92             switch (arg) {
93                 case NodeWithValue<?> nodeId -> {
94                     if (!(parent instanceof LeafListSchemaNode)) {
95                         throw new IOException(parent + " does not support leaf-list entry " + arg);
96                     }
97                     if (!reuse) {
98                         throw new IOException(parent + " is already at its entry, cannot enter " + arg);
99                     }
100
101                     reuse = false;
102                     terminal = true;
103                     writer.startLeafSetEntryNode(nodeId);
104                 }
105                 case NodeIdentifierWithPredicates nodeId -> {
106                     if (!(parent instanceof ListSchemaNode list)) {
107                         throw new IOException(parent + " does not support map entry " + arg);
108                     }
109                     if (!reuse) {
110                         throw new IOException(parent + " is already at its entry, cannot enter " + arg);
111                     }
112                     if (!list.getQName().equals(nodeId.getNodeType())) {
113                         throw new IOException(parent + " expects a matching map entry, cannot enter " + arg);
114                     }
115
116                     final var key = list.getKeyDefinition();
117                     if (key.isEmpty()) {
118                         throw new IOException(parent + " does not expect map entry " + arg);
119                     }
120                     if (key.size() != nodeId.size()) {
121                         throw new IOException(parent + " expects " + key.size() + " predicates, cannot use " + arg);
122                     }
123
124                     reuse = false;
125                     writer.startMapEntryNode(normalizePredicates(nodeId, key), 1);
126                 }
127                 case NodeIdentifier nodeId -> {
128                     if (reuse) {
129                         if (!(parent instanceof ListSchemaNode list)) {
130                             throw new IOException(parent + " expects an identifiable entry, cannot enter " + arg);
131                         }
132
133                         if (!list.getKeyDefinition().isEmpty()) {
134                             throw new IOException(parent + " expects a map entry, cannot enter " + arg);
135                         }
136                         if (!list.getQName().equals(nodeId.getNodeType())) {
137                             throw new IOException(parent + " expects a matching entry, cannot enter " + arg);
138                         }
139
140                         reuse = false;
141                         writer.startUnkeyedListItem(nodeId, 1);
142                         endNodes++;
143                         continue;
144                     }
145
146                     final var child = switch (parent) {
147                         case DataNodeContainer container -> container.dataChildByName(nodeId.getNodeType());
148                         case ChoiceSchemaNode choice -> choice.findDataSchemaChild(nodeId.getNodeType()).orElse(null);
149                         default -> throw new IOException("Unhandled parent " + parent + " when looking up " + arg);
150                     };
151
152                     parent = switch (child) {
153                         case null -> throw new IOException("Failed to find child " + arg + " in parent " + parent);
154                         case AnydataSchemaNode anydata -> {
155                             writer.startAnydataNode(nodeId, NormalizedAnydata.class);
156                             terminal = true;
157                             yield anydata;
158                         }
159                         case AnyxmlSchemaNode anyxml -> {
160                             writer.startAnyxmlNode(nodeId, DOMSource.class);
161                             terminal = true;
162                             yield anyxml;
163                         }
164                         case ChoiceSchemaNode choice -> {
165                             writer.startChoiceNode(nodeId, 1);
166                             yield choice;
167                         }
168                         case ContainerLike containerLike -> {
169                             writer.startContainerNode(nodeId, 1);
170                             yield containerLike;
171                         }
172                         case LeafSchemaNode leaf -> {
173                             writer.startLeafNode(nodeId);
174                             terminal = true;
175                             yield leaf;
176                         }
177                         case LeafListSchemaNode leafList -> {
178                             if (leafList.isUserOrdered()) {
179                                 writer.startOrderedLeafSet(nodeId, 1);
180                             } else {
181                                 writer.startLeafSet(nodeId, 1);
182                             }
183                             reuse = true;
184                             yield leafList;
185                         }
186                         case ListSchemaNode list -> {
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                             reuse = true;
195                             yield list;
196                         }
197                         default -> throw new IOException("Unhandled child " + child);
198                     };
199                 }
200             }
201
202             endNodes++;
203         } while (it.hasNext());
204
205         return new YangInstanceIdentifierWriter(writer, endNodes);
206     }
207
208     @Override
209     public void close() throws IOException {
210         if (writer != null) {
211             for (int i = 0; i < endNodeCount; ++i) {
212                 writer.endNode();
213             }
214             writer = null;
215         }
216     }
217
218     private static NodeIdentifierWithPredicates normalizePredicates(final NodeIdentifierWithPredicates input,
219             final List<QName> key) throws IOException {
220         if (Iterables.elementsEqual(input.keySet(), key)) {
221             return input;
222         }
223
224         final var builder = ImmutableMap.<QName, Object>builderWithExpectedSize(key.size());
225         for (var qname : key) {
226             final var value = input.getValue(qname);
227             if (value == null) {
228                 throw new IOException("Cannot normalize " + input + " to " + key + ", missing value for " + qname);
229             }
230             builder.put(qname, value);
231         }
232
233         return NodeIdentifierWithPredicates.of(input.getNodeType(), ImmutableOffsetMap.orderedCopyOf(builder.build()));
234     }
235 }