Cleanup use of Guava library
[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.Collection;
17 import java.util.List;
18 import org.opendaylight.yangtools.concepts.Builder;
19 import org.opendaylight.yangtools.yang.common.QName;
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 class XpathStringParsingPathArgumentBuilder implements Builder<Collection<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 AbstractStringInstanceIdentifierCodec codec;
62     private final String data;
63
64     private final List<PathArgument> product = new ArrayList<>();
65
66     private DataSchemaContextNode<?> current;
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 Collection<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         QName name = nextQName();
89         if (allCharactersConsumed() || SLASH == currentChar()) {
90             return computeIdentifier(name);
91         }
92
93         checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
94         return computeIdentifierWithPredicate(name);
95     }
96
97     private DataSchemaContextNode<?> nextContextNode(final QName name) {
98         current = current.getChild(name);
99         checkValid(current != null, "%s is not correct schema node identifier.",name);
100         while (current.isMixin()) {
101             product.add(current.getIdentifier());
102             current = current.getChild(name);
103         }
104         return current;
105     }
106
107     /**
108      * Creates path argument with predicates and sets offset
109      * to end of path argument.
110      *
111      * {@code
112      *     predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
113      *     predicate-expr = (node-identifier / ".") *WSP "=" *WSP
114      *          ((DQUOTE string DQUOTE) /
115      *           (SQUOTE string SQUOTE))
116      *     pos = non-negative-integer-value
117      * }
118      *
119      * @param name QName of node, for which predicates are computed.
120      * @return PathArgument representing node selection with predictes
121      */
122     private PathArgument computeIdentifierWithPredicate(final QName name) {
123         DataSchemaContextNode<?> currentNode = nextContextNode(name);
124         checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
125
126         ImmutableMap.Builder<QName,Object> keyValues = ImmutableMap.builder();
127         while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
128             skipCurrentChar();
129             skipWhitespaces();
130             final QName key;
131             if (DOT == currentChar()) {
132                 key = null;
133                 skipCurrentChar();
134             } else {
135                 key = nextQName();
136             }
137             skipWhitespaces();
138             checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
139             skipWhitespaces();
140             final String keyValue = nextQuotedValue();
141             skipWhitespaces();
142             checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
143
144             // Break-out from method for leaf-list case
145             if (key == null && currentNode.isLeaf()) {
146                 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
147                 return new NodeWithValue<>(name, keyValue);
148             }
149             final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
150             checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
151             final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(), keyValue);
152             keyValues.put(key, value);
153         }
154         return new NodeIdentifierWithPredicates(name, keyValues.build());
155     }
156
157
158     private PathArgument computeIdentifier(final QName name) {
159         DataSchemaContextNode<?> currentNode = nextContextNode(name);
160         checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
161         return currentNode.getIdentifier();
162     }
163
164     /**
165      * Returns following QName and sets offset to end of QName.
166      *
167      * @return following QName.
168      */
169     private QName nextQName() {
170         // Consume prefix or identifier
171         final String maybePrefix = nextIdentifier();
172         final String prefix;
173         final String localName;
174         if (COLON == currentChar()) {
175             // previous token is prefix;
176             prefix = maybePrefix;
177             skipCurrentChar();
178             localName = nextIdentifier();
179         } else {
180             prefix = "";
181             localName = maybePrefix;
182         }
183         return codec.createQName(prefix, localName);
184     }
185
186     /**
187      * Returns true if all characters from input string were consumed.
188      *
189      * @return true if all characters from input string were consumed.
190      */
191     private boolean allCharactersConsumed() {
192         return offset == data.length();
193     }
194
195     /**
196      * Skips current char if it equals expected otherwise fails parsing.
197      *
198      * @param expected Expected character
199      * @param errorMsg Error message if {@link #currentChar()} does not match expected.
200      */
201     private void checkCurrentAndSkip(final char expected, final String errorMsg) {
202         checkValid(expected == currentChar(), errorMsg);
203         offset++;
204     }
205
206     /**
207      * Fails parsing if a condition is not met.
208      *
209      * <p>
210      * In case of error provides pointer to failed instance identifier,
211      * offset on which failure occurred with explanation.
212      *
213      * @param condition Fails parsing if {@code condition} is false
214      * @param errorMsg Error message which will be provided to user.
215      */
216     private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
217         if (!condition) {
218             throw new IllegalArgumentException(String.format(
219                 "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
220                 String.format(errorMsg, attributes)));
221         }
222     }
223
224     /**
225      * Returns following value of quoted literal (without quotes) and sets offset after literal.
226      *
227      * @return String literal
228      */
229     private String nextQuotedValue() {
230         final char quoteChar = currentChar();
231         checkValid(QUOTE.matches(quoteChar), "Value must be qoute escaped with ''' or '\"'.");
232         skipCurrentChar();
233         final int valueStart = offset;
234         final int endQoute = data.indexOf(quoteChar, offset);
235         final String value = data.substring(valueStart, endQoute);
236         offset = endQoute;
237         skipCurrentChar();
238         return value;
239     }
240
241     /**
242      * Returns character at current offset.
243      *
244      * @return character at current offset.
245      */
246     private char currentChar() {
247         return data.charAt(offset);
248     }
249
250     /**
251      * Increases processing offset by 1.
252      */
253     private void skipCurrentChar() {
254         offset++;
255     }
256
257     /**
258      * Skip whitespace characters, sets offset to first following non-whitespace character.
259      */
260     private void skipWhitespaces() {
261         nextSequenceEnd(WSP);
262     }
263
264     /**
265      * Returns string which matches IDENTIFIER YANG ABNF token
266      * and sets processing offset after end of identifier.
267      *
268      * @return string which matches IDENTIFIER YANG ABNF token
269      */
270     private String nextIdentifier() {
271         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
272             "Identifier must start with character from set 'a-zA-Z_'");
273         final int start = offset;
274         nextSequenceEnd(IDENTIFIER);
275         return data.substring(start, offset);
276     }
277
278     private void nextSequenceEnd(final CharMatcher matcher) {
279         while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
280             offset++;
281         }
282     }
283 }