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 static java.util.Objects.requireNonNull;
12 import com.google.common.base.CharMatcher;
13 import com.google.common.base.Preconditions;
14 import com.google.common.collect.ImmutableList;
15 import com.google.common.collect.ImmutableMap;
16 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
17 import java.util.Iterator;
18 import java.util.LinkedList;
19 import java.util.List;
20 import java.util.Optional;
21 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
22 import org.opendaylight.restconf.common.errors.RestconfError;
23 import org.opendaylight.restconf.common.util.RestUtil;
24 import org.opendaylight.restconf.common.util.RestconfSchemaUtil;
25 import org.opendaylight.restconf.common.validation.RestconfValidationUtils;
26 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
27 import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
28 import org.opendaylight.restconf.nb.rfc8040.utils.parser.builder.ParserBuilderConstants;
29 import org.opendaylight.yangtools.concepts.Codec;
30 import org.opendaylight.yangtools.yang.common.QName;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
34 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
35 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
36 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
37 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
38 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
40 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.Module;
46 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
47 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
48 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
52 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
55 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
59 public final class YangInstanceIdentifierDeserializer {
61 private YangInstanceIdentifierDeserializer() {
62 throw new UnsupportedOperationException("Util class.");
66 * Method to create {@link Iterable} from {@link PathArgument} which are
67 * parsing from data by {@link SchemaContext}.
69 * @param schemaContext
70 * for validate of parsing path arguments
73 * @return {@link Iterable} of {@link PathArgument}
75 public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
76 final List<PathArgument> path = new LinkedList<>();
77 final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
78 data, DataSchemaContextTree.from(schemaContext).getRoot(),
79 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
81 while (!allCharsConsumed(variables)) {
83 final QName qname = prepareQName(variables);
85 // this is the last identifier (input is consumed) or end of identifier (slash)
86 if (allCharsConsumed(variables)
87 || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
88 prepareIdentifier(qname, path, variables);
89 if (variables.getCurrent() == null) {
90 path.add(NodeIdentifier.create(qname));
92 path.add(variables.getCurrent().getIdentifier());
94 } else if (currentChar(variables.getOffset(),
95 variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
96 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
97 prepareNodeWithPredicates(qname, path, variables,
98 (ListSchemaNode) variables.getCurrent().getDataSchemaNode());
100 prepareNodeWithValue(qname, path, variables);
103 throw new IllegalArgumentException(
104 "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
105 + variables.getOffset() + ".");
109 return ImmutableList.copyOf(path);
112 private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
113 final MainVarsWrapper variables, final ListSchemaNode listSchemaNode) {
114 checkValid(listSchemaNode != null, "Data schema node is null", variables.getData(), variables.getOffset());
116 final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
117 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
119 // skip already expected equal sign
120 skipCurrentChar(variables);
122 // read key value separated by comma
123 while (keys.hasNext() && !allCharsConsumed(variables) && currentChar(variables.getOffset(),
124 variables.getData()) != RestconfConstants.SLASH) {
127 if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
128 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
129 skipCurrentChar(variables);
133 // check if next value is parsable
134 RestconfValidationUtils.checkDocumentedError(
135 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
136 .matches(currentChar(variables.getOffset(), variables.getData())),
137 RestconfError.ErrorType.PROTOCOL,
138 RestconfError.ErrorTag.MALFORMED_MESSAGE,
143 final QName key = keys.next();
144 Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
145 if (!leafSchemaNode.isPresent()) {
146 throw new RestconfDocumentedException("Schema not found for " + key,
147 RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
150 final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
151 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables));
152 final Object valueByType = prepareValueByType(leafSchemaNode.get(), value, variables);
153 values.put(key, valueByType);
157 if (keys.hasNext() && !allCharsConsumed(variables) && currentChar(
158 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
159 skipCurrentChar(variables);
163 // the last key is considered to be empty
164 if (keys.hasNext()) {
165 if (allCharsConsumed(variables)
166 || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
167 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
170 // there should be no more missing keys
171 RestconfValidationUtils.checkDocumentedError(
173 RestconfError.ErrorType.PROTOCOL,
174 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
175 "Key value missing for: " + qname
179 path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
182 private static Object prepareValueByType(final DataSchemaNode schemaNode, final String value,
183 final MainVarsWrapper vars) {
184 Object decoded = null;
186 TypeDefinition<? extends TypeDefinition<?>> typedef = null;
187 if (schemaNode instanceof LeafListSchemaNode) {
188 typedef = ((LeafListSchemaNode) schemaNode).getType();
190 typedef = ((LeafSchemaNode) schemaNode).getType();
192 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
193 if (baseType instanceof LeafrefTypeDefinition) {
194 typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, vars.getSchemaContext(),
197 final Codec<Object, Object> codec = RestCodec.from(typedef, null, vars.getSchemaContext());
198 decoded = codec.deserialize(value);
199 if (decoded == null) {
200 if (baseType instanceof IdentityrefTypeDefinition) {
201 decoded = toQName(value, schemaNode, vars.getSchemaContext());
207 private static Object toQName(final String value, final DataSchemaNode schemaNode,
208 final SchemaContext schemaContext) {
209 final String moduleName = toModuleName(value);
210 final String nodeName = toNodeName(value);
211 final Module module = schemaContext.findModules(moduleName).iterator().next();
212 for (final IdentitySchemaNode identitySchemaNode : module.getIdentities()) {
213 final QName qName = identitySchemaNode.getQName();
214 if (qName.getLocalName().equals(nodeName)) {
218 return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
221 private static String toNodeName(final String str) {
222 final int idx = str.indexOf(':');
227 if (str.indexOf(':', idx + 1) != -1) {
231 return str.substring(idx + 1);
234 private static String toModuleName(final String str) {
235 final int idx = str.indexOf(':');
240 if (str.indexOf(':', idx + 1) != -1) {
244 return str.substring(0, idx);
247 private static QName prepareQName(final MainVarsWrapper variables) {
249 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
250 .matches(currentChar(variables.getOffset(), variables.getData())),
251 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
252 final String preparedPrefix = nextIdentifierFromNextSequence(
253 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
255 final String localName;
257 if (allCharsConsumed(variables)) {
258 return getQNameOfDataSchemaNode(preparedPrefix, variables);
261 switch (currentChar(variables.getOffset(), variables.getData())) {
262 case RestconfConstants.SLASH:
263 case ParserBuilderConstants.Deserializer.EQUAL:
264 prefix = preparedPrefix;
265 return getQNameOfDataSchemaNode(prefix, variables);
266 case ParserBuilderConstants.Deserializer.COLON:
267 prefix = preparedPrefix;
268 skipCurrentChar(variables);
270 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
271 .matches(currentChar(variables.getOffset(), variables.getData())),
272 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
273 variables.getOffset());
274 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
276 if (!allCharsConsumed(variables) && currentChar(
277 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
278 return getQNameOfDataSchemaNode(localName, variables);
280 final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
281 Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
282 return QName.create(module.getQNameModule(), localName);
285 throw new IllegalArgumentException("Failed build path.");
289 private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
290 final int start = variables.getOffset();
291 nextSequenceEnd(matcher, variables);
292 return variables.getData().substring(start, variables.getOffset());
295 private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
296 while (!allCharsConsumed(variables)
297 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
298 variables.setOffset(variables.getOffset() + 1);
302 private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
303 final MainVarsWrapper variables) {
304 skipCurrentChar(variables);
305 final String value = nextIdentifierFromNextSequence(
306 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
308 // exception if value attribute is missing
309 RestconfValidationUtils.checkDocumentedError(
311 RestconfError.ErrorType.PROTOCOL,
312 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
313 "Value missing for: " + qname
315 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
316 final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
317 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
320 private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
321 final MainVarsWrapper variables) {
322 final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
323 if (currentNode == null) {
326 checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
327 variables.getData(), variables.getOffset());
330 @SuppressFBWarnings("NP_NULL_ON_SOME_PATH") // code does check for null 'current' but FB doesn't recognize it
331 private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
332 final MainVarsWrapper variables) {
333 final DataSchemaContextNode<?> initialContext = variables.getCurrent();
334 final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
336 DataSchemaContextNode<?> current = initialContext.getChild(qname);
337 variables.setCurrent(current);
339 if (current == null) {
340 final Optional<Module> module = variables.getSchemaContext().findModule(qname.getModule());
341 if (module.isPresent()) {
342 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
343 if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
348 if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
352 checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
353 variables.getOffset());
354 while (current.isMixin()) {
355 path.add(current.getIdentifier());
356 current = current.getChild(qname);
357 variables.setCurrent(current);
362 private static String findAndParsePercentEncoded(final String preparedPrefix) {
363 if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
364 return preparedPrefix;
367 final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
368 final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
370 while (matcher.matchesAnyOf(parsedPrefix)) {
371 final int percentCharPosition = matcher.indexIn(parsedPrefix);
372 parsedPrefix.replace(
374 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
375 String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
376 percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
377 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
378 ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
381 return parsedPrefix.toString();
384 private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
385 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
386 if (dataSchemaNode instanceof ContainerSchemaNode) {
387 return getQNameOfDataSchemaNode((ContainerSchemaNode) dataSchemaNode, nodeName);
388 } else if (dataSchemaNode instanceof ListSchemaNode) {
389 return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
392 throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
395 private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
396 final T parent, String nodeName) {
397 final Optional<ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
398 final SchemaNode node;
399 if (actionDef.isPresent()) {
400 node = actionDef.get();
402 node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
404 return node.getQName();
407 private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
408 return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
411 private static void validArg(final MainVarsWrapper variables) {
412 // every identifier except of the first MUST start with slash
413 if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
414 checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
415 "Identifier must start with '/'.", variables.getData(), variables.getOffset());
417 // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
418 // multiple slashes into a single one
419 while (!allCharsConsumed(variables)
420 && RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData())) {
421 skipCurrentChar(variables);
424 // check if slash is not also the last char in identifier
425 checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
426 variables.getData(), variables.getOffset());
430 private static void skipCurrentChar(final MainVarsWrapper variables) {
431 variables.setOffset(variables.getOffset() + 1);
434 private static char currentChar(final int offset, final String data) {
435 return data.charAt(offset);
438 private static void checkValid(final boolean condition, final String errorMsg, final String data,
440 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
441 data, offset, errorMsg);
444 private static boolean allCharsConsumed(final MainVarsWrapper variables) {
445 return variables.getOffset() == variables.getData().length();
448 private static Optional<ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
449 final String nodeName) {
450 requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
451 if (dataSchemaNode instanceof ActionNodeContainer) {
452 return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
453 .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
455 return Optional.empty();
458 private static final class MainVarsWrapper {
459 private static final int STARTING_OFFSET = 0;
461 private final SchemaContext schemaContext;
462 private final String data;
464 private DataSchemaContextNode<?> current;
467 MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
468 final SchemaContext schemaContext) {
470 this.current = current;
471 this.offset = offset;
472 this.schemaContext = schemaContext;
475 public String getData() {
479 public DataSchemaContextNode<?> getCurrent() {
483 public void setCurrent(final DataSchemaContextNode<?> current) {
484 this.current = current;
487 public int getOffset() {
491 public void setOffset(final int offset) {
492 this.offset = offset;
495 public SchemaContext getSchemaContext() {
496 return this.schemaContext;