Bug 3834 - Unhelpful error messages when parsing
[yangtools.git] / yang / yang-data-util / src / main / java / org / opendaylight / yangtools / yang / data / util / XpathStringParsingPathArgumentBuilder.java
1 /*
2  * Copyright (c) 2015 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.yangtools.yang.data.util;
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 javax.annotation.Nullable;
16 import org.opendaylight.yangtools.concepts.Builder;
17 import org.opendaylight.yangtools.yang.common.QName;
18 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
19 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
20 /**
21  *
22  * Iterator which lazily parses {@link PathArgument} from string representation.
23  *
24  * Note that invocation of {@link #hasNext()} or {@link #next()} may result in
25  * throwing of {@link IllegalArgumentException} if underlying string represenation
26  * is not correctly serialized or does not represent instance identifier valid
27  * for associated schema context.
28  *
29  * In order to obtain {@link Iterable} or {@link java.util.Collection} please use
30  * {@link com.google.common.collect.ImmutableList#copyOf(java.util.Iterator)}
31  * with this Iterator, which will trigger computation of all path arguments.
32  *
33  */
34 class XpathStringParsingPathArgumentBuilder implements Builder<Iterable<PathArgument>> {
35
36     /**
37      * Matcher matching WSP YANG ABNF token
38      *
39      */
40     private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
41
42     /**
43      * Matcher matching IDENTIFIER first char token.
44      *
45      */
46     private static final CharMatcher IDENTIFIER_FIRST_CHAR =
47             CharMatcher.inRange('a', 'z')
48             .or(CharMatcher.inRange('A', 'Z'))
49             .or(CharMatcher.is('_')).precomputed();
50     /**
51      *
52      * Matcher matching IDENTIFIER token
53      *
54      */
55     private static final CharMatcher IDENTIFIER =
56             IDENTIFIER_FIRST_CHAR
57             .or(CharMatcher.inRange('0', '9'))
58             .or(CharMatcher.anyOf(".-")).precomputed();
59
60     private static final CharMatcher SQUOTE = CharMatcher.is('\'');
61     private static final CharMatcher DQUOTE = CharMatcher.is('"');
62
63     private static final char SLASH = '/';
64     private static final char COLON = ':';
65     private static final char DOT = '.';
66     private static final char EQUALS = '=';
67     private static final char PRECONDITION_START = '[';
68     private static final char PRECONDITION_END = ']';
69
70     private final AbstractStringInstanceIdentifierCodec codec;
71     private final String data;
72
73     private final LinkedList<PathArgument> product = new LinkedList<>();
74
75     private DataSchemaContextNode<?> current;
76     private int offset;
77
78     XpathStringParsingPathArgumentBuilder(AbstractStringInstanceIdentifierCodec codec, String data) {
79         this.codec = Preconditions.checkNotNull(codec);
80         this.data = Preconditions.checkNotNull(data);
81         this.current = codec.getDataContextTree().getRoot();
82         this.offset = 0;
83     }
84
85
86     @Override
87     public Iterable<PathArgument> build() {
88         while (!allCharactersConsumed()) {
89             product.add(computeNextArgument());
90         }
91         return ImmutableList.copyOf(product);
92     }
93
94     private PathArgument computeNextArgument() {
95         checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
96         skipCurrentChar();
97         checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
98         QName name = nextQName();
99         if(allCharactersConsumed() || SLASH == currentChar()) {
100             return computeIdentifier(name);
101         } else {
102             checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
103             return computeIdentifierWithPredicate(name);
104         }
105     }
106
107
108     private DataSchemaContextNode<?> nextContextNode(QName name) {
109         current = current.getChild(name);
110         checkValid(current != null, "%s is not correct schema node identifier.",name);
111         while(current.isMixin()) {
112             product.add(current.getIdentifier());
113             current = current.getChild(name);
114         }
115         return current;
116     }
117
118
119     /**
120      *
121      * Creates path argument with predicates and sets offset
122      * to end of path argument.
123      *
124      * {@code
125      *     predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
126      *     predicate-expr = (node-identifier / ".") *WSP "=" *WSP
127      *          ((DQUOTE string DQUOTE) /
128      *           (SQUOTE string SQUOTE))
129      *     pos = non-negative-integer-value
130      * }
131      *
132      * @param name QName of node, for which predicates are computed.
133      * @return PathArgument representing node selection with predictes
134      */
135     private PathArgument computeIdentifierWithPredicate(QName name) {
136         DataSchemaContextNode<?> currentNode = nextContextNode(name);
137         checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
138
139         ImmutableMap.Builder<QName,Object> keyValues = ImmutableMap.builder();
140         while(!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
141             skipCurrentChar();
142             skipWhitespaces();
143             final QName key;
144             if(DOT == currentChar()) {
145                 key = null;
146                 skipCurrentChar();
147             } else {
148                 key = nextQName();
149             }
150             skipWhitespaces();
151             checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
152             skipWhitespaces();
153             final String keyValue = nextQuotedValue();
154             skipWhitespaces();
155             checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
156
157             // Break-out from method for leaf-list case
158             if(key == null && currentNode.isLeaf()) {
159                 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
160                 return new YangInstanceIdentifier.NodeWithValue(name, keyValue);
161             }
162             final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
163             checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
164             final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(), keyValue);
165             keyValues.put(key, value);
166         }
167         return new YangInstanceIdentifier.NodeIdentifierWithPredicates(name, keyValues.build());
168     }
169
170
171     private PathArgument computeIdentifier(QName name) {
172         DataSchemaContextNode<?> currentNode = nextContextNode(name);
173         checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
174         return currentNode.getIdentifier();
175     }
176
177
178     /**
179      *
180      * Returns following QName and sets offset to end of QName.
181      *
182      * @return following QName.
183      */
184     private QName nextQName() {
185         // Consume prefix or identifie
186         final String maybePrefix = nextIdentifier();
187         final String prefix,localName;
188         if(COLON == currentChar()) {
189             // previous token is prefix;
190             prefix = maybePrefix;
191             skipCurrentChar();
192             localName = nextIdentifier();
193         } else {
194             prefix = "";
195             localName = maybePrefix;
196         }
197         return createQName(prefix, localName);
198     }
199
200     /**
201      * Returns true if all characters from input string
202      * were consumed.
203      *
204      * @return true if all characters from input string
205      * were consumed.
206      */
207     private boolean allCharactersConsumed() {
208         return offset == data.length();
209     }
210
211
212     private QName createQName(String prefix, String localName) {
213         return codec.createQName(prefix, localName);
214     }
215
216     /**
217      *
218      * Skips current char if it equals expected otherwise fails parsing.
219      *
220      * @param expected Expected character
221      * @param errorMsg Error message if {@link #currentChar()} does not match expected.
222      */
223     private void checkCurrentAndSkip(char expected, String errorMsg) {
224         checkValid(expected == currentChar(), errorMsg);
225         offset++;
226     }
227
228
229     /**
230      *
231      * Deserializes value for supplied key
232      *
233      * @param key Name of referenced key, If null, referenced leaf is previous encountered item.
234      * @param value Value to be checked and deserialized
235      * @return Object representing value in yang-data-api format.
236      */
237     private Object deserializeValue(@Nullable QName key, String value) {
238         // FIXME: Use codec to deserialize value to correct Java type
239         return value;
240     }
241
242     /**
243      *
244      * Fails parsing if condition is not met.
245      *
246      * In case of error provides pointer to failed instance identifier,
247      * offset on which failure occured with explanation.
248      *
249      * @param condition Fails parsing if {@code condition} is false
250      * @param errorMsg Error message which will be provided to user.
251      * @param attributes
252      */
253     private void checkValid(boolean condition, String errorMsg, Object... attributes) {
254         Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
255                 data,
256                 offset,
257                 String.format(errorMsg, attributes));
258     }
259
260     /**
261      *
262      * Returns following value of quoted literal (without qoutes)
263      * and sets offset after literal.
264      *
265      * @return String literal
266      */
267     private String nextQuotedValue() {
268         char quoteChar = currentChar();
269         checkValidQuotation(quoteChar);
270         skipCurrentChar();
271         int valueStart = offset;
272         int endQoute = data.indexOf(quoteChar, offset);
273         String value = data.substring(valueStart, endQoute);
274         offset = endQoute;
275         skipCurrentChar();
276         return value;
277     }
278
279     /**
280      * Returns character at current offset.
281      *
282      * @return character at current offset.
283      */
284     private char currentChar() {
285         return data.charAt(offset);
286     }
287
288     /**
289      * Increases processing offset by 1
290      */
291     private void skipCurrentChar() {
292         offset++;
293     }
294
295     /**
296      * Skip whitespace characters, sets offset to first following
297      * non-whitespace character.
298      */
299     private void skipWhitespaces() {
300         nextSequenceEnd(WSP);
301     }
302
303     /**
304      * Returns string which matches IDENTIFIER YANG ABNF token
305      * and sets processing offset after end of identifier.
306      *
307      * @return string which matches IDENTIFIER YANG ABNF token
308      */
309     private String nextIdentifier() {
310         int start = offset;
311         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()), "Identifier must start with character from set 'a-zA-Z_'");
312         nextSequenceEnd(IDENTIFIER);
313         return data.substring(start, offset);
314     }
315
316     private void nextSequenceEnd(CharMatcher matcher) {
317         while(!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
318             offset++;
319         }
320     }
321
322     private void checkValidQuotation(char quoteChar) {
323         checkValid(
324                 SQUOTE.matches(quoteChar) || DQUOTE.matches(quoteChar),
325                 "Value must be qoute escaped with ''' or '\"'.");
326
327     }
328
329 }