Cleanup use of Guava library
[yangtools.git] / yang / yang-model-util / src / main / java / org / opendaylight / yangtools / yang / model / util / SchemaContextUtil.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.yangtools.yang.model.util;
9
10 import com.google.common.annotations.Beta;
11 import com.google.common.annotations.VisibleForTesting;
12 import com.google.common.base.Preconditions;
13 import com.google.common.base.Splitter;
14 import com.google.common.collect.Iterables;
15 import java.util.Iterator;
16 import java.util.LinkedList;
17 import java.util.List;
18 import java.util.Optional;
19 import java.util.Set;
20 import java.util.regex.Pattern;
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23 import org.opendaylight.yangtools.yang.common.QName;
24 import org.opendaylight.yangtools.yang.common.QNameModule;
25 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
26 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
27 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
29 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.DerivableSchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.GroupingDefinition;
32 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.Module;
35 import org.opendaylight.yangtools.yang.model.api.ModuleImport;
36 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
37 import org.opendaylight.yangtools.yang.model.api.RevisionAwareXPath;
38 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
39 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
40 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
42 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
43 import org.opendaylight.yangtools.yang.model.api.TypedSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * The Schema Context Util contains support methods for searching through Schema
50  * Context modules for specified schema nodes via Schema Path or Revision Aware
51  * XPath. The Schema Context Util is designed as mixin, so it is not
52  * instantiable.
53  *
54  */
55 public final class SchemaContextUtil {
56     private static final Logger LOG = LoggerFactory.getLogger(SchemaContextUtil.class);
57     private static final Splitter COLON_SPLITTER = Splitter.on(':');
58     private static final Splitter SLASH_SPLITTER = Splitter.on('/');
59
60     private SchemaContextUtil() {
61     }
62
63     /**
64      * Method attempts to find DataSchemaNode in Schema Context via specified
65      * Schema Path. The returned DataSchemaNode from method will be the node at
66      * the end of the SchemaPath. If the DataSchemaNode is not present in the
67      * Schema Context the method will return <code>null</code>. <br>
68      * In case that Schema Context or Schema Path are not specified correctly
69      * (i.e. contains <code>null</code> values) the method will throw
70      * IllegalArgumentException.
71      *
72      * @param context
73      *            Schema Context
74      * @param schemaPath
75      *            Schema Path to search for
76      * @return SchemaNode from the end of the Schema Path or <code>null</code>
77      *         if the Node is not present.
78      * @throws IllegalArgumentException if context or schemaPath is not correct.
79      */
80     public static SchemaNode findDataSchemaNode(final SchemaContext context, final SchemaPath schemaPath) {
81         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
82         Preconditions.checkArgument(schemaPath != null, "Schema Path reference cannot be NULL");
83
84         final Iterable<QName> prefixedPath = schemaPath.getPathFromRoot();
85         if (prefixedPath == null) {
86             LOG.debug("Schema path {} has null path", schemaPath);
87             return null;
88         }
89
90         LOG.trace("Looking for path {} in context {}", schemaPath, context);
91         return findNodeInSchemaContext(context, prefixedPath);
92     }
93
94     /**
95      * Method attempts to find DataSchemaNode inside of provided Schema Context
96      * and Yang Module accordingly to Non-conditional Revision Aware XPath. The
97      * specified Module MUST be present in Schema Context otherwise the
98      * operation would fail and return <code>null</code>. <br>
99      * The Revision Aware XPath MUST be specified WITHOUT the conditional
100      * statement (i.e. without [cond]) in path, because in this state the Schema
101      * Context is completely unaware of data state and will be not able to
102      * properly resolve XPath. If the XPath contains condition the method will
103      * return IllegalArgumentException. <br>
104      * In case that Schema Context or Module or Revision Aware XPath contains
105      * <code>null</code> references the method will throw
106      * IllegalArgumentException <br>
107      * If the Revision Aware XPath is correct and desired Data Schema Node is
108      * present in Yang module or in depending module in Schema Context the
109      * method will return specified Data Schema Node, otherwise the operation
110      * will fail and method will return <code>null</code>.
111      *
112      * @param context
113      *            Schema Context
114      * @param module
115      *            Yang Module
116      * @param nonCondXPath
117      *            Non Conditional Revision Aware XPath
118      * @return Returns Data Schema Node for specified Schema Context for given
119      *         Non-conditional Revision Aware XPath, or <code>null</code> if the
120      *         DataSchemaNode is not present in Schema Context.
121      */
122     public static SchemaNode findDataSchemaNode(final SchemaContext context, final Module module,
123             final RevisionAwareXPath nonCondXPath) {
124         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
125         Preconditions.checkArgument(module != null, "Module reference cannot be NULL");
126         Preconditions.checkArgument(nonCondXPath != null, "Non Conditional Revision Aware XPath cannot be NULL");
127
128         final String strXPath = nonCondXPath.toString();
129         if (strXPath != null) {
130             Preconditions.checkArgument(strXPath.indexOf('[') == -1,
131                     "Revision Aware XPath may not contain a condition");
132             if (nonCondXPath.isAbsolute()) {
133                 final List<QName> qnamedPath = xpathToQNamePath(context, module, strXPath);
134                 if (qnamedPath != null) {
135                     return findNodeInSchemaContext(context, qnamedPath);
136                 }
137             }
138         }
139         return null;
140     }
141
142     /**
143      * Method attempts to find DataSchemaNode inside of provided Schema Context
144      * and Yang Module accordingly to Non-conditional relative Revision Aware
145      * XPath. The specified Module MUST be present in Schema Context otherwise
146      * the operation would fail and return <code>null</code>. <br>
147      * The relative Revision Aware XPath MUST be specified WITHOUT the
148      * conditional statement (i.e. without [cond]) in path, because in this
149      * state the Schema Context is completely unaware of data state and will be
150      * not able to properly resolve XPath. If the XPath contains condition the
151      * method will return IllegalArgumentException. <br>
152      * The Actual Schema Node MUST be specified correctly because from this
153      * Schema Node will search starts. If the Actual Schema Node is not correct
154      * the operation will simply fail, because it will be unable to find desired
155      * DataSchemaNode. <br>
156      * In case that Schema Context or Module or Actual Schema Node or relative
157      * Revision Aware XPath contains <code>null</code> references the method
158      * will throw IllegalArgumentException <br>
159      * If the Revision Aware XPath doesn't have flag
160      * <code>isAbsolute == false</code> the method will throw
161      * IllegalArgumentException. <br>
162      * If the relative Revision Aware XPath is correct and desired Data Schema
163      * Node is present in Yang module or in depending module in Schema Context
164      * the method will return specified Data Schema Node, otherwise the
165      * operation will fail and method will return <code>null</code>.
166      *
167      * @param context
168      *            Schema Context
169      * @param module
170      *            Yang Module
171      * @param actualSchemaNode
172      *            Actual Schema Node
173      * @param relativeXPath
174      *            Relative Non Conditional Revision Aware XPath
175      * @return DataSchemaNode if is present in specified Schema Context for
176      *         given relative Revision Aware XPath, otherwise will return
177      *         <code>null</code>.
178      */
179     public static SchemaNode findDataSchemaNodeForRelativeXPath(final SchemaContext context, final Module module,
180             final SchemaNode actualSchemaNode, final RevisionAwareXPath relativeXPath) {
181         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
182         Preconditions.checkArgument(module != null, "Module reference cannot be NULL");
183         Preconditions.checkArgument(actualSchemaNode != null, "Actual Schema Node reference cannot be NULL");
184         Preconditions.checkArgument(relativeXPath != null, "Non Conditional Revision Aware XPath cannot be NULL");
185         Preconditions.checkState(!relativeXPath.isAbsolute(),
186                 "Revision Aware XPath MUST be relative i.e. MUST contains ../, "
187                         + "for non relative Revision Aware XPath use findDataSchemaNode method");
188
189         final SchemaPath actualNodePath = actualSchemaNode.getPath();
190         if (actualNodePath != null) {
191             final Iterable<QName> qnamePath = resolveRelativeXPath(context, module, relativeXPath, actualSchemaNode);
192
193             if (qnamePath != null) {
194                 return findNodeInSchemaContext(context, qnamePath);
195             }
196         }
197         return null;
198     }
199
200     /**
201      * Returns parent Yang Module for specified Schema Context in which Schema
202      * Node is declared. If the Schema Node is not present in Schema Context the
203      * operation will return <code>null</code>. <br>
204      * If Schema Context or Schema Node contains <code>null</code> references
205      * the method will throw IllegalArgumentException
206      *
207      * @param context
208      *            Schema Context
209      * @param schemaNode
210      *            Schema Node
211      * @return Yang Module for specified Schema Context and Schema Node, if Schema Node is NOT present, the method will
212      *         return <code>null</code>
213      */
214     public static Module findParentModule(final SchemaContext context, final SchemaNode schemaNode) {
215         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL!");
216         Preconditions.checkArgument(schemaNode != null, "Schema Node cannot be NULL!");
217         Preconditions.checkState(schemaNode.getPath() != null, "Schema Path for Schema Node is not "
218                 + "set properly (Schema Path is NULL)");
219
220         final QName qname = schemaNode.getPath().getLastComponent();
221         Preconditions.checkState(qname != null, "Schema Path contains invalid state of path parts. "
222                 + "The Schema Path MUST contain at least ONE QName  which defines namespace and Local name of path.");
223         return context.findModuleByNamespaceAndRevision(qname.getNamespace(), qname.getRevision());
224     }
225
226     public static SchemaNode findNodeInSchemaContext(final SchemaContext context, final Iterable<QName> path) {
227         final QName current = path.iterator().next();
228
229         LOG.trace("Looking up module {} in context {}", current, path);
230         final Module module = context.findModuleByNamespaceAndRevision(current.getNamespace(), current.getRevision());
231         if (module == null) {
232             LOG.debug("Module {} not found", current);
233             return null;
234         }
235
236         return findNodeInModule(module, path);
237     }
238
239     /**
240      * Returns NotificationDefinition from Schema Context.
241      *
242      * @param schema SchemaContext in which lookup should be performed.
243      * @param path Schema Path of notification
244      * @return Notification schema or null, if notification is not present in schema context.
245      */
246     @Beta
247     @Nullable
248     public static NotificationDefinition getNotificationSchema(@Nonnull final SchemaContext schema,
249             @Nonnull final SchemaPath path) {
250         Preconditions.checkNotNull(schema, "Schema context must not be null.");
251         Preconditions.checkNotNull(path, "Schema path must not be null.");
252         for (final NotificationDefinition potential : schema.getNotifications()) {
253             if (path.equals(potential.getPath())) {
254                 return potential;
255             }
256         }
257         return null;
258     }
259
260     /**
261      * Returns RPC Input or Output Data container from RPC definition.
262      *
263      * @param schema SchemaContext in which lookup should be performed.
264      * @param path Schema path of RPC input/output data container
265      * @return Notification schema or null, if notification is not present in schema context.
266      */
267     @Beta
268     @Nullable
269     public static ContainerSchemaNode getRpcDataSchema(@Nonnull final SchemaContext schema,
270             @Nonnull final SchemaPath path) {
271         Preconditions.checkNotNull(schema, "Schema context must not be null.");
272         Preconditions.checkNotNull(path, "Schema path must not be null.");
273         final Iterator<QName> it = path.getPathFromRoot().iterator();
274         Preconditions.checkArgument(it.hasNext(), "Rpc must have QName.");
275         final QName rpcName = it.next();
276         Preconditions.checkArgument(it.hasNext(), "input or output must be part of path.");
277         final QName inOrOut = it.next();
278         for (final RpcDefinition potential : schema.getOperations()) {
279             if (rpcName.equals(potential.getQName())) {
280                 return SchemaNodeUtils.getRpcDataSchema(potential, inOrOut);
281             }
282         }
283         return null;
284     }
285
286     private static SchemaNode findNodeInModule(final Module module, final Iterable<QName> path) {
287
288         Preconditions.checkArgument(module != null, "Parent reference cannot be NULL");
289         Preconditions.checkArgument(path != null, "Path reference cannot be NULL");
290
291         if (!path.iterator().hasNext()) {
292             LOG.debug("No node matching {} found in node {}", path, module);
293             return null;
294         }
295
296         final QName current = path.iterator().next();
297         LOG.trace("Looking for node {} in module {}", current, module);
298
299         SchemaNode foundNode = null;
300         final Iterable<QName> nextPath = nextLevel(path);
301
302         foundNode = module.getDataChildByName(current);
303         if (foundNode != null && nextPath.iterator().hasNext()) {
304             foundNode = findNodeIn(foundNode, nextPath);
305         }
306
307         if (foundNode == null) {
308             foundNode = getGroupingByName(module, current);
309             if (foundNode != null && nextPath.iterator().hasNext()) {
310                 foundNode = findNodeIn(foundNode, nextPath);
311             }
312         }
313
314         if (foundNode == null) {
315             foundNode = getRpcByName(module, current);
316             if (foundNode != null && nextPath.iterator().hasNext()) {
317                 foundNode = findNodeIn(foundNode, nextPath);
318             }
319         }
320
321         if (foundNode == null) {
322             foundNode = getNotificationByName(module, current);
323             if (foundNode != null && nextPath.iterator().hasNext()) {
324                 foundNode = findNodeIn(foundNode, nextPath);
325             }
326         }
327
328         if (foundNode == null) {
329             LOG.debug("No node matching {} found in node {}", path, module);
330         }
331
332         return foundNode;
333
334     }
335
336     private static SchemaNode findNodeIn(final SchemaNode parent, final Iterable<QName> path) {
337
338         Preconditions.checkArgument(parent != null, "Parent reference cannot be NULL");
339         Preconditions.checkArgument(path != null, "Path reference cannot be NULL");
340
341         if (!path.iterator().hasNext()) {
342             LOG.debug("No node matching {} found in node {}", path, parent);
343             return null;
344         }
345
346         final QName current = path.iterator().next();
347         LOG.trace("Looking for node {} in node {}", current, parent);
348
349         SchemaNode foundNode = null;
350         final Iterable<QName> nextPath = nextLevel(path);
351
352         if (parent instanceof DataNodeContainer) {
353             final DataNodeContainer parentDataNodeContainer = (DataNodeContainer) parent;
354
355             foundNode = parentDataNodeContainer.getDataChildByName(current);
356             if (foundNode != null && nextPath.iterator().hasNext()) {
357                 foundNode = findNodeIn(foundNode, nextPath);
358             }
359
360             if (foundNode == null) {
361                 foundNode = getGroupingByName(parentDataNodeContainer, current);
362                 if (foundNode != null && nextPath.iterator().hasNext()) {
363                     foundNode = findNodeIn(foundNode, nextPath);
364                 }
365             }
366         }
367
368         if (foundNode == null && parent instanceof RpcDefinition) {
369             final RpcDefinition parentRpcDefinition = (RpcDefinition) parent;
370
371             if (current.getLocalName().equals("input")) {
372                 foundNode = parentRpcDefinition.getInput();
373                 if (foundNode != null && nextPath.iterator().hasNext()) {
374                     foundNode = findNodeIn(foundNode, nextPath);
375                 }
376             }
377
378             if (current.getLocalName().equals("output")) {
379                 foundNode = parentRpcDefinition.getOutput();
380                 if (foundNode != null && nextPath.iterator().hasNext()) {
381                     foundNode = findNodeIn(foundNode, nextPath);
382                 }
383             }
384
385             if (foundNode == null) {
386                 foundNode = getGroupingByName(parentRpcDefinition, current);
387                 if (foundNode != null && nextPath.iterator().hasNext()) {
388                     foundNode = findNodeIn(foundNode, nextPath);
389                 }
390             }
391         }
392
393         if (foundNode == null && parent instanceof ChoiceSchemaNode) {
394             foundNode = ((ChoiceSchemaNode) parent).getCaseNodeByName(current);
395
396             if (foundNode != null && nextPath.iterator().hasNext()) {
397                 foundNode = findNodeIn(foundNode, nextPath);
398             }
399
400             if (foundNode == null) {
401                 // fallback that tries to map into one of the child cases
402                 for (final ChoiceCaseNode caseNode : ((ChoiceSchemaNode) parent).getCases()) {
403                     final DataSchemaNode maybeChild = caseNode.getDataChildByName(current);
404                     if (maybeChild != null) {
405                         foundNode = findNodeIn(maybeChild, nextPath);
406                         break;
407                     }
408                 }
409             }
410         }
411
412         if (foundNode == null) {
413             LOG.debug("No node matching {} found in node {}", path, parent);
414         }
415
416         return foundNode;
417
418     }
419
420     private static Iterable<QName> nextLevel(final Iterable<QName> path) {
421         return Iterables.skip(path, 1);
422     }
423
424     private static RpcDefinition getRpcByName(final Module module, final QName name) {
425         for (final RpcDefinition rpc : module.getRpcs()) {
426             if (rpc.getQName().equals(name)) {
427                 return rpc;
428             }
429         }
430         return null;
431     }
432
433     private static NotificationDefinition getNotificationByName(final Module module, final QName name) {
434         for (final NotificationDefinition notification : module.getNotifications()) {
435             if (notification.getQName().equals(name)) {
436                 return notification;
437             }
438         }
439         return null;
440     }
441
442     private static GroupingDefinition getGroupingByName(final DataNodeContainer dataNodeContainer, final QName name) {
443         for (final GroupingDefinition grouping : dataNodeContainer.getGroupings()) {
444             if (grouping.getQName().equals(name)) {
445                 return grouping;
446             }
447         }
448         return null;
449     }
450
451     private static GroupingDefinition getGroupingByName(final RpcDefinition rpc, final QName name) {
452         for (final GroupingDefinition grouping : rpc.getGroupings()) {
453             if (grouping.getQName().equals(name)) {
454                 return grouping;
455             }
456         }
457         return null;
458     }
459
460     /**
461      * Transforms string representation of XPath to Queue of QNames. The XPath
462      * is split by "/" and for each part of XPath is assigned correct module in
463      * Schema Path. <br>
464      * If Schema Context, Parent Module or XPath string contains
465      * <code>null</code> values, the method will throws IllegalArgumentException
466      *
467      * @param context
468      *            Schema Context
469      * @param parentModule
470      *            Parent Module
471      * @param xpath
472      *            XPath String
473      * @return return a list of QName
474      *
475      * @throws IllegalArgumentException if any arguments are null
476      *
477      */
478     private static List<QName> xpathToQNamePath(final SchemaContext context, final Module parentModule,
479             final String xpath) {
480         // FIXME: 2.0.0: this should throw NPE, not IAE
481         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
482         Preconditions.checkArgument(parentModule != null, "Parent Module reference cannot be NULL");
483         Preconditions.checkArgument(xpath != null, "XPath string reference cannot be NULL");
484
485         final List<QName> path = new LinkedList<>();
486         for (final String pathComponent : SLASH_SPLITTER.split(xpath)) {
487             if (!pathComponent.isEmpty()) {
488                 path.add(stringPathPartToQName(context, parentModule, pathComponent));
489             }
490         }
491         return path;
492     }
493
494     /**
495      * Transforms part of Prefixed Path as java String to QName. <br>
496      * If the string contains module prefix separated by ":" (i.e.
497      * mod:container) this module is provided from from Parent Module list of
498      * imports. If the Prefixed module is present in Schema Context the QName
499      * can be constructed. <br>
500      * If the Prefixed Path Part does not contains prefix the Parent's Module
501      * namespace is taken for construction of QName. <br>
502      * If Schema Context, Parent Module or Prefixed Path Part refers to
503      * <code>null</code> the method will throw IllegalArgumentException
504      *
505      * @param context
506      *            Schema Context
507      * @param parentModule
508      *            Parent Module
509      * @param prefixedPathPart
510      *            Prefixed Path Part string
511      * @return QName from prefixed Path Part String.
512      * @throws IllegalArgumentException if any arguments are null
513      */
514     private static QName stringPathPartToQName(final SchemaContext context, final Module parentModule,
515             final String prefixedPathPart) {
516         // FIXME: 2.0.0: this should throw NPE, not IAE
517         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
518         Preconditions.checkArgument(parentModule != null, "Parent Module reference cannot be NULL");
519         Preconditions.checkArgument(prefixedPathPart != null, "Prefixed Path Part cannot be NULL!");
520
521         if (prefixedPathPart.indexOf(':') != -1) {
522             final Iterator<String> prefixedName = COLON_SPLITTER.split(prefixedPathPart).iterator();
523             final String modulePrefix = prefixedName.next();
524
525             final Module module = resolveModuleForPrefix(context, parentModule, modulePrefix);
526             Preconditions.checkArgument(module != null,
527                     "Failed to resolve xpath: no module found for prefix %s in module %s", modulePrefix,
528                     parentModule.getName());
529
530             return QName.create(module.getQNameModule(), prefixedName.next());
531         }
532
533         return QName.create(parentModule.getNamespace(), parentModule.getRevision(), prefixedPathPart);
534     }
535
536     /**
537      * Method will attempt to resolve and provide Module reference for specified
538      * module prefix. Each Yang module could contains multiple imports which
539      * MUST be associated with corresponding module prefix. The method simply
540      * looks into module imports and returns the module that is bounded with
541      * specified prefix. If the prefix is not present in module or the prefixed
542      * module is not present in specified Schema Context, the method will return
543      * <code>null</code>. <br>
544      * If String prefix is the same as prefix of the specified Module the
545      * reference to this module is returned. <br>
546      * If Schema Context, Module or Prefix are referring to <code>null</code>
547      * the method will return IllegalArgumentException
548      *
549      * @param context
550      *            Schema Context
551      * @param module
552      *            Yang Module
553      * @param prefix
554      *            Module Prefix
555      * @return Module for given prefix in specified Schema Context if is
556      *         present, otherwise returns <code>null</code>
557      * @throws IllegalArgumentException if any arguments are null
558      */
559     private static Module resolveModuleForPrefix(final SchemaContext context, final Module module,
560             final String prefix) {
561         // FIXME: 2.0.0: this should throw NPE, not IAE
562         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
563         Preconditions.checkArgument(module != null, "Module reference cannot be NULL");
564         Preconditions.checkArgument(prefix != null, "Prefix string cannot be NULL");
565
566         if (prefix.equals(module.getPrefix())) {
567             return module;
568         }
569
570         final Set<ModuleImport> imports = module.getImports();
571         for (final ModuleImport mi : imports) {
572             if (prefix.equals(mi.getPrefix())) {
573                 return context.findModuleByName(mi.getModuleName(), mi.getRevision());
574             }
575         }
576         return null;
577     }
578
579     /**
580      * Resolve a relative XPath into a set of QNames.
581      *
582      * @param context
583      *            Schema Context
584      * @param module
585      *            Yang Module
586      * @param relativeXPath
587      *            Non conditional Revision Aware Relative XPath
588      * @param actualSchemaNode
589      *            actual schema node
590      * @return list of QName
591      * @throws IllegalArgumentException if any arguments are null
592      */
593     private static Iterable<QName> resolveRelativeXPath(final SchemaContext context, final Module module,
594             final RevisionAwareXPath relativeXPath, final SchemaNode actualSchemaNode) {
595         // FIXME: 2.0.0: this should throw NPE, not IAE
596         Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
597         Preconditions.checkArgument(module != null, "Module reference cannot be NULL");
598         Preconditions.checkArgument(relativeXPath != null, "Non Conditional Revision Aware XPath cannot be NULL");
599         Preconditions.checkState(!relativeXPath.isAbsolute(),
600                 "Revision Aware XPath MUST be relative i.e. MUST contains ../, "
601                         + "for non relative Revision Aware XPath use findDataSchemaNode method");
602         Preconditions.checkState(actualSchemaNode.getPath() != null,
603                 "Schema Path reference for Leafref cannot be NULL");
604
605         final Iterable<String> xpaths = SLASH_SPLITTER.split(relativeXPath.toString());
606
607         // Find out how many "parent" components there are
608         // FIXME: is .contains() the right check here?
609         // FIXME: case ../../node1/node2/../node3/../node4
610         int colCount = 0;
611         for (final Iterator<String> it = xpaths.iterator(); it.hasNext() && it.next().contains(".."); ) {
612             ++colCount;
613         }
614
615         final Iterable<QName> schemaNodePath = actualSchemaNode.getPath().getPathFromRoot();
616
617         if (Iterables.size(schemaNodePath) - colCount >= 0) {
618             return Iterables.concat(Iterables.limit(schemaNodePath, Iterables.size(schemaNodePath) - colCount),
619                 Iterables.transform(Iterables.skip(xpaths, colCount),
620                     input -> stringPathPartToQName(context, module, input)));
621         }
622         return Iterables.concat(schemaNodePath,
623                 Iterables.transform(Iterables.skip(xpaths, colCount),
624                     input -> stringPathPartToQName(context, module, input)));
625     }
626
627     /**
628      * Extracts the base type of node on which schema node points to. If target node is again of type
629      * LeafrefTypeDefinition, methods will be call recursively until it reach concrete type definition.
630      *
631      * @param typeDefinition
632      *            type of node which will be extracted
633      * @param schemaContext
634      *            Schema Context
635      * @param schema
636      *            Schema Node
637      * @return recursively found type definition this leafref is pointing to or null if the xpath is incorrect (null
638      *         is there to preserve backwards compatibility)
639      */
640     public static TypeDefinition<?> getBaseTypeForLeafRef(final LeafrefTypeDefinition typeDefinition,
641             final SchemaContext schemaContext, final SchemaNode schema) {
642         RevisionAwareXPath pathStatement = typeDefinition.getPathStatement();
643         pathStatement = new RevisionAwareXPathImpl(stripConditionsFromXPathString(pathStatement),
644             pathStatement.isAbsolute());
645
646         final DataSchemaNode dataSchemaNode;
647         if (pathStatement.isAbsolute()) {
648             SchemaNode baseSchema = schema;
649             while (baseSchema instanceof DerivableSchemaNode) {
650                 final Optional<? extends SchemaNode> basePotential = ((DerivableSchemaNode) baseSchema).getOriginal();
651                 if (basePotential.isPresent()) {
652                     baseSchema = basePotential.get();
653                 } else {
654                     break;
655                 }
656             }
657
658             Module parentModule = findParentModuleOfReferencingType(schemaContext, baseSchema);
659             dataSchemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(schemaContext, parentModule,
660                     pathStatement);
661         } else {
662             Module parentModule = findParentModule(schemaContext, schema);
663             dataSchemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNodeForRelativeXPath(schemaContext,
664                     parentModule, schema, pathStatement);
665         }
666
667         // FIXME this is just to preserve backwards compatibility since yangtools do not mind wrong leafref xpaths
668         // and current expected behaviour for such cases is to just use pure string
669         // This should throw an exception about incorrect XPath in leafref
670         if (dataSchemaNode == null) {
671             return null;
672         }
673
674         final TypeDefinition<?> targetTypeDefinition = typeDefinition(dataSchemaNode);
675
676         if (targetTypeDefinition instanceof LeafrefTypeDefinition) {
677             return getBaseTypeForLeafRef((LeafrefTypeDefinition) targetTypeDefinition, schemaContext, dataSchemaNode);
678         }
679
680         return targetTypeDefinition;
681     }
682
683     /**
684      * Returns base type for {@code typeDefinition} which belongs to module specified via {@code qname}. This handle
685      * the case when leafref type isn't specified as type substatement of leaf or leaf-list but is defined in other
686      * module as typedef which is then imported to referenced module.
687      *
688      * <p>
689      * Because {@code typeDefinition} is definied via typedef statement, only absolute path is meaningful.
690      */
691     public static TypeDefinition<?> getBaseTypeForLeafRef(final LeafrefTypeDefinition typeDefinition,
692             final SchemaContext schemaContext, final QName qname) {
693         final RevisionAwareXPath pathStatement = typeDefinition.getPathStatement();
694         final RevisionAwareXPath strippedPathStatement = new RevisionAwareXPathImpl(
695             stripConditionsFromXPathString(pathStatement), pathStatement.isAbsolute());
696         if (!strippedPathStatement.isAbsolute()) {
697             return null;
698         }
699
700         final Module parentModule = schemaContext.findModuleByNamespaceAndRevision(qname.getNamespace(),
701                 qname.getRevision());
702         final DataSchemaNode dataSchemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(schemaContext,
703             parentModule, strippedPathStatement);
704         final TypeDefinition<?> targetTypeDefinition = typeDefinition(dataSchemaNode);
705         if (targetTypeDefinition instanceof LeafrefTypeDefinition) {
706             return getBaseTypeForLeafRef((LeafrefTypeDefinition) targetTypeDefinition, schemaContext, dataSchemaNode);
707         }
708
709         return targetTypeDefinition;
710     }
711
712     private static Module findParentModuleOfReferencingType(final SchemaContext schemaContext,
713             final SchemaNode schemaNode) {
714         Preconditions.checkArgument(schemaContext != null, "Schema Context reference cannot be NULL!");
715         Preconditions.checkArgument(schemaNode instanceof TypedSchemaNode, "Unsupported node %s", schemaNode);
716
717         TypeDefinition<?> nodeType = ((TypedSchemaNode) schemaNode).getType();
718         if (nodeType.getBaseType() != null) {
719             while (nodeType.getBaseType() != null) {
720                 nodeType = nodeType.getBaseType();
721             }
722
723             final QNameModule typeDefModuleQname = nodeType.getQName().getModule();
724             return schemaContext.findModuleByNamespaceAndRevision(typeDefModuleQname.getNamespace(),
725                     typeDefModuleQname.getRevision());
726         }
727
728         return SchemaContextUtil.findParentModule(schemaContext, schemaNode);
729     }
730
731     private static final Pattern STRIP_PATTERN = Pattern.compile("\\[[^\\[\\]]*\\]");
732
733     /**
734      * Removes conditions from xPath pointed to target node.
735      *
736      * @param pathStatement
737      *            xPath to target node
738      * @return string representation of xPath without conditions
739      */
740     @VisibleForTesting
741     static String stripConditionsFromXPathString(final RevisionAwareXPath pathStatement) {
742         return STRIP_PATTERN.matcher(pathStatement.toString()).replaceAll("");
743     }
744
745     /**
746      * Extracts the base type of leaf schema node until it reach concrete type of TypeDefinition.
747      *
748      * @param node
749      *            a node representing LeafSchemaNode
750      * @return concrete type definition of node value
751      */
752     private static TypeDefinition<?> typeDefinition(final LeafSchemaNode node) {
753         TypeDefinition<?> baseType = node.getType();
754         while (baseType.getBaseType() != null) {
755             baseType = baseType.getBaseType();
756         }
757         return baseType;
758     }
759
760     /**
761      * Extracts the base type of leaf schema node until it reach concrete type of TypeDefinition.
762      *
763      * @param node
764      *            a node representing LeafListSchemaNode
765      * @return concrete type definition of node value
766      */
767     private static TypeDefinition<?> typeDefinition(final LeafListSchemaNode node) {
768         TypeDefinition<?> baseType = node.getType();
769         while (baseType.getBaseType() != null) {
770             baseType = baseType.getBaseType();
771         }
772         return baseType;
773     }
774
775     /**
776      * Gets the base type of DataSchemaNode value.
777      *
778      * @param node
779      *            a node representing DataSchemaNode
780      * @return concrete type definition of node value
781      */
782     private static TypeDefinition<?> typeDefinition(final DataSchemaNode node) {
783         if (node instanceof LeafListSchemaNode) {
784             return typeDefinition((LeafListSchemaNode) node);
785         } else if (node instanceof LeafSchemaNode) {
786             return typeDefinition((LeafSchemaNode) node);
787         } else {
788             throw new IllegalArgumentException("Unhandled parameter type: " + node);
789         }
790     }
791 }