2 * Copyright © 2020 FRINX s.r.o. and others. All rights reserved.
3 * Copyright © 2021 PANTHEON.tech, s.r.o.
5 * This program and the accompanying materials are made available under the
6 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
7 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
11 import static java.util.Objects.requireNonNull;
13 import java.util.ArrayList;
14 import java.util.HashSet;
15 import java.util.LinkedList;
16 import java.util.List;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
21 import org.opendaylight.restconf.api.query.FieldsParam;
22 import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
23 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
24 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
25 import org.opendaylight.yangtools.yang.common.ErrorTag;
26 import org.opendaylight.yangtools.yang.common.ErrorType;
27 import org.opendaylight.yangtools.yang.common.QName;
28 import org.opendaylight.yangtools.yang.common.QNameModule;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
32 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
34 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
38 * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers
39 * in {@code netconf-dom-api}.
42 * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}.
43 * Using {@link LinkedPathElement} it is possible to create a chain of path arguments and build complete paths
44 * since this element contains identifiers of intermediary mixin nodes and also linked to its parent
45 * {@link LinkedPathElement}.
48 * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
50 * - './a' +- 'a/b' - 'b/c'
55 public final class NetconfFieldsTranslator {
57 * {@link DataSchemaContextNode} of data element grouped with identifiers of leading mixin nodes and previous
59 * - identifiers of mixin nodes on the path to the target node - required for construction of full valid
61 * - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
62 * of {@link PathArgument}s
64 * @param parentPathElement parent path element
65 * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node
66 * @param targetNode target non-mixin node
68 private record LinkedPathElement(
69 @Nullable LinkedPathElement parentPathElement,
70 @NonNull List<PathArgument> mixinNodesToTarget,
71 @NonNull DataSchemaContextNode<?> targetNode) {
73 requireNonNull(mixinNodesToTarget);
74 requireNonNull(targetNode);
78 private NetconfFieldsTranslator() {
83 * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
84 * {@link NetconfDataTreeService}.
86 * @param identifier identifier context created from request URI
87 * @param input input value of fields parameter
88 * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument}
89 * of provided {@code identifier}
91 public static @NonNull List<YangInstanceIdentifier> translate(
92 final @NonNull InstanceIdentifierContext identifier, final @NonNull FieldsParam input) {
93 final var parsed = parseFields(identifier, input);
94 return parsed.stream().map(NetconfFieldsTranslator::buildPath).toList();
97 private static @NonNull Set<LinkedPathElement> parseFields(final @NonNull InstanceIdentifierContext identifier,
98 final @NonNull FieldsParam input) {
99 final var startNode = DataSchemaContextNode.fromDataSchemaNode((DataSchemaNode) identifier.getSchemaNode());
100 if (startNode == null) {
101 throw new RestconfDocumentedException(
102 "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
105 final var parsed = new HashSet<LinkedPathElement>();
106 processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
107 new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors());
112 private static void processSelectors(final Set<LinkedPathElement> parsed, final EffectiveModelContext context,
113 final QNameModule startNamespace, final LinkedPathElement startPathElement,
114 final List<NodeSelector> selectors) {
115 for (var selector : selectors) {
116 var pathElement = startPathElement;
117 var namespace = startNamespace;
119 // Note: path is guaranteed to have at least one step
120 final var it = selector.path().iterator();
122 final var step = it.next();
123 final var module = step.module();
124 if (module != null) {
125 // FIXME: this is not defensive enough, as we can fail to find the module
126 namespace = context.findModules(module).iterator().next().getQNameModule();
129 // add parsed path element linked to its parent
130 pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
131 } while (it.hasNext());
133 final var subs = selector.subSelectors();
134 if (!subs.isEmpty()) {
135 processSelectors(parsed, context, namespace, pathElement, subs);
137 parsed.add(pathElement);
142 private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
143 final QName childQName) {
144 final var collectedMixinNodes = new ArrayList<PathArgument>();
146 DataSchemaContextNode<?> currentNode = currentElement.targetNode;
147 DataSchemaContextNode<?> actualContextNode = currentNode.getChild(childQName);
148 if (actualContextNode == null) {
149 actualContextNode = resolveMixinNode(currentNode, currentNode.getIdentifier().getNodeType());
150 actualContextNode = actualContextNode.getChild(childQName);
153 while (actualContextNode != null && actualContextNode.isMixin()) {
154 final var actualDataSchemaNode = actualContextNode.getDataSchemaNode();
155 if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) {
156 // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise
157 // we need both (which is the default case)
158 actualContextNode = actualContextNode.getChild(childQName);
159 } else if (actualDataSchemaNode instanceof LeafListSchemaNode) {
160 // NodeWithValue is unusable - stop parsing
163 collectedMixinNodes.add(actualContextNode.getIdentifier());
164 actualContextNode = actualContextNode.getChild(childQName);
168 if (actualContextNode == null) {
169 throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
170 + currentNode.getIdentifier().getNodeType().getLocalName(),
171 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
174 return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
177 private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
178 LinkedPathElement pathElement = lastPathElement;
179 final var path = new LinkedList<PathArgument>();
181 path.addFirst(pathElement.targetNode.getIdentifier());
182 path.addAll(0, pathElement.mixinNodesToTarget);
183 pathElement = pathElement.parentPathElement;
184 } while (pathElement.parentPathElement != null);
186 return YangInstanceIdentifier.create(path);
189 private static DataSchemaContextNode<?> resolveMixinNode(final DataSchemaContextNode<?> node,
190 final @NonNull QName qualifiedName) {
191 DataSchemaContextNode<?> currentNode = node;
192 while (currentNode != null && currentNode.isMixin()) {
193 currentNode = currentNode.getChild(qualifiedName);