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