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