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