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