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