2 * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.yangtools.yang.data.util;
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;
23 * Iterator which lazily parses {@link PathArgument} from string representation.
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.
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.
35 class XpathStringParsingPathArgumentBuilder implements Builder<Iterable<PathArgument>> {
38 * Matcher matching WSP YANG ABNF token
41 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
44 * Matcher matching IDENTIFIER first char token.
47 private static final CharMatcher IDENTIFIER_FIRST_CHAR =
48 CharMatcher.inRange('a', 'z')
49 .or(CharMatcher.inRange('A', 'Z'))
50 .or(CharMatcher.is('_')).precomputed();
53 * Matcher matching IDENTIFIER token
56 private static final CharMatcher IDENTIFIER =
58 .or(CharMatcher.inRange('0', '9'))
59 .or(CharMatcher.anyOf(".-")).precomputed();
61 private static final CharMatcher SQUOTE = CharMatcher.is('\'');
62 private static final CharMatcher DQUOTE = CharMatcher.is('"');
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 = ']';
71 private final AbstractStringInstanceIdentifierCodec codec;
72 private final String data;
74 private final List<PathArgument> product = new LinkedList<>();
76 private DataSchemaContextNode<?> current;
79 XpathStringParsingPathArgumentBuilder(AbstractStringInstanceIdentifierCodec codec, String data) {
80 this.codec = Preconditions.checkNotNull(codec);
81 this.data = Preconditions.checkNotNull(data);
82 this.current = codec.getDataContextTree().getRoot();
88 public Iterable<PathArgument> build() {
89 while (!allCharactersConsumed()) {
90 product.add(computeNextArgument());
92 return ImmutableList.copyOf(product);
95 private PathArgument computeNextArgument() {
96 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
98 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
99 QName name = nextQName();
100 if (allCharactersConsumed() || SLASH == currentChar()) {
101 return computeIdentifier(name);
103 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
104 return computeIdentifierWithPredicate(name);
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);
122 * Creates path argument with predicates and sets offset
123 * to end of path argument.
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
133 * @param name QName of node, for which predicates are computed.
134 * @return PathArgument representing node selection with predictes
136 private PathArgument computeIdentifierWithPredicate(QName name) {
137 DataSchemaContextNode<?> currentNode = nextContextNode(name);
138 checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
140 ImmutableMap.Builder<QName,Object> keyValues = ImmutableMap.builder();
141 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
145 if (DOT == currentChar()) {
152 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
154 final String keyValue = nextQuotedValue();
156 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
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);
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);
168 return new YangInstanceIdentifier.NodeIdentifierWithPredicates(name, keyValues.build());
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();
181 * Returns following QName and sets offset to end of QName.
183 * @return following QName.
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;
193 localName = nextIdentifier();
196 localName = maybePrefix;
198 return createQName(prefix, localName);
202 * Returns true if all characters from input string
205 * @return true if all characters from input string
208 private boolean allCharactersConsumed() {
209 return offset == data.length();
213 private QName createQName(String prefix, String localName) {
214 return codec.createQName(prefix, localName);
219 * Skips current char if it equals expected otherwise fails parsing.
221 * @param expected Expected character
222 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
224 private void checkCurrentAndSkip(char expected, String errorMsg) {
225 checkValid(expected == currentChar(), errorMsg);
232 * Deserializes value for supplied key
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.
238 private Object deserializeValue(@Nullable QName key, String value) {
239 // FIXME: Use codec to deserialize value to correct Java type
245 * Fails parsing if condition is not met.
247 * In case of error provides pointer to failed instance identifier,
248 * offset on which failure occured with explanation.
250 * @param condition Fails parsing if {@code condition} is false
251 * @param errorMsg Error message which will be provided to user.
254 private void checkValid(boolean condition, String errorMsg, Object... attributes) {
255 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
258 String.format(errorMsg, attributes));
263 * Returns following value of quoted literal (without qoutes)
264 * and sets offset after literal.
266 * @return String literal
268 private String nextQuotedValue() {
269 char quoteChar = currentChar();
270 checkValidQuotation(quoteChar);
272 int valueStart = offset;
273 int endQoute = data.indexOf(quoteChar, offset);
274 String value = data.substring(valueStart, endQoute);
281 * Returns character at current offset.
283 * @return character at current offset.
285 private char currentChar() {
286 return data.charAt(offset);
290 * Increases processing offset by 1
292 private void skipCurrentChar() {
297 * Skip whitespace characters, sets offset to first following
298 * non-whitespace character.
300 private void skipWhitespaces() {
301 nextSequenceEnd(WSP);
305 * Returns string which matches IDENTIFIER YANG ABNF token
306 * and sets processing offset after end of identifier.
308 * @return string which matches IDENTIFIER YANG ABNF token
310 private String nextIdentifier() {
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);
317 private void nextSequenceEnd(CharMatcher matcher) {
318 while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
323 private void checkValidQuotation(char quoteChar) {
325 SQUOTE.matches(quoteChar) || DQUOTE.matches(quoteChar),
326 "Value must be qoute escaped with ''' or '\"'.");