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 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;
22 * Iterator which lazily parses {@link PathArgument} from string representation.
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.
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.
34 class XpathStringParsingPathArgumentBuilder implements Builder<Iterable<PathArgument>> {
37 * Matcher matching WSP YANG ABNF token
40 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
43 * Matcher matching IDENTIFIER first char token.
46 private static final CharMatcher IDENTIFIER_FIRST_CHAR =
47 CharMatcher.inRange('a', 'z')
48 .or(CharMatcher.inRange('A', 'Z'))
49 .or(CharMatcher.is('_')).precomputed();
52 * Matcher matching IDENTIFIER token
55 private static final CharMatcher IDENTIFIER =
57 .or(CharMatcher.inRange('0', '9'))
58 .or(CharMatcher.anyOf(".-")).precomputed();
60 private static final CharMatcher SQUOTE = CharMatcher.is('\'');
61 private static final CharMatcher DQUOTE = CharMatcher.is('"');
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 = ']';
70 private final AbstractStringInstanceIdentifierCodec codec;
71 private final String data;
73 private final LinkedList<PathArgument> product = new LinkedList<>();
75 private DataSchemaContextNode<?> current;
78 XpathStringParsingPathArgumentBuilder(AbstractStringInstanceIdentifierCodec codec, String data) {
79 this.codec = Preconditions.checkNotNull(codec);
80 this.data = Preconditions.checkNotNull(data);
81 this.current = codec.getDataContextTree().getRoot();
87 public Iterable<PathArgument> build() {
88 while (!allCharactersConsumed()) {
89 product.add(computeNextArgument());
91 return ImmutableList.copyOf(product);
94 private PathArgument computeNextArgument() {
95 checkValid(SLASH == currentChar(),"Identifier must start with '/'.");
98 QName name = nextQName();
99 if(allCharactersConsumed() || SLASH == currentChar()) {
100 return computeIdentifier(name);
102 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
103 return computeIdentifierWithPredicate(name);
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);
121 * Creates path argument with predicates and sets offset
122 * to end of path argument.
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
132 * @param name QName of node, for which predicates are computed.
133 * @return PathArgument representing node selection with predictes
135 private PathArgument computeIdentifierWithPredicate(QName name) {
136 DataSchemaContextNode<?> currentNode = nextContextNode(name);
137 checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
139 ImmutableMap.Builder<QName,Object> keyValues = ImmutableMap.builder();
140 while(!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
144 if(DOT == currentChar()) {
151 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
153 final String keyValue = nextQuotedValue();
155 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
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, keyValue);
162 final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
163 checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
164 final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(), keyValue);
165 keyValues.put(key, value);
167 return new YangInstanceIdentifier.NodeIdentifierWithPredicates(name, keyValues.build());
171 private PathArgument computeIdentifier(QName name) {
172 DataSchemaContextNode<?> currentNode = nextContextNode(name);
173 checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
174 return currentNode.getIdentifier();
180 * Returns following QName and sets offset to end of QName.
182 * @return following QName.
184 private QName nextQName() {
185 // Consume prefix or identifie
186 final String maybePrefix = nextIdentifier();
187 final String prefix,localName;
188 if(COLON == currentChar()) {
189 // previous token is prefix;
190 prefix = maybePrefix;
192 localName = nextIdentifier();
195 localName = maybePrefix;
197 return createQName(prefix, localName);
201 * Returns true if all characters from input string
204 * @return true if all characters from input string
207 private boolean allCharactersConsumed() {
208 return offset == data.length();
212 private QName createQName(String prefix, String localName) {
213 return codec.createQName(prefix, localName);
218 * Skips current char if it equals expected otherwise fails parsing.
220 * @param expected Expected character
221 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
223 private void checkCurrentAndSkip(char expected, String errorMsg) {
224 checkValid(expected == currentChar(), errorMsg);
231 * Deserializes value for supplied key
233 * @param key Name of referenced key, If null, referenced leaf is previous encountered item.
234 * @param value Value to be checked and deserialized
235 * @return Object representing value in yang-data-api format.
237 private Object deserializeValue(@Nullable QName key, String value) {
238 // FIXME: Use codec to deserialize value to correct Java type
244 * Fails parsing if condition is not met.
246 * In case of error provides pointer to failed instance identifier,
247 * offset on which failure occured with explanation.
249 * @param condition Fails parsing if {@code condition} is false
250 * @param errorMsg Error message which will be provided to user.
253 private void checkValid(boolean condition, String errorMsg, Object... attributes) {
254 Preconditions.checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s",
257 String.format(errorMsg, attributes));
262 * Returns following value of quoted literal (without qoutes)
263 * and sets offset after literal.
265 * @return String literal
267 private String nextQuotedValue() {
268 char quoteChar = currentChar();
269 checkValidQuotation(quoteChar);
271 int valueStart = offset;
272 int endQoute = data.indexOf(quoteChar, offset);
273 String value = data.substring(valueStart, endQoute);
280 * Returns character at current offset.
282 * @return character at current offset.
284 private char currentChar() {
285 return data.charAt(offset);
289 * Increases processing offset by 1
291 private void skipCurrentChar() {
296 * Skip whitespace characters, sets offset to first following
297 * non-whitespace character.
299 private void skipWhitespaces() {
300 nextSequenceEnd(WSP);
304 * Returns string which matches IDENTIFIER YANG ABNF token
305 * and sets processing offset after end of identifier.
307 * @return string which matches IDENTIFIER YANG ABNF token
309 private String nextIdentifier() {
311 checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()), "Identifier must start with character from set 'a-zA-Z_'");
312 nextSequenceEnd(IDENTIFIER);
313 return data.substring(start, offset);
316 private void nextSequenceEnd(CharMatcher matcher) {
317 while(!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {
322 private void checkValidQuotation(char quoteChar) {
324 SQUOTE.matches(quoteChar) || DQUOTE.matches(quoteChar),
325 "Value must be qoute escaped with ''' or '\"'.");