Merge "Migrate restconf to MD-SAL APIs"
[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 com.google.common.base.CharMatcher;
11 import com.google.common.base.Preconditions;
12 import com.google.common.collect.ImmutableList;
13 import com.google.common.collect.ImmutableMap;
14 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
15 import java.util.Iterator;
16 import java.util.LinkedList;
17 import java.util.List;
18 import java.util.Optional;
19 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
20 import org.opendaylight.restconf.common.errors.RestconfError;
21 import org.opendaylight.restconf.common.util.RestUtil;
22 import org.opendaylight.restconf.common.util.RestconfSchemaUtil;
23 import org.opendaylight.restconf.common.validation.RestconfValidationUtils;
24 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
25 import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
26 import org.opendaylight.restconf.nb.rfc8040.utils.parser.builder.ParserBuilderConstants;
27 import org.opendaylight.yangtools.concepts.Codec;
28 import org.opendaylight.yangtools.yang.common.QName;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
33 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
34 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.Module;
41 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
42 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
43 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
44 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
45 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
46 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
47
48 /**
49  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
50  * restconf.
51  *
52  */
53 public final class YangInstanceIdentifierDeserializer {
54
55     private YangInstanceIdentifierDeserializer() {
56         throw new UnsupportedOperationException("Util class.");
57     }
58
59     /**
60      * Method to create {@link Iterable} from {@link PathArgument} which are
61      * parsing from data by {@link SchemaContext}.
62      *
63      * @param schemaContext
64      *             for validate of parsing path arguments
65      * @param data
66      *             path to data
67      * @return {@link Iterable} of {@link PathArgument}
68      */
69     public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
70         final List<PathArgument> path = new LinkedList<>();
71         final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
72                 data, DataSchemaContextTree.from(schemaContext).getRoot(),
73                 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
74
75         while (!allCharsConsumed(variables)) {
76             validArg(variables);
77             final QName qname = prepareQName(variables);
78
79             // this is the last identifier (input is consumed) or end of identifier (slash)
80             if (allCharsConsumed(variables)
81                     || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
82                 prepareIdentifier(qname, path, variables);
83                 if (variables.getCurrent() == null) {
84                     path.add(NodeIdentifier.create(qname));
85                 } else {
86                     path.add(variables.getCurrent().getIdentifier());
87                 }
88             } else if (currentChar(variables.getOffset(),
89                     variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
90                 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
91                     prepareNodeWithPredicates(qname, path, variables,
92                             (ListSchemaNode) variables.getCurrent().getDataSchemaNode());
93                 } else {
94                     prepareNodeWithValue(qname, path, variables);
95                 }
96             } else {
97                 throw new IllegalArgumentException(
98                         "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
99                                 + variables.getOffset() + ".");
100             }
101         }
102
103         return ImmutableList.copyOf(path);
104     }
105
106     private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
107             final MainVarsWrapper variables, final ListSchemaNode listSchemaNode) {
108         checkValid(listSchemaNode != null, "Data schema node is null", variables.getData(), variables.getOffset());
109
110         final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
111         final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
112
113         // skip already expected equal sign
114         skipCurrentChar(variables);
115
116         // read key value separated by comma
117         while (keys.hasNext() && !allCharsConsumed(variables) && currentChar(variables.getOffset(),
118                 variables.getData()) != RestconfConstants.SLASH) {
119
120             // empty key value
121             if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
122                 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
123                 skipCurrentChar(variables);
124                 continue;
125             }
126
127             // check if next value is parsable
128             RestconfValidationUtils.checkDocumentedError(
129                     ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
130                             .matches(currentChar(variables.getOffset(), variables.getData())),
131                     RestconfError.ErrorType.PROTOCOL,
132                     RestconfError.ErrorTag.MALFORMED_MESSAGE,
133                     ""
134             );
135
136             // parse value
137             final QName key = keys.next();
138             Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
139             if (!leafSchemaNode.isPresent()) {
140                 throw new RestconfDocumentedException("Schema not found for " + key,
141                         RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
142             }
143
144             final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(
145                     ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables));
146             final Object valueByType = prepareValueByType(leafSchemaNode.get(), value, variables);
147             values.put(key, valueByType);
148
149
150             // skip comma
151             if (keys.hasNext() && !allCharsConsumed(variables) && currentChar(
152                     variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
153                 skipCurrentChar(variables);
154             }
155         }
156
157         // the last key is considered to be empty
158         if (keys.hasNext()) {
159             if (allCharsConsumed(variables)
160                     || currentChar(variables.getOffset(), variables.getData()) == 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
244                         .matches(currentChar(variables.getOffset(), variables.getData())),
245                 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
246         final String preparedPrefix = nextIdentifierFromNextSequence(
247                 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
248         final String prefix;
249         final String localName;
250
251         if (allCharsConsumed(variables)) {
252             return getQNameOfDataSchemaNode(preparedPrefix, variables);
253         }
254
255         switch (currentChar(variables.getOffset(), variables.getData())) {
256             case RestconfConstants.SLASH:
257             case ParserBuilderConstants.Deserializer.EQUAL:
258                 prefix = preparedPrefix;
259                 return getQNameOfDataSchemaNode(prefix, variables);
260             case ParserBuilderConstants.Deserializer.COLON:
261                 prefix = preparedPrefix;
262                 skipCurrentChar(variables);
263                 checkValid(
264                         ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
265                                 .matches(currentChar(variables.getOffset(), variables.getData())),
266                         "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
267                         variables.getOffset());
268                 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
269
270                 if (!allCharsConsumed(variables) && currentChar(
271                         variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
272                     return getQNameOfDataSchemaNode(localName, variables);
273                 } else {
274                     final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
275                     Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
276                     return QName.create(module.getQNameModule(), localName);
277                 }
278             default:
279                 throw new IllegalArgumentException("Failed build path.");
280         }
281     }
282
283     private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
284         final int start = variables.getOffset();
285         nextSequenceEnd(matcher, variables);
286         return variables.getData().substring(start, variables.getOffset());
287     }
288
289     private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
290         while (!allCharsConsumed(variables)
291                 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
292             variables.setOffset(variables.getOffset() + 1);
293         }
294     }
295
296     private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
297             final MainVarsWrapper variables) {
298         skipCurrentChar(variables);
299         final String value = nextIdentifierFromNextSequence(
300                 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
301
302         // exception if value attribute is missing
303         RestconfValidationUtils.checkDocumentedError(
304                 !value.isEmpty(),
305                 RestconfError.ErrorType.PROTOCOL,
306                 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
307                 "Value missing for: " + qname
308         );
309         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
310         final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
311         path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
312     }
313
314     private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
315             final MainVarsWrapper variables) {
316         final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
317         if (currentNode == null) {
318             return;
319         }
320         checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
321                 variables.getData(), variables.getOffset());
322     }
323
324     @SuppressFBWarnings("NP_NULL_ON_SOME_PATH") // code does check for null 'current' but FB doesn't recognize it
325     private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
326             final MainVarsWrapper variables) {
327         variables.setCurrent(variables.getCurrent().getChild(qname));
328         DataSchemaContextNode<?> current = variables.getCurrent();
329         if (current == null) {
330             final Optional<Module> module = variables.getSchemaContext().findModule(qname.getModule());
331             if (module.isPresent()) {
332                 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
333                     if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
334                         return null;
335                     }
336                 }
337             }
338         }
339         checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
340                 variables.getOffset());
341         while (current.isMixin()) {
342             path.add(current.getIdentifier());
343             current = current.getChild(qname);
344             variables.setCurrent(current);
345         }
346         return current;
347     }
348
349     private static String findAndParsePercentEncoded(final String preparedPrefix) {
350         if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
351             return preparedPrefix;
352         }
353
354         final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
355         final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
356
357         while (matcher.matchesAnyOf(parsedPrefix)) {
358             final int percentCharPosition = matcher.indexIn(parsedPrefix);
359             parsedPrefix.replace(
360                     percentCharPosition,
361                     percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
362                     String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
363                             percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
364                             percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
365                             ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
366         }
367
368         return parsedPrefix.toString();
369     }
370
371     private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
372         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
373         if (dataSchemaNode instanceof ContainerSchemaNode) {
374             final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
375             final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
376                     nodeName);
377             return node.getQName();
378         } else if (dataSchemaNode instanceof ListSchemaNode) {
379             final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
380             final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
381                     nodeName);
382             return node.getQName();
383         }
384         throw new UnsupportedOperationException();
385     }
386
387     private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
388         return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
389     }
390
391     private static void validArg(final MainVarsWrapper variables) {
392         // every identifier except of the first MUST start with slash
393         if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
394             checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
395                     "Identifier must start with '/'.", variables.getData(), variables.getOffset());
396
397             // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
398             // multiple slashes into a single one
399             while (!allCharsConsumed(variables)
400                     && RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData())) {
401                 skipCurrentChar(variables);
402             }
403
404             // check if slash is not also the last char in identifier
405             checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
406                     variables.getData(), variables.getOffset());
407         }
408     }
409
410     private static void skipCurrentChar(final MainVarsWrapper variables) {
411         variables.setOffset(variables.getOffset() + 1);
412     }
413
414     private static char currentChar(final int offset, final String data) {
415         return data.charAt(offset);
416     }
417
418     private static void checkValid(final boolean condition, final String errorMsg, final String data,
419             final int offset) {
420         Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
421                 data, offset, errorMsg);
422     }
423
424     private static boolean allCharsConsumed(final MainVarsWrapper variables) {
425         return variables.getOffset() == variables.getData().length();
426     }
427
428     private static final class MainVarsWrapper {
429         private static final int STARTING_OFFSET = 0;
430
431         private final SchemaContext schemaContext;
432         private final String data;
433
434         private DataSchemaContextNode<?> current;
435         private int offset;
436
437         MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
438                 final SchemaContext schemaContext) {
439             this.data = data;
440             this.current = current;
441             this.offset = offset;
442             this.schemaContext = schemaContext;
443         }
444
445         public String getData() {
446             return this.data;
447         }
448
449         public DataSchemaContextNode<?> getCurrent() {
450             return this.current;
451         }
452
453         public void setCurrent(final DataSchemaContextNode<?> current) {
454             this.current = current;
455         }
456
457         public int getOffset() {
458             return this.offset;
459         }
460
461         public void setOffset(final int offset) {
462             this.offset = offset;
463         }
464
465         public SchemaContext getSchemaContext() {
466             return this.schemaContext;
467         }
468     }
469 }