Expose streams with all supported encodings
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / streams / AbstractWebsocketSerializer.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.nb.rfc8040.streams;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.base.VerifyException;
14 import java.util.ArrayDeque;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Comparator;
18 import java.util.Deque;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.DataChangeEvent;
22 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.DataChangeEvent.Operation;
23 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.controller.md.sal.remote.rev140114.data.changed.notification.data.change.event.Data;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
27 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
28 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
29 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
30 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
31 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
32 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
33 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
34 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
35 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidateNode;
36 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
37 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
38 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
39 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
40 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 abstract class AbstractWebsocketSerializer<T extends Exception> {
45     private static final Logger LOG = LoggerFactory.getLogger(AbstractWebsocketSerializer.class);
46     static final @NonNull QName PATH_QNAME = QName.create(DataChangeEvent.QNAME, "path").intern();
47     static final @NonNull NodeIdentifier PATH_NID = NodeIdentifier.create(PATH_QNAME);
48     static final @NonNull QName OPERATION_QNAME = QName.create(DataChangeEvent.QNAME, "operation").intern();
49     static final @NonNull NodeIdentifier OPERATION_NID = NodeIdentifier.create(OPERATION_QNAME);
50     static final @NonNull String DATA_NAME = Data.QNAME.getLocalName();
51
52     private final EffectiveModelContext context;
53
54     AbstractWebsocketSerializer(final EffectiveModelContext context) {
55         this.context = requireNonNull(context);
56     }
57
58     public final boolean serialize(final DataTreeCandidate candidate, final TextParameters params) throws T {
59         final var skipData = params.skipData();
60         final var changedLeafNodesOnly = params.changedLeafNodesOnly();
61         if (changedLeafNodesOnly || params.leafNodesOnly()) {
62             return serializeLeafNodesOnly(mutableRootPath(candidate), candidate.getRootNode(), skipData,
63                 changedLeafNodesOnly);
64         }
65         if (params.childNodesOnly()) {
66             return serializeChildNodesOnly(mutableRootPath(candidate), candidate.getRootNode(), skipData);
67         }
68
69         serializeData(candidate.getRootPath().getPathArguments(), candidate.getRootNode(), skipData);
70         return true;
71     }
72
73     private static Deque<PathArgument> mutableRootPath(final DataTreeCandidate candidate) {
74         final var ret = new ArrayDeque<PathArgument>();
75         ret.addAll(candidate.getRootPath().getPathArguments());
76         return ret;
77     }
78
79     final boolean serializeLeafNodesOnly(final Deque<PathArgument> path, final DataTreeCandidateNode candidate,
80             final boolean skipData, final boolean changedLeafNodesOnly) throws T {
81         final var node = switch (candidate.modificationType()) {
82             case SUBTREE_MODIFIED, APPEARED -> candidate.getDataAfter();
83             case DELETE, DISAPPEARED -> candidate.getDataBefore();
84             case WRITE -> changedLeafNodesOnly && isNotUpdate(candidate) ? null : candidate.getDataAfter();
85             case UNMODIFIED -> {
86                 // no reason to do anything with an unmodified node
87                 LOG.debug("DataTreeCandidate for a notification is unmodified, not serializing leaves. Candidate: {}",
88                         candidate);
89                 yield null;
90             }
91         };
92
93         if (node == null) {
94             return false;
95         }
96         if (node instanceof LeafNode || node instanceof LeafSetNode) {
97             serializeData(path, candidate, skipData);
98             return true;
99         }
100
101         // Retain a modicum of sanity here: children may come from different namespaces. Report children from the same
102         // namespace first, holding others back. Once that is done, sort the remaining children by their PathArgument
103         // and report them in that order.
104         final var myNamespace = node.name().getNodeType().getModule();
105         final var heldBack = new ArrayList<DataTreeCandidateNode>();
106         boolean ret = false;
107         for (var childNode : candidate.childNodes()) {
108             final var childName = childNode.name();
109             if (myNamespace.equals(childName.getNodeType().getModule())) {
110                 ret |= serializeChild(path, childNode, skipData, changedLeafNodesOnly);
111             } else {
112                 heldBack.add(childNode);
113             }
114         }
115         if (!heldBack.isEmpty()) {
116             // This is not exactly nice, as we really should be using schema definition order, but we do not have it
117             // available here, so we fall back to the next best thing.
118             heldBack.sort(Comparator.comparing(DataTreeCandidateNode::name));
119             for (var childNode : heldBack) {
120                 ret |= serializeChild(path, childNode, skipData, changedLeafNodesOnly);
121             }
122         }
123         return ret;
124     }
125
126     private boolean serializeChild(final Deque<PathArgument> path, final DataTreeCandidateNode childNode,
127             final boolean skipData, final boolean changedLeafNodesOnly) throws T {
128         final boolean ret;
129         path.add(childNode.name());
130         ret = serializeLeafNodesOnly(path, childNode, skipData, changedLeafNodesOnly);
131         path.removeLast();
132         return ret;
133     }
134
135     private boolean serializeChildNodesOnly(final Deque<PathArgument> path, final DataTreeCandidateNode current,
136             final boolean skipData) throws T {
137         return switch (current.modificationType()) {
138             case APPEARED, WRITE -> serializeChildNodesOnlyTerminal(path, current, skipData, current.getDataAfter());
139             case DISAPPEARED, DELETE -> serializeChildNodesOnlyTerminal(path, current, skipData,
140                 current.getDataBefore());
141             case SUBTREE_MODIFIED -> serializeChildNodesOnlyChildren(path, current, skipData);
142             case UNMODIFIED -> false;
143         };
144     }
145
146     private boolean serializeChildNodesOnlyTerminal(final Deque<PathArgument> path, final DataTreeCandidateNode current,
147             final boolean skipData, final NormalizedNode data) throws T {
148         if (data instanceof ChoiceNode || data instanceof LeafSetNode || data instanceof MapNode) {
149             return serializeChildNodesOnlyChildren(path, current, skipData);
150         }
151
152         final var updated = !isNotUpdate(current);
153         if (updated) {
154             serializeData(path, current, skipData);
155         }
156         return updated;
157     }
158
159     private boolean serializeChildNodesOnlyChildren(final Deque<PathArgument> path, final DataTreeCandidateNode current,
160             final boolean skipData) throws T {
161         var updated = false;
162         for (var child : current.childNodes()) {
163             path.add(child.name());
164             updated |= serializeChildNodesOnly(path, child, skipData);
165             path.removeLast();
166         }
167         return updated;
168     }
169
170     private void serializeData(final Collection<PathArgument> dataPath, final DataTreeCandidateNode candidate,
171             final boolean skipData) throws T {
172         var stack = SchemaInferenceStack.of(context);
173         DataSchemaContext current = DataSchemaContextTree.from(context).getRoot();
174         for (var arg : dataPath) {
175             final var next = current instanceof DataSchemaContext.Composite composite ? composite.enterChild(stack, arg)
176                 : null;
177             current = verifyNotNull(next,  "Failed to resolve %s: cannot find %s in %s", dataPath, arg, current);
178         }
179
180         // Exit to parent if needed
181         if (!stack.isEmpty()) {
182             stack.exit();
183         }
184
185         serializeData(stack.toInference(), dataPath, candidate, skipData);
186     }
187
188     abstract void serializeData(Inference parent, Collection<PathArgument> dataPath, DataTreeCandidateNode candidate,
189         boolean skipData) throws T;
190
191     private static boolean isNotUpdate(final DataTreeCandidateNode node) {
192         final var before = node.dataBefore();
193         final var after = node.dataAfter();
194
195         return before != null && after != null && before.body().equals(after.body());
196     }
197
198     static final @Nullable NormalizedNode getDataAfter(final DataTreeCandidateNode candidate) {
199         final var data = candidate.dataAfter();
200         if (data instanceof MapEntryNode mapEntry) {
201             return ImmutableNodes.mapNodeBuilder(data.name().getNodeType()).withChild(mapEntry).build();
202         }
203         return data;
204     }
205
206     static final @NonNull String modificationTypeToOperation(final DataTreeCandidateNode candidate) {
207         final var operation = switch (candidate.modificationType()) {
208             case APPEARED, SUBTREE_MODIFIED, WRITE -> candidate.dataBefore() != null ? Operation.Updated
209                 : Operation.Created;
210             case DELETE, DISAPPEARED -> Operation.Deleted;
211             case UNMODIFIED -> {
212                 throw new VerifyException("DataTreeCandidate for a notification is unmodified. Candidate: "
213                     + candidate);
214             }
215         };
216         return operation.getName();
217     }
218 }