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