c200da1364b1ee65112b2719d9fc8ff1e0ceaed4
[smo/teiv.git] /
1 /*
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
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
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.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21 package org.oran.smo.yangtools.parser.util;
22
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Objects;
29
30 import org.oran.smo.yangtools.parser.PrefixResolver;
31 import org.oran.smo.yangtools.parser.model.schema.ModuleAndNamespaceResolver;
32
33 /**
34  * Represents instance-identifiers as specified in RFC7950, clause 9.13
35  *
36  * @author Mark Hollmann
37  */
38 public class InstanceIdentifier {
39
40     /**
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
43      * prefix resolver.
44      * <p/>
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."
48      * <p/>
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.
52      * <p/>
53      * Will throw a RuntimeException if the syntax of the supplied string is wrong.
54      */
55     public static InstanceIdentifier parseXmlEncodedString(final String input, final PrefixResolver prefixResolver) {
56         return parseEncodedString(Objects.requireNonNull(input), Objects.requireNonNull(prefixResolver));
57     }
58
59     /**
60      * Given an II encoded as specified in RFC7951, clause 6.11, returns the II.
61      * <p/>
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."
67      * <p/>
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.
71      * <p/>
72      * Will throw a RuntimeException if the syntax of the supplied string is wrong.
73      */
74     public static InstanceIdentifier parseJsonEncodedString(final String input) {
75         return parseEncodedString(Objects.requireNonNull(input), null);
76     }
77
78     /**
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
82      * to be module names.
83      * <p/>
84      * Will throw a RuntimeException if the syntax of the supplied string is wrong.
85      */
86     public static InstanceIdentifier parseEncodedString(final String input, final PrefixResolver prefixResolver) {
87
88         try {
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.");
95         }
96     }
97
98     private static List<Step> extractSteps(final List<IiXPathToken> tokens, final PrefixResolver prefixResolver) {
99
100         final List<Step> steps = new ArrayList<>();
101
102         /*
103          * Each part of the path is in the form:
104          *
105          * <slash><optional-prefix-and-colon><data-node-name><optional-predicate(s)>
106          */
107
108         int index = 0;
109         while (true) {
110
111             if (index == tokens.size()) {               // Nothing left to consume, we are done.
112                 return steps;
113             }
114
115             if (tokens.get(index).type != IiXPathToken.Type.SLASH) {
116                 throw new RuntimeException("Expected a slash at pos " + tokens.get(index).pos);
117             }
118
119             index++;
120
121             /*
122              * We extract the data node identifier. This will create a new Part object.
123              */
124             index = extractStepDataNodeIdentifier(tokens, index, prefixResolver, steps);
125
126             /*
127              * Extract the predicates, if any.
128              */
129             index = extractPredicates(tokens, index, prefixResolver, steps);
130         }
131     }
132
133     private static int extractStepDataNodeIdentifier(final List<IiXPathToken> tokens, final int index,
134             final PrefixResolver prefixResolver, final List<Step> steps) {
135
136         final NamespaceModuleIdentifier unprefixedName = getUnprefixedName(tokens, index, steps,
137                 STEP_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
138         final NamespaceModuleIdentifier prefixedName = unprefixedName != null ?
139                 null :
140                 getPrefixedName(tokens, index, prefixResolver, steps, STEP_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
141
142         if (unprefixedName == null && prefixedName == null) {
143             throw new RuntimeException("Unresolvable data node name at pos " + tokens.get(index).pos);
144         }
145
146         steps.add(new Step(unprefixedName != null ? unprefixedName : prefixedName));
147
148         return index + (unprefixedName != null ? 1 : 3);
149     }
150
151     private static int extractPredicates(final List<IiXPathToken> tokens, int index, final PrefixResolver prefixResolver,
152             final List<Step> steps) {
153
154         /*
155          * A predicate may refer to:
156          *
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']
159          *
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]
162          *
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']
165          *
166          * - A leaf-list entry, identified by position (since state leaf-lists may be non-unique).
167          *   Example: /ex:stats/ex:cpuLoad[3]
168          */
169
170         if (index == tokens.size()) {           // end of tokens
171             return index;
172         }
173         if (tokens.get(index).type != IiXPathToken.Type.SQUARE_BRACKET_OPEN) {          // there is no predicate
174             return index;
175         }
176
177         final Step step = steps.get(steps.size() - 1);
178
179         /*
180          * Figure out the kind of predicate - keys, value or position
181          */
182
183         if (tokens.get(index + 1).type == IiXPathToken.Type.UNQUOTED_STRING && tokens.get(
184                 index + 2).type == IiXPathToken.Type.SQUARE_BRACKET_CLOSE) {
185             /*
186              * Position. The string must be parseable to an integer.
187              *
188              * NOTE that according to the XPath spec, position indexes start at 1, and not 0 as is customary in programming languages.
189              */
190             try {
191                 final int pos = Integer.parseInt(tokens.get(index + 1).value);
192                 if (pos < 1) {
193                     throw new RuntimeException("Position '" + pos + "' is illegal. Position must be larger/equal to 1.");
194                 }
195                 step.setPredicateListEntryOrLeafListMemberIndex(pos);
196                 return index + 3;
197             } catch (final NumberFormatException nfex) {
198                 throw new RuntimeException("Predicate '" + tokens.get(
199                         index + 1).value + "' not parseable to a position value.");
200             }
201         }
202
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) {
208             /*
209              * A value.
210              */
211             step.setPredicateLeafListMemberValue(tokens.get(index + 3).value);
212             return index + 5;
213         }
214
215         /*
216          * Must be one or more key values
217          */
218         while (true) {
219
220             if (index == tokens.size()) {               // end of tokens
221                 return index;
222             }
223             if (tokens.get(index).type != IiXPathToken.Type.SQUARE_BRACKET_OPEN) {              // done with predicates
224                 return index;
225             }
226
227             index++;            // consume the [
228
229             /*
230              * The data node may be prefixed or not...
231              */
232             final NamespaceModuleIdentifier unprefixedName = getUnprefixedName(tokens, index, steps,
233                     PREDICATE_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
234             final NamespaceModuleIdentifier prefixedName = unprefixedName != null ?
235                     null :
236                     getPrefixedName(tokens, index, prefixResolver, steps, PREDICATE_DATA_NODE_ALLOWED_TERMINATING_TOKENS);
237
238             if (unprefixedName == null && prefixedName == null) {
239                 throw new RuntimeException("Unresolvable data node name at pos " + tokens.get(index).pos);
240             }
241
242             index += (unprefixedName != null ? 1 : 3);          // consume the data node name
243
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.
245
246             /*
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.
250              */
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);
254             }
255
256             final String stringefiedKeyValue = tokens.get(index).value;
257
258             step.addPredicateKeyValue(unprefixedName != null ? unprefixedName : prefixedName, stringefiedKeyValue);
259
260             index++;            // consume the value
261
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);
264             }
265
266             index++;            // consume the ]
267         }
268     }
269
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);
274
275     private static NamespaceModuleIdentifier getUnprefixedName(final List<IiXPathToken> tokens, final int index,
276             final List<Step> steps, final List<IiXPathToken.Type> allowedTerminatingToken) {
277
278         /*
279          * An unprefixed name simply has a name, followed by either nothing, or a predicate, or the next step.
280          */
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) {
285             return null;
286         }
287
288         if (steps.isEmpty()) {
289             throw new RuntimeException("The first step of the path must have a prefix / module-name.");
290         }
291
292         final String dataNodeIdentifier = tokens.get(index).value;
293
294         /*
295          * We need to take the module name/namespace from the parent data node, i.e. the preceding step.
296          */
297         final NamespaceModuleIdentifier parentDataNodeNsmi = steps.get(steps.size() - 1).getDataNodeNsai();
298         return new NamespaceModuleIdentifier(parentDataNodeNsmi.getNamespace(), parentDataNodeNsmi.getModuleName(),
299                 dataNodeIdentifier);
300     }
301
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) {
305
306         /*
307          * An prefixed name is in the form <string><colon><string>, followed by either nothing, or a predicate, or the next step.
308          */
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));
313         if (!prefixedName) {
314             return null;
315         }
316
317         final String prefixOrModuleName = tokens.get(index).value;
318         final String dataNodeIdentifier = tokens.get(index + 2).value;
319
320         /*
321          * The prefix can be either a module name (input was JSON) or an actual XML prefix (input was XML).
322          */
323         final String moduleName = prefixResolver == null ? prefixOrModuleName : null;
324         final String namespace = prefixResolver != null ? prefixResolver.resolveNamespaceUri(prefixOrModuleName) : null;
325
326         if (moduleName == null && namespace == null) {
327             throw new RuntimeException("Unresolvable prefix '" + prefixOrModuleName + "' at pos " + tokens.get(index).pos);
328         }
329
330         return new NamespaceModuleIdentifier(namespace, moduleName, dataNodeIdentifier);
331     }
332
333     /**
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.
336      */
337     public static class Step {
338
339         private final NamespaceModuleIdentifier nsai;
340
341         private Map<NamespaceModuleIdentifier, String> predicateKeyValues;
342
343         private String predicateLeafListMemberValue;
344
345         private Integer predicateListEntryOrLeafListMemberIndex;
346
347         public Step(final NamespaceModuleIdentifier nsai) {
348             this.nsai = Objects.requireNonNull(nsai);
349         }
350
351         /**
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).
354          */
355         public NamespaceModuleIdentifier getDataNodeNsai() {
356             return nsai;
357         }
358
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.");
363             }
364
365             if (predicateKeyValues == null) {
366                 predicateKeyValues = new HashMap<>();
367             }
368             predicateKeyValues.put(Objects.requireNonNull(nsai), Objects.requireNonNull(value));
369             return this;
370         }
371
372         /**
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).
377          * <p>
378          * Returns null if there are no key values as predicate.
379          */
380         public Map<NamespaceModuleIdentifier, String> getPredicateKeyValues() {
381             return predicateKeyValues;
382         }
383
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.");
388             }
389
390             this.predicateLeafListMemberValue = Objects.requireNonNull(leafListMemberValue);
391             return this;
392         }
393
394         /**
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.
397          */
398         public String getPredicateLeafListMemberValue() {
399             return predicateLeafListMemberValue;
400         }
401
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.");
406             }
407
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).");
411             }
412
413             this.predicateListEntryOrLeafListMemberIndex = listEntryOrLeafListMemberIndex;
414             return this;
415         }
416
417         /**
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.
420          * <p/>
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.
424          */
425         public Integer getPredicateListEntryOrLeafListMemberIndex() {
426             return predicateListEntryOrLeafListMemberIndex;
427         }
428
429         public void resolveModuleOrNamespace(final ModuleAndNamespaceResolver namespaceResolver) {
430             nsai.resolveModuleOrNamespace(namespaceResolver);
431             if (predicateKeyValues != null) {
432                 predicateKeyValues.keySet().forEach(k -> k.resolveModuleOrNamespace(namespaceResolver));
433             }
434         }
435
436         @Override
437         public int hashCode() {
438             return toString().hashCode();
439         }
440
441         @Override
442         public boolean equals(Object obj) {
443             if (!(obj instanceof Step)) {
444                 return false;
445             }
446
447             final Step other = (Step) obj;
448
449             if (!this.nsai.equals(other.nsai)) {
450                 return false;
451             }
452
453             if (!Objects.equals(this.predicateKeyValues, other.predicateKeyValues)) {
454                 return false;
455             }
456
457             if (!Objects.equals(this.predicateLeafListMemberValue, other.predicateLeafListMemberValue)) {
458                 return false;
459             }
460
461             return Objects.equals(this.predicateListEntryOrLeafListMemberIndex,
462                     other.predicateListEntryOrLeafListMemberIndex);
463         }
464
465         @Override
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);
471             }
472             if (predicateLeafListMemberValue != null) {
473                 sb.append("; value=").append(predicateLeafListMemberValue);
474             }
475             if (predicateListEntryOrLeafListMemberIndex != null) {
476                 sb.append("; index=").append(predicateListEntryOrLeafListMemberIndex);
477             }
478             return sb.toString();
479         }
480     }
481
482     private static List<IiXPathToken> tokenize(final String s) {
483
484         final List<IiXPathToken> result = new ArrayList<>(50);
485
486         int charPos = 0;
487         char c;
488         while (charPos < s.length()) {
489
490             c = s.charAt(charPos);
491
492             switch (c) {
493                 case '/':
494                     result.add(IiXPathToken.newSlash(charPos));
495                     break;
496                 case ':':
497                     result.add(IiXPathToken.newColon(charPos));
498                     break;
499                 case '[':
500                     result.add(IiXPathToken.newSquareBracketOpen(charPos));
501                     break;
502                 case ']':
503                     result.add(IiXPathToken.newSquareBracketClose(charPos));
504                     break;
505                 case '=':
506                     result.add(IiXPathToken.newEquals(charPos));
507                     break;
508                 case '.':
509                     result.add(IiXPathToken.newDot(charPos));
510                     break;
511                 case ' ':
512                 case '\t':
513                     /*
514                      * We really don't expect whitespaces in the II, but according to XPath spec these are actually allowed.
515                      */
516                     break;
517                 default:
518                     charPos = tokenizeExtractString(s, charPos, result);
519                     break;
520             }
521
522             charPos++;
523         }
524
525         return result;
526     }
527
528     /**
529      * Returns the last character consumed.
530      */
531     private static int tokenizeExtractString(final String s, int charPos, final List<IiXPathToken> result) {
532
533         /*
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.
536          *
537          * This method also extracts QNames, so we also allow unquoted strings. Different tokens are produced thus.
538          */
539
540         final boolean stringIsSingleQuoted = (s.charAt(charPos) == '\'');
541         final boolean stringIsDoubleQuoted = (s.charAt(charPos) == '"');
542
543         if (stringIsSingleQuoted || stringIsDoubleQuoted) {
544             charPos++;
545         }
546
547         char c;
548         final StringBuilder sb = new StringBuilder();
549
550         while (charPos < s.length()) {
551             c = s.charAt(charPos);
552
553             switch (c) {
554                 case '\'':
555                     if (stringIsSingleQuoted) {                                                                 // Done. Swallow the trailing ' character
556                         result.add(IiXPathToken.newQuotedStringToken(charPos, sb.toString()));
557                         return charPos;
558                     }
559                     if (stringIsDoubleQuoted) {         // OK - single quote in double-quoted string.
560                         sb.append('\'');
561                     } else {
562                         throw new RuntimeException("Single quote character may not appear at position " + charPos);
563                     }
564                     break;
565                 case '"':
566                     if (stringIsDoubleQuoted) {         // Done. Swallow the trailing " character
567                         result.add(IiXPathToken.newQuotedStringToken(charPos, sb.toString()));
568                         return charPos;
569                     }
570                     if (stringIsSingleQuoted) {         // OK - double quote in single-quoted string.
571                         sb.append('"');
572                     } else {
573                         throw new RuntimeException("Double quote character may not appear at position " + charPos);
574                     }
575                     break;
576                 case ' ':
577                 case '\t':
578                 case '/':
579                 case ':':
580                 case '[':
581                 case ']':
582                 case '=':
583                 case '.':
584                     if (stringIsSingleQuoted || stringIsDoubleQuoted) {
585                         sb.append(c);
586                     } else {
587                         /*
588                          * We are done here.
589                          */
590                         result.add(IiXPathToken.newUnquotedStringToken(charPos, sb.toString()));
591                         return charPos - 1;
592                     }
593                     break;
594                 default:
595                     sb.append(c);
596                     break;
597             }
598
599             charPos++;
600         }
601
602         if (stringIsSingleQuoted || stringIsDoubleQuoted) {
603             throw new RuntimeException("Single/double-quoted string not correctly terminated.");
604         }
605
606         result.add(IiXPathToken.newUnquotedStringToken(charPos, sb.toString()));
607
608         return charPos;
609     }
610
611     private static class IiXPathToken {
612
613         private static IiXPathToken newSlash(final int pos) {
614             final IiXPathToken token = new IiXPathToken();
615             token.type = Type.SLASH;
616             token.pos = pos;
617             token.value = "/";
618             return token;
619         }
620
621         private static IiXPathToken newColon(final int pos) {
622             final IiXPathToken token = new IiXPathToken();
623             token.type = Type.COLON;
624             token.pos = pos;
625             token.value = ":";
626             return token;
627         }
628
629         private static IiXPathToken newSquareBracketOpen(final int pos) {
630             final IiXPathToken token = new IiXPathToken();
631             token.type = Type.SQUARE_BRACKET_OPEN;
632             token.pos = pos;
633             token.value = "[";
634             return token;
635         }
636
637         private static IiXPathToken newSquareBracketClose(final int pos) {
638             final IiXPathToken token = new IiXPathToken();
639             token.type = Type.SQUARE_BRACKET_CLOSE;
640             token.pos = pos;
641             token.value = "]";
642             return token;
643         }
644
645         private static IiXPathToken newEquals(final int pos) {
646             final IiXPathToken token = new IiXPathToken();
647             token.type = Type.EQUALS;
648             token.pos = pos;
649             token.value = "=";
650             return token;
651         }
652
653         private static IiXPathToken newDot(final int pos) {
654             final IiXPathToken token = new IiXPathToken();
655             token.type = Type.DOT;
656             token.pos = pos;
657             token.value = ".";
658             return token;
659         }
660
661         private static IiXPathToken newQuotedStringToken(final int pos, final String str) {
662             final IiXPathToken token = new IiXPathToken();
663             token.type = Type.QUOTED_STRING;
664             token.pos = pos;
665             token.value = str;
666             return token;
667         }
668
669         private static IiXPathToken newUnquotedStringToken(final int pos, final String str) {
670             final IiXPathToken token = new IiXPathToken();
671             token.type = Type.UNQUOTED_STRING;
672             token.pos = pos;
673             token.value = str;
674             return token;
675         }
676
677         private Type type = null;
678         private int pos = 0;
679         private String value = null;
680
681         private IiXPathToken() {
682         }
683
684         private enum Type {
685             SLASH,
686             COLON,
687             SQUARE_BRACKET_OPEN,
688             SQUARE_BRACKET_CLOSE,
689             EQUALS,
690             DOT,
691             QUOTED_STRING,
692             UNQUOTED_STRING;
693         }
694     }
695
696     private final List<Step> steps;
697
698     public InstanceIdentifier(final List<Step> steps) {
699         this.steps = steps;
700     }
701
702     public List<Step> getSteps() {
703         return steps;
704     }
705
706     /**
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.
709      */
710     public void resolveModuleOrNamespace(final ModuleAndNamespaceResolver namespaceResolver) {
711         steps.forEach(s -> s.resolveModuleOrNamespace(namespaceResolver));
712     }
713
714     @Override
715     public int hashCode() {
716         return toString().hashCode();
717     }
718
719     @Override
720     public boolean equals(Object obj) {
721         if (!(obj instanceof InstanceIdentifier)) {
722             return false;
723         }
724
725         final InstanceIdentifier other = (InstanceIdentifier) obj;
726         return this.steps.equals(other.steps);
727     }
728
729     @Override
730     public String toString() {
731         return "II " + steps.toString();
732     }
733 }