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