553f86da8c0b9fb7b26d5607039352ed3988ccb6
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / YangInstanceIdentifierDeserializer.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
9
10 import static java.util.Objects.requireNonNull;
11
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.common.YangNames;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
33 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
34 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
35 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
36 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
37 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
39 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.Module;
44 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
45 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
46 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
49 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
51
52 /**
53  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
54  */
55 public final class YangInstanceIdentifierDeserializer {
56     private static final CharMatcher IDENTIFIER_PREDICATE =
57             CharMatcher.noneOf(ParserConstants.RFC3986_RESERVED_CHARACTERS).precomputed();
58     private static final CharMatcher PERCENT_ENCODING = CharMatcher.is('%');
59     // position of the first encoded char after percent sign in percent encoded string
60     private static final int FIRST_ENCODED_CHAR = 1;
61     // position of the last encoded char after percent sign in percent encoded string
62     private static final int LAST_ENCODED_CHAR = 3;
63     // percent encoded radix for parsing integers
64     private static final int PERCENT_ENCODED_RADIX = 16;
65
66     private final EffectiveModelContext schemaContext;
67     private final String data;
68
69     private DataSchemaContextNode<?> current;
70     private int offset;
71
72     private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final String data) {
73         this.schemaContext = requireNonNull(schemaContext);
74         this.data = requireNonNull(data);
75         current = DataSchemaContextTree.from(schemaContext).getRoot();
76     }
77
78     /**
79      * Method to create {@link Iterable} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
80      *
81      * @param schemaContext for validate of parsing path arguments
82      * @param data path to data, in URL string form
83      * @return {@link Iterable} of {@link PathArgument}
84      */
85     public static List<PathArgument> create(final EffectiveModelContext schemaContext, final String data) {
86         return new YangInstanceIdentifierDeserializer(schemaContext, data).parse();
87     }
88
89     private List<PathArgument> parse() {
90         final List<PathArgument> path = new ArrayList<>();
91
92         while (!allCharsConsumed()) {
93             validArg();
94             final QName qname = prepareQName();
95
96             // this is the last identifier (input is consumed) or end of identifier (slash)
97             if (allCharsConsumed() || currentChar() == '/') {
98                 prepareIdentifier(qname, path);
99                 path.add(current == null ? NodeIdentifier.create(qname) : current.getIdentifier());
100             } else if (currentChar() == '=') {
101                 if (nextContextNode(qname, path).getDataSchemaNode() instanceof ListSchemaNode) {
102                     prepareNodeWithPredicates(qname, path, (ListSchemaNode) current.getDataSchemaNode());
103                 } else {
104                     prepareNodeWithValue(qname, path);
105                 }
106             } else {
107                 throw getParsingCharFailedException();
108             }
109         }
110
111         return ImmutableList.copyOf(path);
112     }
113
114     private void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
115             final ListSchemaNode listSchemaNode) {
116         checkValid(listSchemaNode != null, ErrorTag.MALFORMED_MESSAGE, "Data schema node is null");
117
118         final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
119         final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
120
121         // skip already expected equal sign
122         skipCurrentChar();
123
124         // read key value separated by comma
125         while (keys.hasNext() && !allCharsConsumed() && currentChar() != '/') {
126
127             // empty key value
128             if (currentChar() == ',') {
129                 values.put(keys.next(), "");
130                 skipCurrentChar();
131                 continue;
132             }
133
134             // check if next value is parsable
135             checkValid(IDENTIFIER_PREDICATE.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
136                     "Value that starts with character %c is not parsable.", currentChar());
137
138             // parse value
139             final QName key = keys.next();
140             Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
141             RestconfDocumentedException.throwIf(leafSchemaNode.isEmpty(), ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT,
142                     "Schema not found for %s", key);
143
144             final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE));
145             final Object valueByType = prepareValueByType(leafSchemaNode.get(), value);
146             values.put(key, valueByType);
147
148             // skip comma
149             if (keys.hasNext() && !allCharsConsumed() && currentChar() == ',') {
150                 skipCurrentChar();
151             }
152         }
153
154         // the last key is considered to be empty
155         if (keys.hasNext()) {
156             // at this point, it must be true that current char is '/' or all chars have already been consumed
157             values.put(keys.next(), "");
158
159             // there should be no more missing keys
160             RestconfDocumentedException.throwIf(keys.hasNext(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
161                     "Cannot parse input identifier '%s'. Key value is missing for QName: %s", data, qname);
162         }
163
164         path.add(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(qname, values.build()));
165     }
166
167     private Object prepareValueByType(final DataSchemaNode schemaNode, final String value) {
168         Object decoded;
169
170         TypeDefinition<? extends TypeDefinition<?>> typedef;
171         if (schemaNode instanceof LeafListSchemaNode) {
172             typedef = ((LeafListSchemaNode) schemaNode).getType();
173         } else {
174             typedef = ((LeafSchemaNode) schemaNode).getType();
175         }
176         final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
177         if (baseType instanceof LeafrefTypeDefinition) {
178             typedef = SchemaInferenceStack.ofInstantiatedPath(schemaContext, schemaNode.getPath())
179                 .resolveLeafref((LeafrefTypeDefinition) baseType);
180         }
181         decoded = RestCodec.from(typedef, null, schemaContext).deserialize(value);
182         if (decoded == null && typedef instanceof IdentityrefTypeDefinition) {
183             decoded = toIdentityrefQName(value, schemaNode);
184         }
185         return decoded;
186     }
187
188     private QName prepareQName() {
189         checkValidIdentifierStart();
190         final String preparedPrefix = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
191         final String prefix;
192         final String localName;
193
194         if (allCharsConsumed()) {
195             return getQNameOfDataSchemaNode(preparedPrefix);
196         }
197
198         switch (currentChar()) {
199             case '/':
200             case '=':
201                 prefix = preparedPrefix;
202                 return getQNameOfDataSchemaNode(prefix);
203             case ':':
204                 prefix = preparedPrefix;
205                 skipCurrentChar();
206                 checkValidIdentifierStart();
207                 localName = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
208
209                 if (!allCharsConsumed() && currentChar() == '=') {
210                     return getQNameOfDataSchemaNode(localName);
211                 } else {
212                     final Module module = moduleForPrefix(prefix);
213                     RestconfDocumentedException.throwIf(module == null, ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
214                             "Failed to lookup for module with name '%s'.", prefix);
215                     return QName.create(module.getQNameModule(), localName);
216                 }
217             default:
218                 throw getParsingCharFailedException();
219         }
220     }
221
222     private void prepareNodeWithValue(final QName qname, final List<PathArgument> path) {
223         skipCurrentChar();
224         final String value = nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE);
225
226         // exception if value attribute is missing
227         RestconfDocumentedException.throwIf(value.isEmpty(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
228                 "Cannot parse input identifier '%s' - value is missing for QName: %s.", data, qname);
229         final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
230         final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value));
231         path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
232     }
233
234     private void prepareIdentifier(final QName qname, final List<PathArgument> path) {
235         final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path);
236         if (currentNode != null) {
237             checkValid(!currentNode.isKeyedEntry(), ErrorTag.MISSING_ATTRIBUTE,
238                     "Entry '%s' requires key or value predicate to be present.", qname);
239         }
240     }
241
242     @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH",
243             justification = "code does check for null 'current' but FB doesn't recognize it")
244     private DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path) {
245         final DataSchemaContextNode<?> initialContext = current;
246         final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
247
248         current = initialContext.getChild(qname);
249
250         if (current == null) {
251             final Optional<Module> module = schemaContext.findModule(qname.getModule());
252             if (module.isPresent()) {
253                 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
254                     if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
255                         return null;
256                     }
257                 }
258             }
259             if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
260                 return null;
261             }
262         }
263         checkValid(current != null, ErrorTag.MALFORMED_MESSAGE, "'%s' is not correct schema node identifier.", qname);
264         while (current.isMixin()) {
265             path.add(current.getIdentifier());
266             current = current.getChild(qname);
267         }
268         return current;
269     }
270
271     private Module moduleForPrefix(final String prefix) {
272         return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
273     }
274
275     private boolean allCharsConsumed() {
276         return offset == data.length();
277     }
278
279     private void checkValid(final boolean condition, final ErrorTag errorTag, final String errorMsg) {
280         if (!condition) {
281             throw createParsingException(errorTag, errorMsg);
282         }
283     }
284
285     private void checkValid(final boolean condition, final ErrorTag errorTag, final String fmt, final Object arg) {
286         if (!condition) {
287             throw createParsingException(errorTag, String.format(fmt, arg));
288         }
289     }
290
291     private void checkValidIdentifierStart() {
292         checkValid(YangNames.IDENTIFIER_START.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
293             "Identifier must start with character from set 'a-zA-Z_'");
294     }
295
296     private RestconfDocumentedException getParsingCharFailedException() {
297         return createParsingException(ErrorTag.MALFORMED_MESSAGE,
298             "Bad char '" + currentChar() + "' on the current position.");
299     }
300
301     private RestconfDocumentedException createParsingException(final ErrorTag errorTag, final String reason) {
302         return new RestconfDocumentedException(
303             "Could not parse Instance Identifier '" + data + "'. Offset: '" + offset + "' : Reason: " + reason,
304             ErrorType.PROTOCOL, errorTag);
305     }
306
307     private char currentChar() {
308         return data.charAt(offset);
309     }
310
311     private void skipCurrentChar() {
312         offset++;
313     }
314
315     private String nextIdentifierFromNextSequence(final CharMatcher matcher) {
316         final int start = offset;
317         while (!allCharsConsumed() && matcher.matches(currentChar())) {
318             skipCurrentChar();
319         }
320         return data.substring(start, offset);
321     }
322
323     private void validArg() {
324         // every identifier except of the first MUST start with slash
325         if (offset != 0) {
326             checkValid('/' == currentChar(), ErrorTag.MALFORMED_MESSAGE, "Identifier must start with '/'.");
327
328             // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
329             // multiple slashes into a single one
330             while (!allCharsConsumed() && '/' == currentChar()) {
331                 skipCurrentChar();
332             }
333
334             // check if slash is not also the last char in identifier
335             checkValid(!allCharsConsumed(), ErrorTag.MALFORMED_MESSAGE, "Identifier cannot end with '/'.");
336         }
337     }
338
339     private QName getQNameOfDataSchemaNode(final String nodeName) {
340         final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
341         if (dataSchemaNode instanceof ContainerLike) {
342             return getQNameOfDataSchemaNode((ContainerLike) dataSchemaNode, nodeName);
343         } else if (dataSchemaNode instanceof ListSchemaNode) {
344             return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
345         }
346
347         throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
348     }
349
350     private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
351             final T parent, final String nodeName) {
352         final Optional<? extends ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
353         final SchemaNode node;
354         if (actionDef.isPresent()) {
355             node = actionDef.get();
356         } else {
357             node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
358         }
359         return node.getQName();
360     }
361
362     private static Optional<? extends ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
363             final String nodeName) {
364         requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
365         if (dataSchemaNode instanceof ActionNodeContainer) {
366             return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
367                     .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
368         }
369         return Optional.empty();
370     }
371
372     private static String findAndParsePercentEncoded(final String preparedPrefix) {
373         if (preparedPrefix.indexOf('%') == -1) {
374             return preparedPrefix;
375         }
376
377         // FIXME: this is extremely inefficient: we should be converting ranges of characters, not driven by
378         //        CharMatcher, but by String.indexOf()
379         final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
380         while (PERCENT_ENCODING.matchesAnyOf(parsedPrefix)) {
381             final int percentCharPosition = PERCENT_ENCODING.indexIn(parsedPrefix);
382             parsedPrefix.replace(percentCharPosition, percentCharPosition + LAST_ENCODED_CHAR,
383                     String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
384                             percentCharPosition + FIRST_ENCODED_CHAR, percentCharPosition + LAST_ENCODED_CHAR),
385                             PERCENT_ENCODED_RADIX)));
386         }
387
388         return parsedPrefix.toString();
389     }
390
391     private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
392         final String moduleName = toModuleName(value);
393         final String nodeName = toNodeName(value);
394         final Iterator<? extends Module> modulesIterator = schemaContext.findModules(moduleName).iterator();
395         if (!modulesIterator.hasNext()) {
396             throw new RestconfDocumentedException(String.format("Cannot decode value '%s' for identityref type "
397                     + "in %s. Make sure reserved characters such as comma, single-quote, double-quote, colon,"
398                     + " double-quote, space, and forward slash (,'\":\" /) are percent-encoded,"
399                     + " for example ':' is '%%3A'", value, current.getIdentifier().getNodeType()),
400                     ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
401         }
402         for (final IdentitySchemaNode identitySchemaNode : modulesIterator.next().getIdentities()) {
403             final QName qName = identitySchemaNode.getQName();
404             if (qName.getLocalName().equals(nodeName)) {
405                 return qName;
406             }
407         }
408         return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
409     }
410
411     private static String toNodeName(final String str) {
412         final int idx = str.indexOf(':');
413         if (idx == -1) {
414             return str;
415         }
416
417         if (str.indexOf(':', idx + 1) != -1) {
418             return str;
419         }
420
421         return str.substring(idx + 1);
422     }
423
424     private static String toModuleName(final String str) {
425         final int idx = str.indexOf(':');
426         if (idx == -1) {
427             return null;
428         }
429
430         if (str.indexOf(':', idx + 1) != -1) {
431             return null;
432         }
433
434         return str.substring(0, idx);
435     }
436 }