Teach RFC8040 URL parser about actions 66/82866/12
authorajay.dp001 <ajay.deep.singh@ericsson.com>
Mon, 1 Jul 2019 11:40:03 +0000 (12:40 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 29 Jul 2019 22:42:23 +0000 (00:42 +0200)
RFC8040 allows invocation of actions, but unfortunately our
parser is not equipped to deal with them.

This updates the utilities to try to locate actions when the
data is not found.

JIRA: NETCONF-619
Change-Id: I93d69ea76fd928f963cb7a8c703c97c125820818
Signed-off-by: ajay.dp001 <ajay.deep.singh@ericsson.com>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserIdentifier.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierDeserializer.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserIdentifierTest.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierDeserializerTest.java
restconf/restconf-nb-rfc8040/src/test/resources/parser-identifier/example-actions.yang [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/restconf/parser/deserializer/example-actions.yang [new file with mode: 0644]

index cadc7854fb9cba1c5d1c4116bdca8ae64721e500..a909150ff789d7f65b62751d23b7eeaa0e16870a 100644 (file)
@@ -16,6 +16,8 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
 import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
@@ -29,12 +31,16 @@ import org.opendaylight.restconf.nb.rfc8040.utils.validations.RestconfValidation
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
+import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
 import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -44,6 +50,7 @@ import org.slf4j.LoggerFactory;
 public final class ParserIdentifier {
 
     private static final Logger LOG = LoggerFactory.getLogger(ParserIdentifier.class);
+    private static final Splitter MP_SPLITTER = Splitter.on("/" + RestconfConstants.MOUNT);
 
     private ParserIdentifier() {
         throw new UnsupportedOperationException("Util class.");
@@ -72,64 +79,63 @@ public final class ParserIdentifier {
             final String identifier,
             final SchemaContext schemaContext,
             final Optional<DOMMountPointService> mountPointService) {
-        if (identifier != null && identifier.contains(RestconfConstants.MOUNT)) {
-            if (!mountPointService.isPresent()) {
-                throw new RestconfDocumentedException("Mount point service is not available");
-            }
-
-            final Iterator<String> pathsIt = Splitter.on("/" + RestconfConstants.MOUNT).split(identifier).iterator();
-
-            final String mountPointId = pathsIt.next();
-            final YangInstanceIdentifier mountYangInstanceIdentifier = IdentifierCodec.deserialize(
-                    mountPointId, schemaContext);
-            final Optional<DOMMountPoint> mountPoint =
-                    mountPointService.get().getMountPoint(mountYangInstanceIdentifier);
-
-            if (!mountPoint.isPresent()) {
-                throw new RestconfDocumentedException(
-                        "Mount point does not exist.", ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
-            }
+        if (identifier == null || !identifier.contains(RestconfConstants.MOUNT)) {
+            return createIIdContext(IdentifierCodec.deserialize(identifier, schemaContext), schemaContext, null);
+        }
+        if (!mountPointService.isPresent()) {
+            throw new RestconfDocumentedException("Mount point service is not available");
+        }
 
-            final DOMMountPoint domMountPoint = mountPoint.get();
-            final SchemaContext mountSchemaContext = domMountPoint.getSchemaContext();
+        final Iterator<String> pathsIt = MP_SPLITTER.split(identifier).iterator();
+        final String mountPointId = pathsIt.next();
+        final YangInstanceIdentifier mountPath = IdentifierCodec.deserialize(mountPointId, schemaContext);
+        final DOMMountPoint mountPoint = mountPointService.get().getMountPoint(mountPath)
+                .orElseThrow(() -> new RestconfDocumentedException("Mount point does not exist.",
+                    ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
+
+        final SchemaContext mountSchemaContext = mountPoint.getSchemaContext();
+        final String pathId = pathsIt.next().replaceFirst("/", "");
+        return createIIdContext(IdentifierCodec.deserialize(pathId, mountSchemaContext), mountSchemaContext,
+            mountPoint);
+    }
 
-            final String pathId = pathsIt.next().replaceFirst("/", "");
-            final YangInstanceIdentifier pathYangInstanceIdentifier = IdentifierCodec.deserialize(
-                    pathId, mountSchemaContext);
+    /**
+     * Method to create {@link InstanceIdentifierContext} from {@link YangInstanceIdentifier}
+     * and {@link SchemaContext}, {@link DOMMountPoint}.
+     *
+     * @param yangIId
+     *               - instance identifier
+     * @param schemaContext
+     *               - schema context
+     * @param domMountPoint
+     *               - optional mount point context
+     * @return {@link InstanceIdentifierContext}
+     */
+    private static InstanceIdentifierContext<?> createIIdContext(final YangInstanceIdentifier yangIId,
+            final SchemaContext schemaContext, final @Nullable DOMMountPoint domMountPoint) {
+
+        // Deal with data nodes first
+        final DataSchemaContextTree contextTree = DataSchemaContextTree.from(schemaContext);
+        final Optional<DataSchemaContextNode<?>> child = contextTree.findChild(yangIId);
+        if (child.isPresent()) {
+            return new InstanceIdentifierContext<SchemaNode>(yangIId, child.get().getDataSchemaNode(), domMountPoint,
+                schemaContext);
+        }
 
-            final DataSchemaContextNode<?> child = DataSchemaContextTree.from(mountSchemaContext)
-                .getChild(pathYangInstanceIdentifier);
-            if (child != null) {
-                return new InstanceIdentifierContext<SchemaNode>(pathYangInstanceIdentifier, child.getDataSchemaNode(),
-                        domMountPoint, mountSchemaContext);
-            }
-            final QName rpcQName = pathYangInstanceIdentifier.getLastPathArgument().getNodeType();
-            RpcDefinition def = null;
-            for (final RpcDefinition rpcDefinition : mountSchemaContext
-                    .findModule(rpcQName.getModule()).get().getRpcs()) {
-                if (rpcDefinition.getQName().getLocalName().equals(rpcQName.getLocalName())) {
-                    def = rpcDefinition;
-                    break;
-                }
-            }
-            return new InstanceIdentifierContext<>(pathYangInstanceIdentifier, def, domMountPoint, mountSchemaContext);
+        // That failed, now try to recover by searching through the schema context
+        final List<QName> qnames = yangIId.getPathArguments().stream()
+            .filter(pathArgument -> pathArgument instanceof NodeIdentifier).map(PathArgument::getNodeType)
+            .collect(Collectors.toList());
+        final SchemaNode schemaNode = SchemaContextUtil.findNodeInSchemaContext(schemaContext, qnames);
+        if (schemaNode instanceof RpcDefinition) {
+            return new InstanceIdentifierContext<>(yangIId, (RpcDefinition) schemaNode, domMountPoint, schemaContext);
+        } else if (schemaNode instanceof ActionDefinition) {
+            return new InstanceIdentifierContext<>(yangIId, (ActionDefinition) schemaNode, domMountPoint,
+                    schemaContext);
+        } else if (schemaNode == null) {
+            throw new IllegalArgumentException("Failed to find schema for " + qnames);
         } else {
-            final YangInstanceIdentifier deserialize = IdentifierCodec.deserialize(identifier, schemaContext);
-            final DataSchemaContextNode<?> child = DataSchemaContextTree.from(schemaContext).getChild(deserialize);
-
-            if (child != null) {
-                return new InstanceIdentifierContext<SchemaNode>(
-                            deserialize, child.getDataSchemaNode(), null, schemaContext);
-            }
-            final QName rpcQName = deserialize.getLastPathArgument().getNodeType();
-            RpcDefinition def = null;
-            for (final RpcDefinition rpcDefinition : schemaContext.findModule(rpcQName.getModule()).get().getRpcs()) {
-                if (rpcDefinition.getQName().getLocalName().equals(rpcQName.getLocalName())) {
-                    def = rpcDefinition;
-                    break;
-                }
-            }
-            return new InstanceIdentifierContext<>(deserialize, def, null, schemaContext);
+            throw new IllegalStateException("Unhandled schema " + schemaNode);
         }
     }
 
index 89b6fc899b1e387366fc2aacfcb9d72063e62834..4fb775da008ff4854724d34b4fa772e12fc9f842 100644 (file)
@@ -7,6 +7,8 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
 
+import static java.util.Objects.requireNonNull;
+
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -31,7 +33,10 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdent
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
+import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
+import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
@@ -40,6 +45,7 @@ import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
@@ -324,8 +330,12 @@ public final class YangInstanceIdentifierDeserializer {
     @SuppressFBWarnings("NP_NULL_ON_SOME_PATH") // code does check for null 'current' but FB doesn't recognize it
     private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
             final MainVarsWrapper variables) {
-        variables.setCurrent(variables.getCurrent().getChild(qname));
-        DataSchemaContextNode<?> current = variables.getCurrent();
+        final DataSchemaContextNode<?> initialContext = variables.getCurrent();
+        final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
+
+        DataSchemaContextNode<?> current = initialContext.getChild(qname);
+        variables.setCurrent(current);
+
         if (current == null) {
             final Optional<Module> module = variables.getSchemaContext().findModule(qname.getModule());
             if (module.isPresent()) {
@@ -335,6 +345,9 @@ public final class YangInstanceIdentifierDeserializer {
                     }
                 }
             }
+            if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
+                return null;
+            }
         }
         checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
                 variables.getOffset());
@@ -371,17 +384,24 @@ public final class YangInstanceIdentifierDeserializer {
     private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
         if (dataSchemaNode instanceof ContainerSchemaNode) {
-            final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
-            final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
-                    nodeName);
-            return node.getQName();
+            return getQNameOfDataSchemaNode((ContainerSchemaNode) dataSchemaNode, nodeName);
         } else if (dataSchemaNode instanceof ListSchemaNode) {
-            final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
-            final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
-                    nodeName);
-            return node.getQName();
+            return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
+        }
+
+        throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
+    }
+
+    private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
+            final T parent, String nodeName) {
+        final Optional<ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
+        final SchemaNode node;
+        if (actionDef.isPresent()) {
+            node = actionDef.get();
+        } else {
+            node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
         }
-        throw new UnsupportedOperationException();
+        return node.getQName();
     }
 
     private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
@@ -425,6 +445,16 @@ public final class YangInstanceIdentifierDeserializer {
         return variables.getOffset() == variables.getData().length();
     }
 
+    private static Optional<ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
+            final String nodeName) {
+        requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
+        if (dataSchemaNode instanceof ActionNodeContainer) {
+            return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
+                    .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
+        }
+        return Optional.empty();
+    }
+
     private static final class MainVarsWrapper {
         private static final int STARTING_OFFSET = 0;
 
index c2158038b17e04cec931b1b9d850c1c89c280e49..349e62f873f7e03ef1b354fac51460f57b312261 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.restconf.nb.rfc8040.utils.parser;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.when;
 
@@ -86,6 +87,7 @@ public class ParserIdentifierTest {
     private static final String TEST_MODULE_NAMESPACE = "test:module";
 
     private static final String INVOKE_RPC = "invoke-rpc-module:rpc-test";
+    private static final String INVOKE_ACTION = "example-actions:interfaces/interface=eth0/reset";
 
     // mount point and mount point service
     private DOMMountPoint mountPoint;
@@ -694,4 +696,45 @@ public class ParserIdentifierTest {
         assertEquals(this.mountPoint, result.getMountPoint());
         assertEquals(this.schemaContextOnMountPoint, result.getSchemaContext());
     }
+
+    /**
+     * Test Action.
+     * Verify if Action schema node was found.
+     */
+    @Test
+    public void invokeActionTest() {
+        final InstanceIdentifierContext<?> result = ParserIdentifier
+            .toInstanceIdentifier(INVOKE_ACTION, this.schemaContext, Optional.empty());
+
+        // Action schema node
+        final QName actionQName = result.getSchemaNode().getQName();
+        assertEquals("https://example.com/ns/example-actions", actionQName.getModule().getNamespace().toString());
+        assertEquals("reset", actionQName.getLocalName());
+
+        // other fields
+        assertEquals(IdentifierCodec.deserialize(INVOKE_ACTION, schemaContext), result.getInstanceIdentifier());
+        assertNull(result.getMountPoint());
+        assertSame(this.schemaContext, result.getSchemaContext());
+    }
+
+    /**
+     * Test invoke Action on mount point.
+     * Verify if Action schema node was found.
+     */
+    @Test
+    public void invokeActionOnMountPointTest() {
+        final InstanceIdentifierContext<?> result = ParserIdentifier
+            .toInstanceIdentifier(MOUNT_POINT_IDENT + "/" + INVOKE_ACTION, this.schemaContext,
+                Optional.of(this.mountPointService));
+
+        // Action schema node
+        final QName actionQName = result.getSchemaNode().getQName();
+        assertEquals("https://example.com/ns/example-actions", actionQName.getModule().getNamespace().toString());
+        assertEquals("reset", actionQName.getLocalName());
+
+        // other fields
+        assertEquals(IdentifierCodec.deserialize(INVOKE_ACTION, schemaContext), result.getInstanceIdentifier());
+        assertEquals(this.mountPoint, result.getMountPoint());
+        assertEquals(this.schemaContextOnMountPoint, result.getSchemaContext());
+    }
 }
index 8ff0572f0ee13875c4cfbf07d85272d749938166..fab501ce8f6b6df112c4deb0127d33728d3e927d 100644 (file)
@@ -121,6 +121,36 @@ public class YangInstanceIdentifierDeserializerTest {
                 iterator.next());
     }
 
+    /**
+     * Test of deserialization <code>String</code> URI with container containing list with Action to
+     * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
+     */
+    @Test
+    public void deserializeContainerWithListWithActionTest() {
+        final Iterable<YangInstanceIdentifier.PathArgument> result = YangInstanceIdentifierDeserializer
+            .create(this.schemaContext, "example-actions:interfaces/interface=eth0/reset");
+        assertEquals("Result does not contains expected number of path arguments", 4, Iterables.size(result));
+
+        final Iterator<YangInstanceIdentifier.PathArgument> iterator = result.iterator();
+
+        // container
+        assertEquals("Not expected path argument", YangInstanceIdentifier.NodeIdentifier
+                .create(QName.create("https://example.com/ns/example-actions", "2016-07-07", "interfaces")),
+            iterator.next());
+
+        // list
+        final QName list = QName.create("https://example.com/ns/example-actions", "2016-07-07", "interface");
+        assertEquals("Not expected path argument", YangInstanceIdentifier.NodeIdentifier.create(list), iterator.next());
+        assertEquals("Not expected path argument",
+            new YangInstanceIdentifier.NodeIdentifierWithPredicates(list, QName.create(list, "name"), "eth0"),
+            iterator.next());
+
+        // action QName
+        final QName action = QName.create("https://example.com/ns/example-actions", "2016-07-07", "reset");
+        assertEquals("Not expected path argument", YangInstanceIdentifier.NodeIdentifier.create(action),
+            iterator.next());
+    }
+
     /**
      * Test of deserialization <code>String</code> URI containing list with no keys to
      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/parser-identifier/example-actions.yang b/restconf/restconf-nb-rfc8040/src/test/resources/parser-identifier/example-actions.yang
new file mode 100644 (file)
index 0000000..950666e
--- /dev/null
@@ -0,0 +1,41 @@
+module example-actions {
+     yang-version 1.1;
+     namespace "https://example.com/ns/example-actions";
+     prefix "act";
+
+     organization "Example, Inc.";
+     contact "support at example.com";
+     description "Example Actions Data Model Module.";
+
+     revision "2016-07-07" {
+       description "Initial version.";
+       reference "example.com document 2-9973.";
+     }
+
+     container interfaces {
+       description "System interfaces.";
+
+       list interface {
+         key name;
+         description "One interface entry.";
+         leaf name {
+           type string;
+           description "Interface name.";
+         }
+
+         action reset {
+           description "Reset an interface.";
+           input {
+             leaf delay {
+               type uint32;
+               units "seconds";
+               default 0;
+               description
+                 "Number of seconds to wait before starting the
+                  interface reset.";
+             }
+           }
+         }
+       }
+     }
+   }
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/restconf/parser/deserializer/example-actions.yang b/restconf/restconf-nb-rfc8040/src/test/resources/restconf/parser/deserializer/example-actions.yang
new file mode 100644 (file)
index 0000000..e5bb3cd
--- /dev/null
@@ -0,0 +1,41 @@
+module example-actions {
+     yang-version 1.1;
+     namespace "https://example.com/ns/example-actions";
+     prefix "act";
+
+     organization "Example, Inc.";
+     contact "support at example.com";
+     description "Example Actions Data Model Module.";
+
+     revision "2016-07-07" {
+       description "Initial version.";
+       reference "example.com document 2-9973.";
+     }
+
+     container interfaces {
+       description "System interfaces.";
+
+      list interface {
+         key name;
+         description "One interface entry.";
+         leaf name {
+           type string;
+           description "Interface name.";
+         }
+
+         action reset {
+           description "Reset an interface.";
+           input {
+             leaf delay {
+               type uint32;
+               units "seconds";
+               default 0;
+               description
+                 "Number of seconds to wait before starting the
+                  interface reset.";
+             }
+           }
+         }
+       }
+     }
+   }