5cf6a7e732cc3e124a8c574822ee4ff47c11cca3
[netconf.git] / netconf / netconf-util / src / main / java / org / opendaylight / netconf / util / StreamingContext.java
1 /*
2  * Copyright (c) 2018 Pantheon Technologies, 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.netconf.util;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter.UNKNOWN_SIZE;
12
13 import com.google.common.collect.ImmutableMap;
14 import com.google.common.collect.Iterables;
15 import java.io.IOException;
16 import java.util.Collections;
17 import java.util.HashMap;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.Optional;
23 import javax.xml.transform.dom.DOMSource;
24 import org.opendaylight.yangtools.concepts.Identifiable;
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
33 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
34 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
37 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
41 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.util.EffectiveAugmentationSchema;
46
47 abstract class StreamingContext<T extends PathArgument> implements Identifiable<T> {
48     private final T identifier;
49
50     StreamingContext(final T identifier) {
51         this.identifier = identifier;
52     }
53
54     static StreamingContext<?> fromSchemaAndQNameChecked(final DataNodeContainer schema, final QName child) {
55         final Optional<DataSchemaNode> potential = findChildSchemaNode(schema, child);
56         checkArgument(potential.isPresent(),
57                 "Supplied QName %s is not valid according to schema %s, potential children nodes: %s", child, schema,
58                 schema.getChildNodes());
59
60         final DataSchemaNode result = potential.get();
61         // We try to look up if this node was added by augmentation
62         if (schema instanceof DataSchemaNode && result.isAugmenting()) {
63             for (final AugmentationSchemaNode aug : ((AugmentationTarget)schema).getAvailableAugmentations()) {
64                 if (aug.findDataChildByName(result.getQName()).isPresent()) {
65                     return new Augmentation(aug, schema);
66                 }
67             }
68         }
69         return fromDataSchemaNode(result);
70     }
71
72     static StreamingContext<?> fromDataSchemaNode(final DataSchemaNode potential) {
73         if (potential instanceof ContainerSchemaNode) {
74             return new Container((ContainerSchemaNode) potential);
75         } else if (potential instanceof ListSchemaNode) {
76             return fromListSchemaNode((ListSchemaNode) potential);
77         } else if (potential instanceof LeafSchemaNode) {
78             return new Leaf((LeafSchemaNode) potential);
79         } else if (potential instanceof ChoiceSchemaNode) {
80             return new Choice((ChoiceSchemaNode) potential);
81         } else if (potential instanceof LeafListSchemaNode) {
82             return fromLeafListSchemaNode((LeafListSchemaNode) potential);
83         } else if (potential instanceof AnyxmlSchemaNode) {
84             return new AnyXml((AnyxmlSchemaNode) potential);
85         }
86         return null;
87     }
88
89     @Override
90     public final T getIdentifier() {
91         return identifier;
92     }
93
94     abstract StreamingContext<?> getChild(PathArgument child);
95
96     /**
97      * Writing node structure that is described by series of {@link PathArgument}
98      * into {@link NormalizedNodeStreamWriter}.
99      *
100      * @param writer output {@link NormalizedNode} writer
101      * @param first  the first {@link PathArgument}
102      * @param others iterator that points to next path arguments
103      * @throws IOException failed to write a stream of path arguments into {@link NormalizedNodeStreamWriter}
104      */
105     abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, Iterator<PathArgument> others)
106             throws IOException;
107
108     /**
109      * Writing node structure that is described by provided {@link PathNode} into {@link NormalizedNodeStreamWriter}.
110      *
111      * @param writer output {@link NormalizedNode} writer
112      * @param first  the first {@link PathArgument}
113      * @param tree   subtree of path arguments that starts with the first path argument
114      * @throws IOException failed to write a stream of path arguments into {@link NormalizedNodeStreamWriter}
115      */
116     abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, PathNode tree)
117             throws IOException;
118
119     abstract boolean isMixin();
120
121     private static Optional<DataSchemaNode> findChildSchemaNode(final DataNodeContainer parent, final QName child) {
122         final Optional<DataSchemaNode> potential = parent.findDataChildByName(child);
123         return potential.isPresent() ? potential
124                 : findChoice(Iterables.filter(parent.getChildNodes(), ChoiceSchemaNode.class), child);
125     }
126
127     private static Optional<DataSchemaNode> findChoice(final Iterable<ChoiceSchemaNode> choices, final QName child) {
128         for (final ChoiceSchemaNode choice : choices) {
129             for (final CaseSchemaNode caze : choice.getCases()) {
130                 if (findChildSchemaNode(caze, child).isPresent()) {
131                     return Optional.of(choice);
132                 }
133             }
134         }
135         return Optional.empty();
136     }
137
138     private static StreamingContext<?> fromListSchemaNode(final ListSchemaNode potential) {
139         final List<QName> keyDefinition = potential.getKeyDefinition();
140         if (keyDefinition == null || keyDefinition.isEmpty()) {
141             return new UnkeyedListMixin(potential);
142         }
143         return potential.isUserOrdered() ? new OrderedMapMixin(potential)
144                 : new UnorderedMapMixin(potential);
145     }
146
147     private static StreamingContext<?> fromLeafListSchemaNode(final LeafListSchemaNode potential) {
148         return potential.isUserOrdered() ? new OrderedLeafListMixin(potential)
149                 : new UnorderedLeafListMixin(potential);
150     }
151
152     private abstract static class AbstractComposite<T extends PathArgument> extends StreamingContext<T> {
153         AbstractComposite(final T identifier) {
154             super(identifier);
155         }
156
157         @Override
158         final void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
159                 final Iterator<PathArgument> others) throws IOException {
160             verifyActualPathArgument(first);
161
162             emitElementStart(writer, first);
163             if (others.hasNext()) {
164                 final PathArgument childPath = others.next();
165                 final StreamingContext<?> childOp = getChildOperation(childPath);
166                 childOp.streamToWriter(writer, childPath, others);
167             }
168             writer.endNode();
169         }
170
171         @Override
172         void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
173                             final PathNode subtree) throws IOException {
174             verifyActualPathArgument(first);
175
176             emitElementStart(writer, first);
177             for (final PathNode node : subtree.children()) {
178                 final PathArgument childPath = node.element();
179                 final StreamingContext<?> childOp = getChildOperation(childPath);
180                 childOp.streamToWriter(writer, childPath, node);
181             }
182             writer.endNode();
183         }
184
185         private void verifyActualPathArgument(final PathArgument first) {
186             if (!isMixin()) {
187                 final QName type = getIdentifier().getNodeType();
188                 final QName firstType = first.getNodeType();
189                 checkArgument(type.equals(firstType), "Node QName must be %s was %s", type, firstType);
190             }
191         }
192
193         abstract void emitElementStart(NormalizedNodeStreamWriter writer, PathArgument arg) throws IOException;
194
195         @SuppressWarnings("checkstyle:illegalCatch")
196         private StreamingContext<?> getChildOperation(final PathArgument childPath) {
197             final StreamingContext<?> childOp;
198             try {
199                 childOp = getChild(childPath);
200             } catch (final RuntimeException e) {
201                 throw new IllegalArgumentException(String.format("Failed to process child node %s", childPath), e);
202             }
203             checkArgument(childOp != null, "Node %s is not allowed inside %s", childPath, getIdentifier());
204             return childOp;
205         }
206     }
207
208     private abstract static class AbstractDataContainer<T extends PathArgument> extends AbstractComposite<T> {
209         private final Map<PathArgument, StreamingContext<?>> byArg = new HashMap<>();
210         private final DataNodeContainer schema;
211
212         AbstractDataContainer(final T identifier, final DataNodeContainer schema) {
213             super(identifier);
214             this.schema = schema;
215         }
216
217         @Override
218         final StreamingContext<?> getChild(final PathArgument child) {
219             StreamingContext<?> potential = byArg.get(child);
220             if (potential != null) {
221                 return potential;
222             }
223             potential = fromLocalSchema(child);
224             if (potential != null) {
225                 byArg.put(potential.getIdentifier(), potential);
226             }
227             return potential;
228         }
229
230         private StreamingContext<?> fromLocalSchema(final PathArgument child) {
231             if (child instanceof AugmentationIdentifier) {
232                 return fromSchemaAndQNameChecked(schema, ((AugmentationIdentifier) child).getPossibleChildNames()
233                         .iterator().next());
234             }
235             return fromSchemaAndQNameChecked(schema, child.getNodeType());
236         }
237     }
238
239     private abstract static class AbstractMapMixin extends AbstractComposite<NodeIdentifier> {
240         private final ListEntry innerNode;
241
242         AbstractMapMixin(final ListSchemaNode list) {
243             super(NodeIdentifier.create(list.getQName()));
244             this.innerNode = new ListEntry(NodeIdentifierWithPredicates.of(list.getQName()), list);
245         }
246
247         @Override
248         final StreamingContext<?> getChild(final PathArgument child) {
249             return child.getNodeType().equals(getIdentifier().getNodeType()) ? innerNode : null;
250         }
251
252         @Override
253         final boolean isMixin() {
254             return true;
255         }
256     }
257
258     private abstract static class AbstractSimple<T extends PathArgument> extends StreamingContext<T> {
259         AbstractSimple(final T identifier) {
260             super(identifier);
261         }
262
263         @Override
264         final StreamingContext<?> getChild(final PathArgument child) {
265             return null;
266         }
267
268         @Override
269         final boolean isMixin() {
270             return false;
271         }
272
273         @Override
274         void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
275                             final PathNode tree) throws IOException {
276             streamToWriter(writer, first, Collections.emptyIterator());
277         }
278     }
279
280     private static final class AnyXml extends AbstractSimple<NodeIdentifier> {
281         AnyXml(final AnyxmlSchemaNode schema) {
282             super(NodeIdentifier.create(schema.getQName()));
283         }
284
285         @Override
286         void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
287                 final Iterator<PathArgument> others) throws IOException {
288             writer.startAnyxmlNode(getIdentifier(), DOMSource.class);
289             // FIXME: why are we not emitting a value?
290             writer.endNode();
291         }
292     }
293
294     private static final class Choice extends AbstractComposite<NodeIdentifier> {
295         private final ImmutableMap<PathArgument, StreamingContext<?>> byArg;
296
297         Choice(final ChoiceSchemaNode schema) {
298             super(NodeIdentifier.create(schema.getQName()));
299             final ImmutableMap.Builder<PathArgument, StreamingContext<?>> byArgBuilder = ImmutableMap.builder();
300
301             for (final CaseSchemaNode caze : schema.getCases()) {
302                 for (final DataSchemaNode cazeChild : caze.getChildNodes()) {
303                     final StreamingContext<?> childOp = fromDataSchemaNode(cazeChild);
304                     byArgBuilder.put(childOp.getIdentifier(), childOp);
305                 }
306             }
307             byArg = byArgBuilder.build();
308         }
309
310         @Override
311         StreamingContext<?> getChild(final PathArgument child) {
312             return byArg.get(child);
313         }
314
315         @Override
316         boolean isMixin() {
317             return true;
318         }
319
320         @Override
321         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
322             writer.startChoiceNode(getIdentifier(), UNKNOWN_SIZE);
323         }
324     }
325
326     private static final class Leaf extends AbstractSimple<NodeIdentifier> {
327         Leaf(final LeafSchemaNode potential) {
328             super(new NodeIdentifier(potential.getQName()));
329         }
330
331         @Override
332         void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
333                 final Iterator<PathArgument> others) throws IOException {
334             writer.startLeafNode(getIdentifier());
335             // FIXME: why are we not emitting a value?
336             writer.endNode();
337         }
338     }
339
340     private static final class LeafListEntry extends AbstractSimple<NodeWithValue<?>> {
341         LeafListEntry(final LeafListSchemaNode potential) {
342             super(new NodeWithValue<>(potential.getQName(), null));
343         }
344
345         @Override
346         void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
347                 final Iterator<PathArgument> others) throws IOException {
348             checkArgument(first instanceof NodeWithValue);
349             final NodeWithValue<?> identifier = (NodeWithValue<?>) first;
350             writer.startLeafSetEntryNode(identifier);
351             writer.scalarValue(identifier.getValue());
352             writer.endNode();
353         }
354     }
355
356     private static final class ListEntry extends AbstractDataContainer<NodeIdentifierWithPredicates> {
357         ListEntry(final NodeIdentifierWithPredicates identifier, final ListSchemaNode schema) {
358             super(identifier, schema);
359         }
360
361         @Override
362         boolean isMixin() {
363             return false;
364         }
365
366         @Override
367         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
368             final NodeIdentifierWithPredicates identifier = (NodeIdentifierWithPredicates) arg;
369             writer.startMapEntryNode(identifier, UNKNOWN_SIZE);
370
371             for (Entry<QName, Object> entry : identifier.entrySet()) {
372                 writer.startLeafNode(new NodeIdentifier(entry.getKey()));
373                 writer.scalarValue(entry.getValue());
374                 writer.endNode();
375             }
376         }
377     }
378
379     private static final class UnkeyedListItem extends AbstractDataContainer<NodeIdentifier> {
380         UnkeyedListItem(final ListSchemaNode schema) {
381             super(NodeIdentifier.create(schema.getQName()), schema);
382         }
383
384         @Override
385         boolean isMixin() {
386             return false;
387         }
388
389         @Override
390         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
391             writer.startUnkeyedListItem(getIdentifier(), UNKNOWN_SIZE);
392         }
393     }
394
395     private static final class Container extends AbstractDataContainer<NodeIdentifier> {
396         Container(final ContainerSchemaNode schema) {
397             super(NodeIdentifier.create(schema.getQName()), schema);
398         }
399
400         @Override
401         boolean isMixin() {
402             return false;
403         }
404
405         @Override
406         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
407             writer.startContainerNode(getIdentifier(), UNKNOWN_SIZE);
408         }
409     }
410
411     private abstract static class LeafListMixin extends AbstractComposite<NodeIdentifier> {
412         private final StreamingContext<?> innerOp;
413
414         LeafListMixin(final LeafListSchemaNode potential) {
415             super(NodeIdentifier.create(potential.getQName()));
416             innerOp = new LeafListEntry(potential);
417         }
418
419         @Override
420         final StreamingContext<?> getChild(final PathArgument child) {
421             return child instanceof NodeWithValue ? innerOp : null;
422         }
423
424         @Override
425         final boolean isMixin() {
426             return true;
427         }
428     }
429
430     private static final class OrderedLeafListMixin extends LeafListMixin {
431         OrderedLeafListMixin(final LeafListSchemaNode potential) {
432             super(potential);
433         }
434
435         @Override
436         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
437             writer.startOrderedLeafSet(getIdentifier(), UNKNOWN_SIZE);
438         }
439     }
440
441     private static class UnorderedLeafListMixin extends LeafListMixin {
442         UnorderedLeafListMixin(final LeafListSchemaNode potential) {
443             super(potential);
444         }
445
446         @Override
447         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
448             writer.startLeafSet(getIdentifier(), UNKNOWN_SIZE);
449         }
450     }
451
452     private static final class Augmentation extends AbstractDataContainer<AugmentationIdentifier> {
453         Augmentation(final AugmentationSchemaNode augmentation, final DataNodeContainer schema) {
454             super(DataSchemaContextNode.augmentationIdentifierFrom(augmentation),
455                     EffectiveAugmentationSchema.create(augmentation, schema));
456         }
457
458         @Override
459         boolean isMixin() {
460             return true;
461         }
462
463         @Override
464         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
465             writer.startAugmentationNode(getIdentifier());
466         }
467     }
468
469     private static final class UnorderedMapMixin extends AbstractMapMixin {
470         UnorderedMapMixin(final ListSchemaNode list) {
471             super(list);
472         }
473
474         @Override
475         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
476             writer.startMapNode(getIdentifier(), UNKNOWN_SIZE);
477         }
478     }
479
480     private static final class OrderedMapMixin extends AbstractMapMixin {
481         OrderedMapMixin(final ListSchemaNode list) {
482             super(list);
483         }
484
485         @Override
486         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
487             writer.startOrderedMapNode(getIdentifier(), UNKNOWN_SIZE);
488         }
489     }
490
491     private static final class UnkeyedListMixin extends AbstractComposite<NodeIdentifier> {
492         private final UnkeyedListItem innerNode;
493
494         UnkeyedListMixin(final ListSchemaNode list) {
495             super(NodeIdentifier.create(list.getQName()));
496             this.innerNode = new UnkeyedListItem(list);
497         }
498
499         @Override
500         StreamingContext<?> getChild(final PathArgument child) {
501             return child.getNodeType().equals(getIdentifier().getNodeType()) ? innerNode : null;
502         }
503
504         @Override
505         boolean isMixin() {
506             return true;
507         }
508
509         @Override
510         void emitElementStart(final NormalizedNodeStreamWriter writer, final PathArgument arg) throws IOException {
511             writer.startUnkeyedList(getIdentifier(), UNKNOWN_SIZE);
512         }
513     }
514 }