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