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