Fix delete operation for leaf nodes 99/98699/15
authorIvan Hrasko <ivan.hrasko@pantheon.tech>
Mon, 13 Dec 2021 19:31:34 +0000 (20:31 +0100)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Thu, 13 Jan 2022 09:24:40 +0000 (10:24 +0100)
In case of delete operation we do not have
NormalizedNode representing data.

To create NormalizedNode we cannot use
ImmutableNodes.fromInstanceId() because it violates
leaf node non-null value contract.

Instead we have to emit data to delete using
EmptyListXmlWriter the similar way as its
done with filters.

Similar way we have to emit data together
with metadata using EmptyListXmlMetadatWriter
to overcome NormalizedMetadataWriter' write
method which requires data in the form of NormalizedNode.

JIRA: NETCONF-833
Change-Id: I2dc2921a78e7fc41d1d5eda101978c3a2e36ec12
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/EmptyListXmlMetadataWriter.java [new file with mode: 0644]
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java
netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-container-node-candidate.xml [new file with mode: 0644]
netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-leaf-node-candidate.xml [new file with mode: 0644]
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java

diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/EmptyListXmlMetadataWriter.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/EmptyListXmlMetadataWriter.java
new file mode 100644 (file)
index 0000000..ecd4a89
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2021 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.util;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+import javax.xml.stream.XMLStreamWriter;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata;
+import org.opendaylight.yangtools.rfc7952.data.api.StreamWriterMetadataExtension;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.ForwardingNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
+
+/**
+ * This class extends capability of {@link EmptyListXmlWriter} delegate to write metadata
+ * using {@link StreamWriterMetadataExtension} the same way as
+ * {@link org.opendaylight.yangtools.rfc7952.data.util.NormalizedNodeStreamWriterMetadataDecorator}.
+ */
+final class EmptyListXmlMetadataWriter extends ForwardingNormalizedNodeStreamWriter {
+    private final Deque<NormalizedMetadata> stack = new ArrayDeque<>();
+    private final EmptyListXmlWriter dataWriterDelegate;
+    private final StreamWriterMetadataExtension metaWriter;
+    private final NormalizedMetadata metadata;
+
+    private int absentDepth = 0;
+
+    EmptyListXmlMetadataWriter(final @NonNull NormalizedNodeStreamWriter writer,
+            final @NonNull XMLStreamWriter xmlStreamWriter, final @NonNull StreamWriterMetadataExtension metaWriter,
+            final @NonNull NormalizedMetadata metadata) {
+        this.dataWriterDelegate = new EmptyListXmlWriter(requireNonNull(writer), requireNonNull(xmlStreamWriter));
+        this.metaWriter = requireNonNull(metaWriter);
+        this.metadata = requireNonNull(metadata);
+    }
+
+    @Override
+    protected NormalizedNodeStreamWriter delegate() {
+        return dataWriterDelegate.delegate();
+    }
+
+    @Override
+    public void startLeafNode(final NodeIdentifier name) throws IOException {
+        dataWriterDelegate.startLeafNode(name);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startLeafSet(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startOrderedLeafSet(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startOrderedLeafSet(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startLeafSetEntryNode(final YangInstanceIdentifier.NodeWithValue<?> name) throws IOException {
+        dataWriterDelegate.startLeafSetEntryNode(name);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startContainerNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startContainerNode(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startUnkeyedList(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startUnkeyedList(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startUnkeyedListItem(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startUnkeyedListItem(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startMapNode(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startMapEntryNode(final NodeIdentifierWithPredicates identifier, final int childSizeHint)
+            throws IOException {
+        dataWriterDelegate.startMapEntryNode(identifier, childSizeHint);
+        enterMetadataNode(identifier);
+    }
+
+    @Override
+    public void startOrderedMapNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startOrderedMapNode(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startChoiceNode(final NodeIdentifier name, final int childSizeHint) throws IOException {
+        dataWriterDelegate.startChoiceNode(name, childSizeHint);
+        enterMetadataNode(name);
+    }
+
+    @Override
+    public void startAugmentationNode(final AugmentationIdentifier identifier) throws IOException {
+        dataWriterDelegate.startAugmentationNode(identifier);
+        enterMetadataNode(identifier);
+    }
+
+    @Override
+    public boolean startAnyxmlNode(final NodeIdentifier name, final Class<?> objectModel) throws IOException {
+        final boolean ret = dataWriterDelegate.startAnyxmlNode(name, objectModel);
+        if (ret) {
+            enterMetadataNode(name);
+        }
+        return ret;
+    }
+
+    @Override
+    public void endNode() throws IOException {
+        dataWriterDelegate.endNode();
+
+        if (absentDepth > 0) {
+            absentDepth--;
+        } else {
+            stack.pop();
+        }
+    }
+
+    private void enterMetadataNode(final YangInstanceIdentifier.PathArgument name) throws IOException {
+        if (absentDepth > 0) {
+            absentDepth++;
+            return;
+        }
+
+        final NormalizedMetadata current = stack.peek();
+        if (current != null) {
+            final NormalizedMetadata child = current.getChildren().get(name);
+            if (child != null) {
+                enterChild(child);
+            } else {
+                absentDepth = 1;
+            }
+        } else {
+            // Empty stack: enter first entry
+            enterChild(metadata);
+        }
+    }
+
+    private void enterChild(final NormalizedMetadata child) throws IOException {
+        final Map<QName, Object> annotations = child.getAnnotations();
+        if (!annotations.isEmpty()) {
+            metaWriter.metadata(ImmutableMap.copyOf(annotations));
+        }
+        stack.push(child);
+    }
+}
\ No newline at end of file
index d50d0d71502c4a8f672ef5bb5de99be552667507..d2ae6c73fdc25bd2973b370b832121815c4ebbe3 100644 (file)
@@ -27,6 +27,7 @@ import org.opendaylight.netconf.api.xml.XmlElement;
 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
 import org.opendaylight.netconf.api.xml.XmlUtil;
 import org.opendaylight.yangtools.rfc7952.data.api.NormalizedMetadata;
+import org.opendaylight.yangtools.rfc7952.data.api.StreamWriterMetadataExtension;
 import org.opendaylight.yangtools.rfc7952.data.util.NormalizedMetadataWriter;
 import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
 import org.opendaylight.yangtools.rfc8528.data.util.EmptyMountPointContext;
@@ -210,6 +211,79 @@ public final class NetconfUtil {
         }
     }
 
+    /**
+     * Write data specified by {@link YangInstanceIdentifier} into {@link DOMResult}.
+     *
+     * @param query      path to the root node
+     * @param result     DOM result holder
+     * @param schemaPath schema path of the parent node
+     * @param context    mountpoint schema context
+     * @throws IOException        when failed to write data into {@link NormalizedNodeStreamWriter}
+     * @throws XMLStreamException when failed to serialize data into XML document
+     */
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    public static void writeNormalizedNode(final YangInstanceIdentifier query, final DOMResult result,
+            final SchemaPath schemaPath, final EffectiveModelContext context) throws IOException, XMLStreamException {
+        final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
+        XML_NAMESPACE_SETTER.initializeNamespace(xmlWriter);
+        try (NormalizedNodeStreamWriter streamWriter = XMLStreamNormalizedNodeStreamWriter.create(xmlWriter,
+                context, schemaPath);
+             EmptyListXmlWriter writer = new EmptyListXmlWriter(streamWriter, xmlWriter)) {
+            final Iterator<PathArgument> it = query.getPathArguments().iterator();
+            final PathArgument first = it.next();
+            StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()).streamToWriter(writer, first, it);
+        } finally {
+            try {
+                if (xmlWriter != null) {
+                    xmlWriter.close();
+                }
+            } catch (final Exception e) {
+                LOG.warn("Unable to close resource properly", e);
+            }
+        }
+    }
+
+    /**
+     * Write data specified by {@link YangInstanceIdentifier} along with corresponding {@code metadata}
+     * into {@link DOMResult}.
+     *
+     * @param query      path to the root node
+     * @param metadata   metadata to be written
+     * @param result     DOM result holder
+     * @param schemaPath schema path of the parent node
+     * @param context    mountpoint schema context
+     * @throws IOException        when failed to write data into {@link NormalizedNodeStreamWriter}
+     * @throws XMLStreamException when failed to serialize data into XML document
+     */
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    public static void writeNormalizedNode(final YangInstanceIdentifier query,
+            final @Nullable NormalizedMetadata metadata, final DOMResult result, final SchemaPath schemaPath,
+            final EffectiveModelContext context) throws IOException, XMLStreamException {
+        if (metadata == null) {
+            writeNormalizedNode(query, result, schemaPath, context);
+            return;
+        }
+
+        final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
+        XML_NAMESPACE_SETTER.initializeNamespace(xmlWriter);
+        try (NormalizedNodeStreamWriter streamWriter = XMLStreamNormalizedNodeStreamWriter
+                .create(xmlWriter, context, schemaPath);
+             EmptyListXmlMetadataWriter writer = new EmptyListXmlMetadataWriter(streamWriter, xmlWriter, streamWriter
+                     .getExtensions().getInstance(StreamWriterMetadataExtension.class), metadata)) {
+            final Iterator<PathArgument> it = query.getPathArguments().iterator();
+            final PathArgument first = it.next();
+            StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType()).streamToWriter(writer, first, it);
+        } finally {
+            try {
+                if (xmlWriter != null) {
+                    xmlWriter.close();
+                }
+            } catch (final Exception e) {
+                LOG.warn("Unable to close resource properly", e);
+            }
+        }
+    }
+
     /**
      * Writing subtree filter specified by {@link YangInstanceIdentifier} into {@link DOMResult}.
      *
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-container-node-candidate.xml b/netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-container-node-candidate.xml
new file mode 100644 (file)
index 0000000..eeb590a
--- /dev/null
@@ -0,0 +1,18 @@
+<!--
+  ~ Copyright (c) 2021 PANTHEON.tech s.r.o. All Rights Reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <edit-config>
+        <target>
+            <candidate/>
+        </target>
+        <error-option>rollback-on-error</error-option>
+        <config>
+            <c xmlns="test:namespace" xmlns:op="urn:ietf:params:xml:ns:netconf:base:1.0" op:operation="delete"/>
+        </config>
+    </edit-config>
+</rpc>
\ No newline at end of file
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-leaf-node-candidate.xml b/netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-leaf-node-candidate.xml
new file mode 100644 (file)
index 0000000..1cbe97f
--- /dev/null
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright (c) 2021 PANTHEON.tech s.r.o. All Rights Reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <edit-config>
+        <target>
+            <candidate/>
+        </target>
+        <error-option>rollback-on-error</error-option>
+        <config>
+            <c xmlns="test:namespace">
+                <a xmlns:op="urn:ietf:params:xml:ns:netconf:base:1.0" op:operation="delete"/>
+            </c>
+        </config>
+    </edit-config>
+</rpc>
\ No newline at end of file
index 63ec8321645fa2d88e2e09cf915787cf9cac6647..3ee435b998ac6f1e6f4bbe29741c4828e848c8f9 100644 (file)
@@ -347,30 +347,38 @@ public final class NetconfMessageTransformUtil {
         return Builders.containerBuilder().withNodeIdentifier(name).withValue(ImmutableList.copyOf(node)).build();
     }
 
+    /**
+     * Create edit-config structure to invoke {@code operation} with {@code lastChildOverride} data on {@code dataPath}.
+     *
+     * @param ctx {@link EffectiveModelContext} device's model context
+     * @param dataPath {@link YangInstanceIdentifier} path to data in device's data-store
+     * @param operation Optional of {@link ModifyAction} action to be invoked
+     * @param lastChildOverride Optional of {@code NormalizedNode} data on which action will be invoked
+     * @return {@link DOMSourceAnyxmlNode} containing edit-config structure
+     */
     public static DOMSourceAnyxmlNode createEditConfigAnyxml(
             final EffectiveModelContext ctx, final YangInstanceIdentifier dataPath,
             final Optional<ModifyAction> operation, final Optional<NormalizedNode> lastChildOverride) {
-        final NormalizedNode configContent;
-        final NormalizedMetadata metadata;
         if (dataPath.isEmpty()) {
             Preconditions.checkArgument(lastChildOverride.isPresent(),
                     "Data has to be present when creating structure for top level element");
             Preconditions.checkArgument(lastChildOverride.get() instanceof DataContainerChild,
                     "Data has to be either container or a list node when creating structure for top level element, "
                             + "but was: %s", lastChildOverride.get());
-            configContent = lastChildOverride.get();
-            metadata = null;
-        } else {
-            configContent = ImmutableNodes.fromInstanceId(ctx, dataPath, lastChildOverride);
-            metadata = operation.map(oper -> leafMetadata(dataPath, oper)).orElse(null);
         }
 
-        final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_CONFIG_QNAME.getLocalName(),
+        final var element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_CONFIG_QNAME.getLocalName(),
                 Optional.of(NETCONF_CONFIG_QNAME.getNamespace().toString()));
-
+        final var metadata = operation.map(o -> leafMetadata(dataPath, o)).orElse(null);
         try {
-            NetconfUtil.writeNormalizedNode(configContent, metadata, new DOMResult(element), SchemaPath.ROOT, ctx);
-        } catch (IOException | XMLStreamException e) {
+            if (lastChildOverride.isPresent()) {
+                // FIXME remove ImmutableNodes.fromInstanceId usage
+                final var configContent = ImmutableNodes.fromInstanceId(ctx, dataPath, lastChildOverride.get());
+                NetconfUtil.writeNormalizedNode(configContent, metadata, new DOMResult(element), SchemaPath.ROOT, ctx);
+            } else {
+                NetconfUtil.writeNormalizedNode(dataPath, metadata, new DOMResult(element), SchemaPath.ROOT, ctx);
+            }
+        } catch (final IOException | XMLStreamException e) {
             throw new IllegalStateException("Unable to serialize edit config content element for path " + dataPath, e);
         }
 
index e9b6f84de9f7dc19ee737da38f3d1b1bfb9f8061..567fa744d767dffe5e4eda1f9594ca7412f3f119 100644 (file)
@@ -257,6 +257,31 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest {
         verifyMessageSent("edit-config-test-module", NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME);
     }
 
+    @Test
+    public void testDeleteContainerNodeCandidate() throws Exception {
+        final YangInstanceIdentifier containerId = YangInstanceIdentifier.builder()
+                .node(CONTAINER_C_QNAME)
+                .build();
+        final DataContainerChild structure = baseOps.createEditConfigStructure(Optional.empty(),
+                Optional.of(ModifyAction.DELETE), containerId);
+        baseOps.editConfigCandidate(callback, structure, true);
+        verifyMessageSent("edit-config-delete-container-node-candidate",
+                NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME);
+    }
+
+    @Test
+    public void testDeleteLeafNodeCandidate() throws Exception {
+        final YangInstanceIdentifier leafId = YangInstanceIdentifier.builder()
+                .node(CONTAINER_C_QNAME)
+                .node(LEAF_A_NID)
+                .build();
+        final DataContainerChild structure = baseOps.createEditConfigStructure(Optional.empty(),
+                Optional.of(ModifyAction.DELETE), leafId);
+        baseOps.editConfigCandidate(callback, structure, true);
+        verifyMessageSent("edit-config-delete-leaf-node-candidate",
+                NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME);
+    }
+
     @Test
     public void testEditConfigRunning() throws Exception {
         final LeafNode<Object> leaf = Builders.leafBuilder()