2 * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.restconf.nb.rfc8040.streams;
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
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;
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();
52 private final EffectiveModelContext context;
54 AbstractWebsocketSerializer(final EffectiveModelContext context) {
55 this.context = requireNonNull(context);
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);
65 if (params.childNodesOnly()) {
66 return serializeChildNodesOnly(mutableRootPath(candidate), candidate.getRootNode(), skipData);
69 serializeData(candidate.getRootPath().getPathArguments(), candidate.getRootNode(), skipData);
73 private static Deque<PathArgument> mutableRootPath(final DataTreeCandidate candidate) {
74 final var ret = new ArrayDeque<PathArgument>();
75 ret.addAll(candidate.getRootPath().getPathArguments());
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();
86 // no reason to do anything with an unmodified node
87 LOG.debug("DataTreeCandidate for a notification is unmodified, not serializing leaves. Candidate: {}",
96 if (node instanceof LeafNode || node instanceof LeafSetNode) {
97 serializeData(path, candidate, skipData);
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>();
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);
112 heldBack.add(childNode);
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);
126 private boolean serializeChild(final Deque<PathArgument> path, final DataTreeCandidateNode childNode,
127 final boolean skipData, final boolean changedLeafNodesOnly) throws T {
129 path.add(childNode.name());
130 ret = serializeLeafNodesOnly(path, childNode, skipData, changedLeafNodesOnly);
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;
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);
152 final var updated = !isNotUpdate(current);
154 serializeData(path, current, skipData);
159 private boolean serializeChildNodesOnlyChildren(final Deque<PathArgument> path, final DataTreeCandidateNode current,
160 final boolean skipData) throws T {
162 for (var child : current.childNodes()) {
163 path.add(child.name());
164 updated |= serializeChildNodesOnly(path, child, skipData);
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)
177 current = verifyNotNull(next, "Failed to resolve %s: cannot find %s in %s", dataPath, arg, current);
180 // Exit to parent if needed
181 if (!stack.isEmpty()) {
185 serializeData(stack.toInference(), dataPath, candidate, skipData);
188 abstract void serializeData(Inference parent, Collection<PathArgument> dataPath, DataTreeCandidateNode candidate,
189 boolean skipData) throws T;
191 private static boolean isNotUpdate(final DataTreeCandidateNode node) {
192 final var before = node.dataBefore();
193 final var after = node.dataAfter();
195 return before != null && after != null && before.body().equals(after.body());
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();
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
210 case DELETE, DISAPPEARED -> Operation.Deleted;
212 throw new VerifyException("DataTreeCandidate for a notification is unmodified. Candidate: "
216 return operation.getName();