Disconnect yang-data-util from concepts.Builder
[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 CharMatcher QUOTE = CharMatcher.anyOf("'\"");
56
57     private static final char SLASH = '/';
58     private static final char COLON = ':';
59     private static final char DOT = '.';
60     private static final char EQUALS = '=';
61     private static final char PRECONDITION_START = '[';
62     private static final char PRECONDITION_END = ']';
63
64     private final List<PathArgument> product = new ArrayList<>();
65     private final AbstractStringInstanceIdentifierCodec codec;
66     private final SchemaInferenceStack stack;
67     private final String data;
68
69     private DataSchemaContextNode<?> current;
70     private QNameModule lastModule;
71     private int offset;
72
73     XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) {
74         this.codec = requireNonNull(codec);
75         this.data = requireNonNull(data);
76         offset = 0;
77
78         final DataSchemaContextTree tree = codec.getDataContextTree();
79         stack = SchemaInferenceStack.of(tree.getEffectiveModelContext());
80         current = tree.getRoot();
81     }
82
83     /**
84      * Parse input string and return the corresponding list of {@link PathArgument}s.
85      *
86      * @return List of PathArguments
87      * @throws IllegalArgumentException if the input string is not valid
88      */
89     @NonNull List<PathArgument> build() {
90         while (!allCharactersConsumed()) {
91             product.add(computeNextArgument());
92         }
93         return ImmutableList.copyOf(product);
94     }
95
96     private PathArgument computeNextArgument() {
97         checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
98         skipCurrentChar();
99         checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
100         final QName name = nextQName();
101         // Memoize module
102         lastModule = name.getModule();
103         if (allCharactersConsumed() || SLASH == currentChar()) {
104             return computeIdentifier(name);
105         }
106
107         checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
108         return computeIdentifierWithPredicate(name);
109     }
110
111     private DataSchemaContextNode<?> nextContextNode(final QName name) {
112         current = current.getChild(name);
113         checkValid(current != null, "%s is not correct schema node identifier.", name);
114         while (current.isMixin()) {
115             product.add(current.getIdentifier());
116             current = current.getChild(name);
117         }
118         stack.enterDataTree(name);
119         return current;
120     }
121
122     /**
123      * Creates path argument with predicates and sets offset
124      * to end of path argument.
125      *
126      * {@code
127      *     predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
128      *     predicate-expr = (node-identifier / ".") *WSP "=" *WSP
129      *          ((DQUOTE string DQUOTE) /
130      *           (SQUOTE string SQUOTE))
131      *     pos = non-negative-integer-value
132      * }
133      *
134      * @param name QName of node, for which predicates are computed.
135      * @return PathArgument representing node selection with predictes
136      */
137     private PathArgument computeIdentifierWithPredicate(final QName name) {
138         final DataSchemaContextNode<?> currentNode = nextContextNode(name);
139         checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
140
141         ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
142         while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
143             skipCurrentChar();
144             skipWhitespaces();
145             final QName key;
146             if (DOT == currentChar()) {
147                 key = null;
148                 skipCurrentChar();
149             } else {
150                 key = nextQName();
151             }
152             skipWhitespaces();
153             checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
154             skipWhitespaces();
155             final String keyValue = nextQuotedValue();
156             skipWhitespaces();
157             checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
158
159             // Break-out from method for leaf-list case
160             if (key == null && currentNode.isLeaf()) {
161                 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
162                 return new NodeWithValue<>(name, keyValue);
163             }
164             final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
165             checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
166             final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(),
167                 type -> resolveLeafref(key, type), keyValue);
168             keyValues.put(key, value);
169         }
170         return NodeIdentifierWithPredicates.of(name, keyValues.build());
171     }
172
173     private @NonNull TypeDefinition<?> resolveLeafref(final QName qname, final LeafrefTypeDefinition type) {
174         final SchemaInferenceStack tmp = stack.copy();
175         tmp.enterDataTree(qname);
176         return tmp.resolveLeafref(type);
177     }
178
179     private PathArgument computeIdentifier(final QName name) {
180         DataSchemaContextNode<?> currentNode = nextContextNode(name);
181         checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
182         return currentNode.getIdentifier();
183     }
184
185     /**
186      * Returns following QName and sets offset to end of QName.
187      *
188      * @return following QName.
189      */
190     private QName nextQName() {
191         // Consume prefix or identifier
192         final String maybePrefix = nextIdentifier();
193         if (!allCharactersConsumed() && COLON == currentChar()) {
194             // previous token is prefix
195             skipCurrentChar();
196             return codec.createQName(maybePrefix, nextIdentifier());
197         }
198
199         return codec.createQName(lastModule, maybePrefix);
200     }
201
202     /**
203      * Returns true if all characters from input string were consumed.
204      *
205      * @return true if all characters from input string were consumed.
206      */
207     private boolean allCharactersConsumed() {
208         return offset == data.length();
209     }
210
211     /**
212      * Skips current char if it equals expected otherwise fails parsing.
213      *
214      * @param expected Expected character
215      * @param errorMsg Error message if {@link #currentChar()} does not match expected.
216      */
217     private void checkCurrentAndSkip(final char expected, final String errorMsg) {
218         checkValid(expected == currentChar(), errorMsg);
219         offset++;
220     }
221
222     /**
223      * Fails parsing if a condition is not met.
224      *
225      * <p>
226      * In case of error provides pointer to failed instance identifier,
227      * offset on which failure occurred with explanation.
228      *
229      * @param condition Fails parsing if {@code condition} is false
230      * @param errorMsg Error message which will be provided to user.
231      */
232     private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
233         if (!condition) {
234             throw new IllegalArgumentException(String.format(
235                 "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
236                 String.format(errorMsg, attributes)));
237         }
238     }
239
240     /**
241      * Returns following value of quoted literal (without quotes) and sets offset after literal.
242      *
243      * @return String literal
244      */
245     private String nextQuotedValue() {
246         final char quoteChar = currentChar();
247         checkValid(QUOTE.matches(quoteChar), "Value must be qoute escaped with ''' or '\"'.");
248         skipCurrentChar();
249         final int valueStart = offset;
250         final int endQoute = data.indexOf(quoteChar, offset);
251         final String value = data.substring(valueStart, endQoute);
252         offset = endQoute;
253         skipCurrentChar();
254         return value;
255     }
256
257     /**
258      * Returns character at current offset.
259      *
260      * @return character at current offset.
261      */
262     private char currentChar() {
263         return data.charAt(offset);
264     }
265
266     /**
267      * Increases processing offset by 1.
268      */
269     private void skipCurrentChar() {
270         offset++;
271     }
272
273     /**
274      * Skip whitespace characters, sets offset to first following non-whitespace character.
275      */
276     private void skipWhitespaces() {
277         nextSequenceEnd(WSP);
278     }
279
280     /**
281      * Returns string which matches IDENTIFIER YANG ABNF token
282      * and sets processing offset after end of identifier.
283      *
284      * @return string which matches IDENTIFIER YANG ABNF token
285      */
286     private String nextIdentifier() {
287         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
288             "Identifier must start with character from set 'a-zA-Z_'");
289         final int start = offset;
290         nextSequenceEnd(IDENTIFIER);
291         return data.substring(start, offset);
292     }
293
294     private void nextSequenceEnd(final CharMatcher matcher) {
295         while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
296             offset++;
297         }
298     }
299 }