From 78d2744cacf5a8ebccec8727a62a1eea0270d3c5 Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Thu, 4 Apr 2024 03:16:11 +0200 Subject: [PATCH] Hide NetconfFieldsTranslator There is only one user of this class, move it to the same package and hide it by inlining it into NetconfRestconfStrategy. Change-Id: Ib614b046bd6dcdf51e4b09d8b7cdec3b43c16629 Signed-off-by: Robert Varga --- .../transactions/NetconfRestconfStrategy.java | 184 +++++++++++++++- .../utils/parser/NetconfFieldsTranslator.java | 208 ------------------ .../transactions/NetconfFieldsParamTest.java} | 9 +- 3 files changed, 187 insertions(+), 214 deletions(-) delete mode 100644 restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java rename restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/{utils/parser/NetconfFieldsTranslatorTest.java => rests/transactions/NetconfFieldsParamTest.java} (96%) diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java index b29249d6fa..10ff23f32d 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java @@ -9,14 +9,19 @@ package org.opendaylight.restconf.nb.rfc8040.rests.transactions; import static java.util.Objects.requireNonNull; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.FutureCallback; 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.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Optional; +import java.util.Set; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.common.api.CommitInfo; @@ -29,19 +34,32 @@ import org.opendaylight.mdsal.dom.api.DOMSchemaService.YangTextSourceExtension; import org.opendaylight.mdsal.dom.api.DOMTransactionChain; import org.opendaylight.netconf.dom.api.NetconfDataTreeService; import org.opendaylight.restconf.api.query.ContentParam; +import org.opendaylight.restconf.api.query.FieldsParam; +import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector; import org.opendaylight.restconf.api.query.WithDefaultsParam; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.common.errors.RestconfFuture; import org.opendaylight.restconf.common.errors.SettableRestconfFuture; import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters; -import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator; import org.opendaylight.restconf.server.api.DataGetParams; import org.opendaylight.restconf.server.api.DataGetResult; import org.opendaylight.restconf.server.api.DatabindContext; import org.opendaylight.restconf.server.api.DatabindPath.Data; import org.opendaylight.yangtools.yang.common.Empty; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.ErrorType; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.QNameModule; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContext; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; /** * Implementation of RESTCONF operations on top of a raw NETCONF backend. @@ -89,7 +107,7 @@ public final class NetconfRestconfStrategy extends RestconfStrategy { if (fields != null) { final List tmp; try { - tmp = NetconfFieldsTranslator.translate(inference.modelContext(), path.schema(), fields); + tmp = fieldsParamToPaths(inference.modelContext(), path.schema(), fields); } catch (RestconfDocumentedException e) { return RestconfFuture.failed(e); } @@ -193,4 +211,166 @@ public final class NetconfRestconfStrategy extends RestconfStrategy { }, MoreExecutors.directExecutor()); return ret; } + + /** + * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with + * {@link NetconfDataTreeService}. + * + *

+ * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}. + * 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 to its parent + * {@link LinkedPathElement}. + * + *

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

+     *   - './a' +- 'a/b' - 'b/c'
+     *           |
+     *           +- 'a/d' - 'd/x/e'
+     * 
+ * + * + * @param modelContext EffectiveModelContext + * @param startNode Root DataSchemaNode + * @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} + */ + @VisibleForTesting + static @NonNull List fieldsParamToPaths(final @NonNull EffectiveModelContext modelContext, + final @NonNull DataSchemaContext startNode, final @NonNull FieldsParam input) { + final var parsed = new HashSet(); + processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(), + new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors()); + return parsed.stream().map(NetconfRestconfStrategy::buildPath).toList(); + } + + private static void processSelectors(final Set parsed, final EffectiveModelContext context, + final QNameModule startNamespace, final LinkedPathElement startPathElement, + final List selectors) { + for (var selector : selectors) { + var pathElement = startPathElement; + var namespace = startNamespace; + + // Note: path is guaranteed to have at least one step + final var it = selector.path().iterator(); + do { + final var step = it.next(); + final var module = step.module(); + if (module != null) { + // FIXME: this is not defensive enough, as we can fail to find the module + namespace = context.findModules(module).iterator().next().getQNameModule(); + } + + // add parsed path element linked to its parent + pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace)); + } while (it.hasNext()); + + final var subs = selector.subSelectors(); + if (!subs.isEmpty()) { + processSelectors(parsed, context, namespace, pathElement, subs); + } else { + parsed.add(pathElement); + } + } + } + + private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement, + final QName childQName) { + final var collectedMixinNodes = new ArrayList(); + + DataSchemaContext currentNode = currentElement.targetNode; + DataSchemaContext actualContextNode = childByQName(currentNode, childQName); + if (actualContextNode == null) { + actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType()); + actualContextNode = childByQName(actualContextNode, childQName); + } + + while (actualContextNode != null && actualContextNode instanceof PathMixin) { + final var actualDataSchemaNode = actualContextNode.dataSchemaNode(); + if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) { + // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise + // we need both (which is the default case) + actualContextNode = childByQName(actualContextNode, childQName); + } else if (actualDataSchemaNode instanceof LeafListSchemaNode) { + // NodeWithValue is unusable - stop parsing + break; + } else { + collectedMixinNodes.add(actualContextNode.getPathStep()); + actualContextNode = childByQName(actualContextNode, childQName); + } + } + + if (actualContextNode == null) { + throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in " + + currentNode.getPathStep().getNodeType().getLocalName(), + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode); + } + + private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) { + return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null; + } + + private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) { + var pathElement = lastPathElement; + final var path = new LinkedList(); + do { + path.addFirst(contextPathArgument(pathElement.targetNode)); + path.addAll(0, pathElement.mixinNodesToTarget); + pathElement = pathElement.parentPathElement; + } while (pathElement.parentPathElement != null); + + return YangInstanceIdentifier.of(path); + } + + private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) { + final var arg = context.pathStep(); + if (arg != null) { + return arg; + } + + final var schema = context.dataSchemaNode(); + if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) { + return NodeIdentifierWithPredicates.of(listSchema.getQName()); + } + if (schema instanceof LeafListSchemaNode leafListSchema) { + return new NodeWithValue<>(leafListSchema.getQName(), Empty.value()); + } + throw new UnsupportedOperationException("Unsupported schema " + schema); + } + + private static DataSchemaContext resolveMixinNode(final DataSchemaContext node, + final @NonNull QName qualifiedName) { + DataSchemaContext currentNode = node; + while (currentNode != null && currentNode instanceof PathMixin currentMixin) { + currentNode = currentMixin.childByQName(qualifiedName); + } + return currentNode; + } + + /** + * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path + * element.
+ * - identifiers of mixin nodes on the path to the target node - required for construction of full valid + * DOM paths,
+ * - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain + * of {@link PathArgument}s + * + * @param parentPathElement parent path element + * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node + * @param targetNode target non-mixin node + */ + private record LinkedPathElement( + @Nullable LinkedPathElement parentPathElement, + @NonNull List mixinNodesToTarget, + @NonNull DataSchemaContext targetNode) { + LinkedPathElement { + requireNonNull(mixinNodesToTarget); + requireNonNull(targetNode); + } + } } diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java deleted file mode 100644 index 01f804435b..0000000000 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslator.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright © 2020 FRINX s.r.o. and others. All rights reserved. - * Copyright © 2021 PANTHEON.tech, s.r.o. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 which accompanies this distribution, - * and is available at http://www.eclipse.org/legal/epl-v10.html - */ -package org.opendaylight.restconf.nb.rfc8040.utils.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import org.opendaylight.netconf.dom.api.NetconfDataTreeService; -import org.opendaylight.restconf.api.query.FieldsParam; -import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector; -import org.opendaylight.restconf.common.errors.RestconfDocumentedException; -import org.opendaylight.yangtools.yang.common.Empty; -import org.opendaylight.yangtools.yang.common.ErrorTag; -import org.opendaylight.yangtools.yang.common.ErrorType; -import org.opendaylight.yangtools.yang.common.QName; -import org.opendaylight.yangtools.yang.common.QNameModule; -import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; -import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; -import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; -import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; -import org.opendaylight.yangtools.yang.data.util.DataSchemaContext; -import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin; -import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; -import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; -import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; - -/** - * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers - * in {@code netconf-dom-api}. - * - *

- * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}. - * 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 to its parent - * {@link LinkedPathElement}. - * - *

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

- *   - './a' +- 'a/b' - 'b/c'
- *           |
- *           +- 'a/d' - 'd/x/e'
- * 
- */ -public final class NetconfFieldsTranslator { - /** - * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path - * element.
- * - identifiers of mixin nodes on the path to the target node - required for construction of full valid - * DOM paths,
- * - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain - * of {@link PathArgument}s - * - * @param parentPathElement parent path element - * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node - * @param targetNode target non-mixin node - */ - private record LinkedPathElement( - @Nullable LinkedPathElement parentPathElement, - @NonNull List mixinNodesToTarget, - @NonNull DataSchemaContext targetNode) { - LinkedPathElement { - requireNonNull(mixinNodesToTarget); - requireNonNull(targetNode); - } - } - - private NetconfFieldsTranslator() { - // Hidden on purpose - } - - /** - * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with - * {@link NetconfDataTreeService}. - * - * @param modelContext EffectiveModelContext - * @param startNode Root DataSchemaNode - * @param input input value of fields parameter - * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument} - * of provided {@code identifier} - */ - public static @NonNull List translate( - final @NonNull EffectiveModelContext modelContext, final @NonNull DataSchemaContext startNode, - final @NonNull FieldsParam input) { - final var parsed = new HashSet(); - processSelectors(parsed, modelContext, startNode.dataSchemaNode().getQName().getModule(), - new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors()); - return parsed.stream().map(NetconfFieldsTranslator::buildPath).toList(); - } - - private static void processSelectors(final Set parsed, final EffectiveModelContext context, - final QNameModule startNamespace, final LinkedPathElement startPathElement, - final List selectors) { - for (var selector : selectors) { - var pathElement = startPathElement; - var namespace = startNamespace; - - // Note: path is guaranteed to have at least one step - final var it = selector.path().iterator(); - do { - final var step = it.next(); - final var module = step.module(); - if (module != null) { - // FIXME: this is not defensive enough, as we can fail to find the module - namespace = context.findModules(module).iterator().next().getQNameModule(); - } - - // add parsed path element linked to its parent - pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace)); - } while (it.hasNext()); - - final var subs = selector.subSelectors(); - if (!subs.isEmpty()) { - processSelectors(parsed, context, namespace, pathElement, subs); - } else { - parsed.add(pathElement); - } - } - } - - private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement, - final QName childQName) { - final var collectedMixinNodes = new ArrayList(); - - DataSchemaContext currentNode = currentElement.targetNode; - DataSchemaContext actualContextNode = childByQName(currentNode, childQName); - if (actualContextNode == null) { - actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType()); - actualContextNode = childByQName(actualContextNode, childQName); - } - - while (actualContextNode != null && actualContextNode instanceof PathMixin) { - final var actualDataSchemaNode = actualContextNode.dataSchemaNode(); - if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) { - // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise - // we need both (which is the default case) - actualContextNode = childByQName(actualContextNode, childQName); - } else if (actualDataSchemaNode instanceof LeafListSchemaNode) { - // NodeWithValue is unusable - stop parsing - break; - } else { - collectedMixinNodes.add(actualContextNode.getPathStep()); - actualContextNode = childByQName(actualContextNode, childQName); - } - } - - if (actualContextNode == null) { - throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in " - + currentNode.getPathStep().getNodeType().getLocalName(), - ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); - } - - return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode); - } - - private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) { - return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null; - } - - private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) { - LinkedPathElement pathElement = lastPathElement; - final var path = new LinkedList(); - do { - path.addFirst(contextPathArgument(pathElement.targetNode)); - path.addAll(0, pathElement.mixinNodesToTarget); - pathElement = pathElement.parentPathElement; - } while (pathElement.parentPathElement != null); - - return YangInstanceIdentifier.of(path); - } - - private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) { - final var arg = context.pathStep(); - if (arg != null) { - return arg; - } - - final var schema = context.dataSchemaNode(); - if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) { - return NodeIdentifierWithPredicates.of(listSchema.getQName()); - } - if (schema instanceof LeafListSchemaNode leafListSchema) { - return new NodeWithValue<>(leafListSchema.getQName(), Empty.value()); - } - throw new UnsupportedOperationException("Unsupported schema " + schema); - } - - private static DataSchemaContext resolveMixinNode(final DataSchemaContext node, - final @NonNull QName qualifiedName) { - DataSchemaContext currentNode = node; - while (currentNode != null && currentNode instanceof PathMixin currentMixin) { - currentNode = currentMixin.childByQName(qualifiedName); - } - return currentNode; - } -} diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfFieldsParamTest.java similarity index 96% rename from restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java rename to restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfFieldsParamTest.java index 26583eaac3..813be5eedb 100644 --- a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/NetconfFieldsTranslatorTest.java +++ b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfFieldsParamTest.java @@ -6,7 +6,7 @@ * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ -package org.opendaylight.restconf.nb.rfc8040.utils.parser; +package org.opendaylight.restconf.nb.rfc8040.rests.transactions; import static org.junit.Assert.assertEquals; @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.opendaylight.restconf.api.query.FieldsParam; +import org.opendaylight.restconf.nb.rfc8040.utils.parser.AbstractFieldsTranslatorTest; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; @@ -24,14 +25,14 @@ import org.opendaylight.yangtools.yang.data.util.DataSchemaContext; import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; /** - * Unit test for {@link NetconfFieldsTranslator}. + * Unit test for {@link NetconfFieldsParam}. */ @RunWith(MockitoJUnitRunner.class) -public class NetconfFieldsTranslatorTest extends AbstractFieldsTranslatorTest { +public class NetconfFieldsParamTest extends AbstractFieldsTranslatorTest { @Override protected List translateFields(final EffectiveModelContext modelContext, final DataSchemaContext startNode, final FieldsParam fields) { - return NetconfFieldsTranslator.translate(modelContext, startNode, fields); + return NetconfRestconfStrategy.fieldsParamToPaths(modelContext, startNode, fields); } @Override -- 2.36.6