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