Eliminate NormalizedNodePayload
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / NetconfRestconfStrategy.java
index 3e6f94787b208b52e048de913c43de9bdb6ea294..fe5ec4b5d31de6024f69119a3160fc569dad6e85 100644 (file)
@@ -9,14 +9,19 @@ package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 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 com.google.common.util.concurrent.SettableFuture;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.mdsal.common.api.CommitInfo;
@@ -29,19 +34,33 @@ import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension;
 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
 import org.opendaylight.restconf.api.query.ContentParam;
+import org.opendaylight.restconf.api.query.FieldsParam;
+import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
 import org.opendaylight.restconf.api.query.WithDefaultsParam;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
-import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
 import org.opendaylight.restconf.server.api.DataGetParams;
 import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DatabindContext;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Data;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.ServerRequest;
+import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory;
 import org.opendaylight.yangtools.yang.common.Empty;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+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.util.DataSchemaContext;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 
 /**
  * Implementation of RESTCONF operations on top of a raw NETCONF backend.
@@ -65,7 +84,8 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
     }
 
     @Override
-    void delete(final SettableRestconfFuture<Empty> future, final YangInstanceIdentifier path) {
+    void delete(final SettableRestconfFuture<Empty> future, final ServerRequest request,
+            final YangInstanceIdentifier path) {
         final var tx = prepareWriteExecution();
         tx.delete(path);
         Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
@@ -82,14 +102,13 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
     }
 
     @Override
-    RestconfFuture<DataGetResult> dataGET(final Data path, final DataGetParams params) {
-        final var inference = path.inference();
+    RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final Data path, final DataGetParams params) {
         final var fields = params.fields();
         final List<YangInstanceIdentifier> fieldPaths;
         if (fields != null) {
             final List<YangInstanceIdentifier> tmp;
             try {
-                tmp = NetconfFieldsTranslator.translate(inference.modelContext(), path.schema(), fields);
+                tmp = fieldsParamToPaths(path.inference().modelContext(), path.schema(), fields);
             } catch (RestconfDocumentedException e) {
                 return RestconfFuture.failed(e);
             }
@@ -104,7 +123,7 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
         } else {
             node = readData(params.content(), path.instance(), params.withDefaults());
         }
-        return completeDataGET(inference, QueryParameters.of(params), node, null);
+        return completeDataGET(node, path, NormalizedNodeWriterFactory.of(params.depth()), null);
     }
 
     @Override
@@ -193,4 +212,166 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
         }, MoreExecutors.directExecutor());
         return ret;
     }
+
+    /**
+     * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
+     * {@link NetconfDataTreeService}.
+     *
+     * <p>
+     * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}.
+     * Using {@link LinkedPathElement} it is possible to create a chain of path arguments and build complete paths
+     * since this element contains identifiers of intermediary mixin nodes and also linked to its parent
+     * {@link LinkedPathElement}.
+     *
+     * <p>
+     * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
+     * <pre>
+     *   - './a' +- 'a/b' - 'b/c'
+     *           |
+     *           +- 'a/d' - 'd/x/e'
+     * </pre>
+     *
+     *
+     * @param modelContext EffectiveModelContext
+     * @param startNode Root DataSchemaNode
+     * @param input input value of fields parameter
+     * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument}
+     *         of provided {@code identifier}
+     */
+    @VisibleForTesting
+    static @NonNull List<YangInstanceIdentifier> fieldsParamToPaths(final @NonNull EffectiveModelContext modelContext,
+            final @NonNull DataSchemaContext startNode, final @NonNull FieldsParam input) {
+        final var parsed = new HashSet<LinkedPathElement>();
+        processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(),
+            new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors());
+        return parsed.stream().map(NetconfRestconfStrategy::buildPath).toList();
+    }
+
+    private static void processSelectors(final Set<LinkedPathElement> parsed, final EffectiveModelContext context,
+            final QNameModule startNamespace, final LinkedPathElement startPathElement,
+            final List<NodeSelector> selectors) {
+        for (var selector : selectors) {
+            var pathElement = startPathElement;
+            var namespace = startNamespace;
+
+            // Note: path is guaranteed to have at least one step
+            final var it = selector.path().iterator();
+            do {
+                final var step = it.next();
+                final var module = step.module();
+                if (module != null) {
+                    // FIXME: this is not defensive enough, as we can fail to find the module
+                    namespace = context.findModules(module).iterator().next().getQNameModule();
+                }
+
+                // add parsed path element linked to its parent
+                pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
+            } while (it.hasNext());
+
+            final var subs = selector.subSelectors();
+            if (!subs.isEmpty()) {
+                processSelectors(parsed, context, namespace, pathElement, subs);
+            } else {
+                parsed.add(pathElement);
+            }
+        }
+    }
+
+    private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
+            final QName childQName) {
+        final var collectedMixinNodes = new ArrayList<PathArgument>();
+
+        DataSchemaContext currentNode = currentElement.targetNode;
+        DataSchemaContext actualContextNode = childByQName(currentNode, childQName);
+        if (actualContextNode == null) {
+            actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType());
+            actualContextNode = childByQName(actualContextNode, childQName);
+        }
+
+        while (actualContextNode != null && actualContextNode instanceof PathMixin) {
+            final var actualDataSchemaNode = actualContextNode.dataSchemaNode();
+            if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) {
+                // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise
+                // we need both (which is the default case)
+                actualContextNode = childByQName(actualContextNode, childQName);
+            } else if (actualDataSchemaNode instanceof LeafListSchemaNode) {
+                // NodeWithValue is unusable - stop parsing
+                break;
+            } else {
+                collectedMixinNodes.add(actualContextNode.getPathStep());
+                actualContextNode = childByQName(actualContextNode, childQName);
+            }
+        }
+
+        if (actualContextNode == null) {
+            throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
+                + currentNode.getPathStep().getNodeType().getLocalName(),
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
+    }
+
+    private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
+        return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
+    }
+
+    private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
+        var pathElement = lastPathElement;
+        final var path = new LinkedList<PathArgument>();
+        do {
+            path.addFirst(contextPathArgument(pathElement.targetNode));
+            path.addAll(0, pathElement.mixinNodesToTarget);
+            pathElement = pathElement.parentPathElement;
+        } while (pathElement.parentPathElement != null);
+
+        return YangInstanceIdentifier.of(path);
+    }
+
+    private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) {
+        final var arg = context.pathStep();
+        if (arg != null) {
+            return arg;
+        }
+
+        final var schema = context.dataSchemaNode();
+        if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) {
+            return NodeIdentifierWithPredicates.of(listSchema.getQName());
+        }
+        if (schema instanceof LeafListSchemaNode leafListSchema) {
+            return new NodeWithValue<>(leafListSchema.getQName(), Empty.value());
+        }
+        throw new UnsupportedOperationException("Unsupported schema " + schema);
+    }
+
+    private static DataSchemaContext resolveMixinNode(final DataSchemaContext node,
+            final @NonNull QName qualifiedName) {
+        DataSchemaContext currentNode = node;
+        while (currentNode != null && currentNode instanceof PathMixin currentMixin) {
+            currentNode = currentMixin.childByQName(qualifiedName);
+        }
+        return currentNode;
+    }
+
+    /**
+     * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path
+     * element.<br>
+     *  - identifiers of mixin nodes on the path to the target node - required for construction of full valid
+     *    DOM paths,<br>
+     *  - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
+     *    of {@link PathArgument}s
+     *
+     * @param parentPathElement     parent path element
+     * @param mixinNodesToTarget    identifiers of mixin nodes on the path to the target node
+     * @param targetNode            target non-mixin node
+     */
+    private record LinkedPathElement(
+            @Nullable LinkedPathElement parentPathElement,
+            @NonNull List<PathArgument> mixinNodesToTarget,
+            @NonNull DataSchemaContext targetNode) {
+        LinkedPathElement {
+            requireNonNull(mixinNodesToTarget);
+            requireNonNull(targetNode);
+        }
+    }
 }