2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.restconf.parser.builder;
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;
34 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
38 public final class YangInstanceIdentifierDeserializer {
40 private YangInstanceIdentifierDeserializer() {
41 throw new UnsupportedOperationException("Util class.");
45 * Method to create {@link Iterable} from {@link PathArgument} which are
46 * parsing from data by {@link SchemaContext}.
48 * @param schemaContext
49 * - for validate of parsing path arguments
52 * @return {@link Iterable} of {@link PathArgument}
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);
60 while (!allCharsConsumed(variables)) {
62 final QName qname = prepareQName(variables);
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);
74 prepareNodeWithValue(qname, path, variables);
77 throw new IllegalArgumentException(
78 "Bad char " + currentChar(variables.getOffset(), variables.getData()) + " on position "
79 + variables.getOffset() + ".");
83 return ImmutableList.copyOf(path);
86 private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
87 final MainVarsWrapper variables) {
89 final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
90 checkValid((dataSchemaNode != null), "Data schema node is null", variables.getData(), variables.getOffset());
92 final Iterator<QName> keys = ((ListSchemaNode) dataSchemaNode).getKeyDefinition().iterator();
93 final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
95 // skip already expected equal sign
96 skipCurrentChar(variables);
98 // read key value separated by comma
99 while (keys.hasNext() && !allCharsConsumed(variables) && currentChar(variables.getOffset(),
100 variables.getData()) != RestconfConstants.SLASH) {
103 if (currentChar(variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
104 values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
105 skipCurrentChar(variables);
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,
119 values.put(keys.next(), findAndParsePercentEncoded(nextIdentifierFromNextSequence(
120 ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE, variables)));
123 if (keys.hasNext() && !allCharsConsumed(variables) && currentChar(
124 variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.COMMA) {
125 skipCurrentChar(variables);
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);
136 // there should be no more missing keys
137 RestconfValidationUtils.checkDocumentedError(
139 RestconfError.ErrorType.PROTOCOL,
140 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
141 "Key value missing for: " + qname
145 path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
149 private static QName prepareQName(final MainVarsWrapper variables) {
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;
158 if (allCharsConsumed(variables)) {
159 return getQNameOfDataSchemaNode(preparedPrefix, variables);
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);
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);
176 if (!allCharsConsumed(variables) && currentChar
177 (variables.getOffset(), variables.getData()) == ParserBuilderConstants.Deserializer.EQUAL) {
178 return getQNameOfDataSchemaNode(localName, variables);
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);
184 case ParserBuilderConstants.Deserializer.EQUAL:
185 prefix = preparedPrefix;
186 return getQNameOfDataSchemaNode(prefix, variables);
188 throw new IllegalArgumentException("Failed build path.");
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());
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);
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);
211 // exception if value attribute is missing
212 RestconfValidationUtils.checkDocumentedError(
214 RestconfError.ErrorType.PROTOCOL,
215 RestconfError.ErrorTag.MISSING_ATTRIBUTE,
216 "Value missing for: " + qname
219 path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, findAndParsePercentEncoded(value)));
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());
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);
243 private static String findAndParsePercentEncoded(final String preparedPrefix) {
244 if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
245 return preparedPrefix;
248 final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
249 final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
251 while (matcher.matchesAnyOf(parsedPrefix)) {
252 final int percentCharPosition = matcher.indexIn(parsedPrefix);
253 parsedPrefix.replace(
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)));
262 return parsedPrefix.toString();
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(),
271 return node.getQName();
272 } else if (dataSchemaNode instanceof ListSchemaNode) {
273 final ListSchemaNode listSchemaNode = (ListSchemaNode) dataSchemaNode;
274 final DataSchemaNode node = RestconfSchemaUtil.findSchemaNodeInCollection(listSchemaNode.getChildNodes(),
276 return node.getQName();
278 throw new UnsupportedOperationException();
281 private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
282 return schemaContext.findModuleByName(prefix, null);
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());
292 skipCurrentChar(variables);
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());
300 private static void skipCurrentChar(final MainVarsWrapper variables) {
301 variables.setOffset(variables.getOffset() + 1);
304 private static char currentChar(final int offset, final String data) {
305 return data.charAt(offset);
308 private static void checkValid(final boolean condition, final String errorMsg, final String data,
310 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
311 data, offset, errorMsg);
314 private static boolean allCharsConsumed(final MainVarsWrapper variables) {
315 return variables.getOffset() == variables.getData().length();
318 private final static class MainVarsWrapper {
319 private static final int STARTING_OFFSET = 0;
321 private final SchemaContext schemaContext;
322 private final String data;
323 private DataSchemaContextNode<?> current;
326 public MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
327 final SchemaContext schemaContext) {
329 this.setCurrent(current);
330 this.setOffset(offset);
331 this.schemaContext = schemaContext;
334 public String getData() {
338 public DataSchemaContextNode<?> getCurrent() {
342 public void setCurrent(final DataSchemaContextNode<?> current) {
343 this.current = current;
346 public int getOffset() {
350 public void setOffset(final int offset) {
351 this.offset = offset;
354 public SchemaContext getSchemaContext() {
355 return this.schemaContext;