From 14323f32667c16d5206e73bfe34eea48d4e2272f Mon Sep 17 00:00:00 2001 From: Ivan Hrasko Date: Mon, 13 Dec 2021 20:31:34 +0100 Subject: [PATCH] Fix delete operation for leaf nodes 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 --- .../util/EmptyListXmlMetadataWriter.java | 175 ++++++++++++++++++ .../netconf/util/NetconfUtil.java | 74 ++++++++ ...config-delete-container-node-candidate.xml | 18 ++ ...edit-config-delete-leaf-node-candidate.xml | 20 ++ .../util/NetconfMessageTransformUtil.java | 30 +-- .../netconf/util/NetconfBaseOpsTest.java | 25 +++ 6 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/EmptyListXmlMetadataWriter.java create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-container-node-candidate.xml create mode 100644 netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-leaf-node-candidate.xml 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 index 0000000000..ecd4a896a6 --- /dev/null +++ b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/EmptyListXmlMetadataWriter.java @@ -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 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 annotations = child.getAnnotations(); + if (!annotations.isEmpty()) { + metaWriter.metadata(ImmutableMap.copyOf(annotations)); + } + stack.push(child); + } +} \ No newline at end of file diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java index d50d0d7150..d2ae6c73fd 100644 --- a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java +++ b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java @@ -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 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 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 index 0000000000..eeb590aec9 --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-container-node-candidate.xml @@ -0,0 +1,18 @@ + + + + + + + rollback-on-error + + + + + \ 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 index 0000000000..1cbe97f915 --- /dev/null +++ b/netconf/netconf-util/src/test/resources/netconfMessages/edit-config-delete-leaf-node-candidate.xml @@ -0,0 +1,20 @@ + + + + + + + rollback-on-error + + + + + + + \ No newline at end of file diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java index 63ec832164..3ee435b998 100644 --- a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java +++ b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java @@ -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 operation, final Optional 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); } diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java index e9b6f84de9..567fa744d7 100644 --- a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java +++ b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java @@ -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 leaf = Builders.leafBuilder() -- 2.36.6