16faf59b7370fc78c05b3e09338fdf88f9ad991b
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / ParserFieldsParameter.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
9
10 import java.util.ArrayList;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Optional;
14 import java.util.Set;
15 import org.eclipse.jdt.annotation.NonNull;
16 import org.eclipse.jdt.annotation.Nullable;
17 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
18 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
19 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
20 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
21 import org.opendaylight.yangtools.yang.common.QName;
22 import org.opendaylight.yangtools.yang.common.QNameModule;
23 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
24 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
25 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
26
27 public final class ParserFieldsParameter {
28     private ParserFieldsParameter() {
29
30     }
31
32     /**
33      * Parse fields parameter and return complete list of child nodes organized into levels.
34      * @param identifier identifier context created from request URI
35      * @param input input value of fields parameter
36      * @return {@link List}
37      */
38     public static @NonNull List<Set<QName>> parseFieldsParameter(final @NonNull InstanceIdentifierContext<?> identifier,
39                                                                  final @NonNull String input) {
40         final List<Set<QName>> parsed = new ArrayList<>();
41         final SchemaContext context = identifier.getSchemaContext();
42         final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
43         final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
44                 (DataSchemaNode) identifier.getSchemaNode());
45
46         if (startNode == null) {
47             throw new RestconfDocumentedException(
48                     "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
49         }
50
51         parseInput(input, startQNameModule, startNode, parsed, context);
52         return parsed;
53     }
54
55     /**
56      * Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
57      * @param input input value of fields parameter
58      * @param startQNameModule starting qname module
59      * @param startNode starting node
60      * @param parsed list of results
61      * @param context schema context
62      */
63     private static void parseInput(final @NonNull String input, final @NonNull QNameModule startQNameModule,
64                                    final @NonNull DataSchemaContextNode<?> startNode,
65                                    final @NonNull List<Set<QName>> parsed, final SchemaContext context) {
66         int currentPosition = 0;
67         int startPosition = 0;
68         DataSchemaContextNode<?> currentNode = startNode;
69         QNameModule currentQNameModule = startQNameModule;
70
71         Set<QName> currentLevel = new HashSet<>();
72         parsed.add(currentLevel);
73
74         DataSchemaContextNode<?> parenthesisNode = currentNode;
75         Set<QName> parenthesisLevel = currentLevel;
76         QNameModule parenthesisQNameModule = currentQNameModule;
77
78         while (currentPosition < input.length()) {
79             final char currentChar = input.charAt(currentPosition);
80
81             if (ParserConstants.YANG_IDENTIFIER_PART.matches(currentChar)) {
82                 currentPosition++;
83                 continue;
84             }
85
86             switch (currentChar) {
87                 case '/':
88                     // add parsed identifier to results for current level
89                     currentNode = addChildToResult(currentNode, input.substring(startPosition, currentPosition),
90                         currentQNameModule, currentLevel);
91                     // go one level down
92                     currentLevel = prepareQNameLevel(parsed, currentLevel);
93
94                     currentPosition++;
95                     break;
96                 case ':':
97                     // new namespace and revision found
98                     currentQNameModule = context.findModules(
99                             input.substring(startPosition, currentPosition)).iterator().next().getQNameModule();
100                     currentPosition++;
101                     break;
102                 case '(':
103                     // add current child to parsed results for current level
104                     final DataSchemaContextNode<?> child = addChildToResult(
105                             currentNode,
106                             input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
107                     // call with child node as new start node for one level down
108                     final int closingParenthesis = currentPosition
109                             + findClosingParenthesis(input.substring(currentPosition + 1));
110                     parseInput(
111                             input.substring(currentPosition + 1, closingParenthesis),
112                             currentQNameModule,
113                             child,
114                             parsed,
115                             context);
116
117                     // closing parenthesis must be at the end of input or separator and one more character is expected
118                     currentPosition = closingParenthesis + 1;
119                     if (currentPosition != input.length()) {
120                         if (currentPosition + 1 < input.length()) {
121                             if (input.charAt(currentPosition) == ';') {
122                                 currentPosition++;
123                             } else {
124                                 throw new RestconfDocumentedException(
125                                         "Missing semicolon character after "
126                                                 + child.getIdentifier().getNodeType().getLocalName()
127                                                 + " child nodes",
128                                         ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
129                             }
130                         } else {
131                             throw new RestconfDocumentedException(
132                                     "Unexpected character '"
133                                             + input.charAt(currentPosition)
134                                             + "' found in fields parameter value",
135                                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
136                         }
137                     }
138
139                     break;
140                 case ';':
141                     // complete identifier found
142                     addChildToResult(
143                             currentNode,
144                             input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
145                     currentPosition++;
146
147                     // next nodes can be placed on already utilized level-s
148                     currentNode = parenthesisNode;
149                     currentQNameModule = parenthesisQNameModule;
150                     currentLevel = parenthesisLevel;
151                     break;
152                 default:
153                     throw new RestconfDocumentedException(
154                             "Unexpected character '" + currentChar + "' found in fields parameter value",
155                             ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
156             }
157
158             startPosition = currentPosition;
159         }
160
161         // parse input to end
162         if (startPosition < input.length()) {
163             addChildToResult(currentNode, input.substring(startPosition), currentQNameModule, currentLevel);
164         }
165     }
166
167     /**
168      * Preparation of the QName level that is used as storage for parsed QNames. If the current level exist at the
169      * index that doesn't equal to the last index of already parsed QNames, a new level of QNames is allocated and
170      * pushed to input parsed QNames.
171      *
172      * @param parsedQNames Already parsed list of QNames grouped to multiple levels.
173      * @param currentLevel Current level of QNames (set).
174      * @return Existing or new level of QNames.
175      */
176     private static Set<QName> prepareQNameLevel(final List<Set<QName>> parsedQNames, final Set<QName> currentLevel) {
177         final Optional<Set<QName>> existingLevel = parsedQNames.stream()
178                 .filter(qNameSet -> qNameSet.equals(currentLevel))
179                 .findAny();
180         if (existingLevel.isPresent()) {
181             final int index = parsedQNames.indexOf(existingLevel.get());
182             if (index == parsedQNames.size() - 1) {
183                 final Set<QName> nextLevel = new HashSet<>();
184                 parsedQNames.add(nextLevel);
185                 return nextLevel;
186             }
187
188             return parsedQNames.get(index + 1);
189         }
190
191         final Set<QName> nextLevel = new HashSet<>();
192         parsedQNames.add(nextLevel);
193         return nextLevel;
194     }
195
196     /**
197      * Add parsed child of current node to result for current level.
198      * @param currentNode current node
199      * @param identifier parsed identifier of child node
200      * @param currentQNameModule current namespace and revision in {@link QNameModule}
201      * @param level current nodes level
202      * @return {@link DataSchemaContextNode}
203      */
204     private static @NonNull DataSchemaContextNode<?> addChildToResult(
205             final @NonNull DataSchemaContextNode<?> currentNode, final @NonNull String identifier,
206             final @NonNull QNameModule currentQNameModule, final @NonNull Set<QName> level) {
207         final QName childQName = QName.create(currentQNameModule, identifier);
208
209         // resolve parent node
210         final DataSchemaContextNode<?> parentNode = resolveMixinNode(
211                 currentNode, level, currentNode.getIdentifier().getNodeType());
212         if (parentNode == null) {
213             throw new RestconfDocumentedException(
214                     "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
215                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
216         }
217
218         // resolve child node
219         final DataSchemaContextNode<?> childNode = resolveMixinNode(
220                 parentNode.getChild(childQName), level, childQName);
221         if (childNode == null) {
222             throw new RestconfDocumentedException(
223                     "Child " + identifier + " node missing in "
224                             + currentNode.getIdentifier().getNodeType().getLocalName(),
225                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
226         }
227
228         // add final childNode node to level nodes
229         level.add(childNode.getIdentifier().getNodeType());
230         return childNode;
231     }
232
233     /**
234      * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
235      * All nodes expect of not mixin node are added to current level nodes.
236      * @param node initial mixin or not-mixin node
237      * @param level current nodes level
238      * @param qualifiedName qname of initial node
239      * @return {@link DataSchemaContextNode}
240      */
241     private static @Nullable DataSchemaContextNode<?> resolveMixinNode(final @Nullable DataSchemaContextNode<?> node,
242             final @NonNull Set<QName> level, final @NonNull QName qualifiedName) {
243         DataSchemaContextNode<?> currentNode = node;
244         while (currentNode != null && currentNode.isMixin()) {
245             level.add(qualifiedName);
246             currentNode = currentNode.getChild(qualifiedName);
247         }
248
249         return currentNode;
250     }
251
252     /**
253      * Find position of matching parenthesis increased by one, but at most equals to input size.
254      * @param input input where to find for closing parenthesis
255      * @return int position of closing parenthesis increased by one
256      */
257     private static int findClosingParenthesis(final @Nullable String input) {
258         int position = 0;
259         int count = 1;
260
261         while (position < input.length()) {
262             final char currentChar = input.charAt(position);
263
264             if (currentChar == '(') {
265                 count++;
266             }
267
268             if (currentChar == ')') {
269                 count--;
270             }
271
272             if (count == 0) {
273                 break;
274             }
275
276             position++;
277         }
278
279         // closing parenthesis was not found
280         if (position >= input.length()) {
281             throw new RestconfDocumentedException("Missing closing parenthesis in fields parameter",
282                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
283         }
284
285         return ++position;
286     }
287 }