Disconnect RestconfDataServiceImpl from streams
[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.errors.RestconfDocumentedException;
24 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
25 import org.opendaylight.yangtools.yang.common.Empty;
26 import org.opendaylight.yangtools.yang.common.ErrorTag;
27 import org.opendaylight.yangtools.yang.common.ErrorType;
28 import org.opendaylight.yangtools.yang.common.QName;
29 import org.opendaylight.yangtools.yang.common.QNameModule;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
34 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
35 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
36 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
40
41 /**
42  * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers
43  * in {@code netconf-dom-api}.
44  *
45  * <p>
46  * Fields parser that stores a set of all the leaf {@link LinkedPathElement}s specified in {@link FieldsParam}.
47  * Using {@link LinkedPathElement} it is possible to create a chain of path arguments and build complete paths
48  * since this element contains identifiers of intermediary mixin nodes and also linked to its parent
49  * {@link LinkedPathElement}.
50  *
51  * <p>
52  * Example: field 'a(b/c;d/e)' ('e' is place under choice node 'x') is parsed into following levels:
53  * <pre>
54  *   - './a' +- 'a/b' - 'b/c'
55  *           |
56  *           +- 'a/d' - 'd/x/e'
57  * </pre>
58  */
59 public final class NetconfFieldsTranslator {
60     /**
61      * {@link DataSchemaContext} of data element grouped with identifiers of leading mixin nodes and previous path
62      * element.<br>
63      *  - identifiers of mixin nodes on the path to the target node - required for construction of full valid
64      *    DOM paths,<br>
65      *  - {@link LinkedPathElement} of the previous non-mixin node - required to successfully create a chain
66      *    of {@link PathArgument}s
67      *
68      * @param parentPathElement     parent path element
69      * @param mixinNodesToTarget    identifiers of mixin nodes on the path to the target node
70      * @param targetNode            target non-mixin node
71      */
72     private record LinkedPathElement(
73             @Nullable LinkedPathElement parentPathElement,
74             @NonNull List<PathArgument> mixinNodesToTarget,
75             @NonNull DataSchemaContext targetNode) {
76         LinkedPathElement {
77             requireNonNull(mixinNodesToTarget);
78             requireNonNull(targetNode);
79         }
80     }
81
82     private NetconfFieldsTranslator() {
83         // Hidden on purpose
84     }
85
86     /**
87      * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
88      * {@link NetconfDataTreeService}.
89      *
90      * @param identifier identifier context created from request URI
91      * @param input input value of fields parameter
92      * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument}
93      *     of provided {@code identifier}
94      */
95     public static @NonNull List<YangInstanceIdentifier> translate(
96             final @NonNull InstanceIdentifierContext identifier, final @NonNull FieldsParam input) {
97         final var parsed = parseFields(identifier, input);
98         return parsed.stream().map(NetconfFieldsTranslator::buildPath).toList();
99     }
100
101     private static @NonNull Set<LinkedPathElement> parseFields(final @NonNull InstanceIdentifierContext identifier,
102             final @NonNull FieldsParam input) {
103         final DataSchemaContext startNode;
104         try {
105             startNode = DataSchemaContext.of((DataSchemaNode) identifier.getSchemaNode());
106         } catch (IllegalStateException e) {
107             throw new RestconfDocumentedException(
108                 "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
109         }
110
111         final var parsed = new HashSet<LinkedPathElement>();
112         processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
113             new LinkedPathElement(null, List.of(), startNode), input.nodeSelectors());
114
115         return parsed;
116     }
117
118     private static void processSelectors(final Set<LinkedPathElement> parsed, final EffectiveModelContext context,
119             final QNameModule startNamespace, final LinkedPathElement startPathElement,
120             final List<NodeSelector> selectors) {
121         for (var selector : selectors) {
122             var pathElement = startPathElement;
123             var namespace = startNamespace;
124
125             // Note: path is guaranteed to have at least one step
126             final var it = selector.path().iterator();
127             do {
128                 final var step = it.next();
129                 final var module = step.module();
130                 if (module != null) {
131                     // FIXME: this is not defensive enough, as we can fail to find the module
132                     namespace = context.findModules(module).iterator().next().getQNameModule();
133                 }
134
135                 // add parsed path element linked to its parent
136                 pathElement = addChildPathElement(pathElement, step.identifier().bindTo(namespace));
137             } while (it.hasNext());
138
139             final var subs = selector.subSelectors();
140             if (!subs.isEmpty()) {
141                 processSelectors(parsed, context, namespace, pathElement, subs);
142             } else {
143                 parsed.add(pathElement);
144             }
145         }
146     }
147
148     private static LinkedPathElement addChildPathElement(final LinkedPathElement currentElement,
149             final QName childQName) {
150         final var collectedMixinNodes = new ArrayList<PathArgument>();
151
152         DataSchemaContext currentNode = currentElement.targetNode;
153         DataSchemaContext actualContextNode = childByQName(currentNode, childQName);
154         if (actualContextNode == null) {
155             actualContextNode = resolveMixinNode(currentNode, currentNode.getPathStep().getNodeType());
156             actualContextNode = childByQName(actualContextNode, childQName);
157         }
158
159         while (actualContextNode != null && actualContextNode instanceof PathMixin) {
160             final var actualDataSchemaNode = actualContextNode.dataSchemaNode();
161             if (actualDataSchemaNode instanceof ListSchemaNode listSchema && listSchema.getKeyDefinition().isEmpty()) {
162                 // we need just a single node identifier from list in the path IFF it is an unkeyed list, otherwise
163                 // we need both (which is the default case)
164                 actualContextNode = childByQName(actualContextNode, childQName);
165             } else if (actualDataSchemaNode instanceof LeafListSchemaNode) {
166                 // NodeWithValue is unusable - stop parsing
167                 break;
168             } else {
169                 collectedMixinNodes.add(actualContextNode.getPathStep());
170                 actualContextNode = childByQName(actualContextNode, childQName);
171             }
172         }
173
174         if (actualContextNode == null) {
175             throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
176                 + currentNode.getPathStep().getNodeType().getLocalName(),
177                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
178         }
179
180         return new LinkedPathElement(currentElement, collectedMixinNodes, actualContextNode);
181     }
182
183     private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
184         return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
185     }
186
187     private static YangInstanceIdentifier buildPath(final LinkedPathElement lastPathElement) {
188         LinkedPathElement pathElement = lastPathElement;
189         final var path = new LinkedList<PathArgument>();
190         do {
191             path.addFirst(contextPathArgument(pathElement.targetNode));
192             path.addAll(0, pathElement.mixinNodesToTarget);
193             pathElement = pathElement.parentPathElement;
194         } while (pathElement.parentPathElement != null);
195
196         return YangInstanceIdentifier.of(path);
197     }
198
199     private static @NonNull PathArgument contextPathArgument(final DataSchemaContext context) {
200         final var arg = context.pathStep();
201         if (arg != null) {
202             return arg;
203         }
204
205         final var schema = context.dataSchemaNode();
206         if (schema instanceof ListSchemaNode listSchema && !listSchema.getKeyDefinition().isEmpty()) {
207             return NodeIdentifierWithPredicates.of(listSchema.getQName());
208         }
209         if (schema instanceof LeafListSchemaNode leafListSchema) {
210             return new NodeWithValue<>(leafListSchema.getQName(), Empty.value());
211         }
212         throw new UnsupportedOperationException("Unsupported schema " + schema);
213     }
214
215     private static DataSchemaContext resolveMixinNode(final DataSchemaContext node,
216             final @NonNull QName qualifiedName) {
217         DataSchemaContext currentNode = node;
218         while (currentNode != null && currentNode instanceof PathMixin currentMixin) {
219             currentNode = currentMixin.childByQName(qualifiedName);
220         }
221         return currentNode;
222     }
223 }