Bug 2766: Fixed parsing and serializing XPath Instance Identifiers
[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
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 Object value = deserializeValue(key,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, value);
161             }
162             keyValues.put(key, value);
163         }
164         return new YangInstanceIdentifier.NodeIdentifierWithPredicates(name, keyValues.build());
165     }
166
167
168     private PathArgument computeIdentifier(QName name) {
169         DataSchemaContextNode<?> currentNode = nextContextNode(name);
170         checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
171         return currentNode.getIdentifier();
172     }
173
174
175     /**
176      *
177      * Returns following QName and sets offset to end of QName.
178      *
179      * @return following QName.
180      */
181     private QName nextQName() {
182         // Consume prefix or identifie
183         final String maybePrefix = nextIdentifier();
184         final String prefix,localName;
185         if(COLON == currentChar()) {
186             // previous token is prefix;
187             prefix = maybePrefix;
188             skipCurrentChar();
189             localName = nextIdentifier();
190         } else {
191             prefix = "";
192             localName = maybePrefix;
193         }
194         return createQName(prefix, localName);
195     }
196
197     /**
198      * Returns true if all characters from input string
199      * were consumed.
200      *
201      * @return true if all characters from input string
202      * were consumed.
203      */
204     private boolean allCharactersConsumed() {
205         return offset == data.length();
206     }
207
208
209     private QName createQName(String prefix, String localName) {
210         return codec.createQName(prefix, localName);
211     }
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(char expected, String errorMsg) {
221         checkValid(expected == currentChar(), errorMsg);
222         offset++;
223     }
224
225
226     /**
227      *
228      * Deserializes value for supplied key
229      *
230      * @param key Name of referenced key, If null, referenced leaf is previous encountered item.
231      * @param value Value to be checked and deserialized
232      * @return Object representing value in yang-data-api format.
233      */
234     private Object deserializeValue(@Nullable QName key, String value) {
235         // FIXME: Use codec to deserialize value to correct Java type
236         return value;
237     }
238
239     /**
240      *
241      * Fails parsing if condition is not met.
242      *
243      * In case of error provides pointer to failed instance identifier,
244      * offset on which failure occured with explanation.
245      *
246      * @param condition Fails parsing if {@code condition} is false
247      * @param errorMsg Error message which will be provided to user.
248      * @param attributes
249      */
250     private void checkValid(boolean condition, String errorMsg, Object... attributes) {
251         Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
252                 data,
253                 offset,
254                 String.format(errorMsg, attributes));
255     }
256
257     /**
258      *
259      * Returns following value of quoted literal (without qoutes)
260      * and sets offset after literal.
261      *
262      * @return String literal
263      */
264     private String nextQuotedValue() {
265         char quoteChar = currentChar();
266         checkValidQuotation(quoteChar);
267         skipCurrentChar();
268         int valueStart = offset;
269         int endQoute = data.indexOf(quoteChar, offset);
270         String value = data.substring(valueStart, endQoute);
271         offset = endQoute;
272         skipCurrentChar();
273         return value;
274     }
275
276     /**
277      * Returns character at current offset.
278      *
279      * @return character at current offset.
280      */
281     private char currentChar() {
282         return data.charAt(offset);
283     }
284
285     /**
286      * Increases processing offset by 1
287      */
288     private void skipCurrentChar() {
289         offset++;
290     }
291
292     /**
293      * Skip whitespace characters, sets offset to first following
294      * non-whitespace character.
295      */
296     private void skipWhitespaces() {
297         nextSequenceEnd(WSP);
298     }
299
300     /**
301      * Returns string which matches IDENTIFIER YANG ABNF token
302      * and sets processing offset after end of identifier.
303      *
304      * @return string which matches IDENTIFIER YANG ABNF token
305      */
306     private String nextIdentifier() {
307         int start = offset;
308         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()), "Identifier must start with character from set 'a-zA-Z_'");
309         nextSequenceEnd(IDENTIFIER);
310         return data.substring(start, offset);
311     }
312
313     private void nextSequenceEnd(CharMatcher matcher) {
314         while(!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
315             offset++;
316         }
317     }
318
319     private void checkValidQuotation(char quoteChar) {
320         checkValid(
321                 SQUOTE.matches(quoteChar) || DQUOTE.matches(quoteChar),
322                 "Value must be qoute escaped with ''' or '\"'.");
323
324     }
325
326 }