ece8bc6365a8f388bd41f85070ab385c69c9a9a7
[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 java.util.AbstractMap.SimpleEntry;
12 import java.util.ArrayList;
13 import java.util.LinkedList;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.Objects;
17 import java.util.Set;
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;
32
33 /**
34  * A translator between {@link FieldsParam} and {@link YangInstanceIdentifier}s suitable for use as field identifiers
35  * in {@code netconf-dom-api}.
36  *
37  * <p>
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.
41  *
42  * <p>
43  * Example: field 'a(/b/c);d/e' ('e' is place under choice node 'x') is parsed into following levels:
44  * <pre>
45  * level 0: ['./a', './d']
46  * level 1: ['a/b', '/d/x/e']
47  * level 2: ['b/c']
48  * </pre>
49  */
50 public final class NetconfFieldsTranslator extends AbstractFieldsTranslator<NetconfFieldsTranslator.LinkedPathElement> {
51     private static final NetconfFieldsTranslator INSTANCE = new NetconfFieldsTranslator();
52
53     private NetconfFieldsTranslator() {
54         // Hidden on purpose
55     }
56
57     /**
58      * Translate a {@link FieldsParam} to a list of child node paths saved in lists, suitable for use with
59      * {@link NetconfDataTreeService}.
60      *
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}
65      */
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);
71     }
72
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
83                     continue;
84                 }
85                 pathElement.processed = true;
86
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);
90
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;
99                 }
100                 completePaths.add(YangInstanceIdentifier.create(path));
101             }
102         }
103         return completePaths;
104     }
105
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());
115     }
116
117     @Override
118     protected DataSchemaContextNode<?> addChildToResult(final DataSchemaContextNode<?> currentNode,
119             final QName childQName, final Set<LinkedPathElement> level) {
120         final List<PathArgument> collectedMixinNodes = new ArrayList<>();
121
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);
127                 break;
128             } else if (actualContextNode.getDataSchemaNode() instanceof LeafListSchemaNode) {
129                 // NodeWithValue is unusable - stop parsing
130                 break;
131             } else {
132                 collectedMixinNodes.add(actualContextNode.getIdentifier());
133                 actualContextNode = actualContextNode.getChild(childQName);
134             }
135         }
136
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);
141         }
142         final LinkedPathElement linkedPathElement = new LinkedPathElement(currentNode.getIdentifier(),
143                 collectedMixinNodes, actualContextNode.getIdentifier());
144         level.add(linkedPathElement);
145         return actualContextNode;
146     }
147
148     /**
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
151      *    DOM paths,<br>
152      *  - identifier of the previous non-mixin node - required to successfully create a chain of {@link PathArgument}s
153      */
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;
159
160         /**
161          * Creation of new {@link LinkedPathElement}.
162          *
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
166          */
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;
172         }
173
174         @Override
175         public boolean equals(final Object obj) {
176             // this is need in order to make 'prepareQNameLevel(..)' working
177             if (this == obj) {
178                 return true;
179             }
180             if (obj == null || getClass() != obj.getClass()) {
181                 return false;
182             }
183             final LinkedPathElement that = (LinkedPathElement) obj;
184             return targetNodeIdentifier.equals(that.targetNodeIdentifier);
185         }
186
187         @Override
188         public int hashCode() {
189             return Objects.hash(targetNodeIdentifier);
190         }
191     }
192 }