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