From 5a78b0da75f1d2bfc90a5b2fe6c119fae94e1359 Mon Sep 17 00:00:00 2001 From: mhurban Date: Wed, 25 Nov 2020 20:02:01 +0100 Subject: [PATCH] Integration of RESTCONF fields to NETCONF filters MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Robert Varga Signed-off-by: Jaroslav Tóth --- .../common/context/WriterParameters.java | 13 + .../impl/RestconfDataServiceImpl.java | 11 +- .../transactions/MdsalRestconfStrategy.java | 9 + .../transactions/NetconfRestconfStrategy.java | 17 + .../rests/transactions/RestconfStrategy.java | 12 + .../rests/utils/ReadDataTransactionUtil.java | 114 +++++- .../utils/parser/ParserFieldsParameter.java | 366 ++++++++++++++---- .../parser/ParserFieldsParameterTest.java | 158 +++++++- .../jukebox/augmented-jukebox@2016-05-05.yang | 6 + .../test-services@2019-03-25.yang | 4 + 10 files changed, 615 insertions(+), 95 deletions(-) diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/context/WriterParameters.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/context/WriterParameters.java index f3a9eecfa2..d8a42f03f0 100644 --- a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/context/WriterParameters.java +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/context/WriterParameters.java @@ -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> fields; + private final List 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 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> fields; + private List 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 fieldPaths) { + this.fieldPaths = fieldPaths; + return this; + } + public WriterParametersBuilder setPrettyPrint(final boolean prettyPrint) { this.prettyPrint = prettyPrint; return this; diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java index 79ccb8acd5..4062f29f2b 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java @@ -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(); diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java index d0fb5c3352..f863987100 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java @@ -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>> read(final LogicalDatastoreType store, + final YangInstanceIdentifier path, final List fields) { + return Futures.immediateFailedFuture(new UnsupportedOperationException( + "Reading of selected subtrees is currently not supported in: " + MdsalRestconfStrategy.class)); + } + @Override public FluentFuture exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) { try (DOMDataTreeReadTransaction tx = transactionChain.newReadOnlyTransaction()) { diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java index 0f9acb580e..129e19049e 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java @@ -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>> read(final LogicalDatastoreType store, + final YangInstanceIdentifier path, final List 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 exists(final LogicalDatastoreType store, final YangInstanceIdentifier path) { return remapException(read(store, path)) diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java index dc1454a09d..9061ebe764 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java @@ -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>> 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>> read(LogicalDatastoreType store, + YangInstanceIdentifier path, List fields); + /** * Check if data already exists in the datastore. * diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/ReadDataTransactionUtil.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/ReadDataTransactionUtil.java index d4c022fd30..3aeb3518b0 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/ReadDataTransactionUtil.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/ReadDataTransactionUtil.java @@ -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 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>> 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 fields) { + final ListenableFuture>> 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>> 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 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; diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java index 16faf59b73..a5d7291d61 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameter.java @@ -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 type of identifier + */ +public abstract class ParserFieldsParameter { + private static final ParserFieldsParameter QNAME_PARSER = new QNameParser(); + private static final ParserFieldsParameter PATH_PARSER = new PathParser(); + private ParserFieldsParameter() { } /** * Parse fields parameter and return complete list of child nodes organized into levels. + * * @param identifier identifier context created from request URI * @param input input value of fields parameter - * @return {@link List} + * @return {@link List} of levels; each level contains set of {@link QName} */ public static @NonNull List> parseFieldsParameter(final @NonNull InstanceIdentifierContext identifier, final @NonNull String input) { - final List> 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 parseFieldsPaths( + final @NonNull InstanceIdentifierContext identifier, final @NonNull String input) { + final List> levels = PATH_PARSER.parseFields(identifier, input); + final List> mappedLevels = mapLevelsContentByIdentifiers(levels); + return buildPaths(mappedLevels); + } + + private static List> mapLevelsContentByIdentifiers( + final List> levels) { + // this step is used for saving some processing power - we can directly find LinkedPathElement using + // representing PathArgument + return levels.stream() + .map(linkedPathElements -> linkedPathElements.stream() + .map(linkedPathElement -> new SimpleEntry<>(linkedPathElement.targetNodeIdentifier, + linkedPathElement)) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue))) + .collect(Collectors.toList()); + } + + private static List buildPaths( + final List> mappedLevels) { + final List completePaths = new ArrayList<>(); + // we must traverse levels from the deepest level to the top level, because each LinkedPathElement is only + // linked to previous element + for (int levelIndex = mappedLevels.size() - 1; levelIndex >= 0; levelIndex--) { + // we go through unprocessed LinkedPathElements that represent leaves + for (final LinkedPathElement pathElement : mappedLevels.get(levelIndex).values()) { + if (pathElement.processed) { + // this element was already processed from the lower level - skip it + continue; + } + pathElement.processed = true; + + // adding deepest path arguments, LinkedList is used for more effective insertion at the 0 index + final LinkedList path = new LinkedList<>(pathElement.mixinNodesToTarget); + path.add(pathElement.targetNodeIdentifier); + + PathArgument previousIdentifier = pathElement.previousNodeIdentifier; + // adding path arguments from the linked LinkedPathElements recursively + for (int buildingLevel = levelIndex - 1; buildingLevel >= 0; buildingLevel--) { + final LinkedPathElement previousElement = mappedLevels.get(buildingLevel).get(previousIdentifier); + path.addFirst(previousElement.targetNodeIdentifier); + path.addAll(0, previousElement.mixinNodesToTarget); + previousIdentifier = previousElement.previousNodeIdentifier; + previousElement.processed = true; + } + completePaths.add(YangInstanceIdentifier.create(path)); + } + } + return completePaths; + } + + /** + * Parse fields parameter and return complete list of child nodes organized into levels. + * + * @param identifier identifier context created from request URI + * @param input input value of fields parameter + * @return {@link List} of levels; each level contains {@link Set} of identifiers of type {@link T} + */ + private @NonNull List> parseFields(final @NonNull InstanceIdentifierContext identifier, + final @NonNull String input) { + final List> 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> parsed, final SchemaContext context) { + private void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule, + final @NonNull DataSchemaContextNode startNode, + final @NonNull List> parsed, final SchemaContext context) { int currentPosition = 0; int startPosition = 0; DataSchemaContextNode currentNode = startNode; QNameModule currentQNameModule = startQNameModule; - Set currentLevel = new HashSet<>(); + Set currentLevel = new HashSet<>(); parsed.add(currentLevel); DataSchemaContextNode parenthesisNode = currentNode; - Set parenthesisLevel = currentLevel; + Set 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 prepareQNameLevel(final List> parsedQNames, final Set currentLevel) { - final Optional> existingLevel = parsedQNames.stream() + private Set prepareQNameLevel(final List> parsedIdentifiers, final Set currentLevel) { + final Optional> existingLevel = parsedIdentifiers.stream() .filter(qNameSet -> qNameSet.equals(currentLevel)) .findAny(); if (existingLevel.isPresent()) { - final int index = parsedQNames.indexOf(existingLevel.get()); - if (index == parsedQNames.size() - 1) { - final Set nextLevel = new HashSet<>(); - parsedQNames.add(nextLevel); + final int index = parsedIdentifiers.indexOf(existingLevel.get()); + if (index == parsedIdentifiers.size() - 1) { + final Set nextLevel = new HashSet<>(); + parsedIdentifiers.add(nextLevel); return nextLevel; } - return parsedQNames.get(index + 1); + return parsedIdentifiers.get(index + 1); } - final Set nextLevel = new HashSet<>(); - parsedQNames.add(nextLevel); + final Set nextLevel = new HashSet<>(); + parsedIdentifiers.add(nextLevel); return nextLevel; } - /** - * Add parsed child of current node to result for current level. - * @param currentNode current node - * @param 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 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 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 level); + + /** + * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output + * it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary + * mixin nodes are also flatten to the same level as identifiers of data nodes.
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
+ *
+     * level 0: ['a', 'd']
+     * level 1: ['b', 'x', 'e']
+     * level 2: ['c']
+     * 
+ */ + private static final class QNameParser extends ParserFieldsParameter { + @Override + DataSchemaContextNode addChildToResult(final DataSchemaContextNode currentNode, final String identifier, + final QNameModule currentQNameModule, final Set 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 level, + final @NonNull QName qualifiedName) { + DataSchemaContextNode currentNode = node; + while (currentNode != null && currentNode.isMixin()) { + level.add(qualifiedName); + currentNode = currentNode.getChild(qualifiedName); + } + + return currentNode; + } + } + + /** + * Fields parser that stores set of {@link LinkedPathElement}s in each level. Using {@link LinkedPathElement} + * it is possible to create a chain of path arguments and build complete paths since this element contains + * identifiers of intermediary mixin nodes and also linked previous element.
+ * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
+ *
+     * level 0: ['./a', './d']
+     * level 1: ['a/b', '/d/x/e']
+     * level 2: ['b/c']
+     * 
+ */ + private static final class PathParser extends ParserFieldsParameter { + @Override + DataSchemaContextNode addChildToResult(final DataSchemaContextNode currentNode, final String identifier, + final QNameModule currentQNameModule, final Set level) { + final QName childQName = QName.create(currentQNameModule, identifier); + final List collectedMixinNodes = new ArrayList<>(); + + DataSchemaContextNode actualContextNode = currentNode.getChild(childQName); + while (actualContextNode != null && actualContextNode.isMixin()) { + if (actualContextNode.getDataSchemaNode() instanceof ListSchemaNode) { + // we need just a single node identifier from list in the path (key is not available) + actualContextNode = actualContextNode.getChild(childQName); + break; + } else if (actualContextNode.getDataSchemaNode() instanceof LeafListSchemaNode) { + // NodeWithValue is unusable - stop parsing + break; + } else { + collectedMixinNodes.add(actualContextNode.getIdentifier()); + actualContextNode = actualContextNode.getChild(childQName); + } + } + + if (actualContextNode == null) { + throw new RestconfDocumentedException("Child " + 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.
+ * - identifiers of mixin nodes on the path to the target node - required for construction of full valid + * DOM paths,
+ * - identifier of the previous non-mixin node - required to successfully create a chain of {@link PathArgument}s + */ + private static final class LinkedPathElement { + private final PathArgument previousNodeIdentifier; + private final List mixinNodesToTarget; + private final PathArgument targetNodeIdentifier; + private boolean processed = false; + + /** + * Creation of new {@link LinkedPathElement}. + * + * @param previousNodeIdentifier identifier of the previous non-mixin node + * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node + * @param targetNodeIdentifier identifier of target non-mixin node + */ + private LinkedPathElement(final PathArgument previousNodeIdentifier, + final List mixinNodesToTarget, final PathArgument targetNodeIdentifier) { + this.previousNodeIdentifier = previousNodeIdentifier; + this.mixinNodesToTarget = mixinNodesToTarget; + this.targetNodeIdentifier = targetNodeIdentifier; + } + + @Override + public boolean equals(final Object obj) { + // this is need in order to make 'prepareQNameLevel(..)' working + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final LinkedPathElement that = (LinkedPathElement) obj; + return targetNodeIdentifier.equals(that.targetNodeIdentifier); + } + + @Override + public int hashCode() { + return Objects.hash(targetNodeIdentifier); + } + } +} \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java index a2e5063e10..b18a6d874c 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/ParserFieldsParameterTest.java @@ -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 parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierJukebox, input); + + assertNotNull(parsedFields); + assertEquals(1, parsedFields.size()); + final List pathArguments = parsedFields.get(0).getPathArguments(); + assertEquals(1, pathArguments.size()); + assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType()); + } + + @Test + public void parseTwoTopLevelContainersToPathsTest() { + final String input = "library;player"; + final List parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierJukebox, input); + + assertNotNull(parsedFields); + assertEquals(2, parsedFields.size()); + + final Optional libraryPath = findPath(parsedFields, LIBRARY_Q_NAME); + assertTrue(libraryPath.isPresent()); + assertEquals(1, libraryPath.get().getPathArguments().size()); + + final Optional playerPath = findPath(parsedFields, PLAYER_Q_NAME); + assertTrue(playerPath.isPresent()); + assertEquals(1, libraryPath.get().getPathArguments().size()); + } + + @Test + public void parseNestedLeafToPathTest() { + final String input = "library/album/name"; + final List parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierJukebox, input); + + assertEquals(1, parsedFields.size()); + final List pathArguments = parsedFields.get(0).getPathArguments(); + assertEquals(3, pathArguments.size()); + + assertEquals(LIBRARY_Q_NAME, pathArguments.get(0).getNodeType()); + assertEquals(ALBUM_Q_NAME, pathArguments.get(1).getNodeType()); + assertEquals(NAME_Q_NAME, pathArguments.get(2).getNodeType()); + } + + @Test + public void parseAugmentedLeafToPathTest() { + final String input = "player/augmented-jukebox:speed"; + final List parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierJukebox, input); + + assertEquals(1, parsedFields.size()); + final List pathArguments = parsedFields.get(0).getPathArguments(); + + assertEquals(3, pathArguments.size()); + assertEquals(PLAYER_Q_NAME, pathArguments.get(0).getNodeType()); + assertTrue(pathArguments.get(1) instanceof AugmentationIdentifier); + assertEquals(SPEED_Q_NAME, pathArguments.get(2).getNodeType()); + } + + @Test + public void parseMultipleFieldsOnDifferentLevelsToPathsTest() { + final String input = "services(type-of-service;instance/instance-name;instance/provider)"; + final List parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierTestServices, input); + + assertEquals(3, parsedFields.size()); + + final Optional tosPath = findPath(parsedFields, TYPE_OF_SERVICE_Q_NAME); + assertTrue(tosPath.isPresent()); + assertEquals(2, tosPath.get().getPathArguments().size()); + + final Optional instanceNamePath = findPath(parsedFields, INSTANCE_NAME_Q_NAME); + assertTrue(instanceNamePath.isPresent()); + assertEquals(3, instanceNamePath.get().getPathArguments().size()); + + final Optional providerPath = findPath(parsedFields, PROVIDER_Q_NAME); + assertTrue(providerPath.isPresent()); + assertEquals(3, providerPath.get().getPathArguments().size()); + } + + @Test + public void parseListFieldUnderListToPathTest() { + final String input = "services/instance"; + final List parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierTestServices, input); + + assertEquals(1, parsedFields.size()); + final List pathArguments = parsedFields.get(0).getPathArguments(); + assertEquals(2, pathArguments.size()); + + assertEquals(SERVICES_Q_NAME, pathArguments.get(0).getNodeType()); + assertTrue(pathArguments.get(0) instanceof NodeIdentifier); + assertEquals(INSTANCE_Q_NAME, pathArguments.get(1).getNodeType()); + assertTrue(pathArguments.get(1) instanceof NodeIdentifier); + } + + @Test + public void parseLeafListFieldToPathTest() { + final String input = "protocols"; + final List parsedFields = ParserFieldsParameter.parseFieldsPaths( + identifierTestServices, input); + assertEquals(1, parsedFields.size()); + final List pathArguments = parsedFields.get(0).getPathArguments(); + assertEquals(1, pathArguments.size()); + assertTrue(pathArguments.get(0) instanceof NodeIdentifier); + assertEquals(PROTOCOLS_Q_NAME, pathArguments.get(0).getNodeType()); + } + + private static Optional findPath(final List paths, + final QName lastPathArg) { + return paths.stream() + .filter(path -> lastPathArg.equals(path.getLastPathArgument().getNodeType())) + .findAny(); + } } \ No newline at end of file diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang b/restconf/restconf-nb-rfc8040/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang index abbd5d0fd5..b5e937af00 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang +++ b/restconf/restconf-nb-rfc8040/src/test/resources/jukebox/augmented-jukebox@2016-05-05.yang @@ -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 diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/test-services/test-services@2019-03-25.yang b/restconf/restconf-nb-rfc8040/src/test/resources/test-services/test-services@2019-03-25.yang index 48507d234a..5967690a92 100644 --- a/restconf/restconf-nb-rfc8040/src/test/resources/test-services/test-services@2019-03-25.yang +++ b/restconf/restconf-nb-rfc8040/src/test/resources/test-services/test-services@2019-03-25.yang @@ -33,5 +33,9 @@ module test-services { } } } + + leaf-list protocols { + type string; + } } } \ No newline at end of file -- 2.36.6