2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
3 * Copyright (c) 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 java.util.ArrayList;
12 import java.util.HashSet;
13 import java.util.List;
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.errors.RestconfDocumentedException;
20 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.ParameterAwareNormalizedNodeWriter;
21 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
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.DataSchemaContext;
27 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
28 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
29 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
32 * Fields parser that stores set of {@link QName}s in each level. Because of this fact, from the output
33 * it is is only possible to assume on what depth the selected element is placed. Identifiers of intermediary
34 * mixin nodes are also flatten to the same level as identifiers of data nodes.<br>
35 * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:<br>
38 * level 1: ['b', 'x', 'e']
42 public final class WriterFieldsTranslator {
43 private WriterFieldsTranslator() {
48 * Translate a {@link FieldsParam} to a complete list of child nodes organized into levels, suitable for use with
49 * {@link ParameterAwareNormalizedNodeWriter}.
51 * @param identifier identifier context created from request URI
52 * @param input input value of fields parameter
53 * @return {@link List} of levels; each level contains set of {@link QName}
55 public static @NonNull List<Set<QName>> translate(final @NonNull InstanceIdentifierContext identifier,
56 final @NonNull FieldsParam input) {
57 final DataSchemaContext startNode = DataSchemaContext.of((DataSchemaNode) identifier.getSchemaNode());
58 if (startNode == null) {
59 throw new RestconfDocumentedException(
60 "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
63 final var parsed = new ArrayList<Set<QName>>();
64 processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
65 startNode, input.nodeSelectors(), 0);
69 private static void processSelectors(final List<Set<QName>> parsed, final EffectiveModelContext context,
70 final QNameModule startNamespace, final DataSchemaContext startNode, final List<NodeSelector> selectors,
72 final Set<QName> startLevel;
73 if (parsed.size() <= index) {
74 startLevel = new HashSet<>();
75 parsed.add(startLevel);
77 startLevel = parsed.get(index);
79 for (var selector : selectors) {
81 var namespace = startNamespace;
82 var level = startLevel;
83 var levelIndex = index;
85 // Note: path is guaranteed to have at least one step
86 final var it = selector.path().iterator();
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.
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();
97 // FIXME: this is not defensive enough, as we can fail to find the module
98 namespace = context.findModules(module).iterator().next().getQNameModule();
101 // add parsed identifier to results for current level
102 node = addChildToResult(node, step.identifier().bindTo(namespace), level);
108 level = prepareQNameLevel(parsed, level);
112 final var subs = selector.subSelectors();
113 if (!subs.isEmpty()) {
114 processSelectors(parsed, context, namespace, node, subs, levelIndex + 1);
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.
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.
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))
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);
141 return parsedIdentifiers.get(index + 1);
144 final var nextLevel = new HashSet<QName>();
145 parsedIdentifiers.add(nextLevel);
150 * Add parsed child of current node to result for current level.
152 * @param currentNode current node
153 * @param childQName parsed identifier of child node
154 * @param level current nodes level
155 * @return {@link DataSchemaContextNode}
157 private static DataSchemaContext addChildToResult(final DataSchemaContext currentNode, final QName childQName,
158 final Set<QName> level) {
159 // resolve parent node
160 final var parentNode = resolveMixinNode(currentNode, level, currentNode.dataSchemaNode().getQName());
161 if (parentNode == null) {
162 throw new RestconfDocumentedException(
163 "Not-mixin node missing in " + currentNode.getPathStep().getNodeType().getLocalName(),
164 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
167 // resolve child node
168 final DataSchemaContext childNode = resolveMixinNode(childByQName(parentNode, childQName), level, childQName);
169 if (childNode == null) {
170 throw new RestconfDocumentedException(
171 "Child " + childQName.getLocalName() + " node missing in "
172 + currentNode.getPathStep().getNodeType().getLocalName(),
173 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
176 // add final childNode node to level nodes
177 level.add(childNode.dataSchemaNode().getQName());
181 private static @Nullable DataSchemaContext childByQName(final DataSchemaContext parent, final QName qname) {
182 return parent instanceof DataSchemaContext.Composite composite ? composite.childByQName(qname) : null;
186 * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
187 * All nodes expect of not mixin node are added to current level nodes.
189 * @param node initial mixin or not-mixin node
190 * @param level current nodes level
191 * @param qualifiedName qname of initial node
192 * @return {@link DataSchemaContextNode}
194 private static @Nullable DataSchemaContext resolveMixinNode(final @Nullable DataSchemaContext node,
195 final @NonNull Set<QName> level, final @NonNull QName qualifiedName) {
196 DataSchemaContext currentNode = node;
197 while (currentNode instanceof PathMixin currentMixin) {
198 level.add(qualifiedName);
199 currentNode = currentMixin.childByQName(qualifiedName);