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 com.google.common.base.CharMatcher;
11 import com.google.common.base.Preconditions;
12 import com.google.common.collect.ImmutableList;
13 import com.google.common.collect.ImmutableMap;
14 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
15 import java.util.Iterator;
16 import java.util.LinkedList;
17 import java.util.List;
18 import java.util.Optional;
19 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
20 import org.opendaylight.restconf.common.errors.RestconfError;
21 import org.opendaylight.restconf.common.util.RestUtil;
22 import org.opendaylight.restconf.common.util.RestconfSchemaUtil;
23 import org.opendaylight.restconf.common.validation.RestconfValidationUtils;
24 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
25 import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
26 import org.opendaylight.restconf.nb.rfc8040.utils.parser.builder.ParserBuilderConstants;
27 import org.opendaylight.yangtools.concepts.Codec;
28 import org.opendaylight.yangtools.yang.common.QName;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
33 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
34 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.Module;
41 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
42 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
43 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
44 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
45 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
46 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
49 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
53 public final class YangInstanceIdentifierDeserializer {
55 private YangInstanceIdentifierDeserializer() {
56 throw new UnsupportedOperationException("Util class.");
60 * Method to create {@link Iterable} from {@link PathArgument} which are
61 * parsing from data by {@link SchemaContext}.
63 * @param schemaContext
64 * for validate of parsing path arguments
67 * @return {@link Iterable} of {@link PathArgument}
69 public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
70 final List<PathArgument> path = new LinkedList<>();
71 final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
72 data, DataSchemaContextTree.from(schemaContext).getRoot(),
73 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
75 while (!allCharsConsumed(variables)) {
77 final QName qname = prepareQName(variables);
79 // this is the last identifier (input is consumed) or end of identifier (slash)
80 if (allCharsConsumed(variables)
81 || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
82 prepareIdentifier(qname, path, variables);
83 if (variables.getCurrent() == null) {
84 path.add(NodeIdentifier.create(qname));
86 path.add(variables.getCurrent().getIdentifier());
88 } else if (currentChar(variables.getOffset(),
89 variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
90 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
91 prepareNodeWithPredicates(qname, path, variables,
92 (ListSchemaNode) variables.getCurrent().getDataSchemaNode());
94 prepareNodeWithValue(qname, path, variables);
97 throw new IllegalArgumentException(
98 "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
99 + variables.getOffset() + ".");
103 return ImmutableList.copyOf(path);
106 private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
107 final MainVarsWrapper variables, final ListSchemaNode listSchemaNode) {
108 checkValid(listSchemaNode != null, "Data schema node is null", variables.getData(), variables.getOffset());
110 final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
111 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
113 // skip already expected equal sign
114 skipCurrentChar(variables);
116 // read key value separated by comma
117 while (keys.hasNext() && !allCharsConsumed(variables) && currentChar(variables.getOffset(),
118 variables.getData()) != RestconfConstants.SLASH) {
121 if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
122 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
123 skipCurrentChar(variables);
127 // check if next value is parsable
128 RestconfValidationUtils.checkDocumentedError(
129 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
130 .matches(currentChar(variables.getOffset(), variables.getData())),
131 RestconfError.ErrorType.PROTOCOL,
132 RestconfError.ErrorTag.MALFORMED_MESSAGE,
137 final QName key = keys.next();
138 Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
139 if (!leafSchemaNode.isPresent()) {
140 throw new RestconfDocumentedException("Schema not found for " + key,
141 RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
144 final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
145 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables));
146 final Object valueByType = prepareValueByType(leafSchemaNode.get(), value, variables);
147 values.put(key, valueByType);
151 if (keys.hasNext() && !allCharsConsumed(variables) && currentChar(
152 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
153 skipCurrentChar(variables);
157 // the last key is considered to be empty
158 if (keys.hasNext()) {
159 if (allCharsConsumed(variables)
160 || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
161 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
164 // there should be no more missing keys
165 RestconfValidationUtils.checkDocumentedError(
167 RestconfError.ErrorType.PROTOCOL,
168 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
169 "Key value missing for: " + qname
173 path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
176 private static Object prepareValueByType(final DataSchemaNode schemaNode, final String value,
177 final MainVarsWrapper vars) {
178 Object decoded = null;
180 TypeDefinition<? extends TypeDefinition<?>> typedef = null;
181 if (schemaNode instanceof LeafListSchemaNode) {
182 typedef = ((LeafListSchemaNode) schemaNode).getType();
184 typedef = ((LeafSchemaNode) schemaNode).getType();
186 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
187 if (baseType instanceof LeafrefTypeDefinition) {
188 typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, vars.getSchemaContext(),
191 final Codec<Object, Object> codec = RestCodec.from(typedef, null, vars.getSchemaContext());
192 decoded = codec.deserialize(value);
193 if (decoded == null) {
194 if (baseType instanceof IdentityrefTypeDefinition) {
195 decoded = toQName(value, schemaNode, vars.getSchemaContext());
201 private static Object toQName(final String value, final DataSchemaNode schemaNode,
202 final SchemaContext schemaContext) {
203 final String moduleName = toModuleName(value);
204 final String nodeName = toNodeName(value);
205 final Module module = schemaContext.findModules(moduleName).iterator().next();
206 for (final IdentitySchemaNode identitySchemaNode : module.getIdentities()) {
207 final QName qName = identitySchemaNode.getQName();
208 if (qName.getLocalName().equals(nodeName)) {
212 return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
215 private static String toNodeName(final String str) {
216 final int idx = str.indexOf(':');
221 if (str.indexOf(':', idx + 1) != -1) {
225 return str.substring(idx + 1);
228 private static String toModuleName(final String str) {
229 final int idx = str.indexOf(':');
234 if (str.indexOf(':', idx + 1) != -1) {
238 return str.substring(0, idx);
241 private static QName prepareQName(final MainVarsWrapper variables) {
243 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
244 .matches(currentChar(variables.getOffset(), variables.getData())),
245 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
246 final String preparedPrefix = nextIdentifierFromNextSequence(
247 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
249 final String localName;
251 if (allCharsConsumed(variables)) {
252 return getQNameOfDataSchemaNode(preparedPrefix, variables);
255 switch (currentChar(variables.getOffset(), variables.getData())) {
256 case RestconfConstants.SLASH:
257 case ParserBuilderConstants.Deserializer.EQUAL:
258 prefix = preparedPrefix;
259 return getQNameOfDataSchemaNode(prefix, variables);
260 case ParserBuilderConstants.Deserializer.COLON:
261 prefix = preparedPrefix;
262 skipCurrentChar(variables);
264 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
265 .matches(currentChar(variables.getOffset(), variables.getData())),
266 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
267 variables.getOffset());
268 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
270 if (!allCharsConsumed(variables) && currentChar(
271 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
272 return getQNameOfDataSchemaNode(localName, variables);
274 final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
275 Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
276 return QName.create(module.getQNameModule(), localName);
279 throw new IllegalArgumentException("Failed build path.");
283 private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
284 final int start = variables.getOffset();
285 nextSequenceEnd(matcher, variables);
286 return variables.getData().substring(start, variables.getOffset());
289 private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
290 while (!allCharsConsumed(variables)
291 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
292 variables.setOffset(variables.getOffset() + 1);
296 private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
297 final MainVarsWrapper variables) {
298 skipCurrentChar(variables);
299 final String value = nextIdentifierFromNextSequence(
300 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
302 // exception if value attribute is missing
303 RestconfValidationUtils.checkDocumentedError(
305 RestconfError.ErrorType.PROTOCOL,
306 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
307 "Value missing for: " + qname
309 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
310 final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
311 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
314 private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
315 final MainVarsWrapper variables) {
316 final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
317 if (currentNode == null) {
320 checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
321 variables.getData(), variables.getOffset());
324 @SuppressFBWarnings("NP_NULL_ON_SOME_PATH") // code does check for null 'current' but FB doesn't recognize it
325 private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
326 final MainVarsWrapper variables) {
327 variables.setCurrent(variables.getCurrent().getChild(qname));
328 DataSchemaContextNode<?> current = variables.getCurrent();
329 if (current == null) {
330 final Optional<Module> module = variables.getSchemaContext().findModule(qname.getModule());
331 if (module.isPresent()) {
332 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
333 if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
339 checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
340 variables.getOffset());
341 while (current.isMixin()) {
342 path.add(current.getIdentifier());
343 current = current.getChild(qname);
344 variables.setCurrent(current);
349 private static String findAndParsePercentEncoded(final String preparedPrefix) {
350 if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
351 return preparedPrefix;
354 final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
355 final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
357 while (matcher.matchesAnyOf(parsedPrefix)) {
358 final int percentCharPosition = matcher.indexIn(parsedPrefix);
359 parsedPrefix.replace(
361 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
362 String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
363 percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
364 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
365 ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
368 return parsedPrefix.toString();
371 private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
372 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
373 if (dataSchemaNode instanceof ContainerSchemaNode) {
374 final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
375 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
377 return node.getQName();
378 } else if (dataSchemaNode instanceof ListSchemaNode) {
379 final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
380 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
382 return node.getQName();
384 throw new UnsupportedOperationException();
387 private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
388 return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
391 private static void validArg(final MainVarsWrapper variables) {
392 // every identifier except of the first MUST start with slash
393 if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
394 checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
395 "Identifier must start with '/'.", variables.getData(), variables.getOffset());
398 skipCurrentChar(variables);
400 // check if slash is not also the last char in identifier
401 checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
402 variables.getData(), variables.getOffset());
406 private static void skipCurrentChar(final MainVarsWrapper variables) {
407 variables.setOffset(variables.getOffset() + 1);
410 private static char currentChar(final int offset, final String data) {
411 return data.charAt(offset);
414 private static void checkValid(final boolean condition, final String errorMsg, final String data,
416 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
417 data, offset, errorMsg);
420 private static boolean allCharsConsumed(final MainVarsWrapper variables) {
421 return variables.getOffset() == variables.getData().length();
424 private static final class MainVarsWrapper {
425 private static final int STARTING_OFFSET = 0;
427 private final SchemaContext schemaContext;
428 private final String data;
430 private DataSchemaContextNode<?> current;
433 MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
434 final SchemaContext schemaContext) {
436 this.current = current;
437 this.offset = offset;
438 this.schemaContext = schemaContext;
441 public String getData() {
445 public DataSchemaContextNode<?> getCurrent() {
449 public void setCurrent(final DataSchemaContextNode<?> current) {
450 this.current = current;
453 public int getOffset() {
457 public void setOffset(final int offset) {
458 this.offset = offset;
461 public SchemaContext getSchemaContext() {
462 return this.schemaContext;