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.NodeIdentifier;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
25 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.Composite;
26 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
27 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.SimpleValue;
28 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
29 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
30 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
33 * Iterator which lazily parses {@link PathArgument} from string representation.
36 * Note that invocation of {@link #hasNext()} or {@link #next()} may result in
37 * throwing of {@link IllegalArgumentException} if underlying string representation
38 * is not correctly serialized or does not represent instance identifier valid
39 * for associated schema context.
41 final class XpathStringParsingPathArgumentBuilder implements Mutable {
43 * Matcher matching WSP YANG ABNF token.
45 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
48 * Matcher matching IDENTIFIER first char token.
50 private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
51 .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
54 * Matcher matching IDENTIFIER token.
56 private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
57 .or(CharMatcher.anyOf(".-")).precomputed();
59 private static final char SLASH = '/';
60 private static final char BACKSLASH = '\\';
61 private static final char COLON = ':';
62 private static final char DOT = '.';
63 private static final char EQUALS = '=';
64 private static final char PRECONDITION_START = '[';
65 private static final char PRECONDITION_END = ']';
66 private static final char SQUOT = '\'';
67 private static final char DQUOT = '"';
69 private final List<PathArgument> product = new ArrayList<>();
70 private final AbstractStringInstanceIdentifierCodec codec;
71 private final SchemaInferenceStack stack;
72 private final String data;
74 private DataSchemaContext current;
75 private QNameModule lastModule;
78 XpathStringParsingPathArgumentBuilder(final AbstractStringInstanceIdentifierCodec codec, final String data) {
79 this.codec = requireNonNull(codec);
80 this.data = requireNonNull(data);
83 final DataSchemaContextTree tree = codec.getDataContextTree();
84 stack = SchemaInferenceStack.of(tree.modelContext());
85 current = tree.getRoot();
89 * Parse input string and return the corresponding list of {@link PathArgument}s.
91 * @return List of PathArguments
92 * @throws IllegalArgumentException if the input string is not valid
94 @NonNull List<PathArgument> build() {
95 while (!allCharactersConsumed()) {
96 product.add(computeNextArgument());
98 return ImmutableList.copyOf(product);
101 private PathArgument computeNextArgument() {
102 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
104 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
105 final QName name = nextQName();
107 lastModule = name.getModule();
108 if (allCharactersConsumed() || SLASH == currentChar()) {
109 return computeIdentifier(name);
112 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
113 return computeIdentifierWithPredicate(name);
116 private DataSchemaContext nextContextNode(final QName qname) {
117 current = getChild(current, qname);
118 checkValid(current != null, "%s is not correct schema node identifier.", qname);
119 while (current instanceof PathMixin mixin) {
120 product.add(mixin.mixinPathStep());
121 current = getChild(current, qname);
123 stack.enterDataTree(qname);
127 private static DataSchemaContext getChild(final DataSchemaContext parent, final QName qname) {
128 return parent instanceof Composite composite ? composite.childByQName(qname) : null;
132 * Creates path argument with predicates and sets offset
133 * to end of path argument.
136 * predicate = "[" *WSP (predicate-expr / pos) *WSP "]"
137 * predicate-expr = (node-identifier / ".") *WSP "=" *WSP
138 * ((DQUOTE string DQUOTE) /
139 * (SQUOTE string SQUOTE))
140 * pos = non-negative-integer-value
143 * @param name QName of node, for which predicates are computed.
144 * @return PathArgument representing node selection with predictes
146 private PathArgument computeIdentifierWithPredicate(final QName name) {
147 final var currentNode = nextContextNode(name);
148 if (currentNode.pathStep() != null) {
149 throw iae("Entry %s does not allow specifying predicates.", name);
152 final var keyValues = ImmutableMap.<QName, Object>builder();
153 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
157 if (DOT == currentChar()) {
164 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
166 final String keyValue = nextQuotedValue();
168 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
170 // Break-out from method for leaf-list case
171 if (key == null && currentNode instanceof SimpleValue) {
172 checkValid(offset == data.length(), "Leaf argument must be last argument of instance identifier.");
173 final var currentSchema = currentNode.dataSchemaNode();
175 final Object value = codec.deserializeKeyValue(currentSchema,
176 type -> resolveLeafref(currentSchema.getQName(), type), keyValue);
177 return new NodeWithValue<>(name, value);
179 final var keyNode = currentNode instanceof Composite composite ? composite.childByQName(key) : null;
180 if (keyNode == null) {
181 throw iae("%s is not correct schema node identifier.", key);
184 final Object value = codec.deserializeKeyValue(keyNode.dataSchemaNode(),
185 type -> resolveLeafref(key, type), keyValue);
186 keyValues.put(key, value);
188 return NodeIdentifierWithPredicates.of(name, keyValues.build());
191 private @NonNull TypeDefinition<?> resolveLeafref(final QName qname, final LeafrefTypeDefinition type) {
192 final SchemaInferenceStack tmp = stack.copy();
193 tmp.enterDataTree(qname);
194 return tmp.resolveLeafref(type);
197 private @NonNull NodeIdentifier computeIdentifier(final QName name) {
198 final var currentNode = nextContextNode(name);
199 final var currentArg = currentNode.pathStep();
200 if (currentArg == null) {
201 throw iae("Entry %s requires key or value predicate to be present", name);
207 * Returns following QName and sets offset to end of QName.
209 * @return following QName.
211 private @NonNull QName nextQName() {
212 // Consume prefix or identifier
213 final String maybePrefix = nextIdentifier();
214 if (!allCharactersConsumed() && COLON == currentChar()) {
215 // previous token is prefix
217 return codec.createQName(maybePrefix, nextIdentifier());
220 return codec.createQName(lastModule, maybePrefix);
224 * Returns true if all characters from input string were consumed.
226 * @return true if all characters from input string were consumed.
228 private boolean allCharactersConsumed() {
229 return offset == data.length();
233 * Skips current char if it equals expected otherwise fails parsing.
235 * @param expected Expected character
236 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
238 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
239 checkValid(expected == currentChar(), errorMsg);
244 * Fails parsing if a condition is not met.
247 * In case of error provides pointer to failed instance identifier,
248 * offset on which failure occurred with explanation.
250 * @param condition Fails parsing if {@code condition} is false
251 * @param errorMsg Error message which will be provided to user.
253 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
255 throw iae(errorMsg, attributes);
259 private @NonNull IllegalArgumentException iae(final String errorMsg, final Object... attributes) {
260 return new IllegalArgumentException("Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s"
261 .formatted(data, offset, errorMsg.formatted(attributes)));
265 * Returns following value of quoted literal (without quotes) and sets offset after literal.
267 * @return String literal
269 private String nextQuotedValue() {
270 return switch (currentChar()) {
271 case SQUOT -> nextSingleQuotedValue();
272 case DQUOT -> nextDoubleQuotedValue();
273 default -> throw iae("Value must be quote escaped with ''' or '\"'.");
277 // Simple: just look for the matching single quote and return substring
278 private String nextSingleQuotedValue() {
280 final int start = offset;
281 final int end = data.indexOf(SQUOT, start);
282 checkValid(end != -1, "Closing single quote not found");
285 return data.substring(start, end);
288 // Complicated: we need to potentially un-escape
289 private String nextDoubleQuotedValue() {
292 final int maxIndex = data.length() - 1;
293 final var sb = new StringBuilder();
295 final int nextStart = offset;
297 // Find next double quotes
298 final int nextEnd = data.indexOf(DQUOT, nextStart);
299 checkValid(nextEnd != -1, "Closing double quote not found");
302 // Find next backslash
303 final int nextBackslash = data.indexOf(BACKSLASH, nextStart);
304 if (nextBackslash == -1 || nextBackslash > nextEnd) {
305 // No backslash between nextStart and nextEnd -- just copy characters and terminate
308 return sb.append(data, nextStart, nextEnd).toString();
311 // Validate escape completeness and append buffer
312 checkValid(nextBackslash != maxIndex, "Incomplete escape");
313 sb.append(data, nextStart, nextBackslash);
315 // Adjust offset before potentially referencing it and
316 offset = nextBackslash;
317 sb.append(unescape(data.charAt(nextBackslash + 1)));
320 offset = nextBackslash + 2;
324 // As per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
325 private char unescape(final char escape) {
326 return switch (escape) {
330 case BACKSLASH -> BACKSLASH;
331 default -> throw iae("Unrecognized escape");
336 * Returns character at current offset.
338 * @return character at current offset.
340 private char currentChar() {
341 return data.charAt(offset);
345 * Increases processing offset by 1.
347 private void skipCurrentChar() {
352 * Skip whitespace characters, sets offset to first following non-whitespace character.
354 private void skipWhitespaces() {
355 nextSequenceEnd(WSP);
359 * Returns string which matches IDENTIFIER YANG ABNF token
360 * and sets processing offset after end of identifier.
362 * @return string which matches IDENTIFIER YANG ABNF token
364 private String nextIdentifier() {
365 checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
366 "Identifier must start with character from set 'a-zA-Z_'");
367 final int start = offset;
368 nextSequenceEnd(IDENTIFIER);
369 return data.substring(start, offset);
372 private void nextSequenceEnd(final CharMatcher matcher) {
373 while (!allCharactersConsumed() && matcher.matches(data.charAt(offset))) {