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 char SLASH = '/';
56 private static final char BACKSLASH = '\\';
57 private static final char COLON = ':';
58 private static final char DOT = '.';
59 private static final char EQUALS = '=';
60 private static final char PRECONDITION_START = '[';
61 private static final char PRECONDITION_END = ']';
62 private static final char SQUOT = '\'';
63 private static final char DQUOT = '"';
65 private final List<PathArgument> product = new ArrayList<>();
66 private final AbstractStringInstanceIdentifierCodec codec;
67 private final SchemaInferenceStack stack;
68 private final String data;
70 private DataSchemaContextNode<?> current;
71 private QNameModule lastModule;
74 XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) {
75 this.codec = requireNonNull(codec);
76 this.data = requireNonNull(data);
79 final DataSchemaContextTree tree = codec.getDataContextTree();
80 stack = SchemaInferenceStack.of(tree.getEffectiveModelContext());
81 current = tree.getRoot();
85 * Parse input string and return the corresponding list of {@link PathArgument}s.
87 * @return List of PathArguments
88 * @throws IllegalArgumentException if the input string is not valid
90 @NonNull List<PathArgument> build() {
91 while (!allCharactersConsumed()) {
92 product.add(computeNextArgument());
94 return ImmutableList.copyOf(product);
97 private PathArgument computeNextArgument() {
98 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
100 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
101 final QName name = nextQName();
103 lastModule = name.getModule();
104 if (allCharactersConsumed() || SLASH == currentChar()) {
105 return computeIdentifier(name);
108 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
109 return computeIdentifierWithPredicate(name);
112 private DataSchemaContextNode<?> nextContextNode(final QName name) {
113 current = current.getChild(name);
114 checkValid(current != null, "%s is not correct schema node identifier.", name);
115 while (current.isMixin()) {
116 product.add(current.getIdentifier());
117 current = current.getChild(name);
119 stack.enterDataTree(name);
124 * Creates path argument with predicates and sets offset
125 * to end of path argument.
128 * predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
129 * predicate-expr = (node-identifier / ".") *WSP "=" *WSP
130 * ((DQUOTE string DQUOTE) /
131 * (SQUOTE string SQUOTE))
132 * pos = non-negative-integer-value
135 * @param name QName of node, for which predicates are computed.
136 * @return PathArgument representing node selection with predictes
138 private PathArgument computeIdentifierWithPredicate(final QName name) {
139 final DataSchemaContextNode<?> currentNode = nextContextNode(name);
140 checkValid(currentNode.isKeyedEntry(), "Entry %s does not allow specifying predicates.", name);
142 ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
143 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
147 if (DOT == currentChar()) {
154 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
156 final String keyValue = nextQuotedValue();
158 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
160 // Break-out from method for leaf-list case
161 if (key == null && currentNode.isLeaf()) {
162 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
163 final Object value = codec.deserializeKeyValue(currentNode.getDataSchemaNode(),
164 type -> resolveLeafref(currentNode.getIdentifier().getNodeType(), type), keyValue);
165 return new NodeWithValue<>(name, value);
167 final DataSchemaContextNode<?> keyNode = currentNode.getChild(key);
168 checkValid(keyNode != null, "%s is not correct schema node identifier.", key);
169 final Object value = codec.deserializeKeyValue(keyNode.getDataSchemaNode(),
170 type -> resolveLeafref(key, type), keyValue);
171 keyValues.put(key, value);
173 return NodeIdentifierWithPredicates.of(name, keyValues.build());
176 private @NonNull TypeDefinition<?> resolveLeafref(final QName qname, final LeafrefTypeDefinition type) {
177 final SchemaInferenceStack tmp = stack.copy();
178 tmp.enterDataTree(qname);
179 return tmp.resolveLeafref(type);
182 private PathArgument computeIdentifier(final QName name) {
183 DataSchemaContextNode<?> currentNode = nextContextNode(name);
184 checkValid(!currentNode.isKeyedEntry(), "Entry %s requires key or value predicate to be present", name);
185 return currentNode.getIdentifier();
189 * Returns following QName and sets offset to end of QName.
191 * @return following QName.
193 private QName nextQName() {
194 // Consume prefix or identifier
195 final String maybePrefix = nextIdentifier();
196 if (!allCharactersConsumed() && COLON == currentChar()) {
197 // previous token is prefix
199 return codec.createQName(maybePrefix, nextIdentifier());
202 return codec.createQName(lastModule, maybePrefix);
206 * Returns true if all characters from input string were consumed.
208 * @return true if all characters from input string were consumed.
210 private boolean allCharactersConsumed() {
211 return offset == data.length();
215 * Skips current char if it equals expected otherwise fails parsing.
217 * @param expected Expected character
218 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
220 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
221 checkValid(expected == currentChar(), errorMsg);
226 * Fails parsing if a condition is not met.
229 * In case of error provides pointer to failed instance identifier,
230 * offset on which failure occurred with explanation.
232 * @param condition Fails parsing if {@code condition} is false
233 * @param errorMsg Error message which will be provided to user.
235 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
237 throw iae(errorMsg, attributes);
241 private @NonNull IllegalArgumentException iae(final String errorMsg, final Object... attributes) {
242 return new IllegalArgumentException("Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s"
243 .formatted(data, offset, errorMsg.formatted(attributes)));
247 * Returns following value of quoted literal (without quotes) and sets offset after literal.
249 * @return String literal
251 private String nextQuotedValue() {
252 return switch (currentChar()) {
253 case SQUOT -> nextSingleQuotedValue();
254 case DQUOT -> nextDoubleQuotedValue();
255 default -> throw iae("Value must be quote escaped with ''' or '\"'.");
259 // Simple: just look for the matching single quote and return substring
260 private String nextSingleQuotedValue() {
262 final int start = offset;
263 final int end = data.indexOf(SQUOT, start);
264 checkValid(end != -1, "Closing single quote not found");
267 return data.substring(start, end);
270 // Complicated: we need to potentially un-escape
271 private String nextDoubleQuotedValue() {
274 final int maxIndex = data.length() - 1;
275 final var sb = new StringBuilder();
277 final int nextStart = offset;
279 // Find next double quotes
280 final int nextEnd = data.indexOf(DQUOT, nextStart);
281 checkValid(nextEnd != -1, "Closing double quote not found");
284 // Find next backslash
285 final int nextBackslash = data.indexOf(BACKSLASH, nextStart);
286 if (nextBackslash == -1 || nextBackslash > nextEnd) {
287 // No backslash between nextStart and nextEnd -- just copy characters and terminate
290 return sb.append(data, nextStart, nextEnd).toString();
293 // Validate escape completeness and append buffer
294 checkValid(nextBackslash != maxIndex, "Incomplete escape");
295 sb.append(data, nextStart, nextBackslash);
297 // Adjust offset before potentially referencing it and
298 offset = nextBackslash;
299 sb.append(unescape(data.charAt(nextBackslash + 1)));
302 offset = nextBackslash + 2;
306 // As per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
307 private char unescape(final char escape) {
308 return switch (escape) {
312 case BACKSLASH -> BACKSLASH;
313 default -> throw iae("Unrecognized escape");
318 * Returns character at current offset.
320 * @return character at current offset.
322 private char currentChar() {
323 return data.charAt(offset);
327 * Increases processing offset by 1.
329 private void skipCurrentChar() {
334 * Skip whitespace characters, sets offset to first following non-whitespace character.
336 private void skipWhitespaces() {
337 nextSequenceEnd(WSP);
341 * Returns string which matches IDENTIFIER YANG ABNF token
342 * and sets processing offset after end of identifier.
344 * @return string which matches IDENTIFIER YANG ABNF token
346 private String nextIdentifier() {
347 checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
348 "Identifier must start with character from set 'a-zA-Z_'");
349 final int start = offset;
350 nextSequenceEnd(IDENTIFIER);
351 return data.substring(start, offset);
354 private void nextSequenceEnd(final CharMatcher matcher) {
355 while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {