From cf35083bc644ff73fba7a0690570453e8675b2d1 Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Mon, 25 Oct 2021 21:54:57 +0200 Subject: [PATCH] Refactor ParserFieldsParameter 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 --- .../rfc8040/databind/jaxrs/QueryParams.java | 8 +- .../parser/AbstractFieldsTranslator.java | 145 +++++ .../utils/parser/NetconfFieldsTranslator.java | 192 +++++++ .../utils/parser/ParserFieldsParameter.java | 379 ------------- .../utils/parser/WriterFieldsTranslator.java | 102 ++++ .../parser/AbstractFieldsTranslatorTest.java | 363 +++++++++++++ .../parser/NetconfFieldsTranslatorTest.java | 129 +++++ .../parser/ParserFieldsParameterTest.java | 512 ------------------ .../parser/WriterFieldsTranslatorTest.java | 103 ++++ 9 files changed, 1038 insertions(+), 895 deletions(-) create mode 100644 restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslator.java create mode 100644 restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java delete mode 100644 restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java create mode 100644 restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslator.java create mode 100644 restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslatorTest.java create mode 100644 restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java delete mode 100644 restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java create mode 100644 restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslatorTest.java diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java index d1fe9ec092..d0ecbeba3b 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java @@ -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 index 0000000000..6972323725 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslator.java @@ -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 type of identifier + */ +public abstract class AbstractFieldsTranslator { + 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> 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> 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 level); + + private void processSelectors(final List> parsed, final EffectiveModelContext context, + final QNameModule startNamespace, final DataSchemaContextNode startNode, + final List selectors) { + final Set 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 prepareQNameLevel(final List> parsedIdentifiers, final Set currentLevel) { + final Optional> 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 nextLevel = new HashSet<>(); + parsedIdentifiers.add(nextLevel); + return nextLevel; + } + + return parsedIdentifiers.get(index + 1); + } + + final Set 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 index 0000000000..543791e6c3 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java @@ -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}. + * + *

+ * 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. + * + *

+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels: + *

+ * level 0: ['./a', './d']
+ * level 1: ['a/b', '/d/x/e']
+ * level 2: ['b/c']
+ * 
+ */ +public final class NetconfFieldsTranslator extends AbstractFieldsTranslator { + 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 translate( + final @NonNull InstanceIdentifierContext identifier, final @NonNull FieldsParam input) { + final List> levels = INSTANCE.parseFields(identifier, input); + final List> mappedLevels = mapLevelsContentByIdentifiers(levels); + return buildPaths(mappedLevels); + } + + private static List buildPaths( + final List> mappedLevels) { + final List 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 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> mapLevelsContentByIdentifiers( + final List> 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 level) { + final List 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.
+ * - identifiers of mixin nodes on the path to the target node - required for construction of full valid + * DOM paths,
+ * - 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 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 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 index f2325315bb..0000000000 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java +++ /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 type of identifier - */ -public abstract class ParserFieldsParameter { - private static final ParserFieldsParameter QNAME_PARSER = new QNameParser(); - private static final ParserFieldsParameter 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> 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 parseFieldsPaths( - final @NonNull InstanceIdentifierContext identifier, final @NonNull FieldsParam input) { - final List> levels = PATH_PARSER.parseFields(identifier, input); - final List> mappedLevels = mapLevelsContentByIdentifiers(levels); - return buildPaths(mappedLevels); - } - - private static List> mapLevelsContentByIdentifiers( - final List> 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 buildPaths( - final List> mappedLevels) { - final List 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 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> 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> parsed = new ArrayList<>(); - processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(), - startNode, input.nodeSelectors()); - return parsed; - } - - private void processSelectors(final List> parsed, final EffectiveModelContext context, - final QNameModule startNamespace, final DataSchemaContextNode startNode, - final List selectors) { - final Set 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 prepareQNameLevel(final List> parsedIdentifiers, final Set currentLevel) { - final Optional> 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 nextLevel = new HashSet<>(); - parsedIdentifiers.add(nextLevel); - return nextLevel; - } - - return parsedIdentifiers.get(index + 1); - } - - final Set 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 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.
- * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
- *
-     * level 0: ['a', 'd']
-     * level 1: ['b', 'x', 'e']
-     * level 2: ['c']
-     * 
- */ - private static final class QNameParser extends ParserFieldsParameter { - @Override - DataSchemaContextNode addChildToResult(final DataSchemaContextNode currentNode, final QName childQName, - final Set 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 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.
- * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
- *
-     * level 0: ['./a', './d']
-     * level 1: ['a/b', '/d/x/e']
-     * level 2: ['b/c']
-     * 
- */ - private static final class PathParser extends ParserFieldsParameter { - @Override - DataSchemaContextNode addChildToResult(final DataSchemaContextNode currentNode, final QName childQName, - final Set level) { - final List 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.
- * - identifiers of mixin nodes on the path to the target node - required for construction of full valid - * DOM paths,
- * - 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 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 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 index 0000000000..0ba136ac27 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslator.java @@ -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.
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
+ *
+ * level 0: ['a', 'd']
+ * level 1: ['b', 'x', 'e']
+ * level 2: ['c']
+ * 
+ */ +public final class WriterFieldsTranslator extends AbstractFieldsTranslator { + 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> 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 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 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 index 0000000000..8d92176934 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/AbstractFieldsTranslatorTest.java @@ -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 { + @Mock + private InstanceIdentifierContext identifierJukebox; + + @Mock + private InstanceIdentifierContext 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 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 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 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 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 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 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 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 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 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 result); + + @Test + public void testListFieldUnderList() { + final var result = translateFields(identifierTestServices, assertFields("services/instance")); + assertNotNull(result); + assertListFieldUnderList(result); + } + + protected abstract void assertListFieldUnderList(@NonNull List result); + + @Test + public void testLeafList() { + final var result = translateFields(identifierTestServices, assertFields("protocols")); + assertNotNull(result); + assertLeafList(result); + } + + protected abstract void assertLeafList(@NonNull List 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 index 0000000000..18bcdc5da9 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java @@ -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 { + @Override + protected List translateFields(final InstanceIdentifierContext context, + final FieldsParam fields) { + return NetconfFieldsTranslator.translate(context, fields); + } + + @Override + protected void assertSimplePath(final List 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 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 result) { + // FIXME: NETCONF-820: add assertions + } + + @Override + protected void assertChildrenPath(final List 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 result) { + // FIXME: add assertions + } + + @Override + protected void assertMultipleChildren1(final List 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 result) { + // FIXME: add assertions + } + + @Override + protected void assertMultipleChildren3(final List result) { + // FIXME: add assertions + } + + @Override + protected void assertAugmentedChild(final List 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 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 parsedFields) { + assertEquals(1, parsedFields.size()); + assertEquals(List.of(new NodeIdentifier(PROTOCOLS_Q_NAME)), parsedFields.get(0).getPathArguments()); + } + + private static YangInstanceIdentifier assertPath(final List 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 index ba2447f9ac..0000000000 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java +++ /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 identifierJukebox; - - @Mock - private InstanceIdentifierContext 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> 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> 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> 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> 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> 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> 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> 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> 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 parsedFields = assertFieldsPaths(identifierJukebox, "library"); - - assertNotNull(parsedFields); - assertEquals(1, parsedFields.size()); - final List 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 parsedFields = assertFieldsPaths(identifierJukebox, input); - - assertNotNull(parsedFields); - assertEquals(2, parsedFields.size()); - - final Optional libraryPath = findPath(parsedFields, LIBRARY_Q_NAME); - assertTrue(libraryPath.isPresent()); - assertEquals(1, libraryPath.get().getPathArguments().size()); - - final Optional playerPath = findPath(parsedFields, PLAYER_Q_NAME); - assertTrue(playerPath.isPresent()); - assertEquals(1, libraryPath.get().getPathArguments().size()); - } - - @Test - public void parseNestedLeafToPathTest() { - final List parsedFields = assertFieldsPaths(identifierJukebox, "library/album/name"); - - assertEquals(1, parsedFields.size()); - final List 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 parsedFields = assertFieldsPaths(identifierJukebox, - "player/augmented-jukebox:speed"); - - assertEquals(1, parsedFields.size()); - final List 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 parsedFields = assertFieldsPaths(identifierTestServices, - "services(type-of-service;instance/instance-name;instance/provider)"); - - assertEquals(3, parsedFields.size()); - - final Optional tosPath = findPath(parsedFields, TYPE_OF_SERVICE_Q_NAME); - assertTrue(tosPath.isPresent()); - assertEquals(2, tosPath.get().getPathArguments().size()); - - final Optional instanceNamePath = findPath(parsedFields, INSTANCE_NAME_Q_NAME); - assertTrue(instanceNamePath.isPresent()); - assertEquals(3, instanceNamePath.get().getPathArguments().size()); - - final Optional providerPath = findPath(parsedFields, PROVIDER_Q_NAME); - assertTrue(providerPath.isPresent()); - assertEquals(3, providerPath.get().getPathArguments().size()); - } - - @Test - public void parseListFieldUnderListToPathTest() { - final List parsedFields = assertFieldsPaths(identifierTestServices, - "services/instance"); - - assertEquals(1, parsedFields.size()); - final List 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 parsedFields = assertFieldsPaths(identifierTestServices, "protocols"); - - assertEquals(1, parsedFields.size()); - final List 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 findPath(final List paths, - final QName lastPathArg) { - return paths.stream() - .filter(path -> lastPathArg.equals(path.getLastPathArgument().getNodeType())) - .findAny(); - } - - private static List> assertFieldsParameter(final InstanceIdentifierContext identifier, - final String input) { - return ParserFieldsParameter.parseFieldsParameter(identifier, assertFields(input)); - } - - private static List 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 index 0000000000..76e1355927 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/WriterFieldsTranslatorTest.java @@ -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> { + @Override + protected List> translateFields(final InstanceIdentifierContext context, final FieldsParam fields) { + return WriterFieldsTranslator.translate(context, fields); + } + + @Override + protected void assertSimplePath(final List> result) { + assertEquals(1, result.size()); + assertEquals(Set.of(LIBRARY_Q_NAME), result.get(0)); + } + + @Override + protected void assertDoublePath(final List> result) { + assertEquals(1, result.size()); + assertEquals(Set.of(LIBRARY_Q_NAME, PLAYER_Q_NAME), result.get(0)); + } + + @Override + protected void assertSubPath(final List> 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> 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> result) { + assertEquals(1, result.size()); + assertEquals(Set.of(AUGMENTED_LIBRARY_Q_NAME), result.get(0)); + } + + @Override + protected void assertMultipleChildren1(final List> 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> 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> 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> result) { + // FIXME: add assertions + } + + @Override + protected void assertListFieldUnderList(final List> result) { + // FIXME: add assertions + } + + @Override + protected void assertLeafList(final List> result) { + // FIXME: add assertions + } +} -- 2.36.6