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