Bug 6291 - Fix bugs of new Restconf 11 implementation
[netconf.git] / restconf / sal-rest-connector / src / main / java / org / opendaylight / restconf / parser / builder / 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.parser.builder;
9
10 import com.google.common.base.CharMatcher;
11 import com.google.common.base.Preconditions;
12 import com.google.common.collect.ImmutableList;
13 import com.google.common.collect.ImmutableMap;
14 import java.util.Iterator;
15 import java.util.LinkedList;
16 import java.util.List;
17 import org.opendaylight.netconf.md.sal.rest.common.RestconfValidationUtils;
18 import org.opendaylight.netconf.sal.restconf.impl.RestconfError;
19 import org.opendaylight.restconf.utils.RestconfConstants;
20 import org.opendaylight.restconf.utils.parser.builder.ParserBuilderConstants;
21 import org.opendaylight.restconf.utils.schema.context.RestconfSchemaUtil;
22 import org.opendaylight.yangtools.yang.common.QName;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
25 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
26 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
27 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
29 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.Module;
31 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
32
33 /**
34  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
35  * restconf.
36  *
37  */
38 public final class YangInstanceIdentifierDeserializer {
39
40     private YangInstanceIdentifierDeserializer() {
41         throw new UnsupportedOperationException("Util class.");
42     }
43
44     /**
45      * Method to create {@link Iterable} from {@link PathArgument} which are
46      * parsing from data by {@link SchemaContext}.
47      *
48      * @param schemaContext
49      *            - for validate of parsing path arguments
50      * @param data
51      *            - path to data
52      * @return {@link Iterable} of {@link PathArgument}
53      */
54     public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
55         final List<PathArgument> path = new LinkedList<>();
56         final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
57                 data, DataSchemaContextTree.from(schemaContext).getRoot(),
58                 YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
59
60         while (!allCharsConsumed(variables)) {
61             validArg(variables);
62             final QName qname = prepareQName(variables);
63
64             // this is the last identifier (input is consumed) or end of identifier (slash)
65             if (allCharsConsumed(variables)
66                     || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
67                 prepareIdentifier(qname, path, variables);
68                 path.add(variables.getCurrent().getIdentifier());
69             } else if (currentChar(variables.getOffset(),
70                     variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
71                 if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
72                     prepareNodeWithPredicates(qname, path, variables);
73                 } else {
74                     prepareNodeWithValue(qname, path, variables);
75                 }
76             } else {
77                 throw new IllegalArgumentException(
78                         "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
79                                 + variables.getOffset() + ".");
80             }
81         }
82
83         return ImmutableList.copyOf(path);
84     }
85
86     private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
87                                                   final MainVarsWrapper variables) {
88
89         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
90         checkValid((dataSchemaNode != null), "Data schema node is null", variables.getData(), variables.getOffset());
91
92         final Iterator<QName> keys = ((ListSchemaNode) dataSchemaNode).getKeyDefinition().iterator();
93         final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
94
95         // skip already expected equal sign
96         skipCurrentChar(variables);
97
98         // read key value separated by comma
99         while (keys.hasNext() && !allCharsConsumed(variables) && currentChar(variables.getOffset(),
100                 variables.getData()) != RestconfConstants.SLASH) {
101
102             // empty key value
103             if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
104                 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
105                 skipCurrentChar(variables);
106                 continue;
107             }
108
109             // check if next value is parsable
110             RestconfValidationUtils.checkDocumentedError(
111                     ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE
112                             .matches(currentChar(variables.getOffset(), variables.getData())),
113                     RestconfError.ErrorType.PROTOCOL,
114                     RestconfError.ErrorTag.MALFORMED_MESSAGE,
115                     ""
116             );
117
118             // parse value
119             values.put(keys.next(), findAndParsePercentEncoded(nextIdentifierFromNextSequence(
120                     ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables)));
121
122             // skip comma
123             if (keys.hasNext() && !allCharsConsumed(variables) && currentChar(
124                     variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
125                 skipCurrentChar(variables);
126             }
127         }
128
129         // the last key is considered to be empty
130         if (keys.hasNext()) {
131             if (allCharsConsumed(variables)
132                     || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
133                 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
134             }
135
136             // there should be no more missing keys
137             RestconfValidationUtils.checkDocumentedError(
138                     !keys.hasNext(),
139                     RestconfError.ErrorType.PROTOCOL,
140                     RestconfError.ErrorTag.MISSING_ATTRIBUTE,
141                     "Key value missing for: " + qname
142             );
143         }
144
145         path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
146     }
147
148
149     private static QName prepareQName(final MainVarsWrapper variables) {
150         checkValid(
151                 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
152                         .matches(currentChar(variables.getOffset(), variables.getData())),
153                 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
154         final String preparedPrefix = nextIdentifierFromNextSequence(
155                 ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
156         final String prefix, localName;
157
158         if (allCharsConsumed(variables)) {
159             return getQNameOfDataSchemaNode(preparedPrefix, variables);
160         }
161
162         switch (currentChar(variables.getOffset(), variables.getData())) {
163             case RestconfConstants.SLASH:
164                 prefix = preparedPrefix;
165                 return getQNameOfDataSchemaNode(prefix, variables);
166             case ParserBuilderConstants.Deserializer.COLON:
167                 prefix = preparedPrefix;
168                 skipCurrentChar(variables);
169                 checkValid(
170                         ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
171                                 .matches(currentChar(variables.getOffset(), variables.getData())),
172                         "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
173                         variables.getOffset());
174                 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
175
176                 if (!allCharsConsumed(variables) && currentChar
177                         (variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
178                     return getQNameOfDataSchemaNode(localName, variables);
179                 } else {
180                     final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
181                     Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
182                     return QName.create(module.getQNameModule(), localName);
183                 }
184             case ParserBuilderConstants.Deserializer.EQUAL:
185                 prefix = preparedPrefix;
186                 return getQNameOfDataSchemaNode(prefix, variables);
187             default:
188                 throw new IllegalArgumentException("Failed build path.");
189         }
190     }
191
192     private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
193         final int start = variables.getOffset();
194         nextSequenceEnd(matcher, variables);
195         return variables.getData().substring(start, variables.getOffset());
196     }
197
198     private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
199         while (!allCharsConsumed(variables)
200                 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
201             variables.setOffset(variables.getOffset() + 1);
202         }
203     }
204
205     private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
206             final MainVarsWrapper variables) {
207         skipCurrentChar(variables);
208         final String value = nextIdentifierFromNextSequence(
209                 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables);
210
211         // exception if value attribute is missing
212         RestconfValidationUtils.checkDocumentedError(
213                 !value.isEmpty(),
214                 RestconfError.ErrorType.PROTOCOL,
215                 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
216                 "Value missing for: " + qname
217         );
218
219         path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, findAndParsePercentEncoded(value)));
220     }
221
222     private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
223             final MainVarsWrapper variables) {
224         final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
225         checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
226                 variables.getData(), variables.getOffset());
227     }
228
229     private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
230             final MainVarsWrapper variables) {
231         variables.setCurrent(variables.getCurrent().getChild(qname));
232         DataSchemaContextNode<?> current = variables.getCurrent();
233         checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
234                 variables.getOffset());
235         while (current.isMixin()) {
236             path.add(current.getIdentifier());
237             current = current.getChild(qname);
238             variables.setCurrent(current);
239         }
240         return current;
241     }
242
243     private static String findAndParsePercentEncoded(final String preparedPrefix) {
244         if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
245             return preparedPrefix;
246         }
247
248         final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
249         final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
250
251         while (matcher.matchesAnyOf(parsedPrefix)) {
252             final int percentCharPosition = matcher.indexIn(parsedPrefix);
253             parsedPrefix.replace(
254                     percentCharPosition,
255                     percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
256                     String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
257                             percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
258                             percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
259                             ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
260         }
261
262         return parsedPrefix.toString();
263     }
264
265     private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
266         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
267         if (dataSchemaNode instanceof ContainerSchemaNode) {
268             final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
269             final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
270                     nodeName);
271             return node.getQName();
272         } else if (dataSchemaNode instanceof ListSchemaNode) {
273             final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
274             final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
275                     nodeName);
276             return node.getQName();
277         }
278         throw new UnsupportedOperationException();
279     }
280
281     private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
282         return schemaContext.findModuleByName(prefix, null);
283     }
284
285     private static void validArg(final MainVarsWrapper variables) {
286         // every identifier except of the first MUST start with slash
287         if (variables.getOffset() != MainVarsWrapper.STARTING_OFFSET) {
288             checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
289                     "Identifier must start with '/'.", variables.getData(), variables.getOffset());
290
291             // skip slash
292             skipCurrentChar(variables);
293
294             // check if slash is not also the last char in identifier
295             checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
296                     variables.getData(), variables.getOffset());
297         }
298     }
299
300     private static void skipCurrentChar(final MainVarsWrapper variables) {
301         variables.setOffset(variables.getOffset() + 1);
302     }
303
304     private static char currentChar(final int offset, final String data) {
305         return data.charAt(offset);
306     }
307
308     private static void checkValid(final boolean condition, final String errorMsg, final String data,
309             final int offset) {
310         Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
311                 data, offset, errorMsg);
312     }
313
314     private static boolean allCharsConsumed(final MainVarsWrapper variables) {
315         return variables.getOffset() == variables.getData().length();
316     }
317
318     private final static class MainVarsWrapper {
319         private static final int STARTING_OFFSET = 0;
320
321         private final SchemaContext schemaContext;
322         private final String data;
323         private DataSchemaContextNode<?> current;
324         private int offset;
325
326         public MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
327                 final SchemaContext schemaContext) {
328             this.data = data;
329             this.setCurrent(current);
330             this.setOffset(offset);
331             this.schemaContext = schemaContext;
332         }
333
334         public String getData() {
335             return this.data;
336         }
337
338         public DataSchemaContextNode<?> getCurrent() {
339             return this.current;
340         }
341
342         public void setCurrent(final DataSchemaContextNode<?> current) {
343             this.current = current;
344         }
345
346         public int getOffset() {
347             return this.offset;
348         }
349
350         public void setOffset(final int offset) {
351             this.offset = offset;
352         }
353
354         public SchemaContext getSchemaContext() {
355             return this.schemaContext;
356         }
357     }
358 }