Implement subtree filtering using fields 54/93154/19
authorJaroslav Tóth <jtoth@frinx.io>
Mon, 19 Oct 2020 07:28:13 +0000 (09:28 +0200)
committerRobert Varga <nite@hq.sk>
Sun, 21 Feb 2021 17:19:19 +0000 (17:19 +0000)
- Fields are used for selection of specific subtrees under
  parent path - this way it is possible to limit requested
  data to specific entities without the necessity to send
  multiple get/get-config RPCs.
- Look at added unit tests to understand the functionality.

JIRA: NETCONF-735
Change-Id: I17cd364e11f8c8e61e5537fcff71a7ecedefdba3
Signed-off-by: Jaroslav Tóth <jtoth@frinx.io>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
21 files changed:
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/NetconfUtil.java
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java [new file with mode: 0644]
netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/StreamingContext.java
netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml [new file with mode: 0644]
netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml [new file with mode: 0644]
netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml [new file with mode: 0644]
netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml [new file with mode: 0644]
netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml [new file with mode: 0644]
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java [new file with mode: 0644]
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOps.java
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfMessageTransformUtil.java
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfRpcStructureTransformer.java
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/RpcStructureTransformer.java
netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/SchemalessRpcStructureTransformer.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/NetconfDeviceDataBrokerTest.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java [new file with mode: 0644]
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java [new file with mode: 0644]
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/ReadWriteTxTest.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/schema/mapping/NetconfMessageTransformerTest.java
netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/util/NetconfBaseOpsTest.java
netconf/sal-netconf-connector/src/test/resources/schemas/test-module.yang

index 5184d675e24db6c1415ddfee93d303b0502a5e7b..eb7fb247e3151fd3c372133f45a879c6c9f84059 100644 (file)
@@ -13,6 +13,8 @@ import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
 import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
@@ -186,6 +188,16 @@ public final class NetconfUtil {
         }
     }
 
+    /**
+     * Writing subtree filter 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        failed to write filter into {@link NormalizedNodeStreamWriter}
+     * @throws XMLStreamException failed to serialize filter into XML document
+     */
     public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result,
             final SchemaPath schemaPath, final EffectiveModelContext context) throws IOException, XMLStreamException {
         if (query.isEmpty()) {
@@ -207,6 +219,69 @@ public final class NetconfUtil {
         }
     }
 
+    /**
+     * Writing subtree filter specified by parent {@link YangInstanceIdentifier} and specific fields
+     * into {@link DOMResult}. Field paths are relative to parent query path.
+     *
+     * @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
+     * @param fields     list of specific fields for which the filter should be created
+     * @throws IOException        failed to write filter into {@link NormalizedNodeStreamWriter}
+     * @throws XMLStreamException failed to serialize filter into XML document
+     * @throws NullPointerException if any argument is null
+     */
+    public static void writeFilter(final YangInstanceIdentifier query, final DOMResult result,
+                                   final SchemaPath schemaPath, final EffectiveModelContext context,
+                                   final List<YangInstanceIdentifier> fields) throws IOException, XMLStreamException {
+        if (query.isEmpty() || fields.isEmpty()) {
+            // No query at all
+            return;
+        }
+        final List<YangInstanceIdentifier> aggregatedFields = aggregateFields(fields);
+        final PathNode rootNode = constructPathArgumentTree(query, aggregatedFields);
+
+        final XMLStreamWriter xmlWriter = XML_FACTORY.createXMLStreamWriter(result);
+        try {
+            try (NormalizedNodeStreamWriter writer = XMLStreamNormalizedNodeStreamWriter.create(
+                    xmlWriter, context, schemaPath)) {
+                final PathArgument first = rootNode.element();
+                StreamingContext.fromSchemaAndQNameChecked(context, first.getNodeType())
+                        .streamToWriter(writer, first, rootNode);
+            }
+        } finally {
+            xmlWriter.close();
+        }
+    }
+
+    private static List<YangInstanceIdentifier> aggregateFields(final List<YangInstanceIdentifier> fields) {
+        return fields.stream()
+                .filter(field -> fields.stream()
+                        .filter(fieldYiid -> !field.equals(fieldYiid))
+                        .noneMatch(fieldYiid -> fieldYiid.contains(field)))
+                .collect(Collectors.toList());
+    }
+
+    private static PathNode constructPathArgumentTree(final YangInstanceIdentifier query,
+                                                      final List<YangInstanceIdentifier> fields) {
+        final Iterator<PathArgument> queryIterator = query.getPathArguments().iterator();
+        final PathNode rootTreeNode = new PathNode(queryIterator.next());
+
+        PathNode queryTreeNode = rootTreeNode;
+        while (queryIterator.hasNext()) {
+            queryTreeNode = queryTreeNode.ensureChild(queryIterator.next());
+        }
+
+        for (final YangInstanceIdentifier field : fields) {
+            PathNode actualFieldTreeNode = queryTreeNode;
+            for (final PathArgument fieldPathArg : field.getPathArguments()) {
+                actualFieldTreeNode = actualFieldTreeNode.ensureChild(fieldPathArg);
+            }
+        }
+        return rootTreeNode;
+    }
+
     public static NormalizedNodeResult transformDOMSourceToNormalizedNode(final MountPointContext mountContext,
             final DOMSource value) throws XMLStreamException, URISyntaxException, IOException, SAXException {
         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
diff --git a/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java b/netconf/netconf-util/src/main/java/org/opendaylight/netconf/util/PathNode.java
new file mode 100644 (file)
index 0000000..d794bf3
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Copyright © 2020 FRINX s.r.o. All rights reserved.
+ * Copyright © 2021 PANTHEON.tech, s.r.o.
+ *
+ * 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.Maps;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.concepts.Mutable;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+
+/**
+ * Representation of the tree node with possible multiple child nodes. Child nodes are identified uniquely by path
+ * argument.
+ */
+@NonNullByDefault
+final class PathNode implements Mutable {
+    private final PathArgument argument;
+
+    private Map<PathArgument, PathNode> children;
+
+    private PathNode(final PathArgument argument, final LinkedHashMap<PathArgument, PathNode> children) {
+        this.argument = requireNonNull(argument);
+        this.children = requireNonNull(children);
+    }
+
+    /**
+     * Creation of tree node using a path argument.
+     *
+     * @param argument Path argument
+     */
+    PathNode(final PathArgument argument) {
+        this.argument = requireNonNull(argument);
+        this.children = Map.of();
+    }
+
+    /**
+     * Get path argument.
+     *
+     * @return path argument
+     */
+    PathArgument element() {
+        return argument;
+    }
+
+    /**
+     * Return current child nodes.
+     *
+     * @return Current child nodes
+     */
+    Collection<PathNode> children() {
+        return children.values();
+    }
+
+    /**
+     * Return {@code true} if this node has no child nodes.
+     *
+     * @return {@code true} if this node has no child nodes
+     */
+    boolean isEmpty() {
+        return children.isEmpty();
+    }
+
+    /**
+     * Create a copy of this node with specified immediate child nodes appended.
+     *
+     * @param childArguments Child arguments
+     * @return A copy of this {@link PathNode}
+     * @throws NullPointerException if {@code childArguments} is, or contains, {@code null}
+     */
+    PathNode copyWith(final Collection<PathArgument> childArguments) {
+        final LinkedHashMap<PathArgument, PathNode> copy = children instanceof LinkedHashMap
+            ? new LinkedHashMap<>(children) : Maps.newLinkedHashMapWithExpectedSize(childArguments.size());
+        for (PathArgument childArgument : childArguments) {
+            ensureChild(copy, childArgument);
+        }
+        return new PathNode(argument, copy);
+    }
+
+    /**
+     * Ensure a node for specified argument exists.
+     *
+     * @param childArgument Child argument
+     * @return A child {@link PathNode}
+     * @throws NullPointerException if {@code childArgument} is null
+     */
+    PathNode ensureChild(final PathArgument childArgument) {
+        return ensureChild(mutableChildren(), childArgument);
+    }
+
+    private static PathNode ensureChild(final LinkedHashMap<PathArgument, PathNode> children,
+            final PathArgument childArgument) {
+        return children.computeIfAbsent(requireNonNull(childArgument), PathNode::new);
+    }
+
+    private LinkedHashMap<PathArgument, PathNode> mutableChildren() {
+        final Map<PathArgument, PathNode> local = children;
+        if (local instanceof LinkedHashMap) {
+            return (LinkedHashMap<PathArgument, PathNode>) local;
+        }
+
+        // TODO: LinkedHashMap is rather heavy, do we need to retain insertion order?
+        final LinkedHashMap<PathArgument, PathNode> ret = new LinkedHashMap<>(4);
+        children = ret;
+        return ret;
+    }
+}
index 52ce54fd6ecfa0c45cff658dc3298889b08bf826..5cf6a7e732cc3e124a8c574822ee4ff47c11cca3 100644 (file)
@@ -13,6 +13,7 @@ import static org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedN
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -27,6 +28,7 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdent
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
@@ -91,9 +93,29 @@ abstract class StreamingContext<T extends PathArgument> implements Identifiable<
 
     abstract StreamingContext<?> getChild(PathArgument child);
 
+    /**
+     * Writing node structure that is described by series of {@link PathArgument}
+     * into {@link NormalizedNodeStreamWriter}.
+     *
+     * @param writer output {@link NormalizedNode} writer
+     * @param first  the first {@link PathArgument}
+     * @param others iterator that points to next path arguments
+     * @throws IOException failed to write a stream of path arguments into {@link NormalizedNodeStreamWriter}
+     */
     abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, Iterator<PathArgument> others)
             throws IOException;
 
+    /**
+     * Writing node structure that is described by provided {@link PathNode} into {@link NormalizedNodeStreamWriter}.
+     *
+     * @param writer output {@link NormalizedNode} writer
+     * @param first  the first {@link PathArgument}
+     * @param tree   subtree of path arguments that starts with the first path argument
+     * @throws IOException failed to write a stream of path arguments into {@link NormalizedNodeStreamWriter}
+     */
+    abstract void streamToWriter(NormalizedNodeStreamWriter writer, PathArgument first, PathNode tree)
+            throws IOException;
+
     abstract boolean isMixin();
 
     private static Optional<DataSchemaNode> findChildSchemaNode(final DataNodeContainer parent, final QName child) {
@@ -135,13 +157,7 @@ abstract class StreamingContext<T extends PathArgument> implements Identifiable<
         @Override
         final void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
                 final Iterator<PathArgument> others) throws IOException {
-            if (!isMixin()) {
-                final QName type = getIdentifier().getNodeType();
-                if (type != null) {
-                    final QName firstType = first.getNodeType();
-                    checkArgument(type.equals(firstType), "Node QName must be %s was %s", type, firstType);
-                }
-            }
+            verifyActualPathArgument(first);
 
             emitElementStart(writer, first);
             if (others.hasNext()) {
@@ -152,6 +168,28 @@ abstract class StreamingContext<T extends PathArgument> implements Identifiable<
             writer.endNode();
         }
 
+        @Override
+        void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
+                            final PathNode subtree) throws IOException {
+            verifyActualPathArgument(first);
+
+            emitElementStart(writer, first);
+            for (final PathNode node : subtree.children()) {
+                final PathArgument childPath = node.element();
+                final StreamingContext<?> childOp = getChildOperation(childPath);
+                childOp.streamToWriter(writer, childPath, node);
+            }
+            writer.endNode();
+        }
+
+        private void verifyActualPathArgument(final PathArgument first) {
+            if (!isMixin()) {
+                final QName type = getIdentifier().getNodeType();
+                final QName firstType = first.getNodeType();
+                checkArgument(type.equals(firstType), "Node QName must be %s was %s", type, firstType);
+            }
+        }
+
         abstract void emitElementStart(NormalizedNodeStreamWriter writer, PathArgument arg) throws IOException;
 
         @SuppressWarnings("checkstyle:illegalCatch")
@@ -231,6 +269,12 @@ abstract class StreamingContext<T extends PathArgument> implements Identifiable<
         final boolean isMixin() {
             return false;
         }
+
+        @Override
+        void streamToWriter(final NormalizedNodeStreamWriter writer, final PathArgument first,
+                            final PathNode tree) throws IOException {
+            streamToWriter(writer, first, Collections.emptyIterator());
+        }
     }
 
     private static final class AnyXml extends AbstractSimple<NodeIdentifier> {
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-config-fields-request.xml
new file mode 100644 (file)
index 0000000..6ad30b2
--- /dev/null
@@ -0,0 +1,13 @@
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <get-config>
+        <source>
+            <running/>
+        </source>
+        <filter xmlns:ns0="urn:ietf:params:xml:ns:netconf:base:1.0" ns0:type="subtree">
+            <c xmlns="test:namespace">
+                <a/>
+                <b/>
+            </c>
+        </filter>
+    </get-config>
+</rpc>
\ No newline at end of file
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-config-with-multiple-subtrees.xml
new file mode 100644 (file)
index 0000000..fee24ee
--- /dev/null
@@ -0,0 +1,18 @@
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <get-config>
+        <source>
+            <running/>
+        </source>
+        <filter xmlns:ns0="urn:ietf:params:xml:ns:netconf:base:1.0" ns0:type="subtree">
+            <c xmlns="test:namespace">
+                <a/>
+                <d>
+                    <x/>
+                </d>
+            </c>
+            <e xmlns="test:namespace">
+                <z/>
+            </e>
+        </filter>
+    </get-config>
+</rpc>
\ No newline at end of file
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-fields-request.xml
new file mode 100644 (file)
index 0000000..7d42854
--- /dev/null
@@ -0,0 +1,10 @@
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <get>
+        <filter xmlns:ns0="urn:ietf:params:xml:ns:netconf:base:1.0" ns0:type="subtree">
+            <c xmlns="test:namespace">
+                <a/>
+                <b/>
+            </c>
+        </filter>
+    </get>
+</rpc>
\ No newline at end of file
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-root-subtrees.xml
new file mode 100644 (file)
index 0000000..92fe728
--- /dev/null
@@ -0,0 +1,8 @@
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <get>
+        <filter xmlns:ns0="urn:ietf:params:xml:ns:netconf:base:1.0" ns0:type="subtree">
+            <c xmlns="test:namespace"/>
+            <e xmlns="test:namespace"/>
+        </filter>
+    </get>
+</rpc>
\ No newline at end of file
diff --git a/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml b/netconf/netconf-util/src/test/resources/netconfMessages/get-with-multiple-subtrees.xml
new file mode 100644 (file)
index 0000000..334095a
--- /dev/null
@@ -0,0 +1,15 @@
+<rpc message-id="m-0" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <get>
+        <filter xmlns:ns0="urn:ietf:params:xml:ns:netconf:base:1.0" ns0:type="subtree">
+            <c xmlns="test:namespace">
+                <a/>
+                <d>
+                    <x/>
+                </d>
+            </c>
+            <e xmlns="test:namespace">
+                <z/>
+            </e>
+        </filter>
+    </get>
+</rpc>
\ No newline at end of file
diff --git a/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java b/netconf/sal-netconf-connector/src/main/java/org/opendaylight/netconf/sal/connect/netconf/util/FieldsFilter.java
new file mode 100644 (file)
index 0000000..cdbf857
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright © 2020 FRINX s.r.o. All rights reserved.
+ * Copyright © 2021 PANTHEON.tech, s.r.o.
+ *
+ * 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.sal.connect.netconf.util;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+
+/**
+ * Definition of the subtree filter with single parent path and possibly multiple field sub-paths that are used
+ * for reading/selection of specific entities.
+ */
+@Beta
+public final class FieldsFilter implements Immutable {
+    private final @NonNull YangInstanceIdentifier path;
+    private final @NonNull List<YangInstanceIdentifier> fields;
+
+    private FieldsFilter(final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
+        this.path = requireNonNull(path);
+        this.fields = ImmutableList.copyOf(fields);
+    }
+
+    /**
+     * Create a {@link FieldsFilter} using parent path and fields. Field paths are relative to parent path.
+     *
+     * @param path   parent query path
+     * @param fields list of specific selection fields
+     * @return instance of {@link FieldsFilter}
+     * @throws NullPointerException if any argument is null, or if {@code fields} contains a null element
+     */
+    public static @NonNull FieldsFilter of(final YangInstanceIdentifier path,
+            final List<YangInstanceIdentifier> fields) {
+        return new FieldsFilter(path, fields);
+    }
+
+    /**
+     * Get parent path.
+     *
+     * @return instance of {@link YangInstanceIdentifier}
+     */
+    public @NonNull YangInstanceIdentifier path() {
+        return path;
+    }
+
+    /**
+     * Get list of paths that narrows the filter for specific fields. Field paths are relative to parent path.
+     *
+     * @return {@link List} of field paths.
+     */
+    public @NonNull List<YangInstanceIdentifier> fields() {
+        return fields;
+    }
+}
\ No newline at end of file
index d3bbb95797ed1446fc341fbaa08d8b3c2d91c630..8df170e8fc34f934a93db7c6bae9c6c9d6ef0cb7 100644 (file)
@@ -11,6 +11,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.COMMIT_RPC_CONTENT;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.EDIT_CONTENT_NODEID;
+import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.GET_RPC_CONTENT;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_CANDIDATE_QNAME;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_COMMIT_QNAME;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_COPY_CONFIG_NODEID;
@@ -37,13 +38,18 @@ import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTr
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toFilterStructure;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toId;
 
+import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
 import org.opendaylight.mdsal.dom.api.DOMRpcService;
@@ -208,27 +214,93 @@ public final class NetconfBaseOps {
         return future;
     }
 
+    private ListenableFuture<? extends DOMRpcResult> getConfig(final FutureCallback<DOMRpcResult> callback,
+            final QName datastore, final Optional<YangInstanceIdentifier> filterPath,
+            final List<YangInstanceIdentifier> fields) {
+        requireNonNull(callback);
+        requireNonNull(datastore);
+
+        final NormalizedNode<?, ?> rpcInput;
+        if (isFilterPresent(filterPath)) {
+            final DataContainerChild<?, ?> node = transformer.toFilterStructure(
+                    Collections.singletonList(FieldsFilter.of(filterPath.get(), fields)));
+            rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_NODEID, getSourceNode(datastore), node);
+        } else if (containsEmptyPath(fields)) {
+            rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_NODEID, getSourceNode(datastore));
+        } else {
+            final DataContainerChild<?, ?> subtreeFilter = getSubtreeFilterFromRootFields(fields);
+            rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_CONFIG_NODEID,
+                    getSourceNode(datastore), subtreeFilter);
+        }
+        final ListenableFuture<? extends DOMRpcResult> response = rpc.invokeRpc(NETCONF_GET_CONFIG_QNAME, rpcInput);
+        Futures.addCallback(response, callback, MoreExecutors.directExecutor());
+        return response;
+    }
+
+    /**
+     * Calling GET-CONFIG RPC with subtree filter that is specified by {@link YangInstanceIdentifier}.
+     *
+     * @param callback   RPC response callback
+     * @param filterPath path to requested data
+     * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance
+     */
     public ListenableFuture<Optional<NormalizedNode<?, ?>>> getConfigRunningData(
             final FutureCallback<DOMRpcResult> callback, final Optional<YangInstanceIdentifier> filterPath) {
         return extractData(filterPath, getConfigRunning(callback, filterPath));
     }
 
+    /**
+     * Calling GET-CONFIG RPC with subtree filter tha tis specified by parent {@link YangInstanceIdentifier} and list
+     * of specific fields that caller would like to read. Field paths are relative to parent path.
+     *
+     * @param callback   RPC response callback
+     * @param filterPath parent path to requested data
+     * @param fields     paths to specific fields that are selected under parent path
+     * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance
+     */
     public ListenableFuture<Optional<NormalizedNode<?, ?>>> getConfigRunningData(
             final FutureCallback<DOMRpcResult> callback, final Optional<YangInstanceIdentifier> filterPath,
             final List<YangInstanceIdentifier> fields) {
-        // FIXME: implement this method
-        throw new UnsupportedOperationException();
+        if (fields.isEmpty()) {
+            // RFC doesn't allow to build subtree filter that would expect just empty element in response
+            return Futures.immediateFailedFuture(new IllegalArgumentException(
+                "Failed to build NETCONF GET-CONFIG RPC: provided list of fields is empty; filter path: "
+                    + filterPath));
+        }
+        final ListenableFuture<? extends DOMRpcResult> response = getConfigRunning(callback, filterPath, fields);
+        return extractData(filterPath, response);
     }
 
+    /**
+     * Calling GET RPC with subtree filter that is specified by {@link YangInstanceIdentifier}.
+     *
+     * @param callback   RPC response callback
+     * @param filterPath path to requested data
+     * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance
+     */
     public ListenableFuture<Optional<NormalizedNode<?, ?>>> getData(final FutureCallback<DOMRpcResult> callback,
                                                                     final Optional<YangInstanceIdentifier> filterPath) {
         return extractData(filterPath, get(callback, filterPath));
     }
 
+    /**
+     * Calling GET RPC with subtree filter tha tis specified by parent {@link YangInstanceIdentifier} and list
+     * of specific fields that caller would like to read. Field paths are relative to parent path.
+     *
+     * @param callback   RPC response callback
+     * @param filterPath parent path to requested data
+     * @param fields     paths to specific fields that are selected under parent path
+     * @return asynchronous completion token with read {@link NormalizedNode} wrapped in {@link Optional} instance
+     */
     public ListenableFuture<Optional<NormalizedNode<?, ?>>> getData(final FutureCallback<DOMRpcResult> callback,
-            final Optional<YangInstanceIdentifier> path, final List<YangInstanceIdentifier> fields) {
-        // FIXME: implement this method
-        throw new UnsupportedOperationException();
+            final Optional<YangInstanceIdentifier> filterPath, final List<YangInstanceIdentifier> fields) {
+        if (fields.isEmpty()) {
+            // RFC doesn't allow to build subtree filter that would expect just empty element in response
+            return Futures.immediateFailedFuture(new IllegalArgumentException(
+                    "Failed to build NETCONF GET RPC: provided list of fields is empty; filter path: " + filterPath));
+        }
+        final ListenableFuture<? extends DOMRpcResult> response = get(callback, filterPath, fields);
+        return extractData(filterPath, response);
     }
 
     private ListenableFuture<Optional<NormalizedNode<?, ?>>> extractData(
@@ -247,6 +319,11 @@ public final class NetconfBaseOps {
         return getConfig(callback, NETCONF_RUNNING_QNAME, filterPath);
     }
 
+    private ListenableFuture<? extends DOMRpcResult> getConfigRunning(final FutureCallback<DOMRpcResult> callback,
+            final Optional<YangInstanceIdentifier> filterPath, final List<YangInstanceIdentifier> fields) {
+        return getConfig(callback, NETCONF_RUNNING_QNAME, filterPath, fields);
+    }
+
     public ListenableFuture<? extends DOMRpcResult> getConfigCandidate(final FutureCallback<DOMRpcResult> callback,
             final Optional<YangInstanceIdentifier> filterPath) {
         return getConfig(callback, NETCONF_CANDIDATE_QNAME, filterPath);
@@ -265,6 +342,46 @@ public final class NetconfBaseOps {
         return future;
     }
 
+    private ListenableFuture<? extends DOMRpcResult> get(final FutureCallback<DOMRpcResult> callback,
+            final Optional<YangInstanceIdentifier> filterPath, final List<YangInstanceIdentifier> fields) {
+        requireNonNull(callback);
+
+        final NormalizedNode<?, ?> rpcInput;
+        if (isFilterPresent(filterPath)) {
+            rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_NODEID, transformer.toFilterStructure(
+                    Collections.singletonList(FieldsFilter.of(filterPath.get(), fields))));
+        } else if (containsEmptyPath(fields)) {
+            rpcInput = GET_RPC_CONTENT;
+        } else {
+            final DataContainerChild<?, ?> subtreeFilter = getSubtreeFilterFromRootFields(fields);
+            rpcInput = NetconfMessageTransformUtil.wrap(NETCONF_GET_NODEID, subtreeFilter);
+        }
+        final ListenableFuture<? extends DOMRpcResult> response = rpc.invokeRpc(NETCONF_GET_QNAME, rpcInput);
+        Futures.addCallback(response, callback, MoreExecutors.directExecutor());
+        return response;
+    }
+
+    private static boolean containsEmptyPath(final List<YangInstanceIdentifier> fields) {
+        return fields.stream().anyMatch(YangInstanceIdentifier::isEmpty);
+    }
+
+    private DataContainerChild<?, ?> getSubtreeFilterFromRootFields(final List<YangInstanceIdentifier> fields) {
+        final Map<YangInstanceIdentifier, List<YangInstanceIdentifier>> getConfigEntries = fields.stream()
+                .map(fieldPath -> {
+                    final YangInstanceIdentifier rootPath = YangInstanceIdentifier.create(
+                            Iterables.limit(fieldPath.getPathArguments(), 1));
+                    final YangInstanceIdentifier updatedFieldPath = YangInstanceIdentifier.create(
+                            Iterables.skip(fieldPath.getPathArguments(), 1));
+                    return new SimpleEntry<>(rootPath, updatedFieldPath);
+                })
+                .collect(Collectors.groupingBy(SimpleEntry::getKey,
+                        Collectors.mapping(SimpleEntry::getValue, Collectors.toList())));
+        final List<FieldsFilter> fieldsFilters = getConfigEntries.keySet().stream()
+                .map(rootPath -> FieldsFilter.of(rootPath, getConfigEntries.get(rootPath)))
+                .collect(Collectors.toList());
+        return transformer.toFilterStructure(fieldsFilters);
+    }
+
     private static boolean isFilterPresent(final Optional<YangInstanceIdentifier> filterPath) {
         return filterPath.isPresent() && !filterPath.get().isEmpty();
     }
index 2f48d5403d106fe53f910f5e7faefc8f28dc49bc..7dcf80e4adc2a29d4b4395a20bfe960fe9cea875 100644 (file)
@@ -226,29 +226,65 @@ public final class NetconfMessageTransformUtil {
     public static final @NonNull DataContainerChild<?, ?> EMPTY_FILTER;
 
     static {
-        final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(),
-                Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString()));
-        element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(),
-                NETCONF_TYPE_QNAME.getLocalName(), SUBTREE);
-
-        EMPTY_FILTER = Builders.anyXmlBuilder().withNodeIdentifier(NETCONF_FILTER_NODEID)
-                .withValue(new DOMSource(element)).build();
+        final Element element = getNetconfFilterElement();
+        EMPTY_FILTER = buildFilterStructure(element);
     }
 
+    /**
+     * Creation of the subtree filter structure using {@link YangInstanceIdentifier} path.
+     *
+     * @param identifier parent path / query
+     * @param ctx        mountpoint schema context
+     * @return created DOM structure with subtree filter
+     */
     public static DataContainerChild<?, ?> toFilterStructure(final YangInstanceIdentifier identifier,
                                                              final EffectiveModelContext ctx) {
-        final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(),
-                Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString()));
-        element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(), NETCONF_TYPE_QNAME.getLocalName(),
-                SUBTREE);
-
+        final Element element = getNetconfFilterElement();
         try {
             NetconfUtil.writeFilter(identifier, new DOMResult(element), SchemaPath.ROOT, ctx);
         } catch (IOException | XMLStreamException e) {
             throw new IllegalStateException("Unable to serialize filter element for path " + identifier, e);
         }
+        return buildFilterStructure(element);
+    }
+
+    /**
+     * Creation of the subtree filter structure using list of parent {@link YangInstanceIdentifier}
+     * and specific selection fields. Field paths are relative to parent query path.
+     *
+     * @param fieldsFilters list of: parent path and selection fields
+     * @param ctx           mountpoint schema context
+     * @return created DOM structure with subtree filter
+     */
+    public static DataContainerChild<?, ?> toFilterStructure(final List<FieldsFilter> fieldsFilters,
+                                                             final EffectiveModelContext ctx) {
+        Preconditions.checkState(!fieldsFilters.isEmpty(), "An empty list of subtree filters is not allowed");
+        final Element element = getNetconfFilterElement();
+
+        for (final FieldsFilter filter : fieldsFilters) {
+            try {
+                NetconfUtil.writeFilter(filter.path(), new DOMResult(element), SchemaPath.ROOT, ctx, filter.fields());
+            } catch (IOException | XMLStreamException e) {
+                throw new IllegalStateException(String.format(
+                        "Unable to serialize filter element for path %s with fields: %s",
+                        filter.path(), filter.fields()), e);
+            }
+        }
+        return buildFilterStructure(element);
+    }
+
+    private static Element getNetconfFilterElement() {
+        final Element element = XmlUtil.createElement(BLANK_DOCUMENT, NETCONF_FILTER_QNAME.getLocalName(),
+                Optional.of(NETCONF_FILTER_QNAME.getNamespace().toString()));
+        element.setAttributeNS(NETCONF_FILTER_QNAME.getNamespace().toString(), NETCONF_TYPE_QNAME.getLocalName(),
+                SUBTREE);
+        return element;
+    }
 
-        return Builders.anyXmlBuilder().withNodeIdentifier(NETCONF_FILTER_NODEID).withValue(new DOMSource(element))
+    private static DataContainerChild<?, ?> buildFilterStructure(final Element element) {
+        return Builders.anyXmlBuilder()
+                .withNodeIdentifier(NETCONF_FILTER_NODEID)
+                .withValue(new DOMSource(element))
                 .build();
     }
 
index 57e0a547147bb614340813f966a02154bfa81f08..d7d39039facc24f6c2111b0f301b1a5835ae48a3 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.netconf.sal.connect.netconf.util;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
+import java.util.List;
 import java.util.Optional;
 import javax.xml.stream.XMLStreamException;
 import org.opendaylight.netconf.api.ModifyAction;
@@ -69,4 +70,10 @@ class NetconfRpcStructureTransformer implements RpcStructureTransformer {
         // FIXME: propagate MountPointContext
         return NetconfMessageTransformUtil.toFilterStructure(path, mountContext.getEffectiveModelContext());
     }
+
+    @Override
+    public DataContainerChild<?, ?> toFilterStructure(final List<FieldsFilter> fieldsFilters) {
+        // FIXME: propagate MountPointContext
+        return NetconfMessageTransformUtil.toFilterStructure(fieldsFilters, mountContext.getEffectiveModelContext());
+    }
 }
index 17959667b31c48ee5309d2b119a45f97c4c8fc6a..26e6159faa35bbe90096dd8184a32b8be0f98247 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.netconf.sal.connect.netconf.util;
 
+import java.util.List;
 import java.util.Optional;
 import org.opendaylight.netconf.api.ModifyAction;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@@ -22,6 +23,7 @@ interface RpcStructureTransformer {
     /**
      * Transforms data and path to the config element structure. It means creating of parent xml structure
      * specified by path and appending data to the structure. Operation is set as attribute on data element.
+     *
      * @param data data
      * @param dataPath path, where data will be written
      * @param operation operation
@@ -32,14 +34,25 @@ interface RpcStructureTransformer {
 
     /**
      * Transforms path to filter structure.
+     *
      * @param path path
      * @return filter structure
      */
     DataContainerChild<?,?> toFilterStructure(YangInstanceIdentifier path);
 
+    /**
+     * Transforms list of fields filters to filter structure.
+     * Field paths are relative to parent query path.
+     *
+     * @param fieldsFilters list of: parent path and selection fields
+     * @return filter structure
+     */
+    DataContainerChild<?,?> toFilterStructure(List<FieldsFilter> fieldsFilters);
+
     /**
      * Selects data specified by path from data node. Data must be product of get-config rpc with filter created by
-     * {@link #toFilterStructure(YangInstanceIdentifier)} with same path.
+     * {@link #toFilterStructure(YangInstanceIdentifier)} or {@link #toFilterStructure(List)} )} with same path.
+     *
      * @param data data
      * @param path path to select
      * @return selected data
index 936fdf2f863d7b7f647f22ec16565e7098b53001..372b356bfb7cd1261bd3becb4cccac0c29c1cd46 100644 (file)
@@ -137,6 +137,13 @@ class SchemalessRpcStructureTransformer implements RpcStructureTransformer {
                 .build();
     }
 
+    @Override
+    public DataContainerChild<?, ?> toFilterStructure(final List<FieldsFilter> fieldsFilters) {
+        // todo: implementation of this feature
+        throw new UnsupportedOperationException(
+                "Creation of filter structure using fields for schemaless mountpoint is not supported");
+    }
+
     private static void checkDataValidForPath(final YangInstanceIdentifier dataPath, final Element dataNode) {
         //if datapath is empty, consider dataNode to be a root node
         if (dataPath.isEmpty()) {
index 3e21a60279d021d768e6f303f50ccc2171d9d214..b4480433e5afd28b00b828ed577ee98842c33d31 100644 (file)
@@ -7,28 +7,37 @@
  */
 package org.opendaylight.netconf.sal.connect.netconf.sal;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
+import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME;
 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_QNAME;
 
+import com.google.common.collect.ClassToInstanceMap;
 import java.net.InetSocketAddress;
 import java.util.Arrays;
+import java.util.Collections;
 import org.junit.AfterClass;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 import org.opendaylight.mdsal.binding.runtime.spi.BindingRuntimeHelpers;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataBrokerExtension;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
 import org.opendaylight.mdsal.dom.api.DOMRpcService;
 import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult;
+import org.opendaylight.netconf.dom.api.tx.NetconfDOMDataBrokerFieldsExtension;
+import org.opendaylight.netconf.dom.api.tx.NetconfDOMFieldsReadTransaction;
+import org.opendaylight.netconf.dom.api.tx.NetconfDOMFieldsReadWriteTransaction;
 import org.opendaylight.netconf.sal.connect.netconf.listener.NetconfSessionPreferences;
 import org.opendaylight.netconf.sal.connect.netconf.sal.tx.AbstractWriteTx;
 import org.opendaylight.netconf.sal.connect.netconf.sal.tx.WriteCandidateRunningTx;
@@ -41,6 +50,7 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.mon
 import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext;
 import org.opendaylight.yangtools.util.concurrent.FluentFutures;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 
@@ -70,40 +80,60 @@ public class NetconfDeviceDataBrokerTest {
     }
 
     @Test
-    public void testNewReadOnlyTransaction() throws Exception {
+    public void testNewReadOnlyTransaction() {
         final DOMDataTreeReadTransaction tx = dataBroker.newReadOnlyTransaction();
         tx.read(LogicalDatastoreType.OPERATIONAL, null);
         verify(rpcService).invokeRpc(eq(NETCONF_GET_QNAME), any(ContainerNode.class));
     }
 
     @Test
-    public void testNewReadWriteTransaction() throws Exception {
+    public void testNewReadWriteTransaction() {
         final DOMDataTreeReadWriteTransaction tx = dataBroker.newReadWriteTransaction();
         tx.read(LogicalDatastoreType.OPERATIONAL, null);
         verify(rpcService).invokeRpc(eq(NETCONF_GET_QNAME), any(ContainerNode.class));
     }
 
     @Test
-    public void testWritableRunningCandidateWriteTransaction() throws Exception {
+    public void testWritableRunningCandidateWriteTransaction() {
         testWriteTransaction(
                 WriteCandidateRunningTx.class, NetconfMessageTransformUtil.NETCONF_RUNNING_WRITABLE_URI.toString(),
                 NetconfMessageTransformUtil.NETCONF_CANDIDATE_URI.toString());
     }
 
     @Test
-    public void testCandidateWriteTransaction() throws Exception {
+    public void testCandidateWriteTransaction() {
         testWriteTransaction(WriteCandidateTx.class, NetconfMessageTransformUtil.NETCONF_CANDIDATE_URI.toString());
     }
 
     @Test
-    public void testRunningWriteTransaction() throws Exception {
+    public void testRunningWriteTransaction() {
         testWriteTransaction(WriteRunningTx.class, NetconfMessageTransformUtil.NETCONF_RUNNING_WRITABLE_URI.toString());
     }
 
+    @Test
+    public void testDOMFieldsExtensions() {
+        final ClassToInstanceMap<DOMDataBrokerExtension> extensions = dataBroker.getExtensions();
+        final NetconfDOMDataBrokerFieldsExtension fieldsExtension = extensions.getInstance(
+                NetconfDOMDataBrokerFieldsExtension.class);
+        assertNotNull(fieldsExtension);
+
+        // read-only transaction
+        final NetconfDOMFieldsReadTransaction roTx = fieldsExtension.newReadOnlyTransaction();
+        roTx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(),
+                Collections.singletonList(YangInstanceIdentifier.empty()));
+        verify(rpcService).invokeRpc(Mockito.eq(NETCONF_GET_CONFIG_QNAME), any(ContainerNode.class));
+
+        // read-write transaction
+        final NetconfDOMFieldsReadWriteTransaction rwTx = fieldsExtension.newReadWriteTransaction();
+        rwTx.read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.empty(),
+                Collections.singletonList(YangInstanceIdentifier.empty()));
+        verify(rpcService).invokeRpc(Mockito.eq(NETCONF_GET_QNAME), any(ContainerNode.class));
+    }
+
     private void testWriteTransaction(final Class<? extends AbstractWriteTx> transaction,
             final String... capabilities) {
         NetconfDeviceDataBroker db = getDataBroker(capabilities);
-        Assert.assertEquals(transaction, db.newWriteOnlyTransaction().getClass());
+        assertEquals(transaction, db.newWriteOnlyTransaction().getClass());
     }
 
     private NetconfDeviceDataBroker getDataBroker(final String... caps) {
diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadOnlyTxTest.java
new file mode 100644 (file)
index 0000000..dc07c94
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2020 FRINX 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
+ */
+package org.opendaylight.netconf.sal.connect.netconf.sal.tx;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME;
+import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_QNAME;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMRpcService;
+import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult;
+import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps;
+import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
+import org.opendaylight.yangtools.rfc8528.data.api.MountPointContext;
+import org.opendaylight.yangtools.util.concurrent.FluentFutures;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class FieldsAwareReadOnlyTxTest {
+    @Mock
+    private DOMRpcService rpc;
+    @Mock
+    private NormalizedNode<?, ?> mockedNode;
+
+    @Test
+    public void testReadWithFields() {
+        doReturn(FluentFutures.immediateFluentFuture(new DefaultDOMRpcResult(mockedNode))).when(rpc)
+            .invokeRpc(any(QName.class), any(ContainerNode.class));
+
+        final NetconfBaseOps netconfOps = new NetconfBaseOps(rpc, mock(MountPointContext.class));
+        try (FieldsAwareReadOnlyTx readOnlyTx = new FieldsAwareReadOnlyTx(netconfOps,
+                new RemoteDeviceId("a", new InetSocketAddress("localhost", 196)))) {
+
+            readOnlyTx.read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.empty(),
+                List.of(YangInstanceIdentifier.empty()));
+            verify(rpc).invokeRpc(eq(NETCONF_GET_CONFIG_QNAME), any(ContainerNode.class));
+
+            readOnlyTx.read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.empty());
+            verify(rpc).invokeRpc(eq(NETCONF_GET_QNAME), any(ContainerNode.class));
+        }
+    }
+}
\ No newline at end of file
diff --git a/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java b/netconf/sal-netconf-connector/src/test/java/org/opendaylight/netconf/sal/connect/netconf/sal/tx/FieldsAwareReadWriteTxTest.java
new file mode 100644 (file)
index 0000000..b7fedd9
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2020 FRINX 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
+ */
+package org.opendaylight.netconf.sal.connect.netconf.sal.tx;
+
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
+import org.opendaylight.netconf.dom.api.tx.NetconfDOMFieldsReadTransaction;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+
+@RunWith(MockitoJUnitRunner.StrictStubs.class)
+public class FieldsAwareReadWriteTxTest {
+    @Mock
+    private NetconfDOMFieldsReadTransaction delegateReadTx;
+    @Mock
+    private DOMDataTreeWriteTransaction delegateWriteTx;
+
+    @Test
+    public void testReadWithFields() {
+        final FieldsAwareReadWriteTx tx = new FieldsAwareReadWriteTx(delegateReadTx, delegateWriteTx);
+        tx.read(LogicalDatastoreType.CONFIGURATION, TxTestUtils.getContainerId(),
+            List.of(YangInstanceIdentifier.empty()));
+        verify(delegateReadTx).read(LogicalDatastoreType.CONFIGURATION, TxTestUtils.getContainerId(),
+            List.of(YangInstanceIdentifier.empty()));
+    }
+}
\ No newline at end of file
index 8976d82002389fd4de3d7ddacce38b4cfdfdc4fe..e5e10393602b56861089d919afa069e19b211cf4 100644 (file)
@@ -32,11 +32,11 @@ public class ReadWriteTxTest {
     private DOMDataTreeReadTransaction delegateReadTx;
     @Mock
     private DOMDataTreeWriteTransaction delegateWriteTx;
-    private ReadWriteTx tx;
+    private ReadWriteTx<?> tx;
 
     @Before
     public void setUp() throws Exception {
-        tx = new ReadWriteTx(delegateReadTx, delegateWriteTx);
+        tx = new ReadWriteTx<>(delegateReadTx, delegateWriteTx);
     }
 
     @Test
@@ -78,7 +78,7 @@ public class ReadWriteTxTest {
 
     @Test
     public void getIdentifier() throws Exception {
-        final ReadWriteTx tx2 = new ReadWriteTx(null, null);
+        final ReadWriteTx<?> tx2 = new ReadWriteTx<>(null, null);
         assertNotEquals(tx.getIdentifier(), tx2.getIdentifier());
     }
 }
index 1a57aeeffd1bd8493a29fe68002e7bb9fda46d2c..2bfea943962aea83323355bc975b310509769439 100644 (file)
@@ -56,14 +56,21 @@ import org.opendaylight.netconf.api.NetconfMessage;
 import org.opendaylight.netconf.api.xml.XmlUtil;
 import org.opendaylight.netconf.sal.connect.netconf.AbstractBaseSchemasTest;
 import org.opendaylight.netconf.sal.connect.netconf.schema.NetconfRemoteSchemaYangSourceProvider;
+import org.opendaylight.netconf.sal.connect.netconf.util.FieldsFilter;
 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfBaseOps;
 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil;
 import org.opendaylight.netconf.util.NetconfUtil;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.IetfNetconfService;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.NetconfState;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Capabilities;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Datastores;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Schemas;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Sessions;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Statistics;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.datastores.Datastore;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.datastores.datastore.Locks;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.schemas.Schema;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.sessions.Session;
 import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@@ -812,6 +819,190 @@ public class NetconfMessageTransformerTest extends AbstractBaseSchemasTest {
         assertTrue(actionResult.getOutput().isEmpty());
     }
 
+    @Test
+    public void getTwoNonOverlappingFieldsTest() throws IOException, SAXException {
+        // preparation of the fields
+        final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME));
+        final YangInstanceIdentifier netconfStartTimeField = YangInstanceIdentifier.create(toId(Statistics.QNAME),
+                toId(QName.create(Statistics.QNAME, "netconf-start-time")));
+        final YangInstanceIdentifier datastoresField = YangInstanceIdentifier.create(toId(Datastores.QNAME));
+
+        // building filter structure and NETCONF message
+        final DataContainerChild<?, ?> filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of(
+                parentYiid, List.of(netconfStartTimeField, datastoresField))), SCHEMA);
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME,
+                NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure));
+
+        // testing
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get>\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\">\n"
+                + "<statistics>\n"
+                + "<netconf-start-time/>\n"
+                + "</statistics>\n"
+                + "<datastores/>\n"
+                + "</netconf-state>\n"
+                + "</filter>\n"
+                + "</get>\n"
+                + "</rpc>");
+    }
+
+    @Test
+    public void getOverlappingFieldsTest() throws IOException, SAXException {
+        // preparation of the fields
+        final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME));
+        final YangInstanceIdentifier capabilitiesField = YangInstanceIdentifier.create(toId(Capabilities.QNAME));
+        final YangInstanceIdentifier capabilityField = YangInstanceIdentifier.create(toId(Capabilities.QNAME),
+                toId(QName.create(Capabilities.QNAME, "capability").intern()));
+        final YangInstanceIdentifier datastoreField = YangInstanceIdentifier.create(toId(Datastores.QNAME));
+        final YangInstanceIdentifier locksFields = YangInstanceIdentifier.create(toId(Datastores.QNAME),
+                toId(Datastore.QNAME), NodeIdentifierWithPredicates.of(Datastore.QNAME), toId(Locks.QNAME));
+
+        // building filter structure and NETCONF message
+        final DataContainerChild<?, ?> filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of(
+                parentYiid, List.of(capabilitiesField, capabilityField, datastoreField, locksFields))), SCHEMA);
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME,
+                NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure));
+
+        // testing
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get>\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\">\n"
+                + "<capabilities/>\n"
+                + "<datastores/>\n"
+                + "</netconf-state>\n"
+                + "</filter>\n"
+                + "</get>\n"
+                + "</rpc>");
+    }
+
+    @Test
+    public void getOverlappingFieldsWithEmptyFieldTest() throws IOException, SAXException {
+        // preparation of the fields
+        final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME));
+        final YangInstanceIdentifier capabilitiesField = YangInstanceIdentifier.create(toId(Capabilities.QNAME));
+
+        // building filter structure and NETCONF message
+        final DataContainerChild<?, ?> filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of(
+                parentYiid, List.of(capabilitiesField, YangInstanceIdentifier.empty()))), SCHEMA);
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME,
+                NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure));
+
+        // testing
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get>\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\"/>\n"
+                + "</filter>\n"
+                + "</get>\n"
+                + "</rpc>");
+    }
+
+    @Test
+    public void getSpecificFieldsUnderListTest() throws IOException, SAXException {
+        // preparation of the fields
+        final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME),
+                toId(Schemas.QNAME), toId(Schema.QNAME), NodeIdentifierWithPredicates.of(Schema.QNAME));
+        final YangInstanceIdentifier versionField = YangInstanceIdentifier.create(
+                toId(QName.create(Schema.QNAME, "version").intern()));
+        final YangInstanceIdentifier identifierField = YangInstanceIdentifier.create(
+                toId(QName.create(Schema.QNAME, "identifier").intern()));
+
+        // building filter structure and NETCONF message
+        final DataContainerChild<?, ?> filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of(
+                parentYiid, List.of(versionField, identifierField))), SCHEMA);
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME,
+                NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure));
+
+        // testing
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get>\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\">\n"
+                + "<schemas>\n"
+                + "<schema>\n"
+                + "<version/>\n"
+                + "<identifier/>\n"
+                + "</schema>\n"
+                + "</schemas>\n"
+                + "</netconf-state>\n"
+                + "</filter>\n"
+                + "</get>\n"
+                + "</rpc>");
+    }
+
+    @Test
+    public void getWholeListsUsingFieldsTest() throws IOException, SAXException {
+        // preparation of the fields
+        final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME));
+        final YangInstanceIdentifier datastoreListField = YangInstanceIdentifier.create(toId(Datastores.QNAME),
+                toId(Datastore.QNAME), NodeIdentifierWithPredicates.of(Datastore.QNAME));
+        final YangInstanceIdentifier sessionListField = YangInstanceIdentifier.create(toId(Sessions.QNAME),
+                toId(Session.QNAME), NodeIdentifierWithPredicates.of(Session.QNAME));
+
+        // building filter structure and NETCONF message
+        final DataContainerChild<?, ?> filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of(
+                parentYiid, List.of(datastoreListField, sessionListField))), SCHEMA);
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME,
+                NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure));
+
+        // testing
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get>\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\">\n"
+                + "<datastores>\n"
+                + "<datastore/>\n"
+                + "</datastores>\n"
+                + "<sessions>\n"
+                + "<session/>\n"
+                + "</sessions>\n"
+                + "</netconf-state>\n"
+                + "</filter>\n"
+                + "</get>\n"
+                + "</rpc>");
+    }
+
+    @Test
+    public void getSpecificListEntriesWithSpecificFieldsTest() throws IOException, SAXException {
+        // preparation of the fields
+        final YangInstanceIdentifier parentYiid = YangInstanceIdentifier.create(toId(NetconfState.QNAME),
+                toId(Sessions.QNAME));
+        final QName sessionId = QName.create(Session.QNAME, "session-id").intern();
+        final YangInstanceIdentifier session1Field = YangInstanceIdentifier.create(toId(Session.QNAME),
+                NodeIdentifierWithPredicates.of(Session.QNAME, sessionId, 1));
+        final YangInstanceIdentifier session2TransportField = YangInstanceIdentifier.create(toId(Session.QNAME),
+                NodeIdentifierWithPredicates.of(Session.QNAME, sessionId, 2),
+                toId(QName.create(Session.QNAME, "transport").intern()));
+
+        // building filter structure and NETCONF message
+        final DataContainerChild<?, ?> filterStructure = toFilterStructure(Collections.singletonList(FieldsFilter.of(
+                parentYiid, List.of(session1Field, session2TransportField))), SCHEMA);
+        final NetconfMessage netconfMessage = netconfMessageTransformer.toRpcRequest(NETCONF_GET_QNAME,
+                NetconfMessageTransformUtil.wrap(toId(NETCONF_GET_QNAME), filterStructure));
+
+        // testing
+        assertSimilarXml(netconfMessage, "<rpc message-id=\"m-0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+                + "<get>\n"
+                + "<filter xmlns:ns0=\"urn:ietf:params:xml:ns:netconf:base:1.0\" ns0:type=\"subtree\">\n"
+                + "<netconf-state xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring\">\n"
+                + "<sessions>\n"
+                + "<session>\n"
+                + "<session-id>1</session-id>\n"
+                + "</session>\n"
+                + "<session>\n"
+                + "<session-id>2</session-id>\n"
+                + "<transport/>\n"
+                + "</session>\n"
+                + "</sessions>\n"
+                + "</netconf-state>\n"
+                + "</filter>\n"
+                + "</get>\n"
+                + "</rpc>");
+    }
+
     private static void checkAction(final QName actionQname, final Node action , final String inputLocalName,
             final String inputNodeName, final String inputValue) {
         checkNode(action, actionQname.getLocalName(), actionQname.getLocalName(),
@@ -833,8 +1024,8 @@ public class NetconfMessageTransformerTest extends AbstractBaseSchemasTest {
         Node messageId = baseRpc.getAttributes().getNamedItem("message-id");
         assertNotNull(messageId);
         assertTrue(messageId.getNodeValue().contains("m-"));
-
         Node childAction = baseRpc.getFirstChild();
+
         checkNode(childAction, "action", "action", NetconfMessageTransformUtil.NETCONF_ACTION_NAMESPACE.toString());
         return childAction;
     }
index 1b015aaa6195f3933114fb7a256457c82b89987e..f027faa72c82d8b8af5fdc61a54f41eb0a2ac1fd 100644 (file)
@@ -7,6 +7,9 @@
  */
 package org.opendaylight.netconf.sal.connect.netconf.util;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -16,12 +19,14 @@ import static org.mockito.Mockito.when;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.InetSocketAddress;
+import java.net.URI;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.ExecutionException;
 import org.custommonkey.xmlunit.Diff;
 import org.custommonkey.xmlunit.XMLUnit;
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,8 +46,11 @@ import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
 import org.opendaylight.netconf.util.NetconfUtil;
 import org.opendaylight.yangtools.rcf8528.data.util.EmptyMountPointContext;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
@@ -54,7 +62,24 @@ import org.xml.sax.SAXException;
 
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
 public class NetconfBaseOpsTest extends AbstractTestModelTest {
-    private static final QName CONTAINER_Q_NAME = QName.create("test:namespace", "2013-07-22", "c");
+    private static final QNameModule TEST_MODULE = QNameModule.create(
+            URI.create("test:namespace"), Revision.of("2013-07-22"));
+
+    private static final QName CONTAINER_C_QNAME = QName.create(TEST_MODULE, "c");
+    private static final NodeIdentifier CONTAINER_C_NID = NodeIdentifier.create(CONTAINER_C_QNAME);
+    private static final QName LEAF_A_QNAME = QName.create(TEST_MODULE, "a");
+    private static final NodeIdentifier LEAF_A_NID = NodeIdentifier.create(LEAF_A_QNAME);
+    private static final QName LEAF_B_QNAME = QName.create(TEST_MODULE, "b");
+    private static final NodeIdentifier LEAF_B_NID = NodeIdentifier.create(LEAF_B_QNAME);
+    private static final QName CONTAINER_D_QNAME = QName.create(TEST_MODULE, "d");
+    private static final NodeIdentifier CONTAINER_D_NID = NodeIdentifier.create(CONTAINER_D_QNAME);
+    private static final QName LEAF_X_QNAME = QName.create(TEST_MODULE, "x");
+    private static final NodeIdentifier LEAF_X_NID = NodeIdentifier.create(LEAF_X_QNAME);
+
+    private static final QName CONTAINER_E_QNAME = QName.create(TEST_MODULE, "e");
+    private static final NodeIdentifier CONTAINER_E_NID = NodeIdentifier.create(CONTAINER_E_QNAME);
+    private static final QName LEAF_Z_QNAME = QName.create(TEST_MODULE, "z");
+    private static final NodeIdentifier LEAF_Z_NID = NodeIdentifier.create(LEAF_Z_QNAME);
 
     static {
         XMLUnit.setIgnoreWhitespace(true);
@@ -178,16 +203,16 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest {
     public void testGetConfigRunningData() throws Exception {
         final Optional<NormalizedNode<?, ?>> dataOpt =
                 baseOps.getConfigRunningData(callback, Optional.of(YangInstanceIdentifier.empty())).get();
-        Assert.assertTrue(dataOpt.isPresent());
-        Assert.assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType());
+        assertTrue(dataOpt.isPresent());
+        assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType());
     }
 
     @Test
     public void testGetData() throws Exception {
         final Optional<NormalizedNode<?, ?>> dataOpt =
                 baseOps.getData(callback, Optional.of(YangInstanceIdentifier.empty())).get();
-        Assert.assertTrue(dataOpt.isPresent());
-        Assert.assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType());
+        assertTrue(dataOpt.isPresent());
+        assertEquals(NetconfUtil.NETCONF_DATA_QNAME, dataOpt.get().getNodeType());
     }
 
     @Test
@@ -205,7 +230,7 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest {
     @Test
     public void testGetConfigCandidateWithFilter() throws Exception {
         final YangInstanceIdentifier id = YangInstanceIdentifier.builder()
-                .node(CONTAINER_Q_NAME)
+                .node(CONTAINER_C_QNAME)
                 .build();
         baseOps.getConfigCandidate(callback, Optional.of(id));
         verifyMessageSent("getConfig_candidate-filter", NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME);
@@ -219,14 +244,13 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest {
 
     @Test
     public void testEditConfigCandidate() throws Exception {
-        final QName leafQName = QName.create(CONTAINER_Q_NAME, "a");
         final LeafNode<Object> leaf = Builders.leafBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(leafQName))
+                .withNodeIdentifier(LEAF_A_NID)
                 .withValue("leaf-value")
                 .build();
         final YangInstanceIdentifier leafId = YangInstanceIdentifier.builder()
-                .node(CONTAINER_Q_NAME)
-                .node(leafQName)
+                .node(CONTAINER_C_QNAME)
+                .node(LEAF_A_NID)
                 .build();
         final DataContainerChild<?, ?> structure = baseOps.createEditConfigStrcture(Optional.of(leaf),
                 Optional.of(ModifyAction.REPLACE), leafId);
@@ -236,15 +260,13 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest {
 
     @Test
     public void testEditConfigRunning() throws Exception {
-        final QName containerQName = QName.create("test:namespace", "2013-07-22", "c");
-        final QName leafQName = QName.create(containerQName, "a");
         final LeafNode<Object> leaf = Builders.leafBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(leafQName))
+                .withNodeIdentifier(LEAF_A_NID)
                 .withValue("leaf-value")
                 .build();
         final YangInstanceIdentifier leafId = YangInstanceIdentifier.builder()
-                .node(containerQName)
-                .node(leafQName)
+                .node(CONTAINER_C_NID)
+                .node(LEAF_A_NID)
                 .build();
         final DataContainerChild<?, ?> structure = baseOps.createEditConfigStrcture(Optional.of(leaf),
                 Optional.of(ModifyAction.REPLACE), leafId);
@@ -252,6 +274,76 @@ public class NetconfBaseOpsTest extends AbstractTestModelTest {
         verifyMessageSent("edit-config-test-module-running", NetconfMessageTransformUtil.NETCONF_EDIT_CONFIG_QNAME);
     }
 
+    @Test
+    public void testGetWithFields() throws ExecutionException, InterruptedException {
+        final YangInstanceIdentifier path = YangInstanceIdentifier.create(CONTAINER_C_NID);
+        final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(LEAF_A_NID);
+        final YangInstanceIdentifier leafBField = YangInstanceIdentifier.create(LEAF_B_NID);
+
+        baseOps.getData(callback, Optional.of(path), List.of(leafAField, leafBField)).get();
+        verify(listener).sendRequest(msg("/netconfMessages/get-fields-request.xml"),
+                eq(NetconfMessageTransformUtil.NETCONF_GET_QNAME));
+    }
+
+    @Test
+    public void testGetConfigWithFields() throws ExecutionException, InterruptedException {
+        final YangInstanceIdentifier path = YangInstanceIdentifier.create(CONTAINER_C_NID);
+        final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(LEAF_A_NID);
+        final YangInstanceIdentifier leafBField = YangInstanceIdentifier.create(LEAF_B_NID);
+
+        baseOps.getConfigRunningData(callback, Optional.of(path), List.of(leafAField, leafBField)).get();
+        verify(listener).sendRequest(msg("/netconfMessages/get-config-fields-request.xml"),
+                eq(NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME));
+    }
+
+    @Test
+    public void testGetDataWithoutFields() {
+        assertThrows(ExecutionException.class, () -> baseOps.getData(callback,
+                Optional.of(YangInstanceIdentifier.empty()), Collections.emptyList()).get());
+    }
+
+    @Test
+    public void getConfigRunningDataWithoutFields() {
+        assertThrows(ExecutionException.class, () -> baseOps.getConfigRunningData(callback,
+                Optional.of(YangInstanceIdentifier.empty()), Collections.emptyList()).get());
+    }
+
+    @Test
+    public void testGetWithFieldsAndEmptyParentPath() throws ExecutionException, InterruptedException {
+        final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(CONTAINER_C_NID, LEAF_A_NID);
+        final YangInstanceIdentifier leafXField = YangInstanceIdentifier.create(
+                CONTAINER_C_NID, CONTAINER_D_NID, LEAF_X_NID);
+        final YangInstanceIdentifier leafZField = YangInstanceIdentifier.create(CONTAINER_E_NID, LEAF_Z_NID);
+
+        baseOps.getData(callback, Optional.of(YangInstanceIdentifier.empty()),
+                List.of(leafAField, leafXField, leafZField)).get();
+        verify(listener).sendRequest(msg("/netconfMessages/get-with-multiple-subtrees.xml"),
+                eq(NetconfMessageTransformUtil.NETCONF_GET_QNAME));
+    }
+
+    @Test
+    public void testGetConfigWithFieldsAndEmptyParentPath() throws ExecutionException, InterruptedException {
+        final YangInstanceIdentifier leafAField = YangInstanceIdentifier.create(CONTAINER_C_NID, LEAF_A_NID);
+        final YangInstanceIdentifier leafXField = YangInstanceIdentifier.create(
+                CONTAINER_C_NID, CONTAINER_D_NID, LEAF_X_NID);
+        final YangInstanceIdentifier leafZField = YangInstanceIdentifier.create(CONTAINER_E_NID, LEAF_Z_NID);
+
+        baseOps.getConfigRunningData(callback, Optional.of(YangInstanceIdentifier.empty()),
+                List.of(leafAField, leafXField, leafZField)).get();
+        verify(listener).sendRequest(msg("/netconfMessages/get-config-with-multiple-subtrees.xml"),
+                eq(NetconfMessageTransformUtil.NETCONF_GET_CONFIG_QNAME));
+    }
+
+    @Test
+    public void testGetWithRootFieldsAndEmptyParentPath() throws ExecutionException, InterruptedException {
+        final YangInstanceIdentifier contCField = YangInstanceIdentifier.create(CONTAINER_C_NID);
+        final YangInstanceIdentifier contDField = YangInstanceIdentifier.create(CONTAINER_E_NID);
+
+        baseOps.getData(callback, Optional.of(YangInstanceIdentifier.empty()), List.of(contCField, contDField)).get();
+        verify(listener).sendRequest(msg("/netconfMessages/get-with-multiple-root-subtrees.xml"),
+                eq(NetconfMessageTransformUtil.NETCONF_GET_QNAME));
+    }
+
     private void verifyMessageSent(final String fileName, final QName name) {
         final String path = "/netconfMessages/" + fileName + ".xml";
         verify(listener).sendRequest(msg(path), eq(name));
index cd732fca70e1d7d7a4a53cd5e2e7f48cff9c1f64..7430f3308bb405ac50857292013760d940d8baac 100644 (file)
@@ -13,6 +13,21 @@ module test-module {
         leaf a {
             type string;
         }
+
+        leaf b {
+            type string;
+        }
+
+        container d {
+            leaf x {
+                type boolean;
+            }
+        }
     }
 
+    container e {
+        leaf z {
+            type uint8;
+        }
+    }
 }