Eliminate NormalizedNodePayload
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / NetconfRestconfStrategy.java
index 4cb04a0b461a3fc8dd227016f243a12368d6311d..fe5ec4b5d31de6024f69119a3160fc569dad6e85 100644 (file)
@@ -9,21 +9,58 @@ package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.util.concurrent.FluentFuture;
+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;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.common.api.ReadFailedException;
+import org.opendaylight.mdsal.dom.api.DOMActionService;
+import org.opendaylight.mdsal.dom.api.DOMMountPointService;
+import org.opendaylight.mdsal.dom.api.DOMRpcService;
+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.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
+import org.opendaylight.restconf.server.api.DatabindContext;
+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.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+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.
@@ -31,62 +68,137 @@ import org.slf4j.LoggerFactory;
  * @see NetconfDataTreeService
  */
 public final class NetconfRestconfStrategy extends RestconfStrategy {
-    private static final Logger LOG = LoggerFactory.getLogger(NetconfRestconfStrategy.class);
-
     private final NetconfDataTreeService netconfService;
 
-    public NetconfRestconfStrategy(final NetconfDataTreeService netconfService) {
+    public NetconfRestconfStrategy(final DatabindContext databind, final NetconfDataTreeService netconfService,
+            final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService,
+            final @Nullable YangTextSourceExtension sourceProvider,
+            final @Nullable DOMMountPointService mountPointService) {
+        super(databind, ImmutableMap.of(), rpcService, actionService, sourceProvider, mountPointService);
         this.netconfService = requireNonNull(netconfService);
     }
 
     @Override
-    public RestconfTransaction prepareWriteExecution() {
-        return new NetconfRestconfTransaction(netconfService);
+    RestconfTransaction prepareWriteExecution() {
+        return new NetconfRestconfTransaction(modelContext(), netconfService);
     }
 
     @Override
-    public ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
+    void delete(final SettableRestconfFuture<Empty> future, final ServerRequest request,
             final YangInstanceIdentifier path) {
-        switch (store) {
-            case CONFIGURATION:
-                return netconfService.getConfig(path);
-            case OPERATIONAL:
-                return netconfService.get(path);
-            default:
-                LOG.info("Unknown datastore type: {}.", store);
-                throw new IllegalArgumentException(String.format(
-                        "%s, Cannot read data %s for %s datastore, unknown datastore type",
-                        netconfService.getDeviceId(), path, store));
-        }
+        final var tx = prepareWriteExecution();
+        tx.delete(path);
+        Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
+            @Override
+            public void onSuccess(final CommitInfo result) {
+                future.set(Empty.value());
+            }
+
+            @Override
+            public void onFailure(final Throwable cause) {
+                future.setFailure(TransactionUtil.decodeException(cause, "DELETE", path, modelContext()));
+            }
+        }, MoreExecutors.directExecutor());
     }
 
     @Override
-    public ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
-            final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
-        switch (store) {
-            case CONFIGURATION:
-                return netconfService.getConfig(path, fields);
-            case OPERATIONAL:
-                return netconfService.get(path, fields);
-            default:
-                LOG.info("Unknown datastore type: {}.", store);
-                throw new IllegalArgumentException(String.format(
-                        "%s, Cannot read data %s with fields %s for %s datastore, unknown datastore type",
-                        netconfService.getDeviceId(), path, fields, store));
+    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 = fieldsParamToPaths(path.inference().modelContext(), path.schema(), fields);
+            } catch (RestconfDocumentedException e) {
+                return RestconfFuture.failed(e);
+            }
+            fieldPaths = tmp.isEmpty() ? null : tmp;
+        } else {
+            fieldPaths = null;
+        }
+
+        final NormalizedNode node;
+        if (fieldPaths != null) {
+            node = readData(params.content(), path.instance(), params.withDefaults(), fieldPaths);
+        } else {
+            node = readData(params.content(), path.instance(), params.withDefaults());
         }
+        return completeDataGET(node, path, NormalizedNodeWriterFactory.of(params.depth()), null);
     }
 
     @Override
-    public FluentFuture<Boolean> exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
-        return remapException(read(store, path))
-                .transform(optionalNode -> optionalNode != null && optionalNode.isPresent(),
-                        MoreExecutors.directExecutor());
+    ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
+            final YangInstanceIdentifier path) {
+        return switch (store) {
+            case CONFIGURATION -> netconfService.getConfig(path);
+            case OPERATIONAL -> netconfService.get(path);
+        };
     }
 
-    private static <T> FluentFuture<T> remapException(final ListenableFuture<T> input) {
-        final SettableFuture<T> ret = SettableFuture.create();
-        Futures.addCallback(input, new FutureCallback<T>() {
+    private ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
+            final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
+        return switch (store) {
+            case CONFIGURATION -> netconfService.getConfig(path, fields);
+            case OPERATIONAL -> netconfService.get(path, fields);
+        };
+    }
 
+    /**
+     * Read specific type of data from data store via transaction with specified subtrees that should only be read.
+     * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
+     *
+     * @param content  type of data to read (config, state, all)
+     * @param path     the parent path to read
+     * @param withDefa value of with-defaults parameter
+     * @param fields   paths to selected subtrees which should be read, relative to the parent path
+     * @return {@link NormalizedNode}
+     */
+    // FIXME: NETCONF-1155: this method should asynchronous
+    public @Nullable NormalizedNode readData(final @NonNull ContentParam content,
+            final @NonNull YangInstanceIdentifier path, final @Nullable WithDefaultsParam withDefa,
+            final @NonNull List<YangInstanceIdentifier> fields) {
+        return switch (content) {
+            case ALL -> {
+                // PREPARE STATE DATA NODE
+                final var stateDataNode = readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
+                // PREPARE CONFIG DATA NODE
+                final var configDataNode = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
+
+                yield mergeConfigAndSTateDataIfNeeded(stateDataNode, withDefa == null ? configDataNode
+                    : prepareDataByParamWithDef(configDataNode, path, withDefa.mode()));
+            }
+            case CONFIG -> {
+                final var read = readDataViaTransaction(LogicalDatastoreType.CONFIGURATION, path, fields);
+                yield withDefa == null ? read : prepareDataByParamWithDef(read, path, withDefa.mode());
+            }
+            case NONCONFIG -> readDataViaTransaction(LogicalDatastoreType.OPERATIONAL, path, fields);
+        };
+    }
+
+    /**
+     * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
+     * specified subtrees that should only be read.
+     *
+     * @param store                 datastore type
+     * @param path                  parent path to selected fields
+     * @param fields                paths to selected subtrees which should be read, relative to the parent path
+     * @return {@link NormalizedNode}
+     */
+    private @Nullable NormalizedNode readDataViaTransaction(final @NonNull LogicalDatastoreType store,
+            final @NonNull YangInstanceIdentifier path, final @NonNull List<YangInstanceIdentifier> fields) {
+        return TransactionUtil.syncAccess(read(store, path, fields), path).orElse(null);
+    }
+
+    @Override
+    ListenableFuture<Boolean> exists(final YangInstanceIdentifier path) {
+        return Futures.transform(remapException(netconfService.getConfig(path)),
+            optionalNode -> optionalNode != null && optionalNode.isPresent(),
+            MoreExecutors.directExecutor());
+    }
+
+    private static <T> ListenableFuture<T> remapException(final ListenableFuture<T> input) {
+        final var ret = SettableFuture.<T>create();
+        Futures.addCallback(input, new FutureCallback<>() {
             @Override
             public void onSuccess(final T result) {
                 ret.set(result);
@@ -98,6 +210,168 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
                     : new ReadFailedException("NETCONF operation failed", cause));
             }
         }, MoreExecutors.directExecutor());
-        return FluentFuture.from(ret);
+        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);
+        }
     }
 }