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 com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.base.CharMatcher;
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.ArrayList;
18 import java.util.Iterator;
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.yang.common.QName;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
33 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
34 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
35 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
36 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
37 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
39 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.Module;
45 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
46 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
47 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
49 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
54 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
58 public final class YangInstanceIdentifierDeserializer {
59 private final SchemaContext schemaContext;
60 private final String data;
62 private DataSchemaContextNode<?> current;
65 private YangInstanceIdentifierDeserializer(final SchemaContext schemaContext, final String data) {
66 this.schemaContext = requireNonNull(schemaContext);
67 this.data = requireNonNull(data);
68 current = DataSchemaContextTree.from(schemaContext).getRoot();
72 * Method to create {@link Iterable} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
74 * @param schemaContext for validate of parsing path arguments
75 * @param data path to data, in URL string form
76 * @return {@link Iterable} of {@link PathArgument}
78 public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
79 return new YangInstanceIdentifierDeserializer(schemaContext, data).parse();
82 private List<PathArgument> parse() {
83 final List<PathArgument> path = new ArrayList<>();
85 while (!allCharsConsumed()) {
87 final QName qname = prepareQName();
89 // this is the last identifier (input is consumed) or end of identifier (slash)
90 if (allCharsConsumed() || currentChar() == RestconfConstants.SLASH) {
91 prepareIdentifier(qname, path);
92 path.add(current == null ? NodeIdentifier.create(qname) : current.getIdentifier());
93 } else if (currentChar() == ParserBuilderConstants.Deserializer.EQUAL) {
94 if (nextContextNode(qname, path).getDataSchemaNode() instanceof ListSchemaNode) {
95 prepareNodeWithPredicates(qname, path, (ListSchemaNode) current.getDataSchemaNode());
97 prepareNodeWithValue(qname, path);
100 throw new IllegalArgumentException("Bad char " + currentChar() + " on position " + offset + ".");
104 return ImmutableList.copyOf(path);
107 private void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
108 final ListSchemaNode listSchemaNode) {
109 checkValid(listSchemaNode != null, "Data schema node is null");
111 final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
112 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
114 // skip already expected equal sign
117 // read key value separated by comma
118 while (keys.hasNext() && !allCharsConsumed() && currentChar() != RestconfConstants.SLASH) {
121 if (currentChar() == ParserBuilderConstants.Deserializer.COMMA) {
122 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
127 // check if next value is parsable
128 RestconfValidationUtils.checkDocumentedError(
129 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE.matches(currentChar()),
130 RestconfError.ErrorType.PROTOCOL,
131 RestconfError.ErrorTag.MALFORMED_MESSAGE,
136 final QName key = keys.next();
137 Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
138 if (!leafSchemaNode.isPresent()) {
139 throw new RestconfDocumentedException("Schema not found for " + key,
140 RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
143 final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
144 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE));
145 final Object valueByType = prepareValueByType(leafSchemaNode.get(), value);
146 values.put(key, valueByType);
150 if (keys.hasNext() && !allCharsConsumed() && currentChar() == ParserBuilderConstants.Deserializer.COMMA) {
155 // the last key is considered to be empty
156 if (keys.hasNext()) {
157 if (allCharsConsumed() || currentChar() == RestconfConstants.SLASH) {
158 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
161 // there should be no more missing keys
162 RestconfValidationUtils.checkDocumentedError(
164 RestconfError.ErrorType.PROTOCOL,
165 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
166 "Key value missing for: " + qname
170 path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
173 private Object prepareValueByType(final DataSchemaNode schemaNode, final String value) {
174 Object decoded = null;
176 TypeDefinition<? extends TypeDefinition<?>> typedef = null;
177 if (schemaNode instanceof LeafListSchemaNode) {
178 typedef = ((LeafListSchemaNode) schemaNode).getType();
180 typedef = ((LeafSchemaNode) schemaNode).getType();
182 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
183 if (baseType instanceof LeafrefTypeDefinition) {
184 typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, schemaContext,
187 decoded = RestCodec.from(typedef, null, schemaContext).deserialize(value);
188 if (decoded == null) {
189 if (baseType instanceof IdentityrefTypeDefinition) {
190 decoded = toQName(value, schemaNode, schemaContext);
196 private QName prepareQName() {
197 checkValid(ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR.matches(currentChar()),
198 "Identifier must start with character from set 'a-zA-Z_'");
199 final String preparedPrefix = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER);
201 final String localName;
203 if (allCharsConsumed()) {
204 return getQNameOfDataSchemaNode(preparedPrefix);
207 switch (currentChar()) {
208 case RestconfConstants.SLASH:
209 case ParserBuilderConstants.Deserializer.EQUAL:
210 prefix = preparedPrefix;
211 return getQNameOfDataSchemaNode(prefix);
212 case ParserBuilderConstants.Deserializer.COLON:
213 prefix = preparedPrefix;
215 checkValid(ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR.matches(currentChar()),
216 "Identifier must start with character from set 'a-zA-Z_'");
217 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER);
219 if (!allCharsConsumed() && currentChar() == ParserBuilderConstants.Deserializer.EQUAL) {
220 return getQNameOfDataSchemaNode(localName);
222 final Module module = moduleForPrefix(prefix);
223 checkArgument(module != null, "Failed to lookup prefix %s", prefix);
224 return QName.create(module.getQNameModule(), localName);
227 throw new IllegalArgumentException("Failed build path.");
231 private void prepareNodeWithValue(final QName qname, final List<PathArgument> path) {
233 final String value = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE);
235 // exception if value attribute is missing
236 RestconfValidationUtils.checkDocumentedError(
238 RestconfError.ErrorType.PROTOCOL,
239 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
240 "Value missing for: " + qname
242 final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
243 final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value));
244 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
247 private void prepareIdentifier(final QName qname, final List<PathArgument> path) {
248 final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path);
249 if (currentNode == null) {
252 checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present");
255 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH",
256 justification = "code does check for null 'current' but FB doesn't recognize it")
257 private DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path) {
258 final DataSchemaContextNode<?> initialContext = current;
259 final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
261 current = initialContext.getChild(qname);
263 if (current == null) {
264 final Optional<Module> module = schemaContext.findModule(qname.getModule());
265 if (module.isPresent()) {
266 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
267 if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
272 if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
276 checkValid(current != null, qname + " is not correct schema node identifier.");
277 while (current.isMixin()) {
278 path.add(current.getIdentifier());
279 current = current.getChild(qname);
284 private Module moduleForPrefix(final String prefix) {
285 return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
288 private boolean allCharsConsumed() {
289 return offset == data.length();
292 private void checkValid(final boolean condition, final String errorMsg) {
293 checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
297 private char currentChar() {
298 return data.charAt(offset);
301 private void skipCurrentChar() {
305 private String nextIdentifierFromNextSequence(final CharMatcher matcher) {
306 final int start = offset;
307 while (!allCharsConsumed() && matcher.matches(currentChar())) {
310 return data.substring(start, offset);
313 private void validArg() {
314 // every identifier except of the first MUST start with slash
316 checkValid(RestconfConstants.SLASH == currentChar(), "Identifier must start with '/'.");
318 // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
319 // multiple slashes into a single one
320 while (!allCharsConsumed() && RestconfConstants.SLASH == currentChar()) {
324 // check if slash is not also the last char in identifier
325 checkValid(!allCharsConsumed(), "Identifier cannot end with '/'.");
329 private QName getQNameOfDataSchemaNode(final String nodeName) {
330 final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
331 if (dataSchemaNode instanceof ContainerSchemaNode) {
332 return getQNameOfDataSchemaNode((ContainerSchemaNode) dataSchemaNode, nodeName);
333 } else if (dataSchemaNode instanceof ListSchemaNode) {
334 return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
337 throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
340 private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
341 final T parent, String nodeName) {
342 final Optional<ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
343 final SchemaNode node;
344 if (actionDef.isPresent()) {
345 node = actionDef.get();
347 node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
349 return node.getQName();
352 private static Optional<ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
353 final String nodeName) {
354 requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
355 if (dataSchemaNode instanceof ActionNodeContainer) {
356 return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
357 .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
359 return Optional.empty();
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 Object toQName(final String value, final DataSchemaNode schemaNode,
385 final SchemaContext schemaContext) {
386 final String moduleName = toModuleName(value);
387 final String nodeName = toNodeName(value);
388 final Module module = schemaContext.findModules(moduleName).iterator().next();
389 for (final IdentitySchemaNode identitySchemaNode : module.getIdentities()) {
390 final QName qName = identitySchemaNode.getQName();
391 if (qName.getLocalName().equals(nodeName)) {
395 return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
398 private static String toNodeName(final String str) {
399 final int idx = str.indexOf(':');
404 if (str.indexOf(':', idx + 1) != -1) {
408 return str.substring(idx + 1);
411 private static String toModuleName(final String str) {
412 final int idx = str.indexOf(':');
417 if (str.indexOf(':', idx + 1) != -1) {
421 return str.substring(0, idx);