3a9333552b43faf83b29ba48c8a9f11a48d8c7f3
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / NetconfFieldsTranslator.java
1 /*
2  * Copyright © 2020 FRINX s.r.o. and others.  All rights reserved.
3  * Copyright © 2021 PANTHEON.tech, s.r.o.
4  *
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
8  */
9 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
10
11 import static java.util.Objects.requireNonNull;
12
13 import java.util.ArrayList;
14 import java.util.HashSet;
15 import java.util.LinkedList;
16 import java.util.List;
17 import java.util.Set;
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;
36
37 /**
38  * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers
39  * in {@code netconf-dom-api}.
40  *
41  * <p>
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}.
46  *
47  * <p>
48  * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
49  * <pre>
50  *   - './a' +- 'a/b' - 'b/c'
51  *           |
52  *           +- 'a/d' - 'd/x/e'
53  * </pre>
54  */
55 public final class NetconfFieldsTranslator {
56     /**
57      * {@link DataSchemaContextNode} of data element grouped with identifiers of leading mixin nodes and previous
58      * path element.<br>
59      *  - identifiers of mixin nodes on the path to the target node - required for construction of full valid
60      *    DOM paths,<br>
61      *  - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
62      *    of {@link PathArgument}s
63      *
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
67      */
68     private record LinkedPathElement(
69             @Nullable LinkedPathElement parentPathElement,
70             @NonNull List<PathArgument> mixinNodesToTarget,
71             @NonNull DataSchemaContextNode<?> targetNode) {
72         LinkedPathElement {
73             requireNonNull(mixinNodesToTarget);
74             requireNonNull(targetNode);
75         }
76     }
77
78     private NetconfFieldsTranslator() {
79         // Hidden on purpose
80     }
81
82     /**
83      * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
84      * {@link NetconfDataTreeService}.
85      *
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}
90      */
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();
95     }
96
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);
103         }
104
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());
108
109         return parsed;
110     }
111
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;
118
119             // Note: path is guaranteed to have at least one step
120             final var it = selector.path().iterator();
121             do {
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();
127                 }
128
129                 // add parsed path element linked to its parent
130                 pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
131             } while (it.hasNext());
132
133             final var subs = selector.subSelectors();
134             if (!subs.isEmpty()) {
135                 processSelectors(parsed, context, namespace, pathElement, subs);
136             } else {
137                 parsed.add(pathElement);
138             }
139         }
140     }
141
142     private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
143             final QName childQName) {
144         final var collectedMixinNodes = new ArrayList<PathArgument>();
145
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);
151         }
152
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
161                 break;
162             } else {
163                 collectedMixinNodes.add(actualContextNode.getIdentifier());
164                 actualContextNode = actualContextNode.getChild(childQName);
165             }
166         }
167
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);
172         }
173
174         return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
175     }
176
177     private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
178         LinkedPathElement pathElement = lastPathElement;
179         final var path = new LinkedList<PathArgument>();
180         do {
181             path.addFirst(pathElement.targetNode.getIdentifier());
182             path.addAll(0, pathElement.mixinNodesToTarget);
183             pathElement = pathElement.parentPathElement;
184         } while (pathElement.parentPathElement != null);
185
186         return YangInstanceIdentifier.create(path);
187     }
188
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);
194         }
195         return currentNode;
196     }
197 }