2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2024 Ericsson
4 * Modifications Copyright (C) 2024 OpenInfra Foundation Europe
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
21 package org.oran.smo.yangtools.parser.util;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.Objects;
30 import org.oran.smo.yangtools.parser.PrefixResolver;
31 import org.oran.smo.yangtools.parser.model.schema.ModuleAndNamespaceResolver;
34 * Represents instance-identifiers as specified in RFC7950, clause 9.13
36 * @author Mark Hollmann
38 public class InstanceIdentifier {
41 * Given an II encoded as specified in RFC7950, clause 9.13, returns the II. More specifically,
42 * each part of the II must have a prefix that is resolvable to a namespace given the supplied
45 * Note especially this bit from the RFC: "All node names in an instance-identifier value MUST
46 * be qualified with explicit namespace prefixes, and these prefixes MUST be declared in the
47 * XML namespace scope in the instance-identifier's XML element."
49 * Since the input is XML, the module name for each step will remain unresolved. Use method
50 * resolveModuleOrNamespace() to resolve the module names for each step. Not doing so may cause
51 * problems when comparing II objects at a later stage.
53 * Will throw a RuntimeException if the syntax of the supplied string is wrong.
55 public static InstanceIdentifier parseXmlEncodedString(final String input, final PrefixResolver prefixResolver) {
56 return parseEncodedString(Objects.requireNonNull(input), Objects.requireNonNull(prefixResolver));
60 * Given an II encoded as specified in RFC7951, clause 6.11, returns the II.
62 * Note especially this bit from the RFC: "The leftmost (top-level) data node
63 * name is always in the namespace-qualified form. Any subsequent data node
64 * name is in the namespace-qualified form if the node is defined in a module
65 * other than its parent node, and the simple form is used otherwise. This
66 * rule also holds for node names appearing in predicates."
68 * Since the input is JSON, the module namespace for each step will remain unresolved. Use method
69 * resolveModuleOrNamespace() to resolve the namespace for each step. Not doing so may cause
70 * problems when comparing II objects at a later stage.
72 * Will throw a RuntimeException if the syntax of the supplied string is wrong.
74 public static InstanceIdentifier parseJsonEncodedString(final String input) {
75 return parseEncodedString(Objects.requireNonNull(input), null);
79 * Parses the supplied input string into an instance-identifier. If a prefix resolver
80 * is supplied, this method will assume that the source of the input was XML, and will
81 * treat data node prefixes as XML prefixes - otherwise, data node prefixes are assumed
84 * Will throw a RuntimeException if the syntax of the supplied string is wrong.
86 public static InstanceIdentifier parseEncodedString(final String input, final PrefixResolver prefixResolver) {
89 final List<IiXPathToken> tokens = tokenize(input);
90 final List<Step> steps = extractSteps(tokens, prefixResolver);
91 return new InstanceIdentifier(steps);
92 } catch (final IndexOutOfBoundsException ioobex) {
93 throw new RuntimeException(
94 "Syntax error in input string. Check for incorrect syntax towards the end of the XPath expression, and missing brackets / quotes.");
98 private static List<Step> extractSteps(final List<IiXPathToken> tokens, final PrefixResolver prefixResolver) {
100 final List<Step> steps = new ArrayList<>();
103 * Each part of the path is in the form:
105 * <slash><optional-prefix-and-colon><data-node-name><optional-predicate(s)>
111 if (index == tokens.size()) { // Nothing left to consume, we are done.
115 if (tokens.get(index).type != IiXPathToken.Type.SLASH) {
116 throw new RuntimeException("Expected a slash at pos " + tokens.get(index).pos);
122 * We extract the data node identifier. This will create a new Part object.
124 index = extractStepDataNodeIdentifier(tokens, index, prefixResolver, steps);
127 * Extract the predicates, if any.
129 index = extractPredicates(tokens, index, prefixResolver, steps);
133 private static int extractStepDataNodeIdentifier(final List<IiXPathToken> tokens, final int index,
134 final PrefixResolver prefixResolver, final List<Step> steps) {
136 final NamespaceModuleIdentifier unprefixedName = getUnprefixedName(tokens, index, steps,
137 STEP_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
138 final NamespaceModuleIdentifier prefixedName = unprefixedName != null ?
140 getPrefixedName(tokens, index, prefixResolver, steps, STEP_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
142 if (unprefixedName == null && prefixedName == null) {
143 throw new RuntimeException("Unresolvable data node name at pos " + tokens.get(index).pos);
146 steps.add(new Step(unprefixedName != null ? unprefixedName : prefixedName));
148 return index + (unprefixedName != null ? 1 : 3);
151 private static int extractPredicates(final List<IiXPathToken> tokens, int index, final PrefixResolver prefixResolver,
152 final List<Step> steps) {
155 * A predicate may refer to:
157 * - A list entry, and the list is identified by one or more keys.
158 * Example: /ex:system/ex:server[ex:ip='192.0.2.1'][ex:port='80']
160 * - A list entry, and the entry is identified by its position (since the list does not have keys):
161 * Example: /ex:stats/ex:port[3]
163 * - A leaf-list entry, identified by value (since configurable leaf-lists must be unique).
164 * Example: /ex:system/ex:services/ex:ssh/ex:cipher[.='blowfish-cbc']
166 * - A leaf-list entry, identified by position (since state leaf-lists may be non-unique).
167 * Example: /ex:stats/ex:cpuLoad[3]
170 if (index == tokens.size()) { // end of tokens
173 if (tokens.get(index).type != IiXPathToken.Type.SQUARE_BRACKET_OPEN) { // there is no predicate
177 final Step step = steps.get(steps.size() - 1);
180 * Figure out the kind of predicate - keys, value or position
183 if (tokens.get(index + 1).type == IiXPathToken.Type.UNQUOTED_STRING && tokens.get(
184 index + 2).type == IiXPathToken.Type.SQUARE_BRACKET_CLOSE) {
186 * Position. The string must be parseable to an integer.
188 * NOTE that according to the XPath spec, position indexes start at 1, and not 0 as is customary in programming languages.
191 final int pos = Integer.parseInt(tokens.get(index + 1).value);
193 throw new RuntimeException("Position '" + pos + "' is illegal. Position must be larger/equal to 1.");
195 step.setPredicateListEntryOrLeafListMemberIndex(pos);
197 } catch (final NumberFormatException nfex) {
198 throw new RuntimeException("Predicate '" + tokens.get(
199 index + 1).value + "' not parseable to a position value.");
203 if (tokens.get(index + 1).type == IiXPathToken.Type.DOT && tokens.get(
204 index + 2).type == IiXPathToken.Type.EQUALS && (tokens.get(
205 index + 3).type == IiXPathToken.Type.QUOTED_STRING || tokens.get(
206 index + 3).type == IiXPathToken.Type.UNQUOTED_STRING) && tokens.get(
207 index + 4).type == IiXPathToken.Type.SQUARE_BRACKET_CLOSE) {
211 step.setPredicateLeafListMemberValue(tokens.get(index + 3).value);
216 * Must be one or more key values
220 if (index == tokens.size()) { // end of tokens
223 if (tokens.get(index).type != IiXPathToken.Type.SQUARE_BRACKET_OPEN) { // done with predicates
227 index++; // consume the [
230 * The data node may be prefixed or not...
232 final NamespaceModuleIdentifier unprefixedName = getUnprefixedName(tokens, index, steps,
233 PREDICATE_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
234 final NamespaceModuleIdentifier prefixedName = unprefixedName != null ?
236 getPrefixedName(tokens, index, prefixResolver, steps, PREDICATE_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
238 if (unprefixedName == null && prefixedName == null) {
239 throw new RuntimeException("Unresolvable data node name at pos " + tokens.get(index).pos);
242 index += (unprefixedName != null ? 1 : 3); // consume the data node name
244 index++; // consume the = // note no need to check that the token is a =, as this is used as terminating token when extracting the data node name.
247 * We are being lenient here. XPath spec says that LITERAL must be enclosed by single or double quotes.
248 * We will allow unquoted strings as well, as a lot of people will get this wrong. For the instance-identifier
249 * data type we can safely do this, as functions cannot be used inside an II.
251 if (tokens.get(index).type != IiXPathToken.Type.UNQUOTED_STRING && tokens.get(
252 index).type != IiXPathToken.Type.QUOTED_STRING) {
253 throw new RuntimeException("Expected a single- or double-quoted string at pos " + tokens.get(index).pos);
256 final String stringefiedKeyValue = tokens.get(index).value;
258 step.addPredicateKeyValue(unprefixedName != null ? unprefixedName : prefixedName, stringefiedKeyValue);
260 index++; // consume the value
262 if (tokens.get(index).type != IiXPathToken.Type.SQUARE_BRACKET_CLOSE) {
263 throw new RuntimeException("Expected a closing square brace ']' at pos " + tokens.get(index).pos);
266 index++; // consume the ]
270 private static final List<IiXPathToken.Type> STEP_DATA_NODE_ALLOWED_TERMINATING_TOKENS = Arrays.asList(
271 IiXPathToken.Type.SQUARE_BRACKET_OPEN, IiXPathToken.Type.SLASH);
272 private static final List<IiXPathToken.Type> PREDICATE_DATA_NODE_ALLOWED_TERMINATING_TOKENS = Arrays.asList(
273 IiXPathToken.Type.EQUALS);
275 private static NamespaceModuleIdentifier getUnprefixedName(final List<IiXPathToken> tokens, final int index,
276 final List<Step> steps, final List<IiXPathToken.Type> allowedTerminatingToken) {
279 * An unprefixed name simply has a name, followed by either nothing, or a predicate, or the next step.
281 final boolean unprefixedName = (tokens.get(
282 index).type == IiXPathToken.Type.UNQUOTED_STRING) && ((index + 1 == tokens
283 .size()) || allowedTerminatingToken.contains(tokens.get(index + 1).type));
284 if (!unprefixedName) {
288 if (steps.isEmpty()) {
289 throw new RuntimeException("The first step of the path must have a prefix / module-name.");
292 final String dataNodeIdentifier = tokens.get(index).value;
295 * We need to take the module name/namespace from the parent data node, i.e. the preceding step.
297 final NamespaceModuleIdentifier parentDataNodeNsmi = steps.get(steps.size() - 1).getDataNodeNsai();
298 return new NamespaceModuleIdentifier(parentDataNodeNsmi.getNamespace(), parentDataNodeNsmi.getModuleName(),
302 private static NamespaceModuleIdentifier getPrefixedName(final List<IiXPathToken> tokens, final int index,
303 final PrefixResolver prefixResolver, final List<Step> steps,
304 final List<IiXPathToken.Type> allowedTerminatingToken) {
307 * An prefixed name is in the form <string><colon><string>, followed by either nothing, or a predicate, or the next step.
309 final boolean prefixedName = (tokens.get(index).type == IiXPathToken.Type.UNQUOTED_STRING) && (tokens.get(
310 index + 1).type == IiXPathToken.Type.COLON) && (tokens.get(
311 index + 2).type == IiXPathToken.Type.UNQUOTED_STRING) && ((index + 3 == tokens
312 .size()) || allowedTerminatingToken.contains(tokens.get(index + 3).type));
317 final String prefixOrModuleName = tokens.get(index).value;
318 final String dataNodeIdentifier = tokens.get(index + 2).value;
321 * The prefix can be either a module name (input was JSON) or an actual XML prefix (input was XML).
323 final String moduleName = prefixResolver == null ? prefixOrModuleName : null;
324 final String namespace = prefixResolver != null ? prefixResolver.resolveNamespaceUri(prefixOrModuleName) : null;
326 if (moduleName == null && namespace == null) {
327 throw new RuntimeException("Unresolvable prefix '" + prefixOrModuleName + "' at pos " + tokens.get(index).pos);
330 return new NamespaceModuleIdentifier(namespace, moduleName, dataNodeIdentifier);
334 * A step in an Instance Identifier. A step is identified by a data node (itself identified through a
335 * module-name/namespace, and a data node identifier), and possibly a predicate.
337 public static class Step {
339 private final NamespaceModuleIdentifier nsai;
341 private Map<NamespaceModuleIdentifier, String> predicateKeyValues;
343 private String predicateLeafListMemberValue;
345 private Integer predicateListEntryOrLeafListMemberIndex;
347 public Step(final NamespaceModuleIdentifier nsai) {
348 this.nsai = Objects.requireNonNull(nsai);
352 * Returns the NSAI of the data node. Note that depending on the input, either the module name or
353 * namespace may be null, unless resolved (see method resolveModuleOrNamespace() in InstanceIdentifier).
355 public NamespaceModuleIdentifier getDataNodeNsai() {
359 public Step addPredicateKeyValue(final NamespaceModuleIdentifier nsai, final String value) {
360 if (predicateLeafListMemberValue != null || predicateListEntryOrLeafListMemberIndex != null) {
361 throw new RuntimeException(
362 "A step can only, at most, contain either: predicate(s), or leaf-list value, or list entry index.");
365 if (predicateKeyValues == null) {
366 predicateKeyValues = new HashMap<>();
368 predicateKeyValues.put(Objects.requireNonNull(nsai), Objects.requireNonNull(value));
373 * Returns the predicate key values. Each predicate is identified by the data node NSAI, and a value.
374 * When comparing values, data conversion may have to be performed by a client. Note that depending
375 * on the input, either the module name or namespace of the data node NSAI may be null, unless
376 * resolved (see method resolveModuleOrNamespace() in InstanceIdentifier).
378 * Returns null if there are no key values as predicate.
380 public Map<NamespaceModuleIdentifier, String> getPredicateKeyValues() {
381 return predicateKeyValues;
384 public Step setPredicateLeafListMemberValue(final String leafListMemberValue) {
385 if (predicateKeyValues != null || predicateListEntryOrLeafListMemberIndex != null) {
386 throw new RuntimeException(
387 "A step can only, at most, contain either: predicate(s), or leaf-list value, or list entry index.");
390 this.predicateLeafListMemberValue = Objects.requireNonNull(leafListMemberValue);
395 * Returns the value, if any, of a leaf-list member tested for as part of the predicate. When comparing
396 * values, data conversion may have to be performed by a client.
398 public String getPredicateLeafListMemberValue() {
399 return predicateLeafListMemberValue;
402 public Step setPredicateListEntryOrLeafListMemberIndex(final int listEntryOrLeafListMemberIndex) {
403 if (predicateLeafListMemberValue != null || predicateKeyValues != null) {
404 throw new RuntimeException(
405 "A step can only, at most, contain either: predicate(s), or leaf-list value, or list entry index.");
408 if (listEntryOrLeafListMemberIndex < 1) {
409 throw new RuntimeException(
410 "The index value must be >= 1 (in XPath, the index of the first element is 1, not 0).");
413 this.predicateListEntryOrLeafListMemberIndex = listEntryOrLeafListMemberIndex;
418 * Returns the index, if any, of the list entry or leaf-list entry. Returns null if there is no
419 * index defined as part of the predicate.
421 * <b>NOTE that according to the XPath spec position indexes start at 1, and not 0 as is customary
422 * in programming languages.</b> When testing against arrays or lists in Java, the returned value
423 * must therefore be reduced by one.
425 public Integer getPredicateListEntryOrLeafListMemberIndex() {
426 return predicateListEntryOrLeafListMemberIndex;
429 public void resolveModuleOrNamespace(final ModuleAndNamespaceResolver namespaceResolver) {
430 nsai.resolveModuleOrNamespace(namespaceResolver);
431 if (predicateKeyValues != null) {
432 predicateKeyValues.keySet().forEach(k -> k.resolveModuleOrNamespace(namespaceResolver));
437 public int hashCode() {
438 return toString().hashCode();
442 public boolean equals(Object obj) {
443 if (!(obj instanceof Step)) {
447 final Step other = (Step) obj;
449 if (!this.nsai.equals(other.nsai)) {
453 if (!Objects.equals(this.predicateKeyValues, other.predicateKeyValues)) {
457 if (!Objects.equals(this.predicateLeafListMemberValue, other.predicateLeafListMemberValue)) {
461 return Objects.equals(this.predicateListEntryOrLeafListMemberIndex,
462 other.predicateListEntryOrLeafListMemberIndex);
466 public String toString() {
467 final StringBuilder sb = new StringBuilder();
468 sb.append("step node=").append(nsai);
469 if (predicateKeyValues != null) {
470 sb.append("; keys/values=").append(predicateKeyValues);
472 if (predicateLeafListMemberValue != null) {
473 sb.append("; value=").append(predicateLeafListMemberValue);
475 if (predicateListEntryOrLeafListMemberIndex != null) {
476 sb.append("; index=").append(predicateListEntryOrLeafListMemberIndex);
478 return sb.toString();
482 private static List<IiXPathToken> tokenize(final String s) {
484 final List<IiXPathToken> result = new ArrayList<>(50);
488 while (charPos < s.length()) {
490 c = s.charAt(charPos);
494 result.add(IiXPathToken.newSlash(charPos));
497 result.add(IiXPathToken.newColon(charPos));
500 result.add(IiXPathToken.newSquareBracketOpen(charPos));
503 result.add(IiXPathToken.newSquareBracketClose(charPos));
506 result.add(IiXPathToken.newEquals(charPos));
509 result.add(IiXPathToken.newDot(charPos));
514 * We really don't expect whitespaces in the II, but according to XPath spec these are actually allowed.
518 charPos = tokenizeExtractString(s, charPos, result);
529 * Returns the last character consumed.
531 private static int tokenizeExtractString(final String s, int charPos, final List<IiXPathToken> result) {
534 * The production for LITERAL is quite clear in the XPath spec - it is either single-quoted-enclosed text,
535 * or double-quoted-enclosed text. No escaping is applied/allowed within the LITERAL.
537 * This method also extracts QNames, so we also allow unquoted strings. Different tokens are produced thus.
540 final boolean stringIsSingleQuoted = (s.charAt(charPos) == '\'');
541 final boolean stringIsDoubleQuoted = (s.charAt(charPos) == '"');
543 if (stringIsSingleQuoted || stringIsDoubleQuoted) {
548 final StringBuilder sb = new StringBuilder();
550 while (charPos < s.length()) {
551 c = s.charAt(charPos);
555 if (stringIsSingleQuoted) { // Done. Swallow the trailing ' character
556 result.add(IiXPathToken.newQuotedStringToken(charPos, sb.toString()));
559 if (stringIsDoubleQuoted) { // OK - single quote in double-quoted string.
562 throw new RuntimeException("Single quote character may not appear at position " + charPos);
566 if (stringIsDoubleQuoted) { // Done. Swallow the trailing " character
567 result.add(IiXPathToken.newQuotedStringToken(charPos, sb.toString()));
570 if (stringIsSingleQuoted) { // OK - double quote in single-quoted string.
573 throw new RuntimeException("Double quote character may not appear at position " + charPos);
584 if (stringIsSingleQuoted || stringIsDoubleQuoted) {
590 result.add(IiXPathToken.newUnquotedStringToken(charPos, sb.toString()));
602 if (stringIsSingleQuoted || stringIsDoubleQuoted) {
603 throw new RuntimeException("Single/double-quoted string not correctly terminated.");
606 result.add(IiXPathToken.newUnquotedStringToken(charPos, sb.toString()));
611 private static class IiXPathToken {
613 private static IiXPathToken newSlash(final int pos) {
614 final IiXPathToken token = new IiXPathToken();
615 token.type = Type.SLASH;
621 private static IiXPathToken newColon(final int pos) {
622 final IiXPathToken token = new IiXPathToken();
623 token.type = Type.COLON;
629 private static IiXPathToken newSquareBracketOpen(final int pos) {
630 final IiXPathToken token = new IiXPathToken();
631 token.type = Type.SQUARE_BRACKET_OPEN;
637 private static IiXPathToken newSquareBracketClose(final int pos) {
638 final IiXPathToken token = new IiXPathToken();
639 token.type = Type.SQUARE_BRACKET_CLOSE;
645 private static IiXPathToken newEquals(final int pos) {
646 final IiXPathToken token = new IiXPathToken();
647 token.type = Type.EQUALS;
653 private static IiXPathToken newDot(final int pos) {
654 final IiXPathToken token = new IiXPathToken();
655 token.type = Type.DOT;
661 private static IiXPathToken newQuotedStringToken(final int pos, final String str) {
662 final IiXPathToken token = new IiXPathToken();
663 token.type = Type.QUOTED_STRING;
669 private static IiXPathToken newUnquotedStringToken(final int pos, final String str) {
670 final IiXPathToken token = new IiXPathToken();
671 token.type = Type.UNQUOTED_STRING;
677 private Type type = null;
679 private String value = null;
681 private IiXPathToken() {
688 SQUARE_BRACKET_CLOSE,
696 private final List<Step> steps;
698 public InstanceIdentifier(final List<Step> steps) {
702 public List<Step> getSteps() {
707 * Depending on whether prefixes or module names were used as part of the II, the respective
708 * other may not have been resolved. Use this method to perform the resolution.
710 public void resolveModuleOrNamespace(final ModuleAndNamespaceResolver namespaceResolver) {
711 steps.forEach(s -> s.resolveModuleOrNamespace(namespaceResolver));
715 public int hashCode() {
716 return toString().hashCode();
720 public boolean equals(Object obj) {
721 if (!(obj instanceof InstanceIdentifier)) {
725 final InstanceIdentifier other = (InstanceIdentifier) obj;
726 return this.steps.equals(other.steps);
730 public String toString() {
731 return "II " + steps.toString();