Refactor ParserFieldsParameter 29/98129/2
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 25 Oct 2021 19:54:57 +0000 (21:54 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 25 Oct 2021 22:15:57 +0000 (00:15 +0200)
We have two distinct parser tangled in a single file, making it hard
to understand what is going on. Split out the implementations and
rename the base class to have a more friendly name.

JIRA: NETCONF-773
Change-Id: Ida4eb4e39f802efbb85ea236d9e9c3b508d9ae34
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslator.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java [deleted file]
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslator.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslatorTest.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java [deleted file]
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslatorTest.java [new file with mode: 0644]

index d1fe9ec092b9db8e068e77b9c4ad6fe547506d17..d0ecbeba3b3a2fcbde0e88183c7ede64e37fede9 100644 (file)
@@ -8,8 +8,6 @@
 package org.opendaylight.restconf.nb.rfc8040.databind.jaxrs;
 
 import static java.util.Objects.requireNonNull;
-import static org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter.parseFieldsParameter;
-import static org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter.parseFieldsPaths;
 
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
@@ -39,6 +37,8 @@ import org.opendaylight.restconf.nb.rfc8040.StopTimeParam;
 import org.opendaylight.restconf.nb.rfc8040.WithDefaultsParam;
 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
 
@@ -112,8 +112,8 @@ public final class QueryParams {
         }
 
         return identifier.getMountPoint() != null
-            ? QueryParameters.ofFieldPaths(params, parseFieldsPaths(identifier, fields))
-                : QueryParameters.ofFields(params, parseFieldsParameter(identifier, fields));
+            ? QueryParameters.ofFieldPaths(params, NetconfFieldsTranslator.translate(identifier, fields))
+                : QueryParameters.ofFields(params, WriterFieldsTranslator.translate(identifier, fields));
     }
 
     /**
diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslator.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslator.java
new file mode 100644 (file)
index 0000000..6972323
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.utils.parser;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam.NodeSelector;
+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.util.DataSchemaContextNode;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+
+/**
+ * Utilities used for parsing of fields query parameter content.
+ *
+ * @param <T> type of identifier
+ */
+public abstract class AbstractFieldsTranslator<T> {
+    AbstractFieldsTranslator() {
+        // Hidden on purpose
+    }
+
+    /**
+     * Parse fields parameter and return complete list of child nodes organized into levels.
+     *
+     * @param identifier identifier context created from request URI
+     * @param input input value of fields parameter
+     * @return {@link List} of levels; each level contains {@link Set} of identifiers of type {@link T}
+     */
+    protected final @NonNull List<Set<T>> parseFields(final @NonNull InstanceIdentifierContext<?> identifier,
+                                                      final @NonNull FieldsParam input) {
+        final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
+                (DataSchemaNode) identifier.getSchemaNode());
+
+        if (startNode == null) {
+            throw new RestconfDocumentedException(
+                    "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        final List<Set<T>> parsed = new ArrayList<>();
+        processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
+            startNode, input.nodeSelectors());
+        return parsed;
+    }
+
+    /**
+     * Add parsed child of current node to result for current level.
+     *
+     * @param currentNode current node
+     * @param childQName parsed identifier of child node
+     * @param level current nodes level
+     * @return {@link DataSchemaContextNode}
+     */
+    protected abstract @NonNull DataSchemaContextNode<?> addChildToResult(@NonNull DataSchemaContextNode<?> currentNode,
+            @NonNull QName childQName, @NonNull Set<T> level);
+
+    private void processSelectors(final List<Set<T>> parsed, final EffectiveModelContext context,
+            final QNameModule startNamespace, final DataSchemaContextNode<?> startNode,
+            final List<NodeSelector> selectors) {
+        final Set<T> startLevel = new HashSet<>();
+        parsed.add(startLevel);
+
+        for (var selector : selectors) {
+            var node = startNode;
+            var namespace = startNamespace;
+            var level = startLevel;
+
+
+            // Note: path is guaranteed to have at least one step
+            final var it = selector.path().iterator();
+            while (true) {
+                // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
+                //        need to call it only when we know there is another identifier coming, otherwise we would end
+                //        up with empty levels sneaking into the mix.
+                //
+                //        Dealing with that weirdness requires understanding what the expected end results are and a
+                //        larger rewrite of the algorithms involved.
+                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 identifier to results for current level
+                node = addChildToResult(node, step.identifier().bindTo(namespace), level);
+                if (!it.hasNext()) {
+                    break;
+                }
+
+                // go one level down
+                level = prepareQNameLevel(parsed, level);
+            }
+
+            final var subs = selector.subSelectors();
+            if (!subs.isEmpty()) {
+                processSelectors(parsed, context, namespace, node, subs);
+            }
+        }
+    }
+
+    /**
+     * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
+     * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
+     * is allocated and pushed to input parsed identifiers.
+     *
+     * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
+     * @param currentLevel Current level of identifiers (set).
+     * @return Existing or new level of identifiers.
+     */
+    private Set<T> prepareQNameLevel(final List<Set<T>> parsedIdentifiers, final Set<T> currentLevel) {
+        final Optional<Set<T>> existingLevel = parsedIdentifiers.stream()
+                .filter(qNameSet -> qNameSet.equals(currentLevel))
+                .findAny();
+        if (existingLevel.isPresent()) {
+            final int index = parsedIdentifiers.indexOf(existingLevel.get());
+            if (index == parsedIdentifiers.size() - 1) {
+                final Set<T> nextLevel = new HashSet<>();
+                parsedIdentifiers.add(nextLevel);
+                return nextLevel;
+            }
+
+            return parsedIdentifiers.get(index + 1);
+        }
+
+        final Set<T> nextLevel = new HashSet<>();
+        parsedIdentifiers.add(nextLevel);
+        return nextLevel;
+    }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java
new file mode 100644 (file)
index 0000000..543791e
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Copyright © 2020 FRINX s.r.o. and others.  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.restconf.nb.rfc8040.utils.parser;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+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.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
+import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+
+/**
+ * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers
+ * in {@code netconf-dom-api}.
+ *
+ * <p>
+ * Fields parser that stores set of {@link LinkedPathElement}s in each level. 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 previous element.
+ *
+ * <p>
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
+ * <pre>
+ * level 0: ['./a', './d']
+ * level 1: ['a/b', '/d/x/e']
+ * level 2: ['b/c']
+ * </pre>
+ */
+public final class NetconfFieldsTranslator extends AbstractFieldsTranslator<NetconfFieldsTranslator.LinkedPathElement> {
+    private static final NetconfFieldsTranslator INSTANCE = new NetconfFieldsTranslator();
+
+    private NetconfFieldsTranslator() {
+        // Hidden on purpose
+    }
+
+    /**
+     * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
+     * {@link NetconfDataTreeService}.
+     *
+     * @param identifier identifier context created from request URI
+     * @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}
+     */
+    public static @NonNull List<YangInstanceIdentifier> translate(
+            final @NonNull InstanceIdentifierContext<?> identifier, final @NonNull FieldsParam input) {
+        final List<Set<LinkedPathElement>> levels = INSTANCE.parseFields(identifier, input);
+        final List<Map<PathArgument, LinkedPathElement>> mappedLevels = mapLevelsContentByIdentifiers(levels);
+        return buildPaths(mappedLevels);
+    }
+
+    private static List<YangInstanceIdentifier> buildPaths(
+            final List<Map<PathArgument, LinkedPathElement>> mappedLevels) {
+        final List<YangInstanceIdentifier> completePaths = new ArrayList<>();
+        // we must traverse levels from the deepest level to the top level, because each LinkedPathElement is only
+        // linked to previous element
+        for (int levelIndex = mappedLevels.size() - 1; levelIndex >= 0; levelIndex--) {
+            // we go through unprocessed LinkedPathElements that represent leaves
+            for (final LinkedPathElement pathElement : mappedLevels.get(levelIndex).values()) {
+                if (pathElement.processed) {
+                    // this element was already processed from the lower level - skip it
+                    continue;
+                }
+                pathElement.processed = true;
+
+                // adding deepest path arguments, LinkedList is used for more effective insertion at the 0 index
+                final LinkedList<PathArgument> path = new LinkedList<>(pathElement.mixinNodesToTarget);
+                path.add(pathElement.targetNodeIdentifier);
+
+                PathArgument previousIdentifier = pathElement.previousNodeIdentifier;
+                // adding path arguments from the linked LinkedPathElements recursively
+                for (int buildingLevel = levelIndex - 1; buildingLevel >= 0; buildingLevel--) {
+                    final LinkedPathElement previousElement = mappedLevels.get(buildingLevel).get(previousIdentifier);
+                    path.addFirst(previousElement.targetNodeIdentifier);
+                    path.addAll(0, previousElement.mixinNodesToTarget);
+                    previousIdentifier = previousElement.previousNodeIdentifier;
+                    previousElement.processed = true;
+                }
+                completePaths.add(YangInstanceIdentifier.create(path));
+            }
+        }
+        return completePaths;
+    }
+
+    private static List<Map<PathArgument, LinkedPathElement>> mapLevelsContentByIdentifiers(
+            final List<Set<LinkedPathElement>> levels) {
+        // this step is used for saving some processing power - we can directly find LinkedPathElement using
+        // representing PathArgument
+        return levels.stream()
+            .map(linkedPathElements -> linkedPathElements.stream()
+                .map(linkedPathElement -> new SimpleEntry<>(linkedPathElement.targetNodeIdentifier, linkedPathElement))
+                .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)))
+            .collect(Collectors.toList());
+    }
+
+    @Override
+    protected DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode,
+            final QName childQName, final Set<LinkedPathElement> level) {
+        final List<PathArgument> collectedMixinNodes = new ArrayList<>();
+
+        DataSchemaContextNode<?> actualContextNode = currentNode.getChild(childQName);
+        while (actualContextNode != null && actualContextNode.isMixin()) {
+            if (actualContextNode.getDataSchemaNode() instanceof ListSchemaNode) {
+                // we need just a single node identifier from list in the path (key is not available)
+                actualContextNode = actualContextNode.getChild(childQName);
+                break;
+            } else if (actualContextNode.getDataSchemaNode() instanceof LeafListSchemaNode) {
+                // NodeWithValue is unusable - stop parsing
+                break;
+            } else {
+                collectedMixinNodes.add(actualContextNode.getIdentifier());
+                actualContextNode = actualContextNode.getChild(childQName);
+            }
+        }
+
+        if (actualContextNode == null) {
+            throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
+                    + currentNode.getIdentifier().getNodeType().getLocalName(),
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+        final LinkedPathElement linkedPathElement = new LinkedPathElement(currentNode.getIdentifier(),
+                collectedMixinNodes, actualContextNode.getIdentifier());
+        level.add(linkedPathElement);
+        return actualContextNode;
+    }
+
+    /**
+     * {@link PathArgument} of data element grouped with identifiers of leading mixin nodes and previous node.<br>
+     *  - identifiers of mixin nodes on the path to the target node - required for construction of full valid
+     *    DOM paths,<br>
+     *  - identifier of the previous non-mixin node - required to successfully create a chain of {@link PathArgument}s
+     */
+    static final class LinkedPathElement {
+        private final PathArgument previousNodeIdentifier;
+        private final List<PathArgument> mixinNodesToTarget;
+        private final PathArgument targetNodeIdentifier;
+        private boolean processed = false;
+
+        /**
+         * Creation of new {@link LinkedPathElement}.
+         *
+         * @param previousNodeIdentifier identifier of the previous non-mixin node
+         * @param mixinNodesToTarget     identifiers of mixin nodes on the path to the target node
+         * @param targetNodeIdentifier   identifier of target non-mixin node
+         */
+        private LinkedPathElement(final PathArgument previousNodeIdentifier,
+                final List<PathArgument> mixinNodesToTarget, final PathArgument targetNodeIdentifier) {
+            this.previousNodeIdentifier = previousNodeIdentifier;
+            this.mixinNodesToTarget = mixinNodesToTarget;
+            this.targetNodeIdentifier = targetNodeIdentifier;
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            // this is need in order to make 'prepareQNameLevel(..)' working
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            final LinkedPathElement that = (LinkedPathElement) obj;
+            return targetNodeIdentifier.equals(that.targetNodeIdentifier);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(targetNodeIdentifier);
+        }
+    }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java
deleted file mode 100644 (file)
index f232531..0000000
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,
- * and is available at http://www.eclipse.org/legal/epl-v10.html
- */
-package org.opendaylight.restconf.nb.rfc8040.utils.parser;
-
-import java.util.AbstractMap.SimpleEntry;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
-import org.opendaylight.restconf.nb.rfc8040.FieldsParam.NodeSelector;
-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.PathArgument;
-import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
-import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
-
-/**
- * Utilities used for parsing of fields query parameter content.
- *
- * @param <T> type of identifier
- */
-public abstract class ParserFieldsParameter<T> {
-    private static final ParserFieldsParameter<QName> QNAME_PARSER = new QNameParser();
-    private static final ParserFieldsParameter<LinkedPathElement> PATH_PARSER = new PathParser();
-
-    private ParserFieldsParameter() {
-    }
-
-    /**
-     * Parse fields parameter and return complete list of child nodes organized into levels.
-     *
-     * @param identifier identifier context created from request URI
-     * @param input input value of fields parameter
-     * @return {@link List} of levels; each level contains set of {@link QName}
-     */
-    public static @NonNull List<Set<QName>> parseFieldsParameter(final @NonNull InstanceIdentifierContext<?> identifier,
-                                                                 final @NonNull FieldsParam input) {
-        return QNAME_PARSER.parseFields(identifier, input);
-    }
-
-    /**
-     * Parse fields parameter and return list of child node paths saved in lists.
-     *
-     * @param identifier identifier context created from request URI
-     * @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}
-     */
-    public static @NonNull List<YangInstanceIdentifier> parseFieldsPaths(
-            final @NonNull InstanceIdentifierContext<?> identifier, final @NonNull FieldsParam input) {
-        final List<Set<LinkedPathElement>> levels = PATH_PARSER.parseFields(identifier, input);
-        final List<Map<PathArgument, LinkedPathElement>> mappedLevels = mapLevelsContentByIdentifiers(levels);
-        return buildPaths(mappedLevels);
-    }
-
-    private static List<Map<PathArgument, LinkedPathElement>> mapLevelsContentByIdentifiers(
-            final List<Set<LinkedPathElement>> levels) {
-        // this step is used for saving some processing power - we can directly find LinkedPathElement using
-        // representing PathArgument
-        return levels.stream()
-                .map(linkedPathElements -> linkedPathElements.stream()
-                        .map(linkedPathElement -> new SimpleEntry<>(linkedPathElement.targetNodeIdentifier,
-                                linkedPathElement))
-                        .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)))
-                .collect(Collectors.toList());
-    }
-
-    private static List<YangInstanceIdentifier> buildPaths(
-            final List<Map<PathArgument, LinkedPathElement>> mappedLevels) {
-        final List<YangInstanceIdentifier> completePaths = new ArrayList<>();
-        // we must traverse levels from the deepest level to the top level, because each LinkedPathElement is only
-        // linked to previous element
-        for (int levelIndex = mappedLevels.size() - 1; levelIndex >= 0; levelIndex--) {
-            // we go through unprocessed LinkedPathElements that represent leaves
-            for (final LinkedPathElement pathElement : mappedLevels.get(levelIndex).values()) {
-                if (pathElement.processed) {
-                    // this element was already processed from the lower level - skip it
-                    continue;
-                }
-                pathElement.processed = true;
-
-                // adding deepest path arguments, LinkedList is used for more effective insertion at the 0 index
-                final LinkedList<PathArgument> path = new LinkedList<>(pathElement.mixinNodesToTarget);
-                path.add(pathElement.targetNodeIdentifier);
-
-                PathArgument previousIdentifier = pathElement.previousNodeIdentifier;
-                // adding path arguments from the linked LinkedPathElements recursively
-                for (int buildingLevel = levelIndex - 1; buildingLevel >= 0; buildingLevel--) {
-                    final LinkedPathElement previousElement = mappedLevels.get(buildingLevel).get(previousIdentifier);
-                    path.addFirst(previousElement.targetNodeIdentifier);
-                    path.addAll(0, previousElement.mixinNodesToTarget);
-                    previousIdentifier = previousElement.previousNodeIdentifier;
-                    previousElement.processed = true;
-                }
-                completePaths.add(YangInstanceIdentifier.create(path));
-            }
-        }
-        return completePaths;
-    }
-
-    /**
-     * Parse fields parameter and return complete list of child nodes organized into levels.
-     *
-     * @param identifier identifier context created from request URI
-     * @param input input value of fields parameter
-     * @return {@link List} of levels; each level contains {@link Set} of identifiers of type {@link T}
-     */
-    private @NonNull List<Set<T>> parseFields(final @NonNull InstanceIdentifierContext<?> identifier,
-                                              final @NonNull FieldsParam input) {
-        final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
-                (DataSchemaNode) identifier.getSchemaNode());
-
-        if (startNode == null) {
-            throw new RestconfDocumentedException(
-                    "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
-        }
-
-        final List<Set<T>> parsed = new ArrayList<>();
-        processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
-            startNode, input.nodeSelectors());
-        return parsed;
-    }
-
-    private void processSelectors(final List<Set<T>> parsed, final EffectiveModelContext context,
-            final QNameModule startNamespace, final DataSchemaContextNode<?> startNode,
-            final List<NodeSelector> selectors) {
-        final Set<T> startLevel = new HashSet<>();
-        parsed.add(startLevel);
-
-        for (var selector : selectors) {
-            var node = startNode;
-            var namespace = startNamespace;
-            var level = startLevel;
-
-
-            // Note: path is guaranteed to have at least one step
-            final var it = selector.path().iterator();
-            while (true) {
-                // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
-                //        need to call it only when we know there is another identifier coming, otherwise we would end
-                //        up with empty levels sneaking into the mix.
-                //
-                //        Dealing with that weirdness requires understanding what the expected end results are and a
-                //        larger rewrite of the algorithms involved.
-                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 identifier to results for current level
-                node = addChildToResult(node, step.identifier().bindTo(namespace), level);
-                if (!it.hasNext()) {
-                    break;
-                }
-
-                // go one level down
-                level = prepareQNameLevel(parsed, level);
-            }
-
-            final var subs = selector.subSelectors();
-            if (!subs.isEmpty()) {
-                processSelectors(parsed, context, namespace, node, subs);
-            }
-        }
-    }
-
-    /**
-     * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
-     * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
-     * is allocated and pushed to input parsed identifiers.
-     *
-     * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
-     * @param currentLevel Current level of identifiers (set).
-     * @return Existing or new level of identifiers.
-     */
-    private Set<T> prepareQNameLevel(final List<Set<T>> parsedIdentifiers, final Set<T> currentLevel) {
-        final Optional<Set<T>> existingLevel = parsedIdentifiers.stream()
-                .filter(qNameSet -> qNameSet.equals(currentLevel))
-                .findAny();
-        if (existingLevel.isPresent()) {
-            final int index = parsedIdentifiers.indexOf(existingLevel.get());
-            if (index == parsedIdentifiers.size() - 1) {
-                final Set<T> nextLevel = new HashSet<>();
-                parsedIdentifiers.add(nextLevel);
-                return nextLevel;
-            }
-
-            return parsedIdentifiers.get(index + 1);
-        }
-
-        final Set<T> nextLevel = new HashSet<>();
-        parsedIdentifiers.add(nextLevel);
-        return nextLevel;
-    }
-
-    /**
-     * Add parsed child of current node to result for current level.
-     *
-     * @param currentNode current node
-     * @param childQName parsed identifier of child node
-     * @param level current nodes level
-     * @return {@link DataSchemaContextNode}
-     */
-    abstract @NonNull DataSchemaContextNode<?> addChildToResult(@NonNull DataSchemaContextNode<?> currentNode,
-            @NonNull QName childQName, @NonNull Set<T> level);
-
-    /**
-     * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output
-     * it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary
-     * mixin nodes are also flatten to the same level as identifiers of data nodes.<br>
-     * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
-     * <pre>
-     * level 0: ['a', 'd']
-     * level 1: ['b', 'x', 'e']
-     * level 2: ['c']
-     * </pre>
-     */
-    private static final class QNameParser extends ParserFieldsParameter<QName> {
-        @Override
-        DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode, final QName childQName,
-                                                  final Set<QName> level) {
-            // resolve parent node
-            final DataSchemaContextNode<?> parentNode = resolveMixinNode(
-                    currentNode, level, currentNode.getIdentifier().getNodeType());
-            if (parentNode == null) {
-                throw new RestconfDocumentedException(
-                        "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
-                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
-            }
-
-            // resolve child node
-            final DataSchemaContextNode<?> childNode = resolveMixinNode(
-                    parentNode.getChild(childQName), level, childQName);
-            if (childNode == null) {
-                throw new RestconfDocumentedException(
-                        "Child " + childQName.getLocalName() + " node missing in "
-                                + currentNode.getIdentifier().getNodeType().getLocalName(),
-                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
-            }
-
-            // add final childNode node to level nodes
-            level.add(childNode.getIdentifier().getNodeType());
-            return childNode;
-        }
-
-        /**
-         * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
-         * All nodes expect of not mixin node are added to current level nodes.
-         *
-         * @param node          initial mixin or not-mixin node
-         * @param level         current nodes level
-         * @param qualifiedName qname of initial node
-         * @return {@link DataSchemaContextNode}
-         */
-        private static @Nullable DataSchemaContextNode<?> resolveMixinNode(
-                final @Nullable DataSchemaContextNode<?> node, final @NonNull Set<QName> level,
-                final @NonNull QName qualifiedName) {
-            DataSchemaContextNode<?> currentNode = node;
-            while (currentNode != null && currentNode.isMixin()) {
-                level.add(qualifiedName);
-                currentNode = currentNode.getChild(qualifiedName);
-            }
-
-            return currentNode;
-        }
-    }
-
-    /**
-     * Fields parser that stores set of {@link LinkedPathElement}s in each level. 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 previous element.<br>
-     * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
-     * <pre>
-     * level 0: ['./a', './d']
-     * level 1: ['a/b', '/d/x/e']
-     * level 2: ['b/c']
-     * </pre>
-     */
-    private static final class PathParser extends ParserFieldsParameter<LinkedPathElement> {
-        @Override
-        DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode, final QName childQName,
-                                                  final Set<LinkedPathElement> level) {
-            final List<PathArgument> collectedMixinNodes = new ArrayList<>();
-
-            DataSchemaContextNode<?> actualContextNode = currentNode.getChild(childQName);
-            while (actualContextNode != null && actualContextNode.isMixin()) {
-                if (actualContextNode.getDataSchemaNode() instanceof ListSchemaNode) {
-                    // we need just a single node identifier from list in the path (key is not available)
-                    actualContextNode = actualContextNode.getChild(childQName);
-                    break;
-                } else if (actualContextNode.getDataSchemaNode() instanceof LeafListSchemaNode) {
-                    // NodeWithValue is unusable - stop parsing
-                    break;
-                } else {
-                    collectedMixinNodes.add(actualContextNode.getIdentifier());
-                    actualContextNode = actualContextNode.getChild(childQName);
-                }
-            }
-
-            if (actualContextNode == null) {
-                throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
-                        + currentNode.getIdentifier().getNodeType().getLocalName(),
-                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
-            }
-            final LinkedPathElement linkedPathElement = new LinkedPathElement(currentNode.getIdentifier(),
-                    collectedMixinNodes, actualContextNode.getIdentifier());
-            level.add(linkedPathElement);
-            return actualContextNode;
-        }
-    }
-
-    /**
-     * {@link PathArgument} of data element grouped with identifiers of leading mixin nodes and previous node.<br>
-     *  - identifiers of mixin nodes on the path to the target node - required for construction of full valid
-     *    DOM paths,<br>
-     *  - identifier of the previous non-mixin node - required to successfully create a chain of {@link PathArgument}s
-     */
-    private static final class LinkedPathElement {
-        private final PathArgument previousNodeIdentifier;
-        private final List<PathArgument> mixinNodesToTarget;
-        private final PathArgument targetNodeIdentifier;
-        private boolean processed = false;
-
-        /**
-         * Creation of new {@link LinkedPathElement}.
-         *
-         * @param previousNodeIdentifier identifier of the previous non-mixin node
-         * @param mixinNodesToTarget     identifiers of mixin nodes on the path to the target node
-         * @param targetNodeIdentifier   identifier of target non-mixin node
-         */
-        private LinkedPathElement(final PathArgument previousNodeIdentifier,
-                final List<PathArgument> mixinNodesToTarget, final PathArgument targetNodeIdentifier) {
-            this.previousNodeIdentifier = previousNodeIdentifier;
-            this.mixinNodesToTarget = mixinNodesToTarget;
-            this.targetNodeIdentifier = targetNodeIdentifier;
-        }
-
-        @Override
-        public boolean equals(final Object obj) {
-            // this is need in order to make 'prepareQNameLevel(..)' working
-            if (this == obj) {
-                return true;
-            }
-            if (obj == null || getClass() != obj.getClass()) {
-                return false;
-            }
-            final LinkedPathElement that = (LinkedPathElement) obj;
-            return targetNodeIdentifier.equals(that.targetNodeIdentifier);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(targetNodeIdentifier);
-        }
-    }
-}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslator.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslator.java
new file mode 100644 (file)
index 0000000..0ba136a
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ * Copyright (c) 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.restconf.nb.rfc8040.utils.parser;
+
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.ParameterAwareNormalizedNodeWriter;
+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.data.util.DataSchemaContextNode;
+
+/**
+ * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output
+ * it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary
+ * mixin nodes are also flatten to the same level as identifiers of data nodes.<br>
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
+ * <pre>
+ * level 0: ['a', 'd']
+ * level 1: ['b', 'x', 'e']
+ * level 2: ['c']
+ * </pre>
+ */
+public final class WriterFieldsTranslator extends AbstractFieldsTranslator<QName> {
+    private static final WriterFieldsTranslator INSTANCE = new WriterFieldsTranslator();
+
+    private WriterFieldsTranslator() {
+        // Hidden on purpose
+    }
+
+    /**
+     * Translate a {@link FieldsParam} to a complete list of child nodes organized into levels, suitable for use with
+     * {@link ParameterAwareNormalizedNodeWriter}.
+     *
+     * @param identifier identifier context created from request URI
+     * @param input input value of fields parameter
+     * @return {@link List} of levels; each level contains set of {@link QName}
+     */
+    public static @NonNull List<Set<QName>> translate(final @NonNull InstanceIdentifierContext<?> identifier,
+                                                      final @NonNull FieldsParam input) {
+        return INSTANCE.parseFields(identifier, input);
+    }
+
+    @Override
+    protected DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode,
+            final QName childQName, final Set<QName> level) {
+        // resolve parent node
+        final DataSchemaContextNode<?> parentNode = resolveMixinNode(
+                currentNode, level, currentNode.getIdentifier().getNodeType());
+        if (parentNode == null) {
+            throw new RestconfDocumentedException(
+                    "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        // resolve child node
+        final DataSchemaContextNode<?> childNode = resolveMixinNode(
+                parentNode.getChild(childQName), level, childQName);
+        if (childNode == null) {
+            throw new RestconfDocumentedException(
+                    "Child " + childQName.getLocalName() + " node missing in "
+                            + currentNode.getIdentifier().getNodeType().getLocalName(),
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
+        }
+
+        // add final childNode node to level nodes
+        level.add(childNode.getIdentifier().getNodeType());
+        return childNode;
+    }
+
+    /**
+     * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
+     * All nodes expect of not mixin node are added to current level nodes.
+     *
+     * @param node          initial mixin or not-mixin node
+     * @param level         current nodes level
+     * @param qualifiedName qname of initial node
+     * @return {@link DataSchemaContextNode}
+     */
+    private static @Nullable DataSchemaContextNode<?> resolveMixinNode(
+            final @Nullable DataSchemaContextNode<?> node, final @NonNull Set<QName> level,
+            final @NonNull QName qualifiedName) {
+        DataSchemaContextNode<?> currentNode = node;
+        while (currentNode != null && currentNode.isMixin()) {
+            level.add(qualifiedName);
+            currentNode = currentNode.getChild(qualifiedName);
+        }
+
+        return currentNode;
+    }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslatorTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslatorTest.java
new file mode 100644 (file)
index 0000000..8d92176
--- /dev/null
@@ -0,0 +1,363 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.utils.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jdt.annotation.NonNull;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
+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.common.Revision;
+import org.opendaylight.yangtools.yang.common.XMLNamespace;
+import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+
+public abstract class AbstractFieldsTranslatorTest<T> {
+    @Mock
+    private InstanceIdentifierContext<ContainerSchemaNode> identifierJukebox;
+
+    @Mock
+    private InstanceIdentifierContext<ContainerSchemaNode> identifierTestServices;
+
+    private static final QNameModule Q_NAME_MODULE_JUKEBOX = QNameModule.create(
+        XMLNamespace.of("http://example.com/ns/example-jukebox"), Revision.of("2015-04-04"));
+    private static final QNameModule Q_NAME_MODULE_TEST_SERVICES = QNameModule.create(
+        XMLNamespace.of("tests:test-services"), Revision.of("2019-03-25"));
+    private static final QNameModule Q_NAME_MODULE_AUGMENTED_JUKEBOX = QNameModule.create(
+        XMLNamespace.of("http://example.com/ns/augmented-jukebox"), Revision.of("2016-05-05"));
+
+    // container jukebox
+    @Mock
+    private ContainerSchemaNode containerJukebox;
+    private static final QName JUKEBOX_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "jukebox");
+
+    // container player
+    @Mock
+    private ContainerSchemaNode containerPlayer;
+    protected static final QName PLAYER_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "player");
+
+    // container library
+    @Mock
+    private ContainerSchemaNode containerLibrary;
+    protected static final QName LIBRARY_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "library");
+
+    // container augmented library
+    @Mock
+    private ContainerSchemaNode augmentedContainerLibrary;
+    protected static final QName AUGMENTED_LIBRARY_Q_NAME = QName.create(Q_NAME_MODULE_AUGMENTED_JUKEBOX,
+            "augmented-library");
+
+    // augmentation that contains speed leaf
+    @Mock
+    private AugmentationSchemaNode speedAugmentation;
+
+    // leaf speed
+    @Mock
+    private LeafSchemaNode leafSpeed;
+    protected static final QName SPEED_Q_NAME = QName.create(Q_NAME_MODULE_AUGMENTED_JUKEBOX, "speed");
+
+    // list album
+    @Mock
+    private ListSchemaNode listAlbum;
+    public static final QName ALBUM_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "album");
+
+    // leaf name
+    @Mock
+    private LeafSchemaNode leafName;
+    protected static final QName NAME_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "name");
+
+    // container test data
+    @Mock
+    private ContainerSchemaNode containerTestData;
+    private static final QName TEST_DATA_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "test-data");
+
+    // list services
+    @Mock
+    private ListSchemaNode listServices;
+    protected static final QName SERVICES_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "services");
+
+    // leaf type-of-service
+    @Mock
+    private LeafSchemaNode leafTypeOfService;
+    protected static final QName TYPE_OF_SERVICE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "type-of-service");
+
+    // list instance
+    @Mock
+    private ListSchemaNode listInstance;
+    protected static final QName INSTANCE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "instance");
+
+    // leaf instance-name
+    @Mock
+    private LeafSchemaNode leafInstanceName;
+    protected static final QName INSTANCE_NAME_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "instance-name");
+
+    // leaf provider
+    @Mock
+    private LeafSchemaNode leafProvider;
+    protected static final QName PROVIDER_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "provider");
+
+    // container next-data
+    @Mock
+    private ContainerSchemaNode containerNextData;
+    protected static final QName NEXT_DATA_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "next-data");
+
+    // leaf next-service
+    @Mock
+    private LeafSchemaNode leafNextService;
+    protected static final QName NEXT_SERVICE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "next-service");
+
+    // leaf-list protocols
+    @Mock
+    private LeafListSchemaNode leafListProtocols;
+    protected static final QName PROTOCOLS_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "protocols");
+
+    @Before
+    public void setUp() throws Exception {
+        final EffectiveModelContext schemaContextJukebox =
+                YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/jukebox"));
+        initJukeboxSchemaNodes(schemaContextJukebox);
+
+        final EffectiveModelContext schemaContextTestServices =
+                YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/test-services"));
+        initTestServicesSchemaNodes(schemaContextTestServices);
+    }
+
+    private void initJukeboxSchemaNodes(final EffectiveModelContext schemaContext) {
+        when(identifierJukebox.getSchemaContext()).thenReturn(schemaContext);
+        when(containerJukebox.getQName()).thenReturn(JUKEBOX_Q_NAME);
+        when(identifierJukebox.getSchemaNode()).thenReturn(containerJukebox);
+
+        when(containerLibrary.getQName()).thenReturn(LIBRARY_Q_NAME);
+        when(containerJukebox.dataChildByName(LIBRARY_Q_NAME)).thenReturn(containerLibrary);
+
+        when(augmentedContainerLibrary.getQName()).thenReturn(AUGMENTED_LIBRARY_Q_NAME);
+        when(containerJukebox.dataChildByName(AUGMENTED_LIBRARY_Q_NAME))
+                .thenReturn(augmentedContainerLibrary);
+
+        when(containerPlayer.getQName()).thenReturn(PLAYER_Q_NAME);
+        when(containerJukebox.dataChildByName(PLAYER_Q_NAME)).thenReturn(containerPlayer);
+
+        when(listAlbum.getQName()).thenReturn(ALBUM_Q_NAME);
+        when(containerLibrary.dataChildByName(ALBUM_Q_NAME)).thenReturn(listAlbum);
+
+        when(leafName.getQName()).thenReturn(NAME_Q_NAME);
+        when(listAlbum.dataChildByName(NAME_Q_NAME)).thenReturn(leafName);
+
+        when(leafSpeed.getQName()).thenReturn(SPEED_Q_NAME);
+        when(leafSpeed.isAugmenting()).thenReturn(true);
+        when(containerPlayer.dataChildByName(SPEED_Q_NAME)).thenReturn(leafSpeed);
+        when(containerPlayer.getDataChildByName(SPEED_Q_NAME)).thenReturn(leafSpeed);
+        doReturn(List.of(leafSpeed)).when(speedAugmentation).getChildNodes();
+        doReturn(List.of(speedAugmentation)).when(containerPlayer).getAvailableAugmentations();
+        when(speedAugmentation.findDataChildByName(SPEED_Q_NAME)).thenReturn(Optional.of(leafSpeed));
+    }
+
+    private void initTestServicesSchemaNodes(final EffectiveModelContext schemaContext) {
+        when(identifierTestServices.getSchemaContext()).thenReturn(schemaContext);
+        when(containerTestData.getQName()).thenReturn(TEST_DATA_Q_NAME);
+        when(identifierTestServices.getSchemaNode()).thenReturn(containerTestData);
+
+        when(listServices.getQName()).thenReturn(SERVICES_Q_NAME);
+        when(containerTestData.dataChildByName(SERVICES_Q_NAME)).thenReturn(listServices);
+
+        when(leafListProtocols.getQName()).thenReturn(PROTOCOLS_Q_NAME);
+        when(containerTestData.dataChildByName(PROTOCOLS_Q_NAME)).thenReturn(leafListProtocols);
+
+        when(leafTypeOfService.getQName()).thenReturn(TYPE_OF_SERVICE_Q_NAME);
+        when(listServices.dataChildByName(TYPE_OF_SERVICE_Q_NAME)).thenReturn(leafTypeOfService);
+
+        when(listInstance.getQName()).thenReturn(INSTANCE_Q_NAME);
+        when(listServices.dataChildByName(INSTANCE_Q_NAME)).thenReturn(listInstance);
+
+        when(leafInstanceName.getQName()).thenReturn(INSTANCE_NAME_Q_NAME);
+        when(listInstance.dataChildByName(INSTANCE_NAME_Q_NAME)).thenReturn(leafInstanceName);
+
+        when(leafProvider.getQName()).thenReturn(PROVIDER_Q_NAME);
+        when(listInstance.dataChildByName(PROVIDER_Q_NAME)).thenReturn(leafProvider);
+
+        when(containerNextData.getQName()).thenReturn(NEXT_DATA_Q_NAME);
+        when(listServices.dataChildByName(NEXT_DATA_Q_NAME)).thenReturn(containerNextData);
+
+        when(leafNextService.getQName()).thenReturn(NEXT_SERVICE_Q_NAME);
+        when(containerNextData.dataChildByName(NEXT_SERVICE_Q_NAME)).thenReturn(leafNextService);
+    }
+
+    protected abstract List<T> translateFields(InstanceIdentifierContext<?> context, FieldsParam fields);
+
+    /**
+     * Test parse fields parameter containing only one child selected.
+     */
+    @Test
+    public void testSimplePath() {
+        final var result = translateFields(identifierJukebox, assertFields("library"));
+        assertNotNull(result);
+        assertSimplePath(result);
+    }
+
+    protected abstract void assertSimplePath(@NonNull List<T> result);
+
+    /**
+     * Test parse fields parameter containing two child nodes selected.
+     */
+    @Test
+    public void testDoublePath() {
+        final var result = translateFields(identifierJukebox, assertFields("library;player"));
+        assertNotNull(result);
+        assertDoublePath(result);
+    }
+
+    protected abstract void assertDoublePath(@NonNull List<T> result);
+
+    /**
+     * Test parse fields parameter containing sub-children selected delimited by slash.
+     */
+    @Test
+    public void testSubPath() {
+        final var result = translateFields(identifierJukebox, assertFields("library/album/name"));
+        assertNotNull(result);
+        assertSubPath(result);
+    }
+
+    protected abstract void assertSubPath(@NonNull List<T> result);
+
+    /**
+     * Test parse fields parameter containing sub-children selected delimited by parenthesis.
+     */
+    @Test
+    public void testChildrenPath() {
+        final var result = translateFields(identifierJukebox, assertFields("library(album(name))"));
+        assertNotNull(result);
+        assertChildrenPath(result);
+    }
+
+    protected abstract void assertChildrenPath(@NonNull List<T> result);
+
+    /**
+     * Test parse fields parameter when augmentation with different namespace is used.
+     */
+    @Test
+    public void testNamespace() {
+        final var result = translateFields(identifierJukebox, assertFields("augmented-jukebox:augmented-library"));
+        assertNotNull(result);
+        assertNamespace(result);
+    }
+
+    protected abstract void assertNamespace(@NonNull List<T> result);
+
+    /**
+     * Testing of fields parameter parsing when multiple nodes are wrapped in brackets and these nodes are not
+     * direct children of parent node - multiple children which are constructed using '/'.
+     */
+    @Test
+    public void testMultipleChildren1() {
+        final var result = translateFields(identifierTestServices,
+            assertFields("services(type-of-service;instance/instance-name;instance/provider)"));
+        assertNotNull(result);
+        assertMultipleChildren1(result);
+    }
+
+    protected abstract void assertMultipleChildren1(@NonNull List<T> result);
+
+    /**
+     * Testing of fields parameter parsing when multiple nodes are wrapped in brackets and these nodes are not
+     * direct children of parent node - one of children nodes is typed using brackets, other is constructed using '/'.
+     */
+    @Test
+    public void testMultipleChildren2() {
+        final var result = translateFields(identifierTestServices,
+            assertFields("services(type-of-service;instance(instance-name;provider))"));
+        assertNotNull(result);
+        assertMultipleChildren2(result);
+    }
+
+    protected abstract void assertMultipleChildren2(@NonNull List<T> result);
+
+    /**
+     * Testing of fields parameter parsing when multiple nodes are wrapped in brackets and these nodes are not
+     * direct children of parent node - multiple children with different parent nodes.
+     */
+    @Test
+    public void testMultipleChildren3() {
+        final var result = translateFields(identifierTestServices,
+            assertFields("services(instance/instance-name;type-of-service;next-data/next-service)"));
+        assertNotNull(result);
+        assertMultipleChildren3(result);
+    }
+
+    protected abstract void assertMultipleChildren3(@NonNull List<T> result);
+
+    @Test
+    public void testAugmentedChild() {
+        final var result = translateFields(identifierJukebox, assertFields("player/augmented-jukebox:speed"));
+        assertNotNull(result);
+        assertAugmentedChild(result);
+    }
+
+    protected abstract void assertAugmentedChild(@NonNull List<T> result);
+
+    @Test
+    public void testListFieldUnderList() {
+        final var result = translateFields(identifierTestServices, assertFields("services/instance"));
+        assertNotNull(result);
+        assertListFieldUnderList(result);
+    }
+
+    protected abstract void assertListFieldUnderList(@NonNull List<T> result);
+
+    @Test
+    public void testLeafList() {
+        final var result = translateFields(identifierTestServices, assertFields("protocols"));
+        assertNotNull(result);
+        assertLeafList(result);
+    }
+
+    protected abstract void assertLeafList(@NonNull List<T> result);
+
+    /**
+     * Test parse fields parameter when not existing child node selected.
+     */
+    @Test
+    public void testMissingChildSchema() throws ParseException {
+        final FieldsParam input = FieldsParam.parse("library(not-existing)");
+
+        final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
+            () -> translateFields(identifierJukebox, input));
+        // Bad request
+        assertEquals(ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
+        assertEquals(ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
+    }
+
+    private static FieldsParam assertFields(final String input) {
+        try {
+            return FieldsParam.parse(input);
+        } catch (ParseException e) {
+            throw new AssertionError(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java
new file mode 100644 (file)
index 0000000..18bcdc5
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2020 FRINX s.r.o. and others.  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.restconf.nb.rfc8040.utils.parser;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+
+/**
+ * Unit test for {@link NetconfFieldsTranslator}.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class NetconfFieldsTranslatorTest extends AbstractFieldsTranslatorTest<YangInstanceIdentifier> {
+    @Override
+    protected List<YangInstanceIdentifier> translateFields(final InstanceIdentifierContext<?> context,
+            final FieldsParam fields) {
+        return NetconfFieldsTranslator.translate(context, fields);
+    }
+
+    @Override
+    protected void assertSimplePath(final List<YangInstanceIdentifier> result) {
+        assertEquals(1, result.size());
+        final var pathArguments = result.get(0).getPathArguments();
+        assertEquals(1, pathArguments.size());
+        assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType());
+    }
+
+    @Override
+    protected void assertDoublePath(final List<YangInstanceIdentifier> result) {
+        assertEquals(2, result.size());
+
+        final var libraryPath = assertPath(result, LIBRARY_Q_NAME);
+        assertEquals(1, libraryPath.getPathArguments().size());
+
+        final var playerPath = assertPath(result, PLAYER_Q_NAME);
+        assertEquals(1, playerPath.getPathArguments().size());
+    }
+
+    @Override
+    protected void assertSubPath(final List<YangInstanceIdentifier> result) {
+        // FIXME: NETCONF-820: add assertions
+    }
+
+    @Override
+    protected void assertChildrenPath(final List<YangInstanceIdentifier> result) {
+        assertEquals(1, result.size());
+        final var pathArguments = result.get(0).getPathArguments();
+        assertEquals(3, pathArguments.size());
+        assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType());
+        assertEquals(ALBUM_Q_NAME, pathArguments.get(1).getNodeType());
+        assertEquals(NAME_Q_NAME, pathArguments.get(2).getNodeType());
+    }
+
+    @Override
+    protected void assertNamespace(final List<YangInstanceIdentifier> result) {
+        // FIXME: add assertions
+    }
+
+    @Override
+    protected void assertMultipleChildren1(final List<YangInstanceIdentifier> result) {
+        assertEquals(3, result.size());
+
+        final var tosPath = assertPath(result, TYPE_OF_SERVICE_Q_NAME);
+        assertEquals(2, tosPath.getPathArguments().size());
+
+        final var instanceNamePath = assertPath(result, INSTANCE_NAME_Q_NAME);
+        assertEquals(3, instanceNamePath.getPathArguments().size());
+
+        final var providerPath = assertPath(result, PROVIDER_Q_NAME);
+        assertEquals(3, providerPath.getPathArguments().size());
+    }
+
+    @Override
+    protected void assertMultipleChildren2(final List<YangInstanceIdentifier> result) {
+        // FIXME: add assertions
+    }
+
+    @Override
+    protected void assertMultipleChildren3(final List<YangInstanceIdentifier> result) {
+        // FIXME: add assertions
+    }
+
+    @Override
+    protected void assertAugmentedChild(final List<YangInstanceIdentifier> result) {
+        assertEquals(1, result.size());
+        final var pathArguments = result.get(0).getPathArguments();
+
+        assertEquals(3, pathArguments.size());
+        assertEquals(PLAYER_Q_NAME, pathArguments.get(0).getNodeType());
+        assertThat(pathArguments.get(1), instanceOf(AugmentationIdentifier.class));
+        assertEquals(SPEED_Q_NAME, pathArguments.get(2).getNodeType());
+    }
+
+    @Override
+    protected void assertListFieldUnderList(final List<YangInstanceIdentifier> result) {
+        assertEquals(1, result.size());
+        assertEquals(List.of(new NodeIdentifier(SERVICES_Q_NAME), new NodeIdentifier(INSTANCE_Q_NAME)),
+            result.get(0).getPathArguments());
+    }
+
+    @Override
+    protected void assertLeafList(final List<YangInstanceIdentifier> parsedFields) {
+        assertEquals(1, parsedFields.size());
+        assertEquals(List.of(new NodeIdentifier(PROTOCOLS_Q_NAME)), parsedFields.get(0).getPathArguments());
+    }
+
+    private static YangInstanceIdentifier assertPath(final List<YangInstanceIdentifier> paths, final QName lastArg) {
+        return paths.stream()
+            .filter(path -> lastArg.equals(path.getLastPathArgument().getNodeType()))
+            .findAny()
+            .orElseThrow(() -> new AssertionError("Path ending with " + lastArg + " not found"));
+    }
+}
diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java
deleted file mode 100644 (file)
index ba2447f..0000000
+++ /dev/null
@@ -1,512 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,
- * and is available at http://www.eclipse.org/legal/epl-v10.html
- */
-package org.opendaylight.restconf.nb.rfc8040.utils.parser;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.when;
-
-import java.text.ParseException;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
-import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
-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.common.Revision;
-import org.opendaylight.yangtools.yang.common.XMLNamespace;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
-import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
-import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
-
-/**
- * Unit test for {@link ParserFieldsParameter}.
- */
-@RunWith(MockitoJUnitRunner.class)
-public class ParserFieldsParameterTest {
-
-    @Mock
-    private InstanceIdentifierContext<ContainerSchemaNode> identifierJukebox;
-
-    @Mock
-    private InstanceIdentifierContext<ContainerSchemaNode> identifierTestServices;
-
-    private static final QNameModule Q_NAME_MODULE_JUKEBOX = QNameModule.create(
-        XMLNamespace.of("http://example.com/ns/example-jukebox"), Revision.of("2015-04-04"));
-    private static final QNameModule Q_NAME_MODULE_TEST_SERVICES = QNameModule.create(
-        XMLNamespace.of("tests:test-services"), Revision.of("2019-03-25"));
-    private static final QNameModule Q_NAME_MODULE_AUGMENTED_JUKEBOX = QNameModule.create(
-        XMLNamespace.of("http://example.com/ns/augmented-jukebox"), Revision.of("2016-05-05"));
-
-    // container jukebox
-    @Mock
-    private ContainerSchemaNode containerJukebox;
-    private static final QName JUKEBOX_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "jukebox");
-
-    // container player
-    @Mock
-    private ContainerSchemaNode containerPlayer;
-    private static final QName PLAYER_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "player");
-
-    // container library
-    @Mock
-    private ContainerSchemaNode containerLibrary;
-    private static final QName LIBRARY_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "library");
-
-    // container augmented library
-    @Mock
-    private ContainerSchemaNode augmentedContainerLibrary;
-    private static final QName AUGMENTED_LIBRARY_Q_NAME = QName.create(Q_NAME_MODULE_AUGMENTED_JUKEBOX,
-            "augmented-library");
-
-    // augmentation that contains speed leaf
-    @Mock
-    private AugmentationSchemaNode speedAugmentation;
-
-    // leaf speed
-    @Mock
-    private LeafSchemaNode leafSpeed;
-    private static final QName SPEED_Q_NAME = QName.create(Q_NAME_MODULE_AUGMENTED_JUKEBOX, "speed");
-
-    // list album
-    @Mock
-    private ListSchemaNode listAlbum;
-    private static final QName ALBUM_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "album");
-
-    // leaf name
-    @Mock
-    private LeafSchemaNode leafName;
-    private static final QName NAME_Q_NAME = QName.create(Q_NAME_MODULE_JUKEBOX, "name");
-
-    // container test data
-    @Mock
-    private ContainerSchemaNode containerTestData;
-    private static final QName TEST_DATA_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "test-data");
-
-    // list services
-    @Mock
-    private ListSchemaNode listServices;
-    private static final QName SERVICES_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "services");
-
-    // leaf type-of-service
-    @Mock
-    private LeafSchemaNode leafTypeOfService;
-    private static final QName TYPE_OF_SERVICE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "type-of-service");
-
-    // list instance
-    @Mock
-    private ListSchemaNode listInstance;
-    private static final QName INSTANCE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "instance");
-
-    // leaf instance-name
-    @Mock
-    private LeafSchemaNode leafInstanceName;
-    private static final QName INSTANCE_NAME_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "instance-name");
-
-    // leaf provider
-    @Mock
-    private LeafSchemaNode leafProvider;
-    private static final QName PROVIDER_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "provider");
-
-    // container next-data
-    @Mock
-    private ContainerSchemaNode containerNextData;
-    private static final QName NEXT_DATA_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "next-data");
-
-    // leaf next-service
-    @Mock
-    private LeafSchemaNode leafNextService;
-    private static final QName NEXT_SERVICE_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "next-service");
-
-    // leaf-list protocols
-    @Mock
-    private LeafListSchemaNode leafListProtocols;
-    private static final QName PROTOCOLS_Q_NAME = QName.create(Q_NAME_MODULE_TEST_SERVICES, "protocols");
-
-    @Before
-    public void setUp() throws Exception {
-        final EffectiveModelContext schemaContextJukebox =
-                YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/jukebox"));
-        initJukeboxSchemaNodes(schemaContextJukebox);
-
-        final EffectiveModelContext schemaContextTestServices =
-                YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/test-services"));
-        initTestServicesSchemaNodes(schemaContextTestServices);
-    }
-
-    private void initJukeboxSchemaNodes(final EffectiveModelContext schemaContext) {
-        when(identifierJukebox.getSchemaContext()).thenReturn(schemaContext);
-        when(containerJukebox.getQName()).thenReturn(JUKEBOX_Q_NAME);
-        when(identifierJukebox.getSchemaNode()).thenReturn(containerJukebox);
-
-        when(containerLibrary.getQName()).thenReturn(LIBRARY_Q_NAME);
-        when(containerJukebox.dataChildByName(LIBRARY_Q_NAME)).thenReturn(containerLibrary);
-
-        when(augmentedContainerLibrary.getQName()).thenReturn(AUGMENTED_LIBRARY_Q_NAME);
-        when(containerJukebox.dataChildByName(AUGMENTED_LIBRARY_Q_NAME))
-                .thenReturn(augmentedContainerLibrary);
-
-        when(containerPlayer.getQName()).thenReturn(PLAYER_Q_NAME);
-        when(containerJukebox.dataChildByName(PLAYER_Q_NAME)).thenReturn(containerPlayer);
-
-        when(listAlbum.getQName()).thenReturn(ALBUM_Q_NAME);
-        when(containerLibrary.dataChildByName(ALBUM_Q_NAME)).thenReturn(listAlbum);
-
-        when(leafName.getQName()).thenReturn(NAME_Q_NAME);
-        when(listAlbum.dataChildByName(NAME_Q_NAME)).thenReturn(leafName);
-
-        when(leafSpeed.getQName()).thenReturn(SPEED_Q_NAME);
-        when(leafSpeed.isAugmenting()).thenReturn(true);
-        when(containerPlayer.dataChildByName(SPEED_Q_NAME)).thenReturn(leafSpeed);
-        when(containerPlayer.getDataChildByName(SPEED_Q_NAME)).thenReturn(leafSpeed);
-        doReturn(List.of(leafSpeed)).when(speedAugmentation).getChildNodes();
-        doReturn(List.of(speedAugmentation)).when(containerPlayer).getAvailableAugmentations();
-        when(speedAugmentation.findDataChildByName(SPEED_Q_NAME)).thenReturn(Optional.of(leafSpeed));
-    }
-
-    private void initTestServicesSchemaNodes(final EffectiveModelContext schemaContext) {
-        when(identifierTestServices.getSchemaContext()).thenReturn(schemaContext);
-        when(containerTestData.getQName()).thenReturn(TEST_DATA_Q_NAME);
-        when(identifierTestServices.getSchemaNode()).thenReturn(containerTestData);
-
-        when(listServices.getQName()).thenReturn(SERVICES_Q_NAME);
-        when(containerTestData.dataChildByName(SERVICES_Q_NAME)).thenReturn(listServices);
-
-        when(leafListProtocols.getQName()).thenReturn(PROTOCOLS_Q_NAME);
-        when(containerTestData.dataChildByName(PROTOCOLS_Q_NAME)).thenReturn(leafListProtocols);
-
-        when(leafTypeOfService.getQName()).thenReturn(TYPE_OF_SERVICE_Q_NAME);
-        when(listServices.dataChildByName(TYPE_OF_SERVICE_Q_NAME)).thenReturn(leafTypeOfService);
-
-        when(listInstance.getQName()).thenReturn(INSTANCE_Q_NAME);
-        when(listServices.dataChildByName(INSTANCE_Q_NAME)).thenReturn(listInstance);
-
-        when(leafInstanceName.getQName()).thenReturn(INSTANCE_NAME_Q_NAME);
-        when(listInstance.dataChildByName(INSTANCE_NAME_Q_NAME)).thenReturn(leafInstanceName);
-
-        when(leafProvider.getQName()).thenReturn(PROVIDER_Q_NAME);
-        when(listInstance.dataChildByName(PROVIDER_Q_NAME)).thenReturn(leafProvider);
-
-        when(containerNextData.getQName()).thenReturn(NEXT_DATA_Q_NAME);
-        when(listServices.dataChildByName(NEXT_DATA_Q_NAME)).thenReturn(containerNextData);
-
-        when(leafNextService.getQName()).thenReturn(NEXT_SERVICE_Q_NAME);
-        when(containerNextData.dataChildByName(NEXT_SERVICE_Q_NAME)).thenReturn(leafNextService);
-    }
-
-    /**
-     * Test parse fields parameter containing only one child selected.
-     */
-    @Test
-    public void parseFieldsParameterSimplePathTest() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library");
-
-        assertNotNull(parsedFields);
-        assertEquals(1, parsedFields.size());
-        assertEquals(1, parsedFields.get(0).size());
-        assertTrue(parsedFields.get(0).contains(LIBRARY_Q_NAME));
-    }
-
-    /**
-     * Test parse fields parameter containing two child nodes selected.
-     */
-    @Test
-    public void parseFieldsParameterDoublePathTest() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library;player");
-
-        assertNotNull(parsedFields);
-        assertEquals(1, parsedFields.size());
-        assertEquals(2, parsedFields.get(0).size());
-        assertTrue(parsedFields.get(0).contains(LIBRARY_Q_NAME));
-        assertTrue(parsedFields.get(0).contains(PLAYER_Q_NAME));
-    }
-
-    /**
-     * Test parse fields parameter containing sub-children selected delimited by slash.
-     */
-    @Test
-    public void parseFieldsParameterSubPathTest() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library/album/name");
-
-        assertNotNull(parsedFields);
-        assertEquals(3, parsedFields.size());
-
-        assertEquals(1, parsedFields.get(0).size());
-        assertTrue(parsedFields.get(0).contains(LIBRARY_Q_NAME));
-
-        assertEquals(1, parsedFields.get(1).size());
-        assertTrue(parsedFields.get(1).contains(ALBUM_Q_NAME));
-
-        assertEquals(1, parsedFields.get(2).size());
-        assertTrue(parsedFields.get(2).contains(NAME_Q_NAME));
-    }
-
-    /**
-     * Test parse fields parameter containing sub-children selected delimited by parenthesis.
-     */
-    @Test
-    public void parseFieldsParameterChildrenPathTest() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox, "library(album(name))");
-
-        assertNotNull(parsedFields);
-        assertEquals(3, parsedFields.size());
-
-        assertEquals(1, parsedFields.get(0).size());
-        assertTrue(parsedFields.get(0).contains(LIBRARY_Q_NAME));
-
-        assertEquals(1, parsedFields.get(1).size());
-        assertTrue(parsedFields.get(1).contains(ALBUM_Q_NAME));
-
-        assertEquals(1, parsedFields.get(2).size());
-        assertTrue(parsedFields.get(2).contains(NAME_Q_NAME));
-    }
-
-    /**
-     * Test parse fields parameter when augmentation with different namespace is used.
-     */
-    @Test
-    public void parseFieldsParameterNamespaceTest() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierJukebox,
-            "augmented-jukebox:augmented-library");
-
-        assertNotNull(parsedFields);
-        assertEquals(1, parsedFields.size());
-
-        assertEquals(1, parsedFields.get(0).size());
-        assertTrue(parsedFields.get(0).contains(AUGMENTED_LIBRARY_Q_NAME));
-    }
-
-    /**
-     * Testing of fields parameter parsing when multiple nodes are wrapped in brackets and these nodes are not
-     * direct children of parent node - multiple children which are constructed using '/'.
-     */
-    @Test
-    public void parseFieldsParameterWithMultipleChildrenTest1() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierTestServices,
-            "services(type-of-service;instance/instance-name;instance/provider)");
-
-        assertNotNull(parsedFields);
-        assertEquals(parsedFields.size(), 3);
-
-        assertEquals(parsedFields.get(0).size(), 1);
-        assertTrue(parsedFields.get(0).contains(SERVICES_Q_NAME));
-
-        assertEquals(parsedFields.get(1).size(), 2);
-        assertTrue(parsedFields.get(1).containsAll(List.of(TYPE_OF_SERVICE_Q_NAME, INSTANCE_Q_NAME)));
-
-        assertEquals(parsedFields.get(2).size(), 2);
-        assertTrue(parsedFields.get(2).containsAll(List.of(INSTANCE_NAME_Q_NAME, PROVIDER_Q_NAME)));
-    }
-
-    /**
-     * Testing of fields parameter parsing when multiple nodes are wrapped in brackets and these nodes are not
-     * direct children of parent node - one of children nodes is typed using brackets, other is constructed using '/'.
-     */
-    @Test
-    public void parseFieldsParameterWithMultipleChildrenTest2() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierTestServices,
-            "services(type-of-service;instance(instance-name;provider))");
-
-        assertNotNull(parsedFields);
-        assertEquals(parsedFields.size(), 3);
-
-        assertEquals(parsedFields.get(0).size(), 1);
-        assertTrue(parsedFields.get(0).contains(SERVICES_Q_NAME));
-
-        assertEquals(parsedFields.get(1).size(), 2);
-        assertTrue(parsedFields.get(1).containsAll(List.of(TYPE_OF_SERVICE_Q_NAME, INSTANCE_Q_NAME)));
-
-        assertEquals(parsedFields.get(2).size(), 2);
-        assertTrue(parsedFields.get(2).containsAll(List.of(INSTANCE_NAME_Q_NAME, PROVIDER_Q_NAME)));
-    }
-
-    /**
-     * Testing of fields parameter parsing when multiple nodes are wrapped in brackets and these nodes are not
-     * direct children of parent node - multiple children with different parent nodes.
-     */
-    @Test
-    public void parseFieldsParameterWithMultipleChildrenTest3() {
-        final List<Set<QName>> parsedFields = assertFieldsParameter(identifierTestServices,
-            "services(instance/instance-name;type-of-service;next-data/next-service)");
-
-        assertNotNull(parsedFields);
-        assertEquals(parsedFields.size(), 3);
-
-        assertEquals(parsedFields.get(0).size(), 1);
-        assertTrue(parsedFields.get(0).contains(SERVICES_Q_NAME));
-
-        assertEquals(parsedFields.get(1).size(), 3);
-        assertTrue(parsedFields.get(1).containsAll(
-                List.of(TYPE_OF_SERVICE_Q_NAME, INSTANCE_Q_NAME, NEXT_DATA_Q_NAME)));
-
-        assertEquals(parsedFields.get(2).size(), 2);
-        assertTrue(parsedFields.get(2).containsAll(
-                List.of(INSTANCE_NAME_Q_NAME, NEXT_SERVICE_Q_NAME)));
-    }
-
-    /**
-     * Test parse fields parameter when not existing child node selected.
-     */
-    @Test
-    public void parseFieldsParameterMissingChildNodeNegativeTest() throws ParseException {
-        final FieldsParam input = FieldsParam.parse("library(not-existing)");
-
-        final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class,
-            () -> ParserFieldsParameter.parseFieldsParameter(identifierJukebox, input));
-        // Bad request
-        assertEquals("Error type is not correct", ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
-        assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, ex.getErrors().get(0).getErrorTag());
-    }
-
-    @Test
-    public void parseTopLevelContainerToPathTest() {
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox, "library");
-
-        assertNotNull(parsedFields);
-        assertEquals(1, parsedFields.size());
-        final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
-        assertEquals(1, pathArguments.size());
-        assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType());
-    }
-
-    @Test
-    public void parseTwoTopLevelContainersToPathsTest() {
-        final String input = "library;player";
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox, input);
-
-        assertNotNull(parsedFields);
-        assertEquals(2, parsedFields.size());
-
-        final Optional<YangInstanceIdentifier> libraryPath = findPath(parsedFields, LIBRARY_Q_NAME);
-        assertTrue(libraryPath.isPresent());
-        assertEquals(1, libraryPath.get().getPathArguments().size());
-
-        final Optional<YangInstanceIdentifier> playerPath = findPath(parsedFields, PLAYER_Q_NAME);
-        assertTrue(playerPath.isPresent());
-        assertEquals(1, libraryPath.get().getPathArguments().size());
-    }
-
-    @Test
-    public void parseNestedLeafToPathTest() {
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox, "library/album/name");
-
-        assertEquals(1, parsedFields.size());
-        final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
-        assertEquals(3, pathArguments.size());
-
-        assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType());
-        assertEquals(ALBUM_Q_NAME, pathArguments.get(1).getNodeType());
-        assertEquals(NAME_Q_NAME, pathArguments.get(2).getNodeType());
-    }
-
-    @Test
-    public void parseAugmentedLeafToPathTest() {
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierJukebox,
-            "player/augmented-jukebox:speed");
-
-        assertEquals(1, parsedFields.size());
-        final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
-
-        assertEquals(3, pathArguments.size());
-        assertEquals(PLAYER_Q_NAME, pathArguments.get(0).getNodeType());
-        assertTrue(pathArguments.get(1) instanceof AugmentationIdentifier);
-        assertEquals(SPEED_Q_NAME, pathArguments.get(2).getNodeType());
-    }
-
-    @Test
-    public void parseMultipleFieldsOnDifferentLevelsToPathsTest() {
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierTestServices,
-            "services(type-of-service;instance/instance-name;instance/provider)");
-
-        assertEquals(3, parsedFields.size());
-
-        final Optional<YangInstanceIdentifier> tosPath = findPath(parsedFields, TYPE_OF_SERVICE_Q_NAME);
-        assertTrue(tosPath.isPresent());
-        assertEquals(2, tosPath.get().getPathArguments().size());
-
-        final Optional<YangInstanceIdentifier> instanceNamePath = findPath(parsedFields, INSTANCE_NAME_Q_NAME);
-        assertTrue(instanceNamePath.isPresent());
-        assertEquals(3, instanceNamePath.get().getPathArguments().size());
-
-        final Optional<YangInstanceIdentifier> providerPath = findPath(parsedFields, PROVIDER_Q_NAME);
-        assertTrue(providerPath.isPresent());
-        assertEquals(3, providerPath.get().getPathArguments().size());
-    }
-
-    @Test
-    public void parseListFieldUnderListToPathTest() {
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierTestServices,
-            "services/instance");
-
-        assertEquals(1, parsedFields.size());
-        final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
-        assertEquals(2, pathArguments.size());
-
-        assertEquals(SERVICES_Q_NAME, pathArguments.get(0).getNodeType());
-        assertTrue(pathArguments.get(0) instanceof NodeIdentifier);
-        assertEquals(INSTANCE_Q_NAME, pathArguments.get(1).getNodeType());
-        assertTrue(pathArguments.get(1) instanceof NodeIdentifier);
-    }
-
-    @Test
-    public void parseLeafListFieldToPathTest() {
-        final List<YangInstanceIdentifier> parsedFields = assertFieldsPaths(identifierTestServices, "protocols");
-
-        assertEquals(1, parsedFields.size());
-        final List<PathArgument> pathArguments = parsedFields.get(0).getPathArguments();
-        assertEquals(1, pathArguments.size());
-        assertTrue(pathArguments.get(0) instanceof NodeIdentifier);
-        assertEquals(PROTOCOLS_Q_NAME, pathArguments.get(0).getNodeType());
-    }
-
-    private static Optional<YangInstanceIdentifier> findPath(final List<YangInstanceIdentifier> paths,
-                                                             final QName lastPathArg) {
-        return paths.stream()
-                .filter(path -> lastPathArg.equals(path.getLastPathArgument().getNodeType()))
-                .findAny();
-    }
-
-    private static List<Set<QName>> assertFieldsParameter(final InstanceIdentifierContext<?> identifier,
-            final String input) {
-        return ParserFieldsParameter.parseFieldsParameter(identifier, assertFields(input));
-    }
-
-    private static List<YangInstanceIdentifier> assertFieldsPaths(final InstanceIdentifierContext<?> identifier,
-            final String input) {
-        return ParserFieldsParameter.parseFieldsPaths(identifier, assertFields(input));
-    }
-
-    private static FieldsParam assertFields(final String input) {
-        try {
-            return FieldsParam.parse(input);
-        } catch (ParseException e) {
-            throw new AssertionError(e);
-        }
-    }
-}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslatorTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslatorTest.java
new file mode 100644 (file)
index 0000000..76e1355
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ * Copyright (c) 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.restconf.nb.rfc8040.utils.parser;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Set;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
+import org.opendaylight.yangtools.yang.common.QName;
+
+/**
+ * Unit test for {@link WriterFieldsTranslator}.
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class WriterFieldsTranslatorTest extends AbstractFieldsTranslatorTest<Set<QName>> {
+    @Override
+    protected List<Set<QName>> translateFields(final InstanceIdentifierContext<?> context, final FieldsParam fields) {
+        return WriterFieldsTranslator.translate(context, fields);
+    }
+
+    @Override
+    protected void assertSimplePath(final List<Set<QName>> result) {
+        assertEquals(1, result.size());
+        assertEquals(Set.of(LIBRARY_Q_NAME), result.get(0));
+    }
+
+    @Override
+    protected void assertDoublePath(final List<Set<QName>> result) {
+        assertEquals(1, result.size());
+        assertEquals(Set.of(LIBRARY_Q_NAME, PLAYER_Q_NAME), result.get(0));
+    }
+
+    @Override
+    protected void assertSubPath(final List<Set<QName>> result) {
+        assertEquals(3, result.size());
+        assertEquals(Set.of(LIBRARY_Q_NAME), result.get(0));
+        assertEquals(Set.of(ALBUM_Q_NAME), result.get(1));
+        assertEquals(Set.of(NAME_Q_NAME), result.get(2));
+    }
+
+    @Override
+    protected void assertChildrenPath(final List<Set<QName>> result) {
+        assertEquals(3, result.size());
+        assertEquals(Set.of(LIBRARY_Q_NAME), result.get(0));
+        assertEquals(Set.of(ALBUM_Q_NAME), result.get(1));
+        assertEquals(Set.of(NAME_Q_NAME), result.get(2));
+    }
+
+    @Override
+    protected void assertNamespace(final List<Set<QName>> result) {
+        assertEquals(1, result.size());
+        assertEquals(Set.of(AUGMENTED_LIBRARY_Q_NAME), result.get(0));
+    }
+
+    @Override
+    protected void assertMultipleChildren1(final List<Set<QName>> result) {
+        assertEquals(result.size(), 3);
+        assertEquals(Set.of(SERVICES_Q_NAME), result.get(0));
+        assertEquals(Set.of(TYPE_OF_SERVICE_Q_NAME, INSTANCE_Q_NAME), result.get(1));
+        assertEquals(Set.of(INSTANCE_NAME_Q_NAME, PROVIDER_Q_NAME), result.get(2));
+    }
+
+    @Override
+    protected void assertMultipleChildren2(final List<Set<QName>> result) {
+        assertEquals(result.size(), 3);
+        assertEquals(Set.of(SERVICES_Q_NAME), result.get(0));
+        assertEquals(Set.of(TYPE_OF_SERVICE_Q_NAME, INSTANCE_Q_NAME), result.get(1));
+        assertEquals(Set.of(INSTANCE_NAME_Q_NAME, PROVIDER_Q_NAME), result.get(2));
+    }
+
+    @Override
+    protected void assertMultipleChildren3(final List<Set<QName>> result) {
+        assertEquals(result.size(), 3);
+        assertEquals(Set.of(SERVICES_Q_NAME), result.get(0));
+        assertEquals(Set.of(TYPE_OF_SERVICE_Q_NAME, INSTANCE_Q_NAME, NEXT_DATA_Q_NAME), result.get(1));
+        assertEquals(Set.of(INSTANCE_NAME_Q_NAME, NEXT_SERVICE_Q_NAME), result.get(2));
+    }
+
+    @Override
+    protected void assertAugmentedChild(final List<Set<QName>> result) {
+        // FIXME: add assertions
+    }
+
+    @Override
+    protected void assertListFieldUnderList(final List<Set<QName>> result) {
+        // FIXME: add assertions
+    }
+
+    @Override
+    protected void assertLeafList(final List<Set<QName>> result) {
+        // FIXME: add assertions
+    }
+}