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