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 static java.util.Objects.requireNonNull;
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;
25 * Iterator which lazily parses {@link PathArgument} from string representation.
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.
33 final class XpathStringParsingPathArgumentBuilder implements Builder<List<PathArgument>> {
36 * Matcher matching WSP YANG ABNF token.
38 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
41 * Matcher matching IDENTIFIER first char token.
43 private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
44 .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
47 * Matcher matching IDENTIFIER token.
49 private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
50 .or(CharMatcher.anyOf(".-")).precomputed();
52 private static final CharMatcher QUOTE = CharMatcher.anyOf("'\"");
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 = ']';
61 private final List<PathArgument> product = new ArrayList<>();
62 private final AbstractStringInstanceIdentifierCodec codec;
63 private final String data;
65 private DataSchemaContextNode<?> current;
66 private QNameModule lastModule;
69 XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) {
70 this.codec = requireNonNull(codec);
71 this.data = requireNonNull(data);
72 this.current = codec.getDataContextTree().getRoot();
77 public List<PathArgument> build() {
78 while (!allCharactersConsumed()) {
79 product.add(computeNextArgument());
81 return ImmutableList.copyOf(product);
84 private PathArgument computeNextArgument() {
85 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
87 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
88 final QName name = nextQName();
90 lastModule = name.getModule();
91 if (allCharactersConsumed() || SLASH == currentChar()) {
92 return computeIdentifier(name);
95 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
96 return computeIdentifierWithPredicate(name);
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);
110 * Creates path argument with predicates and sets offset
111 * to end of path argument.
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
121 * @param name QName of node, for which predicates are computed.
122 * @return PathArgument representing node selection with predictes
124 private PathArgument computeIdentifierWithPredicate(final QName name) {
125 DataSchemaContextNode<?> currentNode = nextContextNode(name);
126 checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
128 ImmutableMap.Builder<QName,Object> keyValues = ImmutableMap.builder();
129 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
133 if (DOT == currentChar()) {
140 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
142 final String keyValue = nextQuotedValue();
144 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
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);
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);
156 return NodeIdentifierWithPredicates.of(name, keyValues.build());
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();
167 * Returns following QName and sets offset to end of QName.
169 * @return following QName.
171 private QName nextQName() {
172 // Consume prefix or identifier
173 final String maybePrefix = nextIdentifier();
174 if (!allCharactersConsumed() && COLON == currentChar()) {
175 // previous token is prefix
177 return codec.createQName(maybePrefix, nextIdentifier());
180 return codec.createQName(lastModule, maybePrefix);
184 * Returns true if all characters from input string were consumed.
186 * @return true if all characters from input string were consumed.
188 private boolean allCharactersConsumed() {
189 return offset == data.length();
193 * Skips current char if it equals expected otherwise fails parsing.
195 * @param expected Expected character
196 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
198 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
199 checkValid(expected == currentChar(), errorMsg);
204 * Fails parsing if a condition is not met.
207 * In case of error provides pointer to failed instance identifier,
208 * offset on which failure occurred with explanation.
210 * @param condition Fails parsing if {@code condition} is false
211 * @param errorMsg Error message which will be provided to user.
213 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
215 throw new IllegalArgumentException(String.format(
216 "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
217 String.format(errorMsg, attributes)));
222 * Returns following value of quoted literal (without quotes) and sets offset after literal.
224 * @return String literal
226 private String nextQuotedValue() {
227 final char quoteChar = currentChar();
228 checkValid(QUOTE.matches(quoteChar), "Value must be qoute escaped with ''' or '\"'.");
230 final int valueStart = offset;
231 final int endQoute = data.indexOf(quoteChar, offset);
232 final String value = data.substring(valueStart, endQoute);
239 * Returns character at current offset.
241 * @return character at current offset.
243 private char currentChar() {
244 return data.charAt(offset);
248 * Increases processing offset by 1.
250 private void skipCurrentChar() {
255 * Skip whitespace characters, sets offset to first following non-whitespace character.
257 private void skipWhitespaces() {
258 nextSequenceEnd(WSP);
262 * Returns string which matches IDENTIFIER YANG ABNF token
263 * and sets processing offset after end of identifier.
265 * @return string which matches IDENTIFIER YANG ABNF token
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);
275 private void nextSequenceEnd(final CharMatcher matcher) {
276 while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {