- private static Iterable<QName> resolveRelativeXPath(final SchemaContext context, final Module module,
- final RevisionAwareXPath relativeXPath, final SchemaNode actualSchemaNode) {
- Preconditions.checkArgument(context != null, "Schema Context reference cannot be NULL");
- Preconditions.checkArgument(module != null, "Module reference cannot be NULL");
- Preconditions.checkArgument(relativeXPath != null, "Non Conditional Revision Aware XPath cannot be NULL");
- Preconditions.checkState(!relativeXPath.isAbsolute(),
- "Revision Aware XPath MUST be relative i.e. MUST contains ../, "
- + "for non relative Revision Aware XPath use findDataSchemaNode method");
- Preconditions.checkState(actualSchemaNode.getPath() != null,
- "Schema Path reference for Leafref cannot be NULL");
-
- final Iterable<String> xpaths = SLASH_SPLITTER.split(relativeXPath.toString());
-
- // Find out how many "parent" components there are
- // FIXME: is .contains() the right check here?
- // FIXME: case ../../node1/node2/../node3/../node4
- int colCount = 0;
- for (Iterator<String> it = xpaths.iterator(); it.hasNext() && it.next().contains(".."); ) {
- ++colCount;
- }
-
- final Iterable<QName> schemaNodePath = actualSchemaNode.getPath().getPathFromRoot();
-
- if (Iterables.size(schemaNodePath) - colCount >= 0) {
- return Iterables.concat(Iterables.limit(schemaNodePath, Iterables.size(schemaNodePath) - colCount),
- Iterables.transform(Iterables.skip(xpaths, colCount), new Function<String, QName>() {
- @Override
- public QName apply(final String input) {
- return stringPathPartToQName(context, module, input);
- }
- }));
- }
- return Iterables.concat(schemaNodePath,
- Iterables.transform(Iterables.skip(xpaths, colCount), new Function<String, QName>() {
- @Override
- public QName apply(final String input) {
- return stringPathPartToQName(context, module, input);
- }
- }));
+ private static @Nullable SchemaNode resolveRelativeXPath(final SchemaContext context, final Module module,
+ final String pathStr, final SchemaNode actualSchemaNode) {
+ checkState(actualSchemaNode.getPath() != null, "Schema Path reference for Leafref cannot be NULL");
+
+ return pathStr.startsWith("deref(") ? resolveDerefPath(context, module, actualSchemaNode, pathStr)
+ : findTargetNode(context, resolveRelativePath(context, module, actualSchemaNode,
+ doSplitXPath(pathStr)));
+ }
+
+ private static Iterable<QName> resolveRelativePath(final SchemaContext context, final Module module,
+ final SchemaNode actualSchemaNode, final List<String> steps) {
+ // Find out how many "parent" components there are and trim them
+ final int colCount = normalizeXPath(steps);
+ final List<String> xpaths = colCount == 0 ? steps : steps.subList(colCount, steps.size());
+
+ final List<QName> walkablePath = createWalkablePath(actualSchemaNode.getPath().getPathFromRoot(),
+ context, colCount);
+
+ if (walkablePath.size() - colCount >= 0) {
+ return Iterables.concat(Iterables.limit(walkablePath, walkablePath.size() - colCount),
+ Iterables.transform(xpaths, input -> stringPathPartToQName(context, module, input)));
+ }
+ return Iterables.concat(walkablePath,
+ Iterables.transform(xpaths, input -> stringPathPartToQName(context, module, input)));
+ }
+
+ /**
+ * Return List of qNames that are walkable using xPath. When getting a path from schema node it will return path
+ * with parents like CaseSchemaNode and ChoiceSchemaNode as well if they are parents of the node. We need to get
+ * rid of these in order to find the node that xPath is pointing to. Also we can not remove any node beyond the
+ * amount of "../" because we will not be able to find the correct schema node from schema context
+ *
+ * @param schemaNodePath list of qNames as a path to the leaf of type leafref
+ * @param context create schema context
+ * @param colCount amount of "../" in the xPath expression
+ * @return list of QNames as a path where we should be able to find referenced node
+ */
+ private static List<QName> createWalkablePath(final Iterable<QName> schemaNodePath, final SchemaContext context,
+ final int colCount) {
+ final List<Integer> indexToRemove = new ArrayList<>();
+ List<QName> schemaNodePathRet = Lists.newArrayList(schemaNodePath);
+ for (int j = 0, i = schemaNodePathRet.size() - 1; i >= 0 && j != colCount; i--, j++) {
+ final SchemaNode nodeIn = findTargetNode(context, schemaNodePathRet);
+ if (nodeIn instanceof CaseSchemaNode || nodeIn instanceof ChoiceSchemaNode) {
+ indexToRemove.add(i);
+ j--;
+ }
+ schemaNodePathRet.remove(i);
+ }
+ schemaNodePathRet = Lists.newArrayList(schemaNodePath);
+ for (int i : indexToRemove) {
+ schemaNodePathRet.remove(i);
+ }
+ return schemaNodePathRet;
+ }
+
+ private static SchemaNode resolveDerefPath(final SchemaContext context, final Module module,
+ final SchemaNode actualSchemaNode, final String xpath) {
+ final int paren = xpath.indexOf(')', 6);
+ checkArgument(paren != -1, "Cannot find matching parentheses in %s", xpath);
+
+ final String derefArg = xpath.substring(6, paren).strip();
+ // Look up the node which we need to reference
+ final SchemaNode derefTarget = findTargetNode(context, resolveRelativePath(context, module, actualSchemaNode,
+ doSplitXPath(derefArg)));
+ checkArgument(derefTarget != null, "Cannot find deref(%s) target node %s in context of %s", derefArg,
+ actualSchemaNode);
+ checkArgument(derefTarget instanceof TypedDataSchemaNode, "deref(%s) resolved to non-typed %s", derefArg,
+ derefTarget);
+
+ // We have a deref() target, decide what to do about it
+ final TypeDefinition<?> targetType = ((TypedDataSchemaNode) derefTarget).getType();
+ if (targetType instanceof InstanceIdentifierTypeDefinition) {
+ // Static inference breaks down, we cannot determine where this points to
+ // FIXME: dedicated exception, users can recover from it, derive from IAE
+ throw new UnsupportedOperationException("Cannot infer instance-identifier reference " + targetType);
+ }
+
+ // deref() is define only for instance-identifier and leafref types, handle the latter
+ checkArgument(targetType instanceof LeafrefTypeDefinition, "Illegal target type %s", targetType);
+
+ final PathExpression targetPath = ((LeafrefTypeDefinition) targetType).getPathStatement();
+ LOG.debug("Derefencing path {}", targetPath);
+
+ final SchemaNode deref = targetPath.isAbsolute()
+ ? findTargetNode(context, actualSchemaNode.getQName().getModule(),
+ ((LocationPathSteps) targetPath.getSteps()).getLocationPath())
+ : findDataSchemaNodeForRelativeXPath(context, module, actualSchemaNode, targetPath);
+ if (deref == null) {
+ LOG.debug("Path {} could not be derefenced", targetPath);
+ return null;
+ }
+
+ checkArgument(deref instanceof LeafSchemaNode, "Unexpected %s reference in %s", deref, targetPath);
+
+ final List<String> qnames = doSplitXPath(xpath.substring(paren + 1).stripLeading());
+ return findTargetNode(context, resolveRelativePath(context, module, deref, qnames));
+ }
+
+ private static @Nullable SchemaNode findTargetNode(final SchemaContext context, final QNameModule localNamespace,
+ final YangLocationPath path) {
+ final Deque<QName> ret = new ArrayDeque<>();
+ for (Step step : path.getSteps()) {
+ if (step instanceof AxisStep) {
+ // We only support parent axis steps
+ final YangXPathAxis axis = ((AxisStep) step).getAxis();
+ checkState(axis == YangXPathAxis.PARENT, "Unexpected axis %s", axis);
+ ret.removeLast();
+ continue;
+ }
+
+ // This has to be a QNameStep
+ checkState(step instanceof QNameStep, "Unhandled step %s in %s", step, path);
+ ret.addLast(resolve(((QNameStep) step).getQName(), localNamespace));
+ }
+
+ return findTargetNode(context, ret);
+ }
+
+ private static @Nullable SchemaNode findTargetNode(final SchemaContext context, final Iterable<QName> qnamePath) {
+ // We do not have enough information about resolution context, hence cannot account for actions, RPCs
+ // and notifications. We therefore attempt to make a best estimate, but this can still fail.
+ final Optional<DataSchemaNode> pureData = context.findDataTreeChild(qnamePath);
+ return pureData.isPresent() ? pureData.get() : findNodeInSchemaContext(context, qnamePath);
+ }
+
+ private static QName resolve(final AbstractQName toResolve, final QNameModule localNamespace) {
+ if (toResolve instanceof QName) {
+ return (QName) toResolve;
+ } else if (toResolve instanceof UnqualifiedQName) {
+ return ((UnqualifiedQName) toResolve).bindTo(localNamespace);
+ } else {
+ throw new IllegalStateException("Unhandled step " + toResolve);
+ }
+ }
+
+ @VisibleForTesting
+ static int normalizeXPath(final List<String> xpath) {
+ LOG.trace("Normalize {}", xpath);
+
+ // We need to make multiple passes here, as the leading XPaths as we can have "../abc/../../def", which really
+ // is "../../def"
+ while (true) {
+ // Next up: count leading ".." components
+ int leadingParents = 0;
+ while (true) {
+ if (leadingParents == xpath.size()) {
+ return leadingParents;
+ }
+ if (!"..".equals(xpath.get(leadingParents))) {
+ break;
+ }
+
+ ++leadingParents;
+ }
+
+ // Now let's see if there there is a '..' in the rest
+ final int dots = findDots(xpath, leadingParents + 1);
+ if (dots == -1) {
+ return leadingParents;
+ }
+
+ xpath.remove(dots - 1);
+ xpath.remove(dots - 1);
+ LOG.trace("Next iteration {}", xpath);
+ }
+ }
+
+ private static int findDots(final List<String> xpath, final int startIndex) {
+ for (int i = startIndex; i < xpath.size(); ++i) {
+ if ("..".equals(xpath.get(i))) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private static List<String> doSplitXPath(final String xpath) {
+ final List<String> ret = new ArrayList<>();
+ for (String str : SLASH_SPLITTER.split(xpath)) {
+ ret.add(str);
+ }
+ return ret;