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