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.collect.ImmutableList;
14 import com.google.common.collect.ImmutableMap;
15 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
16 import java.util.ArrayList;
17 import java.util.Iterator;
18 import java.util.List;
19 import java.util.Optional;
20 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
21 import org.opendaylight.restconf.common.util.RestUtil;
22 import org.opendaylight.restconf.common.util.RestconfSchemaUtil;
23 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
24 import org.opendaylight.yangtools.yang.common.ErrorTag;
25 import org.opendaylight.yangtools.yang.common.ErrorType;
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
30 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
31 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
32 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
33 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
34 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
35 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
36 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.Module;
43 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
44 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
45 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
47 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
49 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
52 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
54 public final class YangInstanceIdentifierDeserializer {
55 private static final CharMatcher IDENTIFIER_PREDICATE =
56 CharMatcher.noneOf(ParserConstants.RFC3986_RESERVED_CHARACTERS).precomputed();
57 private static final CharMatcher PERCENT_ENCODING = CharMatcher.is('%');
58 // position of the first encoded char after percent sign in percent encoded string
59 private static final int FIRST_ENCODED_CHAR = 1;
60 // position of the last encoded char after percent sign in percent encoded string
61 private static final int LAST_ENCODED_CHAR = 3;
62 // percent encoded radix for parsing integers
63 private static final int PERCENT_ENCODED_RADIX = 16;
65 private final EffectiveModelContext schemaContext;
66 private final String data;
68 private DataSchemaContextNode<?> current;
71 private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final String data) {
72 this.schemaContext = requireNonNull(schemaContext);
73 this.data = requireNonNull(data);
74 current = DataSchemaContextTree.from(schemaContext).getRoot();
78 * Method to create {@link Iterable} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
80 * @param schemaContext for validate of parsing path arguments
81 * @param data path to data, in URL string form
82 * @return {@link Iterable} of {@link PathArgument}
84 public static List<PathArgument> create(final EffectiveModelContext schemaContext, final String data) {
85 return new YangInstanceIdentifierDeserializer(schemaContext, data).parse();
88 private List<PathArgument> parse() {
89 final List<PathArgument> path = new ArrayList<>();
91 while (!allCharsConsumed()) {
93 final QName qname = prepareQName();
95 // this is the last identifier (input is consumed) or end of identifier (slash)
96 if (allCharsConsumed() || currentChar() == '/') {
97 prepareIdentifier(qname, path);
98 path.add(current == null ? NodeIdentifier.create(qname) : current.getIdentifier());
99 } else if (currentChar() == '=') {
100 if (nextContextNode(qname, path).getDataSchemaNode() instanceof ListSchemaNode) {
101 prepareNodeWithPredicates(qname, path, (ListSchemaNode) current.getDataSchemaNode());
103 prepareNodeWithValue(qname, path);
106 throw getParsingCharFailedException();
110 return ImmutableList.copyOf(path);
113 private void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
114 final ListSchemaNode listSchemaNode) {
115 checkValid(listSchemaNode != null, ErrorTag.MALFORMED_MESSAGE, "Data schema node is null");
117 final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
118 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
120 // skip already expected equal sign
123 // read key value separated by comma
124 while (keys.hasNext() && !allCharsConsumed() && currentChar() != '/') {
127 if (currentChar() == ',') {
128 values.put(keys.next(), "");
133 // check if next value is parsable
134 checkValid(IDENTIFIER_PREDICATE.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
135 "Value that starts with character %c is not parsable.", currentChar());
138 final QName key = keys.next();
139 Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
140 RestconfDocumentedException.throwIf(leafSchemaNode.isEmpty(), ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT,
141 "Schema not found for %s", key);
143 final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE));
144 final Object valueByType = prepareValueByType(leafSchemaNode.get(), value);
145 values.put(key, valueByType);
148 if (keys.hasNext() && !allCharsConsumed() && currentChar() == ',') {
153 // the last key is considered to be empty
154 if (keys.hasNext()) {
155 // at this point, it must be true that current char is '/' or all chars have already been consumed
156 values.put(keys.next(), "");
158 // there should be no more missing keys
159 RestconfDocumentedException.throwIf(keys.hasNext(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
160 "Cannot parse input identifier '%s'. Key value is missing for QName: %s", data, qname);
163 path.add(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(qname, values.build()));
166 private Object prepareValueByType(final DataSchemaNode schemaNode, final String value) {
169 TypeDefinition<? extends TypeDefinition<?>> typedef;
170 if (schemaNode instanceof LeafListSchemaNode) {
171 typedef = ((LeafListSchemaNode) schemaNode).getType();
173 typedef = ((LeafSchemaNode) schemaNode).getType();
175 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
176 if (baseType instanceof LeafrefTypeDefinition) {
177 typedef = SchemaInferenceStack.ofInstantiatedPath(schemaContext, schemaNode.getPath())
178 .resolveLeafref((LeafrefTypeDefinition) baseType);
180 decoded = RestCodec.from(typedef, null, schemaContext).deserialize(value);
181 if (decoded == null && typedef instanceof IdentityrefTypeDefinition) {
182 decoded = toIdentityrefQName(value, schemaNode);
187 private QName prepareQName() {
188 checkValidIdentifierStart();
189 final String preparedPrefix = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
191 final String localName;
193 if (allCharsConsumed()) {
194 return getQNameOfDataSchemaNode(preparedPrefix);
197 switch (currentChar()) {
200 prefix = preparedPrefix;
201 return getQNameOfDataSchemaNode(prefix);
203 prefix = preparedPrefix;
205 checkValidIdentifierStart();
206 localName = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
208 if (!allCharsConsumed() && currentChar() == '=') {
209 return getQNameOfDataSchemaNode(localName);
211 final Module module = moduleForPrefix(prefix);
212 RestconfDocumentedException.throwIf(module == null, ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
213 "Failed to lookup for module with name '%s'.", prefix);
214 return QName.create(module.getQNameModule(), localName);
217 throw getParsingCharFailedException();
221 private void prepareNodeWithValue(final QName qname, final List<PathArgument> path) {
223 final String value = nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE);
225 // exception if value attribute is missing
226 RestconfDocumentedException.throwIf(value.isEmpty(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
227 "Cannot parse input identifier '%s' - value is missing for QName: %s.", data, qname);
228 final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
229 final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value));
230 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
233 private void prepareIdentifier(final QName qname, final List<PathArgument> path) {
234 final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path);
235 if (currentNode != null) {
236 checkValid(!currentNode.isKeyedEntry(), ErrorTag.MISSING_ATTRIBUTE,
237 "Entry '%s' requires key or value predicate to be present.", qname);
241 @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH",
242 justification = "code does check for null 'current' but FB doesn't recognize it")
243 private DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path) {
244 final DataSchemaContextNode<?> initialContext = current;
245 final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
247 current = initialContext.getChild(qname);
249 if (current == null) {
250 final Optional<Module> module = schemaContext.findModule(qname.getModule());
251 if (module.isPresent()) {
252 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
253 if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
258 if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
262 checkValid(current != null, ErrorTag.MALFORMED_MESSAGE, "'%s' is not correct schema node identifier.", qname);
263 while (current.isMixin()) {
264 path.add(current.getIdentifier());
265 current = current.getChild(qname);
270 private Module moduleForPrefix(final String prefix) {
271 return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
274 private boolean allCharsConsumed() {
275 return offset == data.length();
278 private void checkValid(final boolean condition, final ErrorTag errorTag, final String errorMsg) {
280 throw createParsingException(errorTag, errorMsg);
284 private void checkValid(final boolean condition, final ErrorTag errorTag, final String fmt, final Object arg) {
286 throw createParsingException(errorTag, String.format(fmt, arg));
290 private void checkValidIdentifierStart() {
291 checkValid(ParserConstants.YANG_IDENTIFIER_START.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
292 "Identifier must start with character from set 'a-zA-Z_'");
295 private RestconfDocumentedException getParsingCharFailedException() {
296 return createParsingException(ErrorTag.MALFORMED_MESSAGE,
297 "Bad char '" + currentChar() + "' on the current position.");
300 private RestconfDocumentedException createParsingException(final ErrorTag errorTag, final String messagePart) {
301 return new RestconfDocumentedException(String.format(
302 "Could not parse Instance Identifier '%s'. Offset: '%d' : Reason: %s", data, offset, messagePart),
303 ErrorType.PROTOCOL, errorTag);
306 private char currentChar() {
307 return data.charAt(offset);
310 private void skipCurrentChar() {
314 private String nextIdentifierFromNextSequence(final CharMatcher matcher) {
315 final int start = offset;
316 while (!allCharsConsumed() && matcher.matches(currentChar())) {
319 return data.substring(start, offset);
322 private void validArg() {
323 // every identifier except of the first MUST start with slash
325 checkValid('/' == currentChar(), ErrorTag.MALFORMED_MESSAGE, "Identifier must start with '/'.");
327 // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
328 // multiple slashes into a single one
329 while (!allCharsConsumed() && '/' == currentChar()) {
333 // check if slash is not also the last char in identifier
334 checkValid(!allCharsConsumed(), ErrorTag.MALFORMED_MESSAGE, "Identifier cannot end with '/'.");
338 private QName getQNameOfDataSchemaNode(final String nodeName) {
339 final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
340 if (dataSchemaNode instanceof ContainerLike) {
341 return getQNameOfDataSchemaNode((ContainerLike) dataSchemaNode, nodeName);
342 } else if (dataSchemaNode instanceof ListSchemaNode) {
343 return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
346 throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
349 private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
350 final T parent, final String nodeName) {
351 final Optional<? extends ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
352 final SchemaNode node;
353 if (actionDef.isPresent()) {
354 node = actionDef.get();
356 node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
358 return node.getQName();
361 private static Optional<? extends ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
362 final String nodeName) {
363 requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
364 if (dataSchemaNode instanceof ActionNodeContainer) {
365 return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
366 .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
368 return Optional.empty();
371 private static String findAndParsePercentEncoded(final String preparedPrefix) {
372 if (preparedPrefix.indexOf('%') == -1) {
373 return preparedPrefix;
376 // FIXME: this is extremely inefficient: we should be converting ranges of characters, not driven by
377 // CharMatcher, but by String.indexOf()
378 final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
379 while (PERCENT_ENCODING.matchesAnyOf(parsedPrefix)) {
380 final int percentCharPosition = PERCENT_ENCODING.indexIn(parsedPrefix);
381 parsedPrefix.replace(percentCharPosition, percentCharPosition + LAST_ENCODED_CHAR,
382 String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
383 percentCharPosition + FIRST_ENCODED_CHAR, percentCharPosition + LAST_ENCODED_CHAR),
384 PERCENT_ENCODED_RADIX)));
387 return parsedPrefix.toString();
390 private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
391 final String moduleName = toModuleName(value);
392 final String nodeName = toNodeName(value);
393 final Iterator<? extends Module> modulesIterator = schemaContext.findModules(moduleName).iterator();
394 if (!modulesIterator.hasNext()) {
395 throw new RestconfDocumentedException(String.format("Cannot decode value '%s' for identityref type "
396 + "in %s. Make sure reserved characters such as comma, single-quote, double-quote, colon,"
397 + " double-quote, space, and forward slash (,'\":\" /) are percent-encoded,"
398 + " for example ':' is '%%3A'", value, current.getIdentifier().getNodeType()),
399 ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
401 for (final IdentitySchemaNode identitySchemaNode : modulesIterator.next().getIdentities()) {
402 final QName qName = identitySchemaNode.getQName();
403 if (qName.getLocalName().equals(nodeName)) {
407 return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
410 private static String toNodeName(final String str) {
411 final int idx = str.indexOf(':');
416 if (str.indexOf(':', idx + 1) != -1) {
420 return str.substring(idx + 1);
423 private static String toModuleName(final String str) {
424 final int idx = str.indexOf(':');
429 if (str.indexOf(':', idx + 1) != -1) {
433 return str.substring(0, idx);