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