Clean up ModifyAction
[netconf.git] / netconf / mdsal-netconf-connector / src / main / java / org / opendaylight / netconf / mdsal / connector / ops / SplittingNormalizedNodeMetadataStreamWriter.java
1 /*
2  * Copyright (c) 2019 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.netconf.mdsal.connector.ops;
9
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.collect.ClassToInstanceMap;
14 import com.google.common.collect.ImmutableClassToInstanceMap;
15 import com.google.common.collect.ImmutableMap;
16 import com.google.common.collect.Maps;
17 import java.io.IOException;
18 import java.util.ArrayDeque;
19 import java.util.ArrayList;
20 import java.util.Deque;
21 import java.util.List;
22 import javax.xml.transform.dom.DOMSource;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.opendaylight.netconf.api.ModifyAction;
25 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
26 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.EditConfigInput;
27 import org.opendaylight.yangtools.rfc7952.data.api.StreamWriterMetadataExtension;
28 import org.opendaylight.yangtools.yang.common.QName;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
34 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
35 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriterExtension;
36 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
37 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeMetadataResult;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 final class SplittingNormalizedNodeMetadataStreamWriter implements NormalizedNodeStreamWriter,
42         StreamWriterMetadataExtension {
43     private static final Logger LOG = LoggerFactory.getLogger(SplittingNormalizedNodeMetadataStreamWriter.class);
44     private static final QName OPERATION_ATTRIBUTE = QName.create(EditConfigInput.QNAME.getNamespace(),
45         XmlNetconfConstants.OPERATION_ATTR_KEY);
46
47     // Top-level result node
48     private final NormalizedNodeMetadataResult result = new NormalizedNodeMetadataResult();
49     // Split-out changes
50     private final List<DataTreeChange> dataTreeChanges = new ArrayList<>();
51     // Path of the node we are currently in
52     private final Deque<PathArgument> currentPath = new ArrayDeque<>();
53     // Stack of parent changes.
54     private final Deque<ModifyAction> actions = new ArrayDeque<>();
55     // Stack of stashed writers which have been split out
56     private final ModifyAction defaultAction;
57     // Backing writer
58     private final ComponentNormalizedNodeStreamWriter writer;
59
60     // Current action, populated to default action on entry
61     private ModifyAction currentAction;
62
63     // Tracks the number of delete operations in actions
64     private int deleteDepth;
65
66     SplittingNormalizedNodeMetadataStreamWriter(final ModifyAction defaultAction) {
67         this.defaultAction = requireNonNull(defaultAction);
68         writer = new ComponentNormalizedNodeStreamWriter(result);
69     }
70
71     List<DataTreeChange> getDataTreeChanges() {
72         return dataTreeChanges;
73     }
74
75     @Override
76     public ClassToInstanceMap<NormalizedNodeStreamWriterExtension> getExtensions() {
77         return ImmutableClassToInstanceMap.of(StreamWriterMetadataExtension.class, this);
78     }
79
80     @Override
81     public void metadata(final ImmutableMap<QName, Object> metadata) throws IOException {
82         final var operation = metadata.get(OPERATION_ATTRIBUTE);
83         if (operation instanceof String str) {
84             currentAction = ModifyAction.ofXmlValue(str);
85         } else if (operation != null) {
86             throw new IllegalStateException("Unexpected operation attribute value " + operation);
87         }
88         writer.metadata(filterMeta(metadata));
89     }
90
91     private static ImmutableMap<QName, Object> filterMeta(final ImmutableMap<QName, Object> metadata) {
92         // FIXME: also remove prefixed attributes?
93         return ImmutableMap.copyOf(Maps.filterKeys(metadata,
94             key -> !XmlParserStream.LEGACY_ATTRIBUTE_NAMESPACE.equals(key.getModule())));
95     }
96
97     @Override
98     public void startLeafNode(final NodeIdentifier name) throws IOException {
99         writer.startLeafNode(name);
100         pushPath(name);
101     }
102
103     @Override
104     public void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
105         writer.startLeafSet(name, childSizeHint);
106         pushPath(name);
107     }
108
109     @Override
110     public void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
111         writer.startOrderedLeafSet(name, childSizeHint);
112         pushPath(name);
113     }
114
115     @Override
116     public void startLeafSetEntryNode(final NodeWithValue<?> name) throws IOException {
117         writer.startLeafSetEntryNode(name);
118         pushPath(name);
119     }
120
121     @Override
122     public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
123         writer.startContainerNode(name, childSizeHint);
124         pushPath(name);
125     }
126
127     @Override
128     public void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
129         writer.startUnkeyedList(name, childSizeHint);
130         pushPath(name);
131     }
132
133     @Override
134     public void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
135         writer.startUnkeyedListItem(name, childSizeHint);
136         pushPath(name);
137     }
138
139     @Override
140     public void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
141         writer.startMapNode(name, childSizeHint);
142         pushPath(name);
143     }
144
145     @Override
146     public void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
147             throws IOException {
148         writer.startMapEntryNode(identifier, childSizeHint);
149         pushPath(identifier);
150     }
151
152     @Override
153     public void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
154         writer.startOrderedMapNode(name, childSizeHint);
155         pushPath(name);
156     }
157
158     @Override
159     public void startChoiceNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
160         writer.startChoiceNode(name, childSizeHint);
161         pushPath(name);
162     }
163
164     @Override
165     public void startAugmentationNode(final AugmentationIdentifier identifier) throws IOException {
166         writer.startAugmentationNode(identifier);
167         pushPath(identifier);
168     }
169
170
171     @Override
172     public boolean startAnydataNode(final NodeIdentifier name, final Class<?> objectModel) throws IOException {
173         // FIXME: add anydata support
174         return false;
175     }
176
177     @Override
178     public boolean startAnyxmlNode(final NodeIdentifier name, final Class<?> objectModel) throws IOException {
179         final boolean ret = writer.startAnyxmlNode(name, objectModel);
180         if (ret) {
181             pushPath(name);
182         }
183         return ret;
184     }
185
186     @Override
187     public void endNode() throws IOException {
188         final var prevAction = actions.peek();
189         if (prevAction != null) {
190             // We only split out a builder if we a changing action relative to parent and we are not inside
191             // a remove/delete operation
192             if (prevAction != currentAction && deleteDepth == 0) {
193                 dataTreeChanges.add(new DataTreeChange(writer.build(), currentAction, currentPath));
194             } else {
195                 writer.endNode();
196             }
197             popPath();
198         } else {
199             // All done, special-cased
200             LOG.debug("All done ... writer {}", writer);
201             writer.endNode();
202             dataTreeChanges.add(new DataTreeChange(result.getResult(), currentAction, currentPath));
203         }
204     }
205
206     @Override
207     public void domSourceValue(final DOMSource value) throws IOException {
208         writer.domSourceValue(value);
209     }
210
211     @Override
212     public void scalarValue(@NonNull final Object value) throws IOException {
213         writer.scalarValue(value);
214     }
215
216     @Override
217     public void close() throws IOException {
218         checkState(currentPath.isEmpty(), "Cannot close with %s", currentPath);
219         writer.close();
220     }
221
222     @Override
223     public void flush() throws IOException {
224         writer.flush();
225     }
226
227     private boolean atRemoval() {
228         return currentAction == ModifyAction.DELETE || currentAction == ModifyAction.REMOVE;
229     }
230
231     private void popPath() {
232         currentPath.pop();
233         currentAction = actions.pop();
234         if (atRemoval()) {
235             checkState(deleteDepth > 0);
236             deleteDepth--;
237         }
238     }
239
240     private void pushPath(final PathArgument pathArgument) {
241         if (currentAction != null) {
242             // Nested element: inherit previous action and track number of REMOVE/DELETE operations in the stack
243             if (atRemoval()) {
244                 deleteDepth++;
245             }
246             actions.push(currentAction);
247         } else {
248             // Top-level element: set the default action
249             currentAction = defaultAction;
250         }
251         currentPath.push(pathArgument);
252     }
253 }