Bug 5912 - Restconf draft11 - utils
[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.LinkedList;
15 import java.util.List;
16 import org.opendaylight.restconf.utils.RestconfConstants;
17 import org.opendaylight.restconf.utils.parser.builder.ParserBuilderConstants;
18 import org.opendaylight.restconf.utils.schema.context.RestconfSchemaUtil;
19 import org.opendaylight.yangtools.yang.common.QName;
20 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
22 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
23 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
24 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
25 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
26 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
27 import org.opendaylight.yangtools.yang.model.api.Module;
28 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
29
30 /**
31  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
32  * restconf.
33  *
34  */
35 public final class YangInstanceIdentifierDeserializer {
36
37     private YangInstanceIdentifierDeserializer() {
38         throw new UnsupportedOperationException("Util class.");
39     }
40
41     /**
42      * Method to create {@link Iterable} from {@link PathArgument} which are
43      * parsing from data by {@link SchemaContext}.
44      *
45      * @param schemaContext
46      *            - for validate of parsing path arguments
47      * @param data
48      *            - path to data
49      * @return {@link Iterable} of {@link PathArgument}
50      */
51     public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
52         final List<PathArgument> path = new LinkedList<>();
53         DataSchemaContextNode<?> current = DataSchemaContextTree.from(schemaContext).getRoot();
54         final int offset = 0;
55         final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(data,
56                 current, offset, schemaContext);
57
58         while (!allCharsConsumed(variables)) {
59             validArg(variables);
60             final QName qname = prepareQName(variables);
61
62             // this is the last identifier (input is consumed) or end of identifier (slash)
63             if (allCharsConsumed(variables)
64                     || currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH) {
65                 prepareIdentifier(qname, path, variables);
66                 path.add(variables.getCurrent().getIdentifier());
67             } else if (currentChar(variables.getOffset(),
68                     variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
69                 current = nextContextNode(qname, path, variables);
70                 if (!current.isKeyedEntry()) {
71                     prepareNodeWithValue(qname, path, variables);
72                 } else {
73                     prepareNodeWithPredicates(qname, path, variables);
74                 }
75             } else {
76                 throw new IllegalArgumentException(
77                         "Bad char " + currentChar(offset, data) + " on position " + offset + ".");
78             }
79         }
80
81         return ImmutableList.copyOf(path);
82     }
83
84     private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
85             final MainVarsWrapper variables) {
86         final List<QName> keyDefinitions = ((ListSchemaNode) variables.getCurrent().getDataSchemaNode())
87                 .getKeyDefinition();
88         final ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
89
90         for (final QName keyQName : keyDefinitions) {
91             skipCurrentChar(variables);
92             String value = null;
93             if ((currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA)
94                     || (currentChar(variables.getOffset(), variables.getData()) == RestconfConstants.SLASH)) {
95                 value = ParserBuilderConstants.Deserializer.EMPTY_STRING;
96             } else {
97                 value = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE,
98                         variables);
99             }
100             value = findAndParsePercentEncoded(value);
101             keyValues.put(keyQName, value);
102         }
103         path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, keyValues.build()));
104     }
105
106     private static QName prepareQName(final MainVarsWrapper variables) {
107         checkValid(
108                 ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
109                         .matches(currentChar(variables.getOffset(), variables.getData())),
110                 "Identifier must start with character from set 'a-zA-Z_'", variables.getData(), variables.getOffset());
111         final String preparedPrefix = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
112         final String prefix, localName;
113
114         switch (currentChar(variables.getOffset(), variables.getData())) {
115             case ParserBuilderConstants.Deserializer.COLON:
116                 prefix = preparedPrefix;
117                 skipCurrentChar(variables);
118                 checkValid(
119                         ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR
120                                 .matches(currentChar(variables.getOffset(), variables.getData())),
121                         "Identifier must start with character from set 'a-zA-Z_'", variables.getData(),
122                         variables.getOffset());
123                 localName = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
124
125                 final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
126                 Preconditions.checkArgument(module != null, "Failed to lookup prefix %s", prefix);
127
128                 return QName.create(module.getQNameModule(), localName);
129             case ParserBuilderConstants.Deserializer.EQUAL:
130                 prefix = preparedPrefix;
131                 return getQNameOfDataSchemaNode(prefix, variables);
132             default:
133                 throw new IllegalArgumentException("Failed build path.");
134         }
135     }
136
137     private static String nextIdentifierFromNextSequence(final CharMatcher matcher, final MainVarsWrapper variables) {
138         final int start = variables.getOffset();
139         nextSequenceEnd(matcher, variables);
140         return variables.getData().substring(start, variables.getOffset());
141     }
142
143     private static void nextSequenceEnd(final CharMatcher matcher, final MainVarsWrapper variables) {
144         while (!allCharsConsumed(variables)
145                 && matcher.matches(variables.getData().charAt(variables.getOffset()))) {
146             variables.setOffset(variables.getOffset() + 1);
147         }
148     }
149
150     private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
151             final MainVarsWrapper variables) {
152         skipCurrentChar(variables);
153         String value = nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER, variables);
154         value = findAndParsePercentEncoded(value);
155         path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, value));
156     }
157
158     private static void prepareIdentifier(final QName qname, final List<PathArgument> path,
159             final MainVarsWrapper variables) {
160         final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
161         checkValid(!currentNode.isKeyedEntry(), "Entry " + qname + " requires key or value predicate to be present",
162                 variables.getData(), variables.getOffset());
163     }
164
165     private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
166             final MainVarsWrapper variables) {
167         variables.setCurrent(variables.getCurrent().getChild(qname));
168         DataSchemaContextNode<?> current = variables.getCurrent();
169         checkValid(current != null, qname + " is not correct schema node identifier.", variables.getData(),
170                 variables.getOffset());
171         while (current.isMixin()) {
172             path.add(current.getIdentifier());
173             current = current.getChild(qname);
174             variables.setCurrent(current);
175         }
176         return current;
177     }
178
179     private static String findAndParsePercentEncoded(String preparedPrefix) {
180         if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
181             return preparedPrefix;
182         }
183         final StringBuilder newPrefix = new StringBuilder();
184         int i = 0;
185         int startPoint = 0;
186         int endPoint = preparedPrefix.length();
187         while ((i = preparedPrefix.indexOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING)) != -1) {
188             newPrefix.append(preparedPrefix.substring(startPoint, i));
189             startPoint = i;
190             startPoint++;
191             final String hex = preparedPrefix.substring(startPoint, startPoint + 2);
192             startPoint += 2;
193             newPrefix.append((char) Integer.parseInt(hex, 16));
194             preparedPrefix = preparedPrefix.substring(startPoint, endPoint);
195             startPoint = 0;
196             endPoint = preparedPrefix.length();
197         }
198         return newPrefix.toString();
199     }
200
201     private static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
202         final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
203         if (dataSchemaNode instanceof ContainerSchemaNode) {
204             final ContainerSchemaNode contSchemaNode = (ContainerSchemaNode) dataSchemaNode;
205             final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(contSchemaNode.getChildNodes(),
206                     nodeName);
207             return node.getQName();
208         } else if (dataSchemaNode instanceof ListSchemaNode) {
209             final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
210             final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
211                     nodeName);
212             return node.getQName();
213         }
214         throw new UnsupportedOperationException();
215     }
216
217     private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
218         return schemaContext.findModuleByName(prefix, null);
219     }
220
221     private static void validArg(final MainVarsWrapper variables) {
222         checkValid(RestconfConstants.SLASH == currentChar(variables.getOffset(), variables.getData()),
223                 "Identifier must start with '/'.", variables.getData(), variables.getOffset());
224         skipCurrentChar(variables);
225         checkValid(!allCharsConsumed(variables), "Identifier cannot end with '/'.",
226                 variables.getData(), variables.getOffset());
227     }
228
229     private static void skipCurrentChar(final MainVarsWrapper variables) {
230         variables.setOffset(variables.getOffset() + 1);
231     }
232
233     private static char currentChar(final int offset, final String data) {
234         return data.charAt(offset);
235     }
236
237     private static void checkValid(final boolean condition, final String errorMsg, final String data,
238             final int offset) {
239         Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
240                 data, offset, errorMsg);
241     }
242
243     private static boolean allCharsConsumed(final MainVarsWrapper variables) {
244         return variables.getOffset() == variables.getData().length();
245     }
246
247     private static class MainVarsWrapper {
248
249         private final SchemaContext schemaContext;
250         private final String data;
251         private DataSchemaContextNode<?> current;
252         private int offset;
253
254         public MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
255                 final SchemaContext schemaContext) {
256             this.data = data;
257             this.schemaContext = schemaContext;
258             this.setCurrent(current);
259             this.setOffset(offset);
260         }
261
262         public String getData() {
263             return this.data;
264         }
265
266         public DataSchemaContextNode<?> getCurrent() {
267             return this.current;
268         }
269
270         public void setCurrent(final DataSchemaContextNode<?> current) {
271             this.current = current;
272         }
273
274         public int getOffset() {
275             return this.offset;
276         }
277
278         public void setOffset(final int offset) {
279             this.offset = offset;
280         }
281
282         public SchemaContext getSchemaContext() {
283             return this.schemaContext;
284         }
285
286     }
287 }