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 java.util.AbstractMap.SimpleEntry;
12 import java.util.ArrayList;
13 import java.util.LinkedList;
14 import java.util.List;
16 import java.util.Objects;
18 import java.util.stream.Collectors;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
21 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
22 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
23 import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
24 import org.opendaylight.yangtools.yang.common.ErrorTag;
25 import org.opendaylight.yangtools.yang.common.ErrorType;
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
29 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
30 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
34 * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers
35 * in {@code netconf-dom-api}.
38 * Fields parser that stores set of {@link LinkedPathElement}s in each level. Using {@link LinkedPathElement} it is
39 * possible to create a chain of path arguments and build complete paths since this element contains identifiers of
40 * intermediary mixin nodes and also linked previous element.
43 * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
45 * level 0: ['./a', './d']
46 * level 1: ['a/b', '/d/x/e']
50 public final class NetconfFieldsTranslator extends AbstractFieldsTranslator<NetconfFieldsTranslator.LinkedPathElement> {
51 private static final NetconfFieldsTranslator INSTANCE = new NetconfFieldsTranslator();
53 private NetconfFieldsTranslator() {
58 * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
59 * {@link NetconfDataTreeService}.
61 * @param identifier identifier context created from request URI
62 * @param input input value of fields parameter
63 * @return {@link List} of {@link YangInstanceIdentifier} that are relative to the last {@link PathArgument}
64 * of provided {@code identifier}
66 public static @NonNull List<YangInstanceIdentifier> translate(
67 final @NonNull InstanceIdentifierContext identifier, final @NonNull FieldsParam input) {
68 final List<Set<LinkedPathElement>> levels = INSTANCE.parseFields(identifier, input);
69 final List<Map<PathArgument, LinkedPathElement>> mappedLevels = mapLevelsContentByIdentifiers(levels);
70 return buildPaths(mappedLevels);
73 private static List<YangInstanceIdentifier> buildPaths(
74 final List<Map<PathArgument, LinkedPathElement>> mappedLevels) {
75 final List<YangInstanceIdentifier> completePaths = new ArrayList<>();
76 // we must traverse levels from the deepest level to the top level, because each LinkedPathElement is only
77 // linked to previous element
78 for (int levelIndex = mappedLevels.size() - 1; levelIndex >= 0; levelIndex--) {
79 // we go through unprocessed LinkedPathElements that represent leaves
80 for (final LinkedPathElement pathElement : mappedLevels.get(levelIndex).values()) {
81 if (pathElement.processed) {
82 // this element was already processed from the lower level - skip it
85 pathElement.processed = true;
87 // adding deepest path arguments, LinkedList is used for more effective insertion at the 0 index
88 final LinkedList<PathArgument> path = new LinkedList<>(pathElement.mixinNodesToTarget);
89 path.add(pathElement.targetNodeIdentifier);
91 PathArgument previousIdentifier = pathElement.previousNodeIdentifier;
92 // adding path arguments from the linked LinkedPathElements recursively
93 for (int buildingLevel = levelIndex - 1; buildingLevel >= 0; buildingLevel--) {
94 final LinkedPathElement previousElement = mappedLevels.get(buildingLevel).get(previousIdentifier);
95 path.addFirst(previousElement.targetNodeIdentifier);
96 path.addAll(0, previousElement.mixinNodesToTarget);
97 previousIdentifier = previousElement.previousNodeIdentifier;
98 previousElement.processed = true;
100 completePaths.add(YangInstanceIdentifier.create(path));
103 return completePaths;
106 private static List<Map<PathArgument, LinkedPathElement>> mapLevelsContentByIdentifiers(
107 final List<Set<LinkedPathElement>> levels) {
108 // this step is used for saving some processing power - we can directly find LinkedPathElement using
109 // representing PathArgument
110 return levels.stream()
111 .map(linkedPathElements -> linkedPathElements.stream()
112 .map(linkedPathElement -> new SimpleEntry<>(linkedPathElement.targetNodeIdentifier, linkedPathElement))
113 .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)))
114 .collect(Collectors.toList());
118 protected DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode,
119 final QName childQName, final Set<LinkedPathElement> level) {
120 final List<PathArgument> collectedMixinNodes = new ArrayList<>();
122 DataSchemaContextNode<?> actualContextNode = currentNode.getChild(childQName);
123 while (actualContextNode != null && actualContextNode.isMixin()) {
124 if (actualContextNode.getDataSchemaNode() instanceof ListSchemaNode) {
125 // we need just a single node identifier from list in the path (key is not available)
126 actualContextNode = actualContextNode.getChild(childQName);
128 } else if (actualContextNode.getDataSchemaNode() instanceof LeafListSchemaNode) {
129 // NodeWithValue is unusable - stop parsing
132 collectedMixinNodes.add(actualContextNode.getIdentifier());
133 actualContextNode = actualContextNode.getChild(childQName);
137 if (actualContextNode == null) {
138 throw new RestconfDocumentedException("Child " + childQName.getLocalName() + " node missing in "
139 + currentNode.getIdentifier().getNodeType().getLocalName(),
140 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
142 final LinkedPathElement linkedPathElement = new LinkedPathElement(currentNode.getIdentifier(),
143 collectedMixinNodes, actualContextNode.getIdentifier());
144 level.add(linkedPathElement);
145 return actualContextNode;
149 * {@link PathArgument} of data element grouped with identifiers of leading mixin nodes and previous node.<br>
150 * - identifiers of mixin nodes on the path to the target node - required for construction of full valid
152 * - identifier of the previous non-mixin node - required to successfully create a chain of {@link PathArgument}s
154 static final class LinkedPathElement {
155 private final PathArgument previousNodeIdentifier;
156 private final List<PathArgument> mixinNodesToTarget;
157 private final PathArgument targetNodeIdentifier;
158 private boolean processed = false;
161 * Creation of new {@link LinkedPathElement}.
163 * @param previousNodeIdentifier identifier of the previous non-mixin node
164 * @param mixinNodesToTarget identifiers of mixin nodes on the path to the target node
165 * @param targetNodeIdentifier identifier of target non-mixin node
167 private LinkedPathElement(final PathArgument previousNodeIdentifier,
168 final List<PathArgument> mixinNodesToTarget, final PathArgument targetNodeIdentifier) {
169 this.previousNodeIdentifier = previousNodeIdentifier;
170 this.mixinNodesToTarget = mixinNodesToTarget;
171 this.targetNodeIdentifier = targetNodeIdentifier;
175 public boolean equals(final Object obj) {
176 // this is need in order to make 'prepareQNameLevel(..)' working
180 if (obj == null || getClass() != obj.getClass()) {
183 final LinkedPathElement that = (LinkedPathElement) obj;
184 return targetNodeIdentifier.equals(that.targetNodeIdentifier);
188 public int hashCode() {
189 return Objects.hash(targetNodeIdentifier);