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