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;
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;
}
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));
}
/**
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
+++ /dev/null
-/*
- * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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"));
+ }
+}
+++ /dev/null
-/*
- * 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
--- /dev/null
+/*
+ * 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
+ }
+}