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