8f2d928d4fe200501b24776a11921d627746b32e
[yangtools.git] / data / 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 static java.util.Objects.requireNonNull;
11
12 import com.google.common.base.CharMatcher;
13 import com.google.common.collect.ImmutableList;
14 import com.google.common.collect.ImmutableMap;
15 import java.util.ArrayList;
16 import java.util.List;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.opendaylight.yangtools.concepts.Mutable;
19 import org.opendaylight.yangtools.yang.common.QName;
20 import org.opendaylight.yangtools.yang.common.QNameModule;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
24 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
25 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
26 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
27
28 /**
29  * Iterator which lazily parses {@link PathArgument} from string representation.
30  *
31  * <p>
32  * Note that invocation of {@link #hasNext()} or {@link #next()} may result in
33  * throwing of {@link IllegalArgumentException} if underlying string representation
34  * is not correctly serialized or does not represent instance identifier valid
35  * for associated schema context.
36  */
37 final class XpathStringParsingPathArgumentBuilder implements Mutable {
38     /**
39      * Matcher matching WSP YANG ABNF token.
40      */
41     private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
42
43     /**
44      * Matcher matching IDENTIFIER first char token.
45      */
46     private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
47             .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
48
49     /**
50      * Matcher matching IDENTIFIER token.
51      */
52     private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
53             .or(CharMatcher.anyOf(".-")).precomputed();
54
55     private static final char SLASH = '/';
56     private static final char BACKSLASH = '\\';
57     private static final char COLON = ':';
58     private static final char DOT = '.';
59     private static final char EQUALS = '=';
60     private static final char PRECONDITION_START = '[';
61     private static final char PRECONDITION_END = ']';
62     private static final char SQUOT = '\'';
63     private static final char DQUOT = '"';
64
65     private final List<PathArgument> product = new ArrayList<>();
66     private final AbstractStringInstanceIdentifierCodec codec;
67     private final SchemaInferenceStack stack;
68     private final String data;
69
70     private DataSchemaContextNode<?> current;
71     private QNameModule lastModule;
72     private int offset;
73
74     XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) {
75         this.codec = requireNonNull(codec);
76         this.data = requireNonNull(data);
77         offset = 0;
78
79         final DataSchemaContextTree tree = codec.getDataContextTree();
80         stack = SchemaInferenceStack.of(tree.getEffectiveModelContext());
81         current = tree.getRoot();
82     }
83
84     /**
85      * Parse input string and return the corresponding list of {@link PathArgument}s.
86      *
87      * @return List of PathArguments
88      * @throws IllegalArgumentException if the input string is not valid
89      */
90     @NonNull List<PathArgument> build() {
91         while (!allCharactersConsumed()) {
92             product.add(computeNextArgument());
93         }
94         return ImmutableList.copyOf(product);
95     }
96
97     private PathArgument computeNextArgument() {
98         checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
99         skipCurrentChar();
100         checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
101         final QName name = nextQName();
102         // Memoize module
103         lastModule = name.getModule();
104         if (allCharactersConsumed() || SLASH == currentChar()) {
105             return computeIdentifier(name);
106         }
107
108         checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
109         return computeIdentifierWithPredicate(name);
110     }
111
112     private DataSchemaContextNode<?> nextContextNode(final QName name) {
113         current = current.getChild(name);
114         checkValid(current != null, "%s is not correct schema node identifier.", name);
115         while (current.isMixin()) {
116             product.add(current.getIdentifier());
117             current = current.getChild(name);
118         }
119         stack.enterDataTree(name);
120         return current;
121     }
122
123     /**
124      * Creates path argument with predicates and sets offset
125      * to end of path argument.
126      *
127      * {@code
128      *     predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
129      *     predicate-expr = (node-identifier / ".") *WSP "=" *WSP
130      *          ((DQUOTE string DQUOTE) /
131      *           (SQUOTE string SQUOTE))
132      *     pos = non-negative-integer-value
133      * }
134      *
135      * @param name QName of node, for which predicates are computed.
136      * @return PathArgument representing node selection with predictes
137      */
138     private PathArgument computeIdentifierWithPredicate(final QName name) {
139         final DataSchemaContextNode<?> currentNode = nextContextNode(name);
140         checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
141
142         ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
143         while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
144             skipCurrentChar();
145             skipWhitespaces();
146             final QName key;
147             if (DOT == currentChar()) {
148                 key = null;
149                 skipCurrentChar();
150             } else {
151                 key = nextQName();
152             }
153             skipWhitespaces();
154             checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
155             skipWhitespaces();
156             final String keyValue = nextQuotedValue();
157             skipWhitespaces();
158             checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
159
160             // Break-out from method for leaf-list case
161             if (key == null && currentNode.isLeaf()) {
162                 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
163                 final Object value = codec.deserializeKeyValue(currentNode.getDataSchemaNode(),
164                     type -> resolveLeafref(currentNode.getIdentifier().getNodeType(), type), keyValue);
165                 return new NodeWithValue<>(name, value);
166             }
167             final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
168             checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
169             final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(),
170                 type -> resolveLeafref(key, type), keyValue);
171             keyValues.put(key, value);
172         }
173         return NodeIdentifierWithPredicates.of(name, keyValues.build());
174     }
175
176     private @NonNull TypeDefinition<?> resolveLeafref(final QName qname, final LeafrefTypeDefinition type) {
177         final SchemaInferenceStack tmp = stack.copy();
178         tmp.enterDataTree(qname);
179         return tmp.resolveLeafref(type);
180     }
181
182     private PathArgument computeIdentifier(final QName name) {
183         DataSchemaContextNode<?> currentNode = nextContextNode(name);
184         checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
185         return currentNode.getIdentifier();
186     }
187
188     /**
189      * Returns following QName and sets offset to end of QName.
190      *
191      * @return following QName.
192      */
193     private QName nextQName() {
194         // Consume prefix or identifier
195         final String maybePrefix = nextIdentifier();
196         if (!allCharactersConsumed() && COLON == currentChar()) {
197             // previous token is prefix
198             skipCurrentChar();
199             return codec.createQName(maybePrefix, nextIdentifier());
200         }
201
202         return codec.createQName(lastModule, maybePrefix);
203     }
204
205     /**
206      * Returns true if all characters from input string were consumed.
207      *
208      * @return true if all characters from input string were consumed.
209      */
210     private boolean allCharactersConsumed() {
211         return offset == data.length();
212     }
213
214     /**
215      * Skips current char if it equals expected otherwise fails parsing.
216      *
217      * @param expected Expected character
218      * @param errorMsg Error message if {@link #currentChar()} does not match expected.
219      */
220     private void checkCurrentAndSkip(final char expected, final String errorMsg) {
221         checkValid(expected == currentChar(), errorMsg);
222         offset++;
223     }
224
225     /**
226      * Fails parsing if a condition is not met.
227      *
228      * <p>
229      * In case of error provides pointer to failed instance identifier,
230      * offset on which failure occurred with explanation.
231      *
232      * @param condition Fails parsing if {@code condition} is false
233      * @param errorMsg Error message which will be provided to user.
234      */
235     private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
236         if (!condition) {
237             throw iae(errorMsg, attributes);
238         }
239     }
240
241     private @NonNull IllegalArgumentException iae(final String errorMsg, final Object... attributes) {
242         return new IllegalArgumentException(
243             String.format("Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
244                 String.format(errorMsg, attributes)));
245     }
246
247     /**
248      * Returns following value of quoted literal (without quotes) and sets offset after literal.
249      *
250      * @return String literal
251      */
252     private String nextQuotedValue() {
253         switch (currentChar()) {
254             case SQUOT:
255                 return nextSingleQuotedValue();
256             case DQUOT:
257                 return nextDoubleQuotedValue();
258             default:
259                 throw iae("Value must be quote escaped with ''' or '\"'.");
260         }
261     }
262
263     // Simple: just look for the matching single quote and return substring
264     private String nextSingleQuotedValue() {
265         skipCurrentChar();
266         final int start = offset;
267         final int end = data.indexOf(SQUOT, start);
268         checkValid(end != -1, "Closing single quote not found");
269         offset = end;
270         skipCurrentChar();
271         return data.substring(start, end);
272     }
273
274     // Complicated: we need to potentially un-escape
275     private String nextDoubleQuotedValue() {
276         skipCurrentChar();
277
278         final int maxIndex = data.length() - 1;
279         final var sb = new StringBuilder();
280         while (true) {
281             final int nextStart = offset;
282
283             // Find next double quotes
284             final int nextEnd = data.indexOf(DQUOT, nextStart);
285             checkValid(nextEnd != -1, "Closing double quote not found");
286             offset = nextEnd;
287
288             // Find next backslash
289             final int nextBackslash = data.indexOf(BACKSLASH, nextStart);
290             if (nextBackslash == -1 || nextBackslash > nextEnd) {
291                 // No backslash between nextStart and nextEnd -- just copy characters and terminate
292                 offset = nextEnd;
293                 skipCurrentChar();
294                 return sb.append(data, nextStart, nextEnd).toString();
295             }
296
297             // Validate escape completeness and append buffer
298             checkValid(nextBackslash != maxIndex, "Incomplete escape");
299             sb.append(data, nextStart, nextBackslash);
300
301             // Adjust offset before potentially referencing it and
302             offset = nextBackslash;
303             sb.append(unescape(data.charAt(nextBackslash + 1)));
304
305             // Rinse and repeat
306             offset = nextBackslash + 2;
307         }
308     }
309
310     // As per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
311     private char unescape(final char escape) {
312         switch (escape) {
313             case 'n':
314                 return '\n';
315             case 't':
316                 return '\t';
317             case DQUOT:
318                 return DQUOT;
319             case BACKSLASH:
320                 return BACKSLASH;
321             default:
322                 throw iae("Unrecognized escape");
323         }
324     }
325
326     /**
327      * Returns character at current offset.
328      *
329      * @return character at current offset.
330      */
331     private char currentChar() {
332         return data.charAt(offset);
333     }
334
335     /**
336      * Increases processing offset by 1.
337      */
338     private void skipCurrentChar() {
339         offset++;
340     }
341
342     /**
343      * Skip whitespace characters, sets offset to first following non-whitespace character.
344      */
345     private void skipWhitespaces() {
346         nextSequenceEnd(WSP);
347     }
348
349     /**
350      * Returns string which matches IDENTIFIER YANG ABNF token
351      * and sets processing offset after end of identifier.
352      *
353      * @return string which matches IDENTIFIER YANG ABNF token
354      */
355     private String nextIdentifier() {
356         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
357             "Identifier must start with character from set 'a-zA-Z_'");
358         final int start = offset;
359         nextSequenceEnd(IDENTIFIER);
360         return data.substring(start, offset);
361     }
362
363     private void nextSequenceEnd(final CharMatcher matcher) {
364         while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
365             offset++;
366         }
367     }
368 }