2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
10 import java.util.ArrayList;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Optional;
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;
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 = ')';
34 private ParserFieldsParameter() {
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}
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());
52 if (startNode == null) {
53 throw new RestconfDocumentedException(
54 "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
57 parseInput(input, startQNameModule, startNode, parsed, context);
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
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;
77 Set<QName> currentLevel = new HashSet<>();
78 parsed.add(currentLevel);
80 DataSchemaContextNode<?> parenthesisNode = currentNode;
81 Set<QName> parenthesisLevel = currentLevel;
82 QNameModule parenthesisQNameModule = currentQNameModule;
84 while (currentPosition < input.length()) {
85 final char currentChar = input.charAt(currentPosition);
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(
92 input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
94 currentLevel = prepareQNameLevel(parsed, currentLevel);
97 startPosition = currentPosition;
105 switch (currentChar) {
107 // new namespace and revision found
108 currentQNameModule = context.findModules(
109 input.substring(startPosition, currentPosition)).iterator().next().getQNameModule();
112 case STARTING_PARENTHESIS:
113 // add current child to parsed results for current level
114 final DataSchemaContextNode<?> child = addChildToResult(
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));
121 input.substring(currentPosition + 1, closingParenthesis),
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) {
134 throw new RestconfDocumentedException(
135 "Missing semicolon character after "
136 + child.getIdentifier().getNodeType().getLocalName()
138 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
141 throw new RestconfDocumentedException(
142 "Unexpected character '"
143 + input.charAt(currentPosition)
144 + "' found in fields parameter value",
145 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
151 // complete identifier found
154 input.substring(startPosition, currentPosition), currentQNameModule, currentLevel);
157 // next nodes can be placed on already utilized level-s
158 currentNode = parenthesisNode;
159 currentQNameModule = parenthesisQNameModule;
160 currentLevel = parenthesisLevel;
163 throw new RestconfDocumentedException(
164 "Unexpected character '" + currentChar + "' found in fields parameter value",
165 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
168 startPosition = currentPosition;
171 // parse input to end
172 if (startPosition < input.length()) {
173 addChildToResult(currentNode, input.substring(startPosition), currentQNameModule, currentLevel);
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.
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.
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))
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);
197 return parsedQNames.get(index + 1);
200 final Set<QName> nextLevel = new HashSet<>();
201 parsedQNames.add(nextLevel);
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}
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);
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);
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);
238 // add final childNode node to level nodes
239 level.add(childNode.getIdentifier().getNodeType());
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}
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);
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
267 private static int findClosingParenthesis(final @Nullable String input) {
271 while (position < input.length()) {
272 final char currentChar = input.charAt(position);
274 if (currentChar == STARTING_PARENTHESIS) {
278 if (currentChar == CLOSING_PARENTHESIS) {
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);