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.eclipse.jdt.annotation.NonNull;
18 import org.opendaylight.yangtools.concepts.Mutable;
19 import org.opendaylight.yangtools.yang.common.QName;
20 import org.opendaylight.yangtools.yang.common.QNameModule;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
24 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
25 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
26 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
29 * Iterator which lazily parses {@link PathArgument} from string representation.
32 * Note that invocation of {@link #hasNext()} or {@link #next()} may result in
33 * throwing of {@link IllegalArgumentException} if underlying string representation
34 * is not correctly serialized or does not represent instance identifier valid
35 * for associated schema context.
37 final class XpathStringParsingPathArgumentBuilder implements Mutable {
39 * Matcher matching WSP YANG ABNF token.
41 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
44 * Matcher matching IDENTIFIER first char token.
46 private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
47 .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
50 * Matcher matching IDENTIFIER token.
52 private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
53 .or(CharMatcher.anyOf(".-")).precomputed();
55 private static final CharMatcher QUOTE = CharMatcher.anyOf("'\"");
57 private static final char SLASH = '/';
58 private static final char COLON = ':';
59 private static final char DOT = '.';
60 private static final char EQUALS = '=';
61 private static final char PRECONDITION_START = '[';
62 private static final char PRECONDITION_END = ']';
64 private final List<PathArgument> product = new ArrayList<>();
65 private final AbstractStringInstanceIdentifierCodec codec;
66 private final SchemaInferenceStack stack;
67 private final String data;
69 private DataSchemaContextNode<?> current;
70 private QNameModule lastModule;
73 XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) {
74 this.codec = requireNonNull(codec);
75 this.data = requireNonNull(data);
78 final DataSchemaContextTree tree = codec.getDataContextTree();
79 stack = SchemaInferenceStack.of(tree.getEffectiveModelContext());
80 current = tree.getRoot();
84 * Parse input string and return the corresponding list of {@link PathArgument}s.
86 * @return List of PathArguments
87 * @throws IllegalArgumentException if the input string is not valid
89 @NonNull List<PathArgument> build() {
90 while (!allCharactersConsumed()) {
91 product.add(computeNextArgument());
93 return ImmutableList.copyOf(product);
96 private PathArgument computeNextArgument() {
97 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
99 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
100 final QName name = nextQName();
102 lastModule = name.getModule();
103 if (allCharactersConsumed() || SLASH == currentChar()) {
104 return computeIdentifier(name);
107 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
108 return computeIdentifierWithPredicate(name);
111 private DataSchemaContextNode<?> nextContextNode(final QName name) {
112 current = current.getChild(name);
113 checkValid(current != null, "%s is not correct schema node identifier.", name);
114 while (current.isMixin()) {
115 product.add(current.getIdentifier());
116 current = current.getChild(name);
118 stack.enterDataTree(name);
123 * Creates path argument with predicates and sets offset
124 * to end of path argument.
127 * predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
128 * predicate-expr = (node-identifier / ".") *WSP "=" *WSP
129 * ((DQUOTE string DQUOTE) /
130 * (SQUOTE string SQUOTE))
131 * pos = non-negative-integer-value
134 * @param name QName of node, for which predicates are computed.
135 * @return PathArgument representing node selection with predictes
137 private PathArgument computeIdentifierWithPredicate(final QName name) {
138 final DataSchemaContextNode<?> currentNode = nextContextNode(name);
139 checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
141 ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
142 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
146 if (DOT == currentChar()) {
153 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
155 final String keyValue = nextQuotedValue();
157 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
159 // Break-out from method for leaf-list case
160 if (key == null && currentNode.isLeaf()) {
161 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
162 return new NodeWithValue<>(name, keyValue);
164 final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
165 checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
166 final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(),
167 type -> resolveLeafref(key, type), keyValue);
168 keyValues.put(key, value);
170 return NodeIdentifierWithPredicates.of(name, keyValues.build());
173 private @NonNull TypeDefinition<?> resolveLeafref(final QName qname, final LeafrefTypeDefinition type) {
174 final SchemaInferenceStack tmp = stack.copy();
175 tmp.enterDataTree(qname);
176 return tmp.resolveLeafref(type);
179 private PathArgument computeIdentifier(final QName name) {
180 DataSchemaContextNode<?> currentNode = nextContextNode(name);
181 checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
182 return currentNode.getIdentifier();
186 * Returns following QName and sets offset to end of QName.
188 * @return following QName.
190 private QName nextQName() {
191 // Consume prefix or identifier
192 final String maybePrefix = nextIdentifier();
193 if (!allCharactersConsumed() && COLON == currentChar()) {
194 // previous token is prefix
196 return codec.createQName(maybePrefix, nextIdentifier());
199 return codec.createQName(lastModule, maybePrefix);
203 * Returns true if all characters from input string were consumed.
205 * @return true if all characters from input string were consumed.
207 private boolean allCharactersConsumed() {
208 return offset == data.length();
212 * Skips current char if it equals expected otherwise fails parsing.
214 * @param expected Expected character
215 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
217 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
218 checkValid(expected == currentChar(), errorMsg);
223 * Fails parsing if a condition is not met.
226 * In case of error provides pointer to failed instance identifier,
227 * offset on which failure occurred with explanation.
229 * @param condition Fails parsing if {@code condition} is false
230 * @param errorMsg Error message which will be provided to user.
232 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
234 throw new IllegalArgumentException(String.format(
235 "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
236 String.format(errorMsg, attributes)));
241 * Returns following value of quoted literal (without quotes) and sets offset after literal.
243 * @return String literal
245 private String nextQuotedValue() {
246 final char quoteChar = currentChar();
247 checkValid(QUOTE.matches(quoteChar), "Value must be qoute escaped with ''' or '\"'.");
249 final int valueStart = offset;
250 final int endQoute = data.indexOf(quoteChar, offset);
251 final String value = data.substring(valueStart, endQoute);
258 * Returns character at current offset.
260 * @return character at current offset.
262 private char currentChar() {
263 return data.charAt(offset);
267 * Increases processing offset by 1.
269 private void skipCurrentChar() {
274 * Skip whitespace characters, sets offset to first following non-whitespace character.
276 private void skipWhitespaces() {
277 nextSequenceEnd(WSP);
281 * Returns string which matches IDENTIFIER YANG ABNF token
282 * and sets processing offset after end of identifier.
284 * @return string which matches IDENTIFIER YANG ABNF token
286 private String nextIdentifier() {
287 checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
288 "Identifier must start with character from set 'a-zA-Z_'");
289 final int start = offset;
290 nextSequenceEnd(IDENTIFIER);
291 return data.substring(start, offset);
294 private void nextSequenceEnd(final CharMatcher matcher) {
295 while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {