Integration of RESTCONF fields to NETCONF filters 90/93390/16
authormhurban <mar.hurban@gmail.com>
Wed, 25 Nov 2020 19:02:01 +0000 (20:02 +0100)
committerRobert Varga <nite@hq.sk>
Wed, 12 May 2021 15:55:12 +0000 (15:55 +0000)
Added:
- NETCONF subtree filtering aware parser (ParserFieldsParameter)
- check if get request is for mountpoint data (ReadDataTransactionUtil)
- new mountpoint fields parameter (WriterParameters)

JIRA: NETCONF-735
Change-Id: I293993958893df866908fa5f16992bf38bd6b085
Signed-off-by: mhurban <mar.hurban@gmail.com>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
Signed-off-by: Jaroslav Tóth <jtoth@frinx.io>
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/context/WriterParameters.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/ReadDataTransactionUtil.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java
restconf/restconf-nb-rfc8040/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang
restconf/restconf-nb-rfc8040/src/test/resources/test-services/test-services@2019-03-25.yang

index f3a9eecfa253e3541a39ab9fa5e0deaecb75c884..d8a42f03f0a425485913004fe5aae440fc6cc218 100644 (file)
@@ -10,11 +10,13 @@ package org.opendaylight.restconf.common.context;
 import java.util.List;
 import java.util.Set;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 
 public final class WriterParameters {
     private final String content;
     private final Integer depth;
     private final List<Set<QName>> fields;
+    private final List<YangInstanceIdentifier> fieldPaths;
     private final boolean prettyPrint;
     private final boolean tagged;
     private final String withDefault;
@@ -23,6 +25,7 @@ public final class WriterParameters {
         this.content = builder.content;
         this.depth = builder.depth;
         this.fields = builder.fields;
+        this.fieldPaths = builder.fieldPaths;
         this.prettyPrint = builder.prettyPrint;
         this.tagged = builder.tagged;
         this.withDefault = builder.withDefault;
@@ -40,6 +43,10 @@ public final class WriterParameters {
         return this.fields;
     }
 
+    public List<YangInstanceIdentifier> getFieldPaths() {
+        return this.fieldPaths;
+    }
+
     public boolean isPrettyPrint() {
         return this.prettyPrint;
     }
@@ -56,6 +63,7 @@ public final class WriterParameters {
         private String content;
         private Integer depth;
         private List<Set<QName>> fields;
+        private List<YangInstanceIdentifier> fieldPaths;
         private boolean prettyPrint;
         private boolean tagged;
         private String withDefault;
@@ -79,6 +87,11 @@ public final class WriterParameters {
             return this;
         }
 
+        public WriterParametersBuilder setFieldPaths(final List<YangInstanceIdentifier> fieldPaths) {
+            this.fieldPaths = fieldPaths;
+            return this;
+        }
+
         public WriterParametersBuilder setPrettyPrint(final boolean prettyPrint) {
             this.prettyPrint = prettyPrint;
             return this;
index 79ccb8acd5bc7f237da7239718a0b4de01b9c57f..4062f29f2b2fd655624c1452d06900eceba2ecbd 100644 (file)
@@ -155,9 +155,14 @@ public class RestconfDataServiceImpl implements RestconfDataService {
 
         final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
         final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
-        final NormalizedNode<?, ?> node = readData(identifier, parameters.getContent(),
-                instanceIdentifier.getInstanceIdentifier(), strategy, parameters.getWithDefault(), schemaContextRef,
-                uriInfo);
+        final NormalizedNode<?, ?> node;
+        if (parameters.getFieldPaths() != null && !parameters.getFieldPaths().isEmpty()) {
+            node = ReadDataTransactionUtil.readData(parameters.getContent(), instanceIdentifier.getInstanceIdentifier(),
+                    strategy, parameters.getWithDefault(), schemaContextRef, parameters.getFieldPaths());
+        } else {
+            node = readData(identifier, parameters.getContent(), instanceIdentifier.getInstanceIdentifier(), strategy,
+                    parameters.getWithDefault(), schemaContextRef, uriInfo);
+        }
         if (identifier != null && identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART)
                 && identifier.contains(STREAM_LOCATION_PATH_PART)) {
             final String value = (String) node.getValue();
index d0fb5c33520a15e8dc41555e5cab00a4c6b6df82..f863987100bcba27c1a871baecb10d608b39a7cb 100644 (file)
@@ -10,7 +10,9 @@ package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import java.util.List;
 import java.util.Optional;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
@@ -51,6 +53,13 @@ public final class MdsalRestconfStrategy extends RestconfStrategy {
         }
     }
 
+    @Override
+    public ListenableFuture<Optional<NormalizedNode<?, ?>>> read(final LogicalDatastoreType store,
+            final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
+        return Futures.immediateFailedFuture(new UnsupportedOperationException(
+                "Reading of selected subtrees is currently not supported in: " + MdsalRestconfStrategy.class));
+    }
+
     @Override
     public FluentFuture<Boolean> exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
         try (DOMDataTreeReadTransaction tx = transactionChain.newReadOnlyTransaction()) {
index 0f9acb580e2a7789d0f4031577750ea6c236b38e..129e19049e93e3ef0c01eb7d58259223ebf87260 100644 (file)
@@ -15,6 +15,7 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
+import java.util.List;
 import java.util.Optional;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.common.api.ReadFailedException;
@@ -59,6 +60,22 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
         }
     }
 
+    @Override
+    public ListenableFuture<Optional<NormalizedNode<?, ?>>> read(final LogicalDatastoreType store,
+            final YangInstanceIdentifier path, final List<YangInstanceIdentifier> fields) {
+        switch (store) {
+            case CONFIGURATION:
+                return netconfService.getConfig(path, fields);
+            case OPERATIONAL:
+                return netconfService.get(path, fields);
+            default:
+                LOG.info("Unknown datastore type: {}.", store);
+                throw new IllegalArgumentException(String.format(
+                        "%s, Cannot read data %s with fields %s for %s datastore, unknown datastore type",
+                        netconfService.getDeviceId(), path, fields, store));
+        }
+    }
+
     @Override
     public FluentFuture<Boolean> exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) {
         return remapException(read(store, path))
index dc1454a09d38e36d6441cf359b1a611361bc4b7f..9061ebe76428ce284d64eeb3f423e8a22306601d 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
 
 import com.google.common.util.concurrent.FluentFuture;
 import com.google.common.util.concurrent.ListenableFuture;
+import java.util.List;
 import java.util.Optional;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
@@ -65,6 +66,17 @@ public abstract class RestconfStrategy implements AutoCloseable {
     public abstract ListenableFuture<Optional<NormalizedNode<?, ?>>> read(LogicalDatastoreType store,
         YangInstanceIdentifier path);
 
+    /**
+     * Read data selected using fields from the datastore.
+     *
+     * @param store the logical data store which should be modified
+     * @param path the parent data object path
+     * @param fields paths to selected fields relative to parent path
+     * @return a ListenableFuture containing the result of the read
+     */
+    public abstract  ListenableFuture<Optional<NormalizedNode<?, ?>>> read(LogicalDatastoreType store,
+            YangInstanceIdentifier path, List<YangInstanceIdentifier> fields);
+
     /**
      * Check if data already exists in the datastore.
      *
index d4c022fd309b4990ea45e1d786a192c737033f0f..3aeb3518b094963488e89c37bffcbc4226806630 100644 (file)
@@ -7,6 +7,9 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.rests.utils;
 
+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.VisibleForTesting;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
@@ -33,7 +36,6 @@ import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant.ReadData.WithDefaults;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserFieldsParameter;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@@ -158,7 +160,11 @@ public final class ReadDataTransactionUtil {
 
         // check and set fields
         if (!fields.isEmpty()) {
-            builder.setFields(ParserFieldsParameter.parseFieldsParameter(identifier, fields.get(0)));
+            if (identifier.getMountPoint() != null) {
+                builder.setFieldPaths(parseFieldsPaths(identifier, fields.get(0)));
+            } else {
+                builder.setFields(parseFieldsParameter(identifier, fields.get(0)));
+            }
         }
 
         // check and set withDefaults parameter
@@ -221,6 +227,42 @@ public final class ReadDataTransactionUtil {
         }
     }
 
+    /**
+     * Read specific type of data from data store via transaction with specified subtrees that should only be read.
+     * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
+     *
+     * @param valueOfContent type of data to read (config, state, all)
+     * @param path           the parent path to read
+     * @param strategy       {@link RestconfStrategy} - object that perform the actual DS operations
+     * @param withDefa       value of with-defaults parameter
+     * @param ctx            schema context
+     * @param fields         paths to selected subtrees which should be read, relative to to the parent path
+     * @return {@link NormalizedNode}
+     */
+    public static @Nullable NormalizedNode<?, ?> readData(final @NonNull String valueOfContent,
+            final @NonNull YangInstanceIdentifier path, final @NonNull RestconfStrategy strategy,
+            final @Nullable String withDefa, @NonNull final EffectiveModelContext ctx,
+            final @NonNull List<YangInstanceIdentifier> fields) {
+        switch (valueOfContent) {
+            case RestconfDataServiceConstant.ReadData.CONFIG:
+                if (withDefa == null) {
+                    return readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields);
+                } else {
+                    return prepareDataByParamWithDef(
+                            readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields),
+                            path, withDefa, ctx);
+                }
+            case RestconfDataServiceConstant.ReadData.NONCONFIG:
+                return readDataViaTransaction(strategy, LogicalDatastoreType.OPERATIONAL, path, true, fields);
+            case RestconfDataServiceConstant.ReadData.ALL:
+                return readAllData(strategy, path, withDefa, ctx, fields);
+            default:
+                strategy.close();
+                throw new RestconfDocumentedException(new RestconfError(RestconfError.ErrorType.PROTOCOL,
+                        RestconfError.ErrorTag.INVALID_VALUE, "Invalid content parameter: " + valueOfContent, null,
+                        "The content parameter value must be either config, nonconfig or all (default)"));
+        }
+    }
 
     /**
      * Check if URI does not contain value for the same parameter more than once.
@@ -394,13 +436,38 @@ public final class ReadDataTransactionUtil {
     static @Nullable NormalizedNode<?, ?> readDataViaTransaction(final @NonNull RestconfStrategy strategy,
             final LogicalDatastoreType store, final YangInstanceIdentifier path,
             final boolean closeTransactionChain) {
-        final NormalizedNodeFactory dataFactory = new NormalizedNodeFactory();
         final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = strategy.read(store, path);
+        return extractReadData(strategy, path, closeTransactionChain, listenableFuture);
+    }
+
+    /**
+     * Read specific type of data {@link LogicalDatastoreType} via transaction in {@link RestconfStrategy} with
+     * specified subtrees that should only be read.
+     *
+     * @param strategy              {@link RestconfStrategy} - object that perform the actual DS operations
+     * @param store                 datastore type
+     * @param path                  parent path to selected fields
+     * @param closeTransactionChain if it is set to {@code true}, after transaction it will close transactionChain
+     *                              in {@link RestconfStrategy} if any
+     * @param fields                paths to selected subtrees which should be read, relative to to the parent path
+     * @return {@link NormalizedNode}
+     */
+    private static @Nullable NormalizedNode<?, ?> readDataViaTransaction(final @NonNull RestconfStrategy strategy,
+            final @NonNull LogicalDatastoreType store, final @NonNull YangInstanceIdentifier path,
+            final boolean closeTransactionChain, final @NonNull List<YangInstanceIdentifier> fields) {
+        final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = strategy.read(store, path, fields);
+        return extractReadData(strategy, path, closeTransactionChain, listenableFuture);
+    }
+
+    private static NormalizedNode<?, ?> extractReadData(final RestconfStrategy strategy,
+            final YangInstanceIdentifier path, final boolean closeTransactionChain,
+            final ListenableFuture<Optional<NormalizedNode<?, ?>>> dataFuture) {
+        final NormalizedNodeFactory dataFactory = new NormalizedNodeFactory();
         if (closeTransactionChain) {
             //Method close transactionChain if any
-            FutureCallbackTx.addCallback(listenableFuture, READ_TYPE_TX, dataFactory, strategy, path);
+            FutureCallbackTx.addCallback(dataFuture, READ_TYPE_TX, dataFactory, strategy, path);
         } else {
-            FutureCallbackTx.addCallback(listenableFuture, READ_TYPE_TX, dataFactory);
+            FutureCallbackTx.addCallback(dataFuture, READ_TYPE_TX, dataFactory);
         }
         return dataFactory.build();
     }
@@ -432,6 +499,43 @@ public final class ReadDataTransactionUtil {
                     path, withDefa, ctx);
         }
 
+        return mergeConfigAndSTateDataIfNeeded(stateDataNode, configDataNode);
+    }
+
+    /**
+     * Read config and state data with selected subtrees that should only be read, then map them.
+     * Close {@link DOMTransactionChain} inside of object {@link RestconfStrategy} provided as a parameter.
+     *
+     * @param strategy {@link RestconfStrategy} - object that perform the actual DS operations
+     * @param path     parent path to selected fields
+     * @param withDefa with-defaults parameter
+     * @param ctx      schema context
+     * @param fields   paths to selected subtrees which should be read, relative to to the parent path
+     * @return {@link NormalizedNode}
+     */
+    private static @Nullable NormalizedNode<?, ?> readAllData(final @NonNull RestconfStrategy strategy,
+            final @NonNull YangInstanceIdentifier path, final @Nullable String withDefa,
+            final @NonNull EffectiveModelContext ctx, final @NonNull List<YangInstanceIdentifier> fields) {
+        // PREPARE STATE DATA NODE
+        final NormalizedNode<?, ?> stateDataNode = readDataViaTransaction(
+                strategy, LogicalDatastoreType.OPERATIONAL, path, false, fields);
+
+        // PREPARE CONFIG DATA NODE
+        final NormalizedNode<?, ?> configDataNode;
+        //Here will be closed transactionChain if any
+        if (withDefa == null) {
+            configDataNode = readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields);
+        } else {
+            configDataNode = prepareDataByParamWithDef(
+                    readDataViaTransaction(strategy, LogicalDatastoreType.CONFIGURATION, path, true, fields),
+                    path, withDefa, ctx);
+        }
+
+        return mergeConfigAndSTateDataIfNeeded(stateDataNode, configDataNode);
+    }
+
+    private static NormalizedNode<?, ?> mergeConfigAndSTateDataIfNeeded(final NormalizedNode<?, ?> stateDataNode,
+                                                                        final NormalizedNode<?, ?> configDataNode) {
         // if no data exists
         if (stateDataNode == null && configDataNode == null) {
             return null;
index 16faf59b7370fc78c05b3e09338fdf88f9ad991b..a5d7291d61b3935a7efbb72374180c98aa8d01b2 100644 (file)
@@ -7,11 +7,16 @@
  */
 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;
@@ -20,24 +25,108 @@ import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
 import org.opendaylight.restconf.common.errors.RestconfError.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.LeafListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
-public final class ParserFieldsParameter {
-    private ParserFieldsParameter() {
+/**
+ * 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}
+     * @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 String input) {
-        final List<Set<QName>> parsed = new ArrayList<>();
+        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 String 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 String input) {
+        final List<Set<T>> parsed = new ArrayList<>();
         final SchemaContext context = identifier.getSchemaContext();
         final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
         final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
@@ -54,25 +143,26 @@ public final class ParserFieldsParameter {
 
     /**
      * Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
+     *
      * @param input input value of fields parameter
      * @param startQNameModule starting qname module
      * @param startNode starting node
      * @param parsed list of results
      * @param context schema context
      */
-    private static void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule,
-                                   final @NonNull DataSchemaContextNode<?> startNode,
-                                   final @NonNull List<Set<QName>> parsed, final SchemaContext context) {
+    private void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule,
+                            final @NonNull DataSchemaContextNode<?> startNode,
+                            final @NonNull List<Set<T>> parsed, final SchemaContext context) {
         int currentPosition = 0;
         int startPosition = 0;
         DataSchemaContextNode<?> currentNode = startNode;
         QNameModule currentQNameModule = startQNameModule;
 
-        Set<QName> currentLevel = new HashSet<>();
+        Set<T> currentLevel = new HashSet<>();
         parsed.add(currentLevel);
 
         DataSchemaContextNode<?> parenthesisNode = currentNode;
-        Set<QName> parenthesisLevel = currentLevel;
+        Set<T> parenthesisLevel = currentLevel;
         QNameModule parenthesisQNameModule = currentQNameModule;
 
         while (currentPosition < input.length()) {
@@ -87,7 +177,7 @@ public final class ParserFieldsParameter {
                 case '/':
                     // add parsed identifier to results for current level
                     currentNode = addChildToResult(currentNode, input.substring(startPosition, currentPosition),
-                        currentQNameModule, currentLevel);
+                            currentQNameModule, currentLevel);
                     // go one level down
                     currentLevel = prepareQNameLevel(parsed, currentLevel);
 
@@ -165,96 +255,41 @@ public final class ParserFieldsParameter {
     }
 
     /**
-     * Preparation of the QName level that is used as storage for parsed QNames. If the current level exist at the
-     * index that doesn't equal to the last index of already parsed QNames, a new level of QNames is allocated and
-     * pushed to input parsed QNames.
+     * 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 parsedQNames Already parsed list of QNames grouped to multiple levels.
-     * @param currentLevel Current level of QNames (set).
-     * @return Existing or new level of QNames.
+     * @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 static Set<QName> prepareQNameLevel(final List<Set<QName>> parsedQNames, final Set<QName> currentLevel) {
-        final Optional<Set<QName>> existingLevel = parsedQNames.stream()
+    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 = parsedQNames.indexOf(existingLevel.get());
-            if (index == parsedQNames.size() - 1) {
-                final Set<QName> nextLevel = new HashSet<>();
-                parsedQNames.add(nextLevel);
+            final int index = parsedIdentifiers.indexOf(existingLevel.get());
+            if (index == parsedIdentifiers.size() - 1) {
+                final Set<T> nextLevel = new HashSet<>();
+                parsedIdentifiers.add(nextLevel);
                 return nextLevel;
             }
 
-            return parsedQNames.get(index + 1);
+            return parsedIdentifiers.get(index + 1);
         }
 
-        final Set<QName> nextLevel = new HashSet<>();
-        parsedQNames.add(nextLevel);
+        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 identifier parsed identifier of child node
-     * @param currentQNameModule current namespace and revision in {@link QNameModule}
-     * @param level current nodes level
-     * @return {@link DataSchemaContextNode}
-     */
-    private static @NonNull DataSchemaContextNode<?> addChildToResult(
-            final @NonNull DataSchemaContextNode<?> currentNode, final @NonNull String identifier,
-            final @NonNull QNameModule currentQNameModule, final @NonNull Set<QName> level) {
-        final QName childQName = QName.create(currentQNameModule, identifier);
-
-        // 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 " + identifier + " 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;
-    }
-
     /**
      * Find position of matching parenthesis increased by one, but at most equals to input size.
+     *
      * @param input input where to find for closing parenthesis
      * @return int position of closing parenthesis increased by one
      */
-    private static int findClosingParenthesis(final @Nullable String input) {
+    private static int findClosingParenthesis(final @NonNull String input) {
         int position = 0;
         int count = 1;
 
@@ -284,4 +319,169 @@ public final class ParserFieldsParameter {
 
         return ++position;
     }
-}
+
+    /**
+     * Add parsed child of current node to result for current level.
+     *
+     * @param currentNode current node
+     * @param identifier parsed identifier of child node
+     * @param currentQNameModule current namespace and revision in {@link QNameModule}
+     * @param level current nodes level
+     * @return {@link DataSchemaContextNode}
+     */
+    abstract @NonNull DataSchemaContextNode<?> addChildToResult(@NonNull DataSchemaContextNode<?> currentNode,
+            @NonNull String identifier, @NonNull QNameModule currentQNameModule, @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 String identifier,
+                                                  final QNameModule currentQNameModule, final Set<QName> level) {
+            final QName childQName = QName.create(currentQNameModule, identifier);
+
+            // 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 " + identifier + " 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 String identifier,
+                final QNameModule currentQNameModule, final Set<LinkedPathElement> level) {
+            final QName childQName = QName.create(currentQNameModule, identifier);
+            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 " + identifier + " 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
index a2e5063e10611160a6c9438ae901f7d9c62ae064..b18a6d874c3f796e314e2556dfa030123ab9f4bd 100644 (file)
@@ -11,11 +11,14 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.Sets;
 import java.net.URI;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import org.junit.Before;
 import org.junit.Test;
@@ -30,8 +33,14 @@ import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
 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.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;
@@ -54,6 +63,9 @@ public class ParserFieldsParameterTest {
     private static final QNameModule Q_NAME_MODULE_TEST_SERVICES = QNameModule.create(
             URI.create("tests:test-services"),
             Revision.of("2019-03-25"));
+    private static final QNameModule Q_NAME_MODULE_AUGMENTED_JUKEBOX = QNameModule.create(
+            URI.create("http://example.com/ns/augmented-jukebox"),
+            Revision.of("2016-05-05"));
 
     // container jukebox
     @Mock
@@ -73,12 +85,18 @@ public class ParserFieldsParameterTest {
     // container augmented library
     @Mock
     private ContainerSchemaNode augmentedContainerLibrary;
-    private static final QName AUGMENTED_LIBRARY_Q_NAME = QName.create(
-            QNameModule.create(
-                    URI.create("http://example.com/ns/augmented-jukebox"),
-                    Revision.of("2016-05-05")),
+    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;
@@ -129,6 +147,11 @@ public class ParserFieldsParameterTest {
     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 =
@@ -160,6 +183,14 @@ public class ParserFieldsParameterTest {
 
         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(Collections.singletonList(leafSpeed)).when(speedAugmentation).getChildNodes();
+        doReturn(Collections.singleton(speedAugmentation)).when(containerPlayer).getAvailableAugmentations();
+        when(speedAugmentation.findDataChildByName(SPEED_Q_NAME)).thenReturn(Optional.of(leafSpeed));
     }
 
     private void initTestServicesSchemaNodes(final EffectiveModelContext schemaContext) {
@@ -170,6 +201,9 @@ public class ParserFieldsParameterTest {
         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);
 
@@ -433,5 +467,121 @@ public class ParserFieldsParameterTest {
         }
     }
 
+    @Test
+    public void parseTopLevelContainerToPathTest() {
+        final String input = "library";
+        final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+                identifierJukebox, input);
+
+        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 = ParserFieldsParameter.parseFieldsPaths(
+                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 String input = "library/album/name";
+        final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+                identifierJukebox, input);
+
+        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 String input = "player/augmented-jukebox:speed";
+        final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+                identifierJukebox, input);
+
+        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 String input = "services(type-of-service;instance/instance-name;instance/provider)";
+        final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+                identifierTestServices, input);
+
+        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 String input = "services/instance";
+        final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+                identifierTestServices, input);
+
+        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 String input = "protocols";
+        final List<YangInstanceIdentifier> parsedFields = ParserFieldsParameter.parseFieldsPaths(
+                identifierTestServices, input);
 
+        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();
+    }
 }
\ No newline at end of file
index abbd5d0fd521bf9b5052f9310dddb2258acb2894..b5e937af009bfe9e1d61eff6dd3ccfd610e86dc5 100644 (file)
@@ -13,4 +13,10 @@ module augmented-jukebox {
         container augmented-library {
         }
      }
+
+     augment "/jbox:jukebox/jbox:player" {
+       leaf speed {
+         type uint8;
+       }
+     }
    }
\ No newline at end of file
index 48507d234a882ce5b986b4d4ea261570a7f1ba4e..5967690a92e752e3a4b490a90ac239e808f0b667 100644 (file)
@@ -33,5 +33,9 @@ module test-services {
                 }
             }
         }
+
+        leaf-list protocols {
+            type string;
+        }
     }
 }
\ No newline at end of file