6931ca28f4a246b384e20576e54acf983e3964c5
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / WriterFieldsTranslator.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  * Copyright (c) 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 java.util.ArrayList;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Set;
15 import org.eclipse.jdt.annotation.NonNull;
16 import org.eclipse.jdt.annotation.Nullable;
17 import org.opendaylight.restconf.api.query.FieldsParam;
18 import org.opendaylight.restconf.api.query.FieldsParam.NodeSelector;
19 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
20 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
21 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.ParameterAwareNormalizedNodeWriter;
22 import org.opendaylight.yangtools.yang.common.ErrorTag;
23 import org.opendaylight.yangtools.yang.common.ErrorType;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.common.QNameModule;
26 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
27 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
29
30 /**
31  * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output
32  * it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary
33  * mixin nodes are also flatten to the same level as identifiers of data nodes.<br>
34  * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
35  * <pre>
36  * level 0: ['a', 'd']
37  * level 1: ['b', 'x', 'e']
38  * level 2: ['c']
39  * </pre>
40  */
41 public final class WriterFieldsTranslator {
42     private WriterFieldsTranslator() {
43         // Hidden on purpose
44     }
45
46     /**
47      * Translate a {@link FieldsParam} to a complete list of child nodes organized into levels, suitable for use with
48      * {@link ParameterAwareNormalizedNodeWriter}.
49      *
50      * @param identifier identifier context created from request URI
51      * @param input input value of fields parameter
52      * @return {@link List} of levels; each level contains set of {@link QName}
53      */
54     public static @NonNull List<Set<QName>> translate(final @NonNull InstanceIdentifierContext identifier,
55             final @NonNull FieldsParam input) {
56         final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
57                 (DataSchemaNode) identifier.getSchemaNode());
58         if (startNode == null) {
59             throw new RestconfDocumentedException(
60                     "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
61         }
62
63         final var parsed = new ArrayList<Set<QName>>();
64         processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
65             startNode, input.nodeSelectors(), 0);
66         return parsed;
67     }
68
69     private static void processSelectors(final List<Set<QName>> parsed, final EffectiveModelContext context,
70             final QNameModule startNamespace, final DataSchemaContextNode<?> startNode,
71             final List<NodeSelector> selectors, final int index) {
72         final Set<QName> startLevel;
73         if (parsed.size() <= index) {
74             startLevel = new HashSet<>();
75             parsed.add(startLevel);
76         } else {
77             startLevel = parsed.get(index);
78         }
79         for (var selector : selectors) {
80             var node = startNode;
81             var namespace = startNamespace;
82             var level = startLevel;
83             var levelIndex = index;
84
85             // Note: path is guaranteed to have at least one step
86             final var it = selector.path().iterator();
87             while (true) {
88                 // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
89                 //        need to call it only when we know there is another identifier coming, otherwise we would end
90                 //        up with empty levels sneaking into the mix.
91                 //
92                 //        Dealing with that weirdness requires understanding what the expected end results are and a
93                 //        larger rewrite of the algorithms involved.
94                 final var step = it.next();
95                 final var module = step.module();
96                 if (module != null) {
97                     // FIXME: this is not defensive enough, as we can fail to find the module
98                     namespace = context.findModules(module).iterator().next().getQNameModule();
99                 }
100
101                 // add parsed identifier to results for current level
102                 node = addChildToResult(node, step.identifier().bindTo(namespace), level);
103                 if (!it.hasNext()) {
104                     break;
105                 }
106
107                 // go one level down
108                 level = prepareQNameLevel(parsed, level);
109                 levelIndex++;
110             }
111
112             final var subs = selector.subSelectors();
113             if (!subs.isEmpty()) {
114                 processSelectors(parsed, context, namespace, node, subs, levelIndex + 1);
115             }
116         }
117     }
118
119     /**
120      * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
121      * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
122      * is allocated and pushed to input parsed identifiers.
123      *
124      * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
125      * @param currentLevel Current level of identifiers (set).
126      * @return Existing or new level of identifiers.
127      */
128     private static Set<QName> prepareQNameLevel(final List<Set<QName>> parsedIdentifiers,
129             final Set<QName> currentLevel) {
130         final var existingLevel = parsedIdentifiers.stream()
131                 .filter(qNameSet -> qNameSet.equals(currentLevel))
132                 .findAny();
133         if (existingLevel.isPresent()) {
134             final int index = parsedIdentifiers.indexOf(existingLevel.orElseThrow());
135             if (index == parsedIdentifiers.size() - 1) {
136                 final var nextLevel = new HashSet<QName>();
137                 parsedIdentifiers.add(nextLevel);
138                 return nextLevel;
139             }
140
141             return parsedIdentifiers.get(index + 1);
142         }
143
144         final var nextLevel = new HashSet<QName>();
145         parsedIdentifiers.add(nextLevel);
146         return nextLevel;
147     }
148
149     /**
150      * Add parsed child of current node to result for current level.
151      *
152      * @param currentNode current node
153      * @param childQName parsed identifier of child node
154      * @param level current nodes level
155      * @return {@link DataSchemaContextNode}
156      */
157     private static DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode,
158             final QName childQName, final Set<QName> level) {
159         // resolve parent node
160         final DataSchemaContextNode<?> parentNode = resolveMixinNode(
161                 currentNode, level, currentNode.getIdentifier().getNodeType());
162         if (parentNode == null) {
163             throw new RestconfDocumentedException(
164                     "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
165                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
166         }
167
168         // resolve child node
169         final DataSchemaContextNode<?> childNode = resolveMixinNode(
170                 parentNode.getChild(childQName), level, childQName);
171         if (childNode == null) {
172             throw new RestconfDocumentedException(
173                     "Child " + childQName.getLocalName() + " node missing in "
174                             + currentNode.getIdentifier().getNodeType().getLocalName(),
175                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
176         }
177
178         // add final childNode node to level nodes
179         level.add(childNode.getIdentifier().getNodeType());
180         return childNode;
181     }
182
183     /**
184      * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
185      * All nodes expect of not mixin node are added to current level nodes.
186      *
187      * @param node          initial mixin or not-mixin node
188      * @param level         current nodes level
189      * @param qualifiedName qname of initial node
190      * @return {@link DataSchemaContextNode}
191      */
192     private static @Nullable DataSchemaContextNode<?> resolveMixinNode(final @Nullable DataSchemaContextNode<?> node,
193             final @NonNull Set<QName> level, final @NonNull QName qualifiedName) {
194         DataSchemaContextNode<?> currentNode = node;
195         while (currentNode != null && currentNode.isMixin()) {
196             level.add(qualifiedName);
197             currentNode = currentNode.getChild(qualifiedName);
198         }
199
200         return currentNode;
201     }
202 }