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