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