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