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