Adjust to yangtools-2.0.0/odlparent-3.0.0 changes
[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
9 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
10
11 import java.util.ArrayList;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Set;
15 import javax.annotation.Nonnull;
16 import javax.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.restconf.nb.rfc8040.utils.parser.builder.ParserBuilderConstants.Deserializer;
22 import org.opendaylight.yangtools.yang.common.QName;
23 import org.opendaylight.yangtools.yang.common.QNameModule;
24 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
25 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
26 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
27
28 public final class ParserFieldsParameter {
29     private static final char COLON = ':';
30     private static final char SEMICOLON = ';';
31     private static final char SLASH = '/';
32     private static final char STARTING_PARENTHESIS = '(';
33     private static final char CLOSING_PARENTHESIS = ')';
34
35     private ParserFieldsParameter() {
36
37     }
38
39     /**
40      * Parse fields parameter and return complete list of child nodes organized into levels.
41      * @param identifier identifier context created from request URI
42      * @param input input value of fields parameter
43      * @return {@link List}
44      */
45     @Nonnull
46     public static List<Set<QName>> parseFieldsParameter(@Nonnull final InstanceIdentifierContext<?> identifier,
47                                                         @Nonnull final String input) {
48         final List<Set<QName>> parsed = new ArrayList<>();
49         final SchemaContext context = identifier.getSchemaContext();
50         final QNameModule startQNameModule = identifier.getSchemaNode().getQName().getModule();
51         final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
52                 (DataSchemaNode) identifier.getSchemaNode());
53
54         if (startNode == null) {
55             throw new RestconfDocumentedException(
56                     "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
57         }
58
59         parseInput(input, startQNameModule, startNode, parsed, context);
60         return parsed;
61     }
62
63     /**
64      * Parse input value of fields parameter and create list of sets. Each set represents one level of child nodes.
65      * @param input input value of fields parameter
66      * @param startQNameModule starting qname module
67      * @param startNode starting node
68      * @param parsed list of results
69      * @param context schema context
70      */
71     private static void parseInput(@Nonnull final String input,
72                                    @Nonnull final QNameModule startQNameModule,
73                                    @Nonnull final DataSchemaContextNode<?> startNode,
74                                    @Nonnull final List<Set<QName>> parsed,
75                                    @Nonnull final SchemaContext context) {
76         int currentPosition = 0;
77         int startPosition = 0;
78         DataSchemaContextNode<?> currentNode = startNode;
79         QNameModule currentQNameModule = startQNameModule;
80
81         Set<QName> currentLevel = new HashSet<>();
82         parsed.add(currentLevel);
83
84         while (currentPosition < input.length()) {
85             final char currentChar = input.charAt(currentPosition);
86
87             if (Deserializer.IDENTIFIER.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 = new HashSet<>();
95                     parsed.add(currentLevel);
96
97                     currentPosition++;
98                     startPosition = currentPosition;
99                 } else {
100                     currentPosition++;
101                 }
102
103                 continue;
104             }
105
106             switch (currentChar) {
107                 case COLON :
108                     // new namespace and revision found
109                     currentQNameModule = context.findModules(
110                             input.substring(startPosition, currentPosition)).iterator().next().getQNameModule();
111                     currentPosition++;
112                     break;
113                 case STARTING_PARENTHESIS:
114                     // add current child to parsed results for current level
115                     final DataSchemaContextNode<?> child = addChildToResult(
116                             currentNode,
117                             input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
118                     // call with child node as new start node for one level down
119                     final int closingParenthesis = currentPosition
120                             + findClosingParenthesis(input.substring(currentPosition + 1));
121                     parseInput(
122                             input.substring(currentPosition + 1, closingParenthesis),
123                             currentQNameModule,
124                             child,
125                             parsed,
126                             context);
127
128                     // closing parenthesis must be at the end of input or separator and one more character is expected
129                     currentPosition = closingParenthesis + 1;
130                     if (currentPosition != input.length()) {
131                         if (currentPosition + 1 < input.length()) {
132                             if (input.charAt(currentPosition) == SEMICOLON) {
133                                 currentPosition++;
134                             } else {
135                                 throw new RestconfDocumentedException(
136                                         "Missing semicolon character after "
137                                                 + child.getIdentifier().getNodeType().getLocalName()
138                                                 + " child nodes",
139                                         ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
140                             }
141                         } else {
142                             throw new RestconfDocumentedException(
143                                     "Unexpected character '"
144                                             + input.charAt(currentPosition)
145                                             + "' found in fields parameter value",
146                                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
147                         }
148                     }
149
150                     break;
151                 case SEMICOLON:
152                     // complete identifier found
153                     addChildToResult(
154                             currentNode,
155                             input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
156                     currentPosition++;
157                     break;
158                 default:
159                     throw new RestconfDocumentedException(
160                             "Unexpected character '" + currentChar + "' found in fields parameter value",
161                             ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
162             }
163
164             startPosition = currentPosition;
165         }
166
167         // parse input to end
168         if (startPosition < input.length()) {
169             addChildToResult(currentNode, input.substring(startPosition), currentQNameModule, currentLevel);
170         }
171     }
172
173     /**
174      * Add parsed child of current node to result for current level.
175      * @param currentNode current node
176      * @param identifier parsed identifier of child node
177      * @param currentQNameModule current namespace and revision in {@link QNameModule}
178      * @param level current nodes level
179      * @return {@link DataSchemaContextNode}
180      */
181     @Nonnull
182     private static DataSchemaContextNode<?> addChildToResult(
183             @Nonnull final DataSchemaContextNode<?> currentNode,
184             @Nonnull final String identifier,
185             @Nonnull final QNameModule currentQNameModule,
186             @Nonnull final Set<QName> level) {
187         final QName childQName = QName.create(currentQNameModule, identifier);
188
189         // resolve parent node
190         final DataSchemaContextNode<?> parentNode = resolveMixinNode(
191                 currentNode, level, currentNode.getIdentifier().getNodeType());
192         if (parentNode == null) {
193             throw new RestconfDocumentedException(
194                     "Not-mixin node missing in " + currentNode.getIdentifier().getNodeType().getLocalName(),
195                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
196         }
197
198         // resolve child node
199         final DataSchemaContextNode<?> childNode = resolveMixinNode(
200                 parentNode.getChild(childQName), level, childQName);
201         if (childNode == null) {
202             throw new RestconfDocumentedException(
203                     "Child " + identifier + " node missing in "
204                             + currentNode.getIdentifier().getNodeType().getLocalName(),
205                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
206         }
207
208         // add final childNode node to level nodes
209         level.add(childNode.getIdentifier().getNodeType());
210         return childNode;
211     }
212
213     /**
214      * Resolve mixin node by searching for inner nodes until not mixin node or null is found.
215      * All nodes expect of not mixin node are added to current level nodes.
216      * @param node initial mixin or not-mixin node
217      * @param level current nodes level
218      * @param qualifiedName qname of initial node
219      * @return {@link DataSchemaContextNode}
220      */
221     @Nullable
222     private static DataSchemaContextNode<?> resolveMixinNode(@Nullable final DataSchemaContextNode<?> node,
223                                                              @Nonnull final Set<QName> level,
224                                                              @Nonnull final QName qualifiedName) {
225         DataSchemaContextNode<?> currentNode = node;
226         while (currentNode != null && currentNode.isMixin()) {
227             level.add(qualifiedName);
228             currentNode = currentNode.getChild(qualifiedName);
229         }
230
231         return currentNode;
232     }
233
234     /**
235      * Find position of matching parenthesis increased by one, but at most equals to input size.
236      * @param input input where to find for closing parenthesis
237      * @return int position of closing parenthesis increased by one
238      */
239     private static int findClosingParenthesis(@Nonnull final String input) {
240         int position = 0;
241         int count = 1;
242
243         while (position < input.length()) {
244             final char currentChar = input.charAt(position);
245
246             if (currentChar == STARTING_PARENTHESIS) {
247                 count++;
248             }
249
250             if (currentChar == CLOSING_PARENTHESIS) {
251                 count--;
252             }
253
254             if (count == 0) {
255                 break;
256             }
257
258             position++;
259         }
260
261         // closing parenthesis was not found
262         if (position >= input.length()) {
263             throw new RestconfDocumentedException("Missing closing parenthesis in fields parameter",
264                     ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
265         }
266
267         return ++position;
268     }
269 }