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.PathArgument;
28 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
29 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
30 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.Module;
37 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
38 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
39 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
40 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
41 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
44 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
48 public final class YangInstanceIdentifierDeserializer {
50 private YangInstanceIdentifierDeserializer() {
51 throw new UnsupportedOperationException("Util class.");
55 * Method to create {@link Iterable} from {@link PathArgument} which are
56 * parsing from data by {@link SchemaContext}.
58 * @param schemaContext
59 * - for validate of parsing path arguments
62 * @return {@link Iterable} of {@link PathArgument}
64 public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
65 final List<PathArgument> path = new LinkedList<>();
66 final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
67 data, DataSchemaContextTree.from(schemaContext).getRoot(),
68 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
70 while (!allCharsConsumed(variables)) {
72 final QName qname = prepareQName(variables);
74 // this is the last identifier (input is consumed) or end of identifier (slash)
75 if (allCharsConsumed(variables)
76 || (currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH)) {
77 prepareIdentifier(qname, path, variables);
78 path.add(variables.getCurrent().getIdentifier());
79 } else if (currentChar(variables.getOffset(),
80 variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
81 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
82 prepareNodeWithPredicates(qname, path, variables);
84 prepareNodeWithValue(qname, path, variables);
87 throw new IllegalArgumentException(
88 "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
89 + variables.getOffset() + ".");
93 return ImmutableList.copyOf(path);
96 private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
97 final MainVarsWrapper variables) {
99 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
100 checkValid((dataSchemaNode != null), "Data schema node is null", variables.getData(), variables.getOffset());
102 final Iterator<QName> keys = ((ListSchemaNode) dataSchemaNode).getKeyDefinition().iterator();
103 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
105 // skip already expected equal sign
106 skipCurrentChar(variables);
108 // read key value separated by comma
109 while (keys.hasNext() && !allCharsConsumed(variables) && (currentChar(variables.getOffset(),
110 variables.getData()) != RestconfConstants.SLASH)) {
113 if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
114 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
115 skipCurrentChar(variables);
119 // check if next value is parsable
120 RestconfValidationUtils.checkDocumentedError(
121 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
122 .matches(currentChar(variables.getOffset(), variables.getData())),
123 RestconfError.ErrorType.PROTOCOL,
124 RestconfError.ErrorTag.MALFORMED_MESSAGE,
129 final QName key = keys.next();
130 DataSchemaNode leafSchemaNode = null;
131 if (dataSchemaNode instanceof ListSchemaNode) {
132 leafSchemaNode = ((ListSchemaNode) dataSchemaNode).getDataChildByName(key);
133 } else if (dataSchemaNode instanceof LeafListSchemaNode) {
134 leafSchemaNode = dataSchemaNode;
136 final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
137 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables));
138 final Object valueByType = prepareValueByType(leafSchemaNode, value, variables);
139 values.put(key, valueByType);
143 if (keys.hasNext() && !allCharsConsumed(variables) && (currentChar(
144 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA)) {
145 skipCurrentChar(variables);
149 // the last key is considered to be empty
150 if (keys.hasNext()) {
151 if (allCharsConsumed(variables)
152 || (currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH)) {
153 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
156 // there should be no more missing keys
157 RestconfValidationUtils.checkDocumentedError(
159 RestconfError.ErrorType.PROTOCOL,
160 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
161 "Key value missing for: " + qname
165 path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
168 private static Object prepareValueByType(final DataSchemaNode schemaNode, final String value,
169 final MainVarsWrapper vars) {
170 Object decoded = null;
172 TypeDefinition<? extends TypeDefinition<?>> typedef = null;
173 if (schemaNode instanceof LeafListSchemaNode) {
174 typedef = ((LeafListSchemaNode) schemaNode).getType();
176 typedef = ((LeafSchemaNode) schemaNode).getType();
178 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
179 if (baseType instanceof LeafrefTypeDefinition) {
180 typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, vars.getSchemaContext(),
183 final Codec<Object, Object> codec = RestCodec.from(typedef, null);
184 decoded = codec.deserialize(value);
185 if (decoded == null) {
186 if ((baseType instanceof IdentityrefTypeDefinition)) {
187 decoded = toQName(value, schemaNode, vars.getSchemaContext());
193 private static Object toQName(final String value, final DataSchemaNode schemaNode,
194 final SchemaContext schemaContext) {
195 final String moduleName = toModuleName(value);
196 final String nodeName = toNodeName(value);
197 final Module module = schemaContext.findModuleByName(moduleName, null);
198 for (final IdentitySchemaNode identitySchemaNode : module.getIdentities()) {
199 final QName qName = identitySchemaNode.getQName();
200 if (qName.getLocalName().equals(nodeName)) {
204 return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), value);
207 private static String toNodeName(final String str) {
208 final int idx = str.indexOf(':');
213 if (str.indexOf(':', idx + 1) != -1) {
217 return str.substring(idx + 1);
220 private static String toModuleName(final String str) {
221 final int idx = str.indexOf(':');
226 if (str.indexOf(':', idx + 1) != -1) {
230 return str.substring(0, idx);
233 private static QName prepareQName(final MainVarsWrapper variables) {
235 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
236 .matches(currentChar(variables.getOffset(), variables.getData())),
237 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
238 final String preparedPrefix = nextIdentifierFromNextSequence(
239 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
240 final String prefix, localName;
242 if (allCharsConsumed(variables)) {
243 return getQNameOfDataSchemaNode(preparedPrefix, variables);
246 switch (currentChar(variables.getOffset(), variables.getData())) {
247 case RestconfConstants.SLASH:
248 prefix = preparedPrefix;
249 return getQNameOfDataSchemaNode(prefix, variables);
250 case ParserBuilderConstants.Deserializer.COLON:
251 prefix = preparedPrefix;
252 skipCurrentChar(variables);
254 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
255 .matches(currentChar(variables.getOffset(), variables.getData())),
256 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
257 variables.getOffset());
258 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
260 if (!allCharsConsumed(variables) && (currentChar
261 (variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL)) {
262 return getQNameOfDataSchemaNode(localName, variables);
264 final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
265 Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
266 return QName.create(module.getQNameModule(), localName);
268 case ParserBuilderConstants.Deserializer.EQUAL:
269 prefix = preparedPrefix;
270 return getQNameOfDataSchemaNode(prefix, variables);
272 throw new IllegalArgumentException("Failed build path.");
276 private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
277 final int start = variables.getOffset();
278 nextSequenceEnd(matcher, variables);
279 return variables.getData().substring(start, variables.getOffset());
282 private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
283 while (!allCharsConsumed(variables)
284 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
285 variables.setOffset(variables.getOffset() + 1);
289 private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
290 final MainVarsWrapper variables) {
291 skipCurrentChar(variables);
292 final String value = nextIdentifierFromNextSequence(
293 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
295 // exception if value attribute is missing
296 RestconfValidationUtils.checkDocumentedError(
298 RestconfError.ErrorType.PROTOCOL,
299 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
300 "Value missing for: " + qname
302 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
303 final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
304 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
307 private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
308 final MainVarsWrapper variables) {
309 final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
310 checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
311 variables.getData(), variables.getOffset());
314 private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
315 final MainVarsWrapper variables) {
316 variables.setCurrent(variables.getCurrent().getChild(qname));
317 DataSchemaContextNode<?> current = variables.getCurrent();
318 checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
319 variables.getOffset());
320 while (current.isMixin()) {
321 path.add(current.getIdentifier());
322 current = current.getChild(qname);
323 variables.setCurrent(current);
328 private static String findAndParsePercentEncoded(final String preparedPrefix) {
329 if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
330 return preparedPrefix;
333 final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
334 final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
336 while (matcher.matchesAnyOf(parsedPrefix)) {
337 final int percentCharPosition = matcher.indexIn(parsedPrefix);
338 parsedPrefix.replace(
340 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
341 String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
342 percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
343 percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
344 ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
347 return parsedPrefix.toString();
350 private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
351 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
352 if (dataSchemaNode instanceof ContainerSchemaNode) {
353 final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
354 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
356 return node.getQName();
357 } else if (dataSchemaNode instanceof ListSchemaNode) {
358 final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
359 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
361 return node.getQName();
363 throw new UnsupportedOperationException();
366 private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
367 return schemaContext.findModuleByName(prefix, null);
370 private static void validArg(final MainVarsWrapper variables) {
371 // every identifier except of the first MUST start with slash
372 if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
373 checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
374 "Identifier must start with '/'.", variables.getData(), variables.getOffset());
377 skipCurrentChar(variables);
379 // check if slash is not also the last char in identifier
380 checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
381 variables.getData(), variables.getOffset());
385 private static void skipCurrentChar(final MainVarsWrapper variables) {
386 variables.setOffset(variables.getOffset() + 1);
389 private static char currentChar(final int offset, final String data) {
390 return data.charAt(offset);
393 private static void checkValid(final boolean condition, final String errorMsg, final String data,
395 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
396 data, offset, errorMsg);
399 private static boolean allCharsConsumed(final MainVarsWrapper variables) {
400 return variables.getOffset() == variables.getData().length();
403 private final static class MainVarsWrapper {
404 private static final int STARTING_OFFSET = 0;
406 private final SchemaContext schemaContext;
407 private final String data;
408 private DataSchemaContextNode<?> current;
411 public MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
412 final SchemaContext schemaContext) {
414 this.setCurrent(current);
415 this.setOffset(offset);
416 this.schemaContext = schemaContext;
419 public String getData() {
423 public DataSchemaContextNode<?> getCurrent() {
427 public void setCurrent(final DataSchemaContextNode<?> current) {
428 this.current = current;
431 public int getOffset() {
435 public void setOffset(final int offset) {
436 this.offset = offset;
439 public SchemaContext getSchemaContext() {
440 return this.schemaContext;