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.parser.builder;
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 java.util.Iterator;
15 import java.util.LinkedList;
16 import java.util.List;
17 import org.opendaylight.netconf.md.sal.rest.common.RestconfValidationUtils;
18 import org.opendaylight.netconf.sal.rest.impl.RestUtil;
19 import org.opendaylight.netconf.sal.restconf.impl.RestCodec;
20 import org.opendaylight.netconf.sal.restconf.impl.RestconfError;
21 import org.opendaylight.restconf.utils.RestconfConstants;
22 import org.opendaylight.restconf.utils.parser.builder.ParserBuilderConstants;
23 import org.opendaylight.restconf.utils.schema.context.RestconfSchemaUtil;
24 import org.opendaylight.yangtools.concepts.Codec;
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
29 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
30 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
31 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.Module;
38 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
41 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
42 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
43 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
46 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
50 public final class YangInstanceIdentifierDeserializer {
52 private YangInstanceIdentifierDeserializer() {
53 throw new UnsupportedOperationException("Util class.");
57 * Method to create {@link Iterable} from {@link PathArgument} which are
58 * parsing from data by {@link SchemaContext}.
60 * @param schemaContext
61 * - for validate of parsing path arguments
64 * @return {@link Iterable} of {@link PathArgument}
66 public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
67 final List<PathArgument> path = new LinkedList<>();
68 final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
69 data, DataSchemaContextTree.from(schemaContext).getRoot(),
70 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
72 while (!allCharsConsumed(variables)) {
74 final QName qname = prepareQName(variables);
76 // this is the last identifier (input is consumed) or end of identifier (slash)
77 if (allCharsConsumed(variables)
78 || (currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH)) {
79 prepareIdentifier(qname, path, variables);
80 if (variables.getCurrent() == null) {
81 path.add(NodeIdentifier.create(qname));
83 path.add(variables.getCurrent().getIdentifier());
85 } else if (currentChar(variables.getOffset(),
86 variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
87 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
88 prepareNodeWithPredicates(qname, path, variables);
90 prepareNodeWithValue(qname, path, variables);
93 throw new IllegalArgumentException(
94 "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
95 + variables.getOffset() + ".");
99 return ImmutableList.copyOf(path);
102 private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
103 final MainVarsWrapper variables) {
105 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
106 checkValid((dataSchemaNode != null), "Data schema node is null", variables.getData(), variables.getOffset());
108 final Iterator<QName> keys = ((ListSchemaNode) dataSchemaNode).getKeyDefinition().iterator();
109 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
111 // skip already expected equal sign
112 skipCurrentChar(variables);
114 // read key value separated by comma
115 while (keys.hasNext() && !allCharsConsumed(variables) && (currentChar(variables.getOffset(),
116 variables.getData()) != RestconfConstants.SLASH)) {
119 if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
120 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
121 skipCurrentChar(variables);
125 // check if next value is parsable
126 RestconfValidationUtils.checkDocumentedError(
127 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
128 .matches(currentChar(variables.getOffset(), variables.getData())),
129 RestconfError.ErrorType.PROTOCOL,
130 RestconfError.ErrorTag.MALFORMED_MESSAGE,
135 final QName key = keys.next();
136 DataSchemaNode leafSchemaNode = null;
137 if (dataSchemaNode instanceof ListSchemaNode) {
138 leafSchemaNode = ((ListSchemaNode) dataSchemaNode).getDataChildByName(key);
139 } else if (dataSchemaNode instanceof LeafListSchemaNode) {
140 leafSchemaNode = dataSchemaNode;
142 final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
143 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables));
144 final Object valueByType = prepareValueByType(leafSchemaNode, value, variables);
145 values.put(key, valueByType);
149 if (keys.hasNext() && !allCharsConsumed(variables) && (currentChar(
150 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA)) {
151 skipCurrentChar(variables);
155 // the last key is considered to be empty
156 if (keys.hasNext()) {
157 if (allCharsConsumed(variables)
158 || (currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH)) {
159 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
162 // there should be no more missing keys
163 RestconfValidationUtils.checkDocumentedError(
165 RestconfError.ErrorType.PROTOCOL,
166 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
167 "Key value missing for: " + qname
171 path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
174 private static Object prepareValueByType(final DataSchemaNode schemaNode, final String value,
175 final MainVarsWrapper vars) {
176 Object decoded = null;
178 TypeDefinition<? extends TypeDefinition<?>> typedef = null;
179 if (schemaNode instanceof LeafListSchemaNode) {
180 typedef = ((LeafListSchemaNode) schemaNode).getType();
182 typedef = ((LeafSchemaNode) schemaNode).getType();
184 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
185 if (baseType instanceof LeafrefTypeDefinition) {
186 typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, vars.getSchemaContext(),
189 final Codec<Object, Object> codec = RestCodec.from(typedef, null);
190 decoded = codec.deserialize(value);
191 if (decoded == null) {
192 if ((baseType instanceof IdentityrefTypeDefinition)) {
193 decoded = toQName(value, schemaNode, vars.getSchemaContext());
199 private static Object toQName(final String value, final DataSchemaNode schemaNode,
200 final SchemaContext schemaContext) {
201 final String moduleName = toModuleName(value);
202 final String nodeName = toNodeName(value);
203 final Module module = schemaContext.findModuleByName(moduleName, null);
204 for (final IdentitySchemaNode identitySchemaNode : module.getIdentities()) {
205 final QName qName = identitySchemaNode.getQName();
206 if (qName.getLocalName().equals(nodeName)) {
210 return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), value);
213 private static String toNodeName(final String str) {
214 final int idx = str.indexOf(':');
219 if (str.indexOf(':', idx + 1) != -1) {
223 return str.substring(idx + 1);
226 private static String toModuleName(final String str) {
227 final int idx = str.indexOf(':');
232 if (str.indexOf(':', idx + 1) != -1) {
236 return str.substring(0, idx);
239 private static QName prepareQName(final MainVarsWrapper variables) {
241 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
242 .matches(currentChar(variables.getOffset(), variables.getData())),
243 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
244 final String preparedPrefix = nextIdentifierFromNextSequence(
245 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
246 final String prefix, localName;
248 if (allCharsConsumed(variables)) {
249 return getQNameOfDataSchemaNode(preparedPrefix, variables);
252 switch (currentChar(variables.getOffset(), variables.getData())) {
253 case RestconfConstants.SLASH:
254 prefix = preparedPrefix;
255 return getQNameOfDataSchemaNode(prefix, variables);
256 case ParserBuilderConstants.Deserializer.COLON:
257 prefix = preparedPrefix;
258 skipCurrentChar(variables);
260 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
261 .matches(currentChar(variables.getOffset(), variables.getData())),
262 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
263 variables.getOffset());
264 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
266 if (!allCharsConsumed(variables) && (currentChar
267 (variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL)) {
268 return getQNameOfDataSchemaNode(localName, variables);
270 final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
271 Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
272 return QName.create(module.getQNameModule(), localName);
274 case ParserBuilderConstants.Deserializer.EQUAL:
275 prefix = preparedPrefix;
276 return getQNameOfDataSchemaNode(prefix, variables);
278 throw new IllegalArgumentException("Failed build path.");
282 private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
283 final int start = variables.getOffset();
284 nextSequenceEnd(matcher, variables);
285 return variables.getData().substring(start, variables.getOffset());
288 private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
289 while (!allCharsConsumed(variables)
290 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
291 variables.setOffset(variables.getOffset() + 1);
295 private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
296 final MainVarsWrapper variables) {
297 skipCurrentChar(variables);
298 final String value = nextIdentifierFromNextSequence(
299 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
301 // exception if value attribute is missing
302 RestconfValidationUtils.checkDocumentedError(
304 RestconfError.ErrorType.PROTOCOL,
305 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
306 "Value missing for: " + qname
308 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
309 final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
310 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
313 private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
314 final MainVarsWrapper variables) {
315 final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
316 if (currentNode == null) {
319 checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
320 variables.getData(), variables.getOffset());
323 private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
324 final MainVarsWrapper variables) {
325 variables.setCurrent(variables.getCurrent().getChild(qname));
326 DataSchemaContextNode<?> current = variables.getCurrent();
327 if (current == null) {
328 for (final RpcDefinition rpcDefinition : variables.getSchemaContext()
329 .findModuleByNamespaceAndRevision(qname.getNamespace(), qname.getRevision()).getRpcs()) {
330 if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
335 checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
336 variables.getOffset());
337 while (current.isMixin()) {
338 path.add(current.getIdentifier());
339 current = current.getChild(qname);
340 variables.setCurrent(current);
345 private static String findAndParsePercentEncoded(final String preparedPrefix) {
346 if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
347 return preparedPrefix;
350 final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
351 final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
353 while (matcher.matchesAnyOf(parsedPrefix)) {
354 final int percentCharPosition = matcher.indexIn(parsedPrefix);
355 parsedPrefix.replace(
357 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
358 String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
359 percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
360 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
361 ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
364 return parsedPrefix.toString();
367 private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
368 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
369 if (dataSchemaNode instanceof ContainerSchemaNode) {
370 final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
371 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
373 return node.getQName();
374 } else if (dataSchemaNode instanceof ListSchemaNode) {
375 final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
376 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
378 return node.getQName();
380 throw new UnsupportedOperationException();
383 private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
384 return schemaContext.findModuleByName(prefix, null);
387 private static void validArg(final MainVarsWrapper variables) {
388 // every identifier except of the first MUST start with slash
389 if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
390 checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
391 "Identifier must start with '/'.", variables.getData(), variables.getOffset());
394 skipCurrentChar(variables);
396 // check if slash is not also the last char in identifier
397 checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
398 variables.getData(), variables.getOffset());
402 private static void skipCurrentChar(final MainVarsWrapper variables) {
403 variables.setOffset(variables.getOffset() + 1);
406 private static char currentChar(final int offset, final String data) {
407 return data.charAt(offset);
410 private static void checkValid(final boolean condition, final String errorMsg, final String data,
412 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
413 data, offset, errorMsg);
416 private static boolean allCharsConsumed(final MainVarsWrapper variables) {
417 return variables.getOffset() == variables.getData().length();
420 private final static class MainVarsWrapper {
421 private static final int STARTING_OFFSET = 0;
423 private final SchemaContext schemaContext;
424 private final String data;
426 private DataSchemaContextNode<?> current;
429 public MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
430 final SchemaContext schemaContext) {
432 this.current = current;
433 this.offset = offset;
434 this.schemaContext = schemaContext;
437 public String getData() {
441 public DataSchemaContextNode<?> getCurrent() {
445 public void setCurrent(final DataSchemaContextNode<?> current) {
446 this.current = current;
449 public int getOffset() {
453 public void setOffset(final int offset) {
454 this.offset = offset;
457 public SchemaContext getSchemaContext() {
458 return this.schemaContext;