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