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