e886a722990b8a87682009875f7270f1eeb4ab49
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / streams / listeners / 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.listeners;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
13 import java.util.ArrayDeque;
14 import java.util.ArrayList;
15 import java.util.Collection;
16 import java.util.Comparator;
17 import java.util.Deque;
18 import org.eclipse.jdt.annotation.Nullable;
19 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
20 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
21 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
22 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
23 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
24 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
25 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
26 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidate;
27 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidateNode;
28 import org.opendaylight.yangtools.yang.data.tree.api.ModificationType;
29 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
30 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
31 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
32 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
33 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 abstract class AbstractWebsocketSerializer<T extends Exception> {
38     private static final Logger LOG = LoggerFactory.getLogger(AbstractWebsocketSerializer.class);
39
40     private final EffectiveModelContext context;
41
42     AbstractWebsocketSerializer(final EffectiveModelContext context) {
43         this.context = requireNonNull(context);
44     }
45
46     public final boolean serialize(final DataTreeCandidate candidate, final boolean leafNodesOnly,
47             final boolean skipData, final boolean changedLeafNodesOnly) throws T {
48         if (leafNodesOnly || changedLeafNodesOnly) {
49             final var path = new ArrayDeque<PathArgument>();
50             path.addAll(candidate.getRootPath().getPathArguments());
51             return serializeLeafNodesOnly(path, candidate.getRootNode(), skipData, changedLeafNodesOnly);
52         }
53
54         serializeData(candidate.getRootPath().getPathArguments(), candidate.getRootNode(), skipData);
55         return true;
56     }
57
58     final boolean serializeLeafNodesOnly(final Deque<PathArgument> path, final DataTreeCandidateNode candidate,
59             final boolean skipData, final boolean changedLeafNodesOnly) throws T {
60         final var node = switch (candidate.modificationType()) {
61             case SUBTREE_MODIFIED, APPEARED -> candidate.getDataAfter();
62             case DELETE, DISAPPEARED -> candidate.getDataBefore();
63             case WRITE -> changedLeafNodesOnly && isNotUpdate(candidate) ? null : candidate.getDataAfter();
64             case UNMODIFIED -> {
65                 // no reason to do anything with an unmodified node
66                 LOG.debug("DataTreeCandidate for a notification is unmodified, not serializing leaves. Candidate: {}",
67                         candidate);
68                 yield null;
69             }
70         };
71
72         if (node == null) {
73             return false;
74         }
75         if (node instanceof LeafNode || node instanceof LeafSetNode) {
76             serializeData(path, candidate, skipData);
77             return true;
78         }
79
80         // Retain a modicum of sanity here: children may come from different namespaces. Report children from the same
81         // namespace first, holding others back. Once that is done, sort the remaining children by their PathArgument
82         // and report them in that order.
83         final var myNamespace = node.name().getNodeType().getModule();
84         final var heldBack = new ArrayList<DataTreeCandidateNode>();
85         boolean ret = false;
86         for (var childNode : candidate.childNodes()) {
87             final var childName = childNode.name();
88             if (myNamespace.equals(childName.getNodeType().getModule())) {
89                 ret |= serializeChild(path, childNode, skipData, changedLeafNodesOnly);
90             } else {
91                 heldBack.add(childNode);
92             }
93         }
94         if (!heldBack.isEmpty()) {
95             // This is not exactly nice, as we really should be using schema definition order, but we do not have it
96             // available here, so we fall back to the next best thing.
97             heldBack.sort(Comparator.comparing(DataTreeCandidateNode::name));
98             for (var childNode : heldBack) {
99                 ret |= serializeChild(path, childNode, skipData, changedLeafNodesOnly);
100             }
101         }
102         return ret;
103     }
104
105     private boolean serializeChild(final Deque<PathArgument> path, final DataTreeCandidateNode childNode,
106             final boolean skipData, final boolean changedLeafNodesOnly) throws T {
107         final boolean ret;
108         path.add(childNode.name());
109         ret = serializeLeafNodesOnly(path, childNode, skipData, changedLeafNodesOnly);
110         path.removeLast();
111         return ret;
112     }
113
114     private void serializeData(final Collection<PathArgument> dataPath, final DataTreeCandidateNode candidate,
115             final boolean skipData) throws T {
116         var stack = SchemaInferenceStack.of(context);
117         DataSchemaContext current = DataSchemaContextTree.from(context).getRoot();
118         for (var arg : dataPath) {
119             final var next = current instanceof DataSchemaContext.Composite composite ? composite.enterChild(stack, arg)
120                 : null;
121             current = verifyNotNull(next,  "Failed to resolve %s: cannot find %s in %s", dataPath, arg, current);
122         }
123
124         // Exit to parent if needed
125         if (!stack.isEmpty()) {
126             stack.exit();
127         }
128
129         serializeData(stack.toInference(), dataPath, candidate, skipData);
130     }
131
132     abstract void serializeData(Inference parent, Collection<PathArgument> dataPath, DataTreeCandidateNode candidate,
133         boolean skipData) throws T;
134
135     private static boolean isNotUpdate(final DataTreeCandidateNode node) {
136         final var before = node.dataBefore();
137         final var after = node.dataAfter();
138
139         return before != null && after != null && before.body().equals(after.body());
140     }
141
142     abstract void serializePath(Collection<PathArgument> pathArguments) throws T;
143
144     abstract void serializeOperation(DataTreeCandidateNode candidate) throws T;
145
146     static final @Nullable NormalizedNode getDataAfter(final DataTreeCandidateNode candidate) {
147         final var data = candidate.dataAfter();
148         if (data instanceof MapEntryNode mapEntry) {
149             return ImmutableNodes.mapNodeBuilder(data.name().getNodeType()).withChild(mapEntry).build();
150         }
151         return data;
152     }
153
154     static final String convertPath(final Collection<PathArgument> path) {
155         final StringBuilder pathBuilder = new StringBuilder();
156
157         for (var pathArgument : path) {
158             pathBuilder.append('/');
159             pathBuilder.append(pathArgument.getNodeType().getNamespace().toString().replace(':', '-'));
160             pathBuilder.append(':');
161             pathBuilder.append(pathArgument.getNodeType().getLocalName());
162
163             if (pathArgument instanceof NodeIdentifierWithPredicates nip) {
164                 pathBuilder.append("[");
165                 for (var key : nip.entrySet()) {
166                     pathBuilder.append(key.getKey().getNamespace().toString().replace(':', '-'));
167                     pathBuilder.append(':');
168                     pathBuilder.append(key.getKey().getLocalName());
169                     pathBuilder.append("='");
170                     pathBuilder.append(key.getValue().toString());
171                     pathBuilder.append('\'');
172                 }
173                 pathBuilder.append(']');
174             }
175         }
176
177         return pathBuilder.toString();
178     }
179
180     static final String modificationTypeToOperation(final DataTreeCandidateNode candidate,
181             final ModificationType modificationType) {
182         return switch (modificationType) {
183             case APPEARED, SUBTREE_MODIFIED, WRITE -> candidate.dataBefore() != null ? "updated" : "created";
184             case DELETE, DISAPPEARED -> "deleted";
185             case UNMODIFIED -> {
186                 // shouldn't ever happen since the root of a modification is only triggered by some event
187                 LOG.warn("DataTreeCandidate for a notification is unmodified. Candidate: {}", candidate);
188                 yield "none";
189             }
190         };
191     }
192 }