Teach RFC8040 URL parser about actions
[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.base.Preconditions;
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.Iterator;
18 import java.util.LinkedList;
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.concepts.Codec;
30 import org.opendaylight.yangtools.yang.common.QName;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
34 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
35 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
36 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
37 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
38 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
40 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.Module;
46 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
47 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
48 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
52 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
53
54 /**
55  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
56  * restconf.
57  *
58  */
59 public final class YangInstanceIdentifierDeserializer {
60
61     private YangInstanceIdentifierDeserializer() {
62         throw new UnsupportedOperationException("Util class.");
63     }
64
65     /**
66      * Method to create {@link Iterable} from {@link PathArgument} which are
67      * parsing from data by {@link SchemaContext}.
68      *
69      * @param schemaContext
70      *             for validate of parsing path arguments
71      * @param data
72      *             path to data
73      * @return {@link Iterable} of {@link PathArgument}
74      */
75     public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
76         final List<PathArgument> path = new LinkedList<>();
77         final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
78                 data, DataSchemaContextTree.from(schemaContext).getRoot(),
79                 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
80
81         while (!allCharsConsumed(variables)) {
82             validArg(variables);
83             final QName qname = prepareQName(variables);
84
85             // this is the last identifier (input is consumed) or end of identifier (slash)
86             if (allCharsConsumed(variables)
87                     || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
88                 prepareIdentifier(qname, path, variables);
89                 if (variables.getCurrent() == null) {
90                     path.add(NodeIdentifier.create(qname));
91                 } else {
92                     path.add(variables.getCurrent().getIdentifier());
93                 }
94             } else if (currentChar(variables.getOffset(),
95                     variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
96                 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
97                     prepareNodeWithPredicates(qname, path, variables,
98                             (ListSchemaNode) variables.getCurrent().getDataSchemaNode());
99                 } else {
100                     prepareNodeWithValue(qname, path, variables);
101                 }
102             } else {
103                 throw new IllegalArgumentException(
104                         "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
105                                 + variables.getOffset() + ".");
106             }
107         }
108
109         return ImmutableList.copyOf(path);
110     }
111
112     private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
113             final MainVarsWrapper variables, final ListSchemaNode listSchemaNode) {
114         checkValid(listSchemaNode != null, "Data schema node is null", variables.getData(), variables.getOffset());
115
116         final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
117         final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
118
119         // skip already expected equal sign
120         skipCurrentChar(variables);
121
122         // read key value separated by comma
123         while (keys.hasNext() && !allCharsConsumed(variables) && currentChar(variables.getOffset(),
124                 variables.getData()) != RestconfConstants.SLASH) {
125
126             // empty key value
127             if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
128                 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
129                 skipCurrentChar(variables);
130                 continue;
131             }
132
133             // check if next value is parsable
134             RestconfValidationUtils.checkDocumentedError(
135                     ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
136                             .matches(currentChar(variables.getOffset(), variables.getData())),
137                     RestconfError.ErrorType.PROTOCOL,
138                     RestconfError.ErrorTag.MALFORMED_MESSAGE,
139                     ""
140             );
141
142             // parse value
143             final QName key = keys.next();
144             Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
145             if (!leafSchemaNode.isPresent()) {
146                 throw new RestconfDocumentedException("Schema not found for " + key,
147                         RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
148             }
149
150             final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
151                     ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables));
152             final Object valueByType = prepareValueByType(leafSchemaNode.get(), value, variables);
153             values.put(key, valueByType);
154
155
156             // skip comma
157             if (keys.hasNext() && !allCharsConsumed(variables) && currentChar(
158                     variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
159                 skipCurrentChar(variables);
160             }
161         }
162
163         // the last key is considered to be empty
164         if (keys.hasNext()) {
165             if (allCharsConsumed(variables)
166                     || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
167                 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
168             }
169
170             // there should be no more missing keys
171             RestconfValidationUtils.checkDocumentedError(
172                     !keys.hasNext(),
173                     RestconfError.ErrorType.PROTOCOL,
174                     RestconfError.ErrorTag.MISSING_ATTRIBUTE,
175                     "Key value missing for: " + qname
176             );
177         }
178
179         path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
180     }
181
182     private static Object prepareValueByType(final DataSchemaNode schemaNode, final String value,
183             final MainVarsWrapper vars) {
184         Object decoded = null;
185
186         TypeDefinition<? extends TypeDefinition<?>> typedef = null;
187         if (schemaNode instanceof LeafListSchemaNode) {
188             typedef = ((LeafListSchemaNode) schemaNode).getType();
189         } else {
190             typedef = ((LeafSchemaNode) schemaNode).getType();
191         }
192         final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
193         if (baseType instanceof LeafrefTypeDefinition) {
194             typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, vars.getSchemaContext(),
195                     schemaNode);
196         }
197         final Codec<Object, Object> codec = RestCodec.from(typedef, null, vars.getSchemaContext());
198         decoded = codec.deserialize(value);
199         if (decoded == null) {
200             if (baseType instanceof IdentityrefTypeDefinition) {
201                 decoded = toQName(value, schemaNode, vars.getSchemaContext());
202             }
203         }
204         return decoded;
205     }
206
207     private static Object toQName(final String value, final DataSchemaNode schemaNode,
208             final SchemaContext schemaContext) {
209         final String moduleName = toModuleName(value);
210         final String nodeName = toNodeName(value);
211         final Module module = schemaContext.findModules(moduleName).iterator().next();
212         for (final IdentitySchemaNode identitySchemaNode : module.getIdentities()) {
213             final QName qName = identitySchemaNode.getQName();
214             if (qName.getLocalName().equals(nodeName)) {
215                 return qName;
216             }
217         }
218         return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
219     }
220
221     private static String toNodeName(final String str) {
222         final int idx = str.indexOf(':');
223         if (idx == -1) {
224             return str;
225         }
226
227         if (str.indexOf(':', idx + 1) != -1) {
228             return str;
229         }
230
231         return str.substring(idx + 1);
232     }
233
234     private static String toModuleName(final String str) {
235         final int idx = str.indexOf(':');
236         if (idx == -1) {
237             return null;
238         }
239
240         if (str.indexOf(':', idx + 1) != -1) {
241             return null;
242         }
243
244         return str.substring(0, idx);
245     }
246
247     private static QName prepareQName(final MainVarsWrapper variables) {
248         checkValid(
249                 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
250                         .matches(currentChar(variables.getOffset(), variables.getData())),
251                 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
252         final String preparedPrefix = nextIdentifierFromNextSequence(
253                 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
254         final String prefix;
255         final String localName;
256
257         if (allCharsConsumed(variables)) {
258             return getQNameOfDataSchemaNode(preparedPrefix, variables);
259         }
260
261         switch (currentChar(variables.getOffset(), variables.getData())) {
262             case RestconfConstants.SLASH:
263             case ParserBuilderConstants.Deserializer.EQUAL:
264                 prefix = preparedPrefix;
265                 return getQNameOfDataSchemaNode(prefix, variables);
266             case ParserBuilderConstants.Deserializer.COLON:
267                 prefix = preparedPrefix;
268                 skipCurrentChar(variables);
269                 checkValid(
270                         ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
271                                 .matches(currentChar(variables.getOffset(), variables.getData())),
272                         "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
273                         variables.getOffset());
274                 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
275
276                 if (!allCharsConsumed(variables) && currentChar(
277                         variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
278                     return getQNameOfDataSchemaNode(localName, variables);
279                 } else {
280                     final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
281                     Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
282                     return QName.create(module.getQNameModule(), localName);
283                 }
284             default:
285                 throw new IllegalArgumentException("Failed build path.");
286         }
287     }
288
289     private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
290         final int start = variables.getOffset();
291         nextSequenceEnd(matcher, variables);
292         return variables.getData().substring(start, variables.getOffset());
293     }
294
295     private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
296         while (!allCharsConsumed(variables)
297                 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
298             variables.setOffset(variables.getOffset() + 1);
299         }
300     }
301
302     private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
303             final MainVarsWrapper variables) {
304         skipCurrentChar(variables);
305         final String value = nextIdentifierFromNextSequence(
306                 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
307
308         // exception if value attribute is missing
309         RestconfValidationUtils.checkDocumentedError(
310                 !value.isEmpty(),
311                 RestconfError.ErrorType.PROTOCOL,
312                 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
313                 "Value missing for: " + qname
314         );
315         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
316         final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
317         path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
318     }
319
320     private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
321             final MainVarsWrapper variables) {
322         final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
323         if (currentNode == null) {
324             return;
325         }
326         checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
327                 variables.getData(), variables.getOffset());
328     }
329
330     @SuppressFBWarnings("NP_NULL_ON_SOME_PATH") // code does check for null 'current' but FB doesn't recognize it
331     private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
332             final MainVarsWrapper variables) {
333         final DataSchemaContextNode<?> initialContext = variables.getCurrent();
334         final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
335
336         DataSchemaContextNode<?> current = initialContext.getChild(qname);
337         variables.setCurrent(current);
338
339         if (current == null) {
340             final Optional<Module> module = variables.getSchemaContext().findModule(qname.getModule());
341             if (module.isPresent()) {
342                 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
343                     if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
344                         return null;
345                     }
346                 }
347             }
348             if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
349                 return null;
350             }
351         }
352         checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
353                 variables.getOffset());
354         while (current.isMixin()) {
355             path.add(current.getIdentifier());
356             current = current.getChild(qname);
357             variables.setCurrent(current);
358         }
359         return current;
360     }
361
362     private static String findAndParsePercentEncoded(final String preparedPrefix) {
363         if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
364             return preparedPrefix;
365         }
366
367         final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
368         final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
369
370         while (matcher.matchesAnyOf(parsedPrefix)) {
371             final int percentCharPosition = matcher.indexIn(parsedPrefix);
372             parsedPrefix.replace(
373                     percentCharPosition,
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)));
379         }
380
381         return parsedPrefix.toString();
382     }
383
384     private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
385         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
386         if (dataSchemaNode instanceof ContainerSchemaNode) {
387             return getQNameOfDataSchemaNode((ContainerSchemaNode) dataSchemaNode, nodeName);
388         } else if (dataSchemaNode instanceof ListSchemaNode) {
389             return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
390         }
391
392         throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
393     }
394
395     private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
396             final T parent, String nodeName) {
397         final Optional<ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
398         final SchemaNode node;
399         if (actionDef.isPresent()) {
400             node = actionDef.get();
401         } else {
402             node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
403         }
404         return node.getQName();
405     }
406
407     private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
408         return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
409     }
410
411     private static void validArg(final MainVarsWrapper variables) {
412         // every identifier except of the first MUST start with slash
413         if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
414             checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
415                     "Identifier must start with '/'.", variables.getData(), variables.getOffset());
416
417             // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
418             // multiple slashes into a single one
419             while (!allCharsConsumed(variables)
420                     && RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData())) {
421                 skipCurrentChar(variables);
422             }
423
424             // check if slash is not also the last char in identifier
425             checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
426                     variables.getData(), variables.getOffset());
427         }
428     }
429
430     private static void skipCurrentChar(final MainVarsWrapper variables) {
431         variables.setOffset(variables.getOffset() + 1);
432     }
433
434     private static char currentChar(final int offset, final String data) {
435         return data.charAt(offset);
436     }
437
438     private static void checkValid(final boolean condition, final String errorMsg, final String data,
439             final int offset) {
440         Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
441                 data, offset, errorMsg);
442     }
443
444     private static boolean allCharsConsumed(final MainVarsWrapper variables) {
445         return variables.getOffset() == variables.getData().length();
446     }
447
448     private static Optional<ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
449             final String nodeName) {
450         requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
451         if (dataSchemaNode instanceof ActionNodeContainer) {
452             return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
453                     .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
454         }
455         return Optional.empty();
456     }
457
458     private static final class MainVarsWrapper {
459         private static final int STARTING_OFFSET = 0;
460
461         private final SchemaContext schemaContext;
462         private final String data;
463
464         private DataSchemaContextNode<?> current;
465         private int offset;
466
467         MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
468                 final SchemaContext schemaContext) {
469             this.data = data;
470             this.current = current;
471             this.offset = offset;
472             this.schemaContext = schemaContext;
473         }
474
475         public String getData() {
476             return this.data;
477         }
478
479         public DataSchemaContextNode<?> getCurrent() {
480             return this.current;
481         }
482
483         public void setCurrent(final DataSchemaContextNode<?> current) {
484             this.current = current;
485         }
486
487         public int getOffset() {
488             return this.offset;
489         }
490
491         public void setOffset(final int offset) {
492             this.offset = offset;
493         }
494
495         public SchemaContext getSchemaContext() {
496             return this.schemaContext;
497         }
498     }
499 }