Bug 6878: Add support for parsing yang-specific XPath functions 38/51938/9
authorIgor Foltin <ifoltin@cisco.com>
Thu, 16 Feb 2017 08:43:15 +0000 (09:43 +0100)
committerRobert Varga <nite@hq.sk>
Wed, 1 Mar 2017 09:39:58 +0000 (09:39 +0000)
YANG 1.1 (RFC7950) introduces a set of yang-specific XPath functions.
YANG statement parser can now parse YANG 1.1 models which contain
these functions

Change-Id: Icfbf95db0d04a3f077133c38341f250cde4e56eb
Signed-off-by: Igor Foltin <ifoltin@cisco.com>
yang/yang-common/src/main/java/org/opendaylight/yangtools/yang/common/YangConstants.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/StmtNamespaceContext.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/Utils.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/stmt/rfc7950/Bug6878Test.java [new file with mode: 0644]
yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo.yang [new file with mode: 0644]
yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo10-invalid-2.yang [new file with mode: 0644]
yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo10-invalid.yang [new file with mode: 0644]

index 8a2d8840dfd939dc3261ed3b666dcf20c9ecfcb0..3b362cf8b8a79a92f81a4163761be79cd73bfa3e 100644 (file)
@@ -70,6 +70,11 @@ public final class YangConstants {
     public static final URI RFC7950_YANG_LIBRARY_CAPABILITY =
         URI.create("urn:ietf:params:netconf:capability:yang-library:1.0");
 
+    /**
+     * Prefix for YANG-specific XPath functions
+     */
+    public static final String YANG_XPATH_FUNCTIONS_PREFIX = "yang";
+
     private YangConstants() {
         throw new UnsupportedOperationException("Utility class");
     }
index 8a54ec25539debd546232d8fda4a2e1dab24b922..91b17389009e46e97365ce8ae3aa4296c4192ff5 100644 (file)
@@ -9,6 +9,8 @@ package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.Iterators;
 import java.util.Iterator;
 import javax.xml.namespace.NamespaceContext;
@@ -22,16 +24,26 @@ import org.opendaylight.yangtools.yang.parser.spi.meta.StmtContext;
  */
 final class StmtNamespaceContext implements NamespaceContext {
     private final StmtContext<?, ?, ?> ctx;
+    private final BiMap<String, String> URIToPrefixMap;
     private String localNamespaceURI;
 
     private StmtNamespaceContext(final StmtContext<?, ?, ?> ctx) {
+        this(ctx, ImmutableBiMap.of());
+    }
+
+    private StmtNamespaceContext(final StmtContext<?, ?, ?> ctx, final BiMap<String, String> URIToPrefixMap) {
         this.ctx = Preconditions.checkNotNull(ctx);
+        this.URIToPrefixMap = ImmutableBiMap.copyOf(Preconditions.checkNotNull(URIToPrefixMap));
     }
 
     public static NamespaceContext create(final StmtContext<?, ?, ?> ctx) {
         return new StmtNamespaceContext(ctx);
     }
 
+    public static NamespaceContext create(final StmtContext<?, ?, ?> ctx, final BiMap<String, String> URIToPrefixMap) {
+        return new StmtNamespaceContext(ctx, URIToPrefixMap);
+    }
+
     private String localNamespaceURI() {
         if (localNamespaceURI == null) {
             localNamespaceURI = Verify.verifyNotNull(
@@ -46,6 +58,11 @@ final class StmtNamespaceContext implements NamespaceContext {
         // API-mandated by NamespaceContext
         Preconditions.checkArgument(prefix != null);
 
+        final String uri = URIToPrefixMap.inverse().get(prefix);
+        if (uri != null) {
+            return uri;
+        }
+
         if (prefix.isEmpty()) {
             return localNamespaceURI();
         }
@@ -59,6 +76,11 @@ final class StmtNamespaceContext implements NamespaceContext {
         // API-mandated by NamespaceContext
         Preconditions.checkArgument(namespaceURI != null);
 
+        final String prefix = URIToPrefixMap.get(namespaceURI);
+        if (prefix != null) {
+            return prefix;
+        }
+
         if (localNamespaceURI().equals(namespaceURI)) {
             return "";
         }
@@ -68,7 +90,8 @@ final class StmtNamespaceContext implements NamespaceContext {
     @Override
     public Iterator<String> getPrefixes(final String namespaceURI) {
         // Ensures underlying map remains constant
-        return Iterators.unmodifiableIterator(
-            ctx.getAllFromNamespace(URIStringToImpPrefix.class).values().iterator());
+        return Iterators.unmodifiableIterator(Iterators.concat(
+                ctx.getAllFromNamespace(URIStringToImpPrefix.class).values().iterator(),
+                URIToPrefixMap.values().iterator()));
     }
 }
index dd23bc4adcb61fc0f3dee5f49d77fa7a21777486..05993a9c3d31a2ca45dfabf0fd8a4cf0bdef9f81 100644 (file)
@@ -7,12 +7,15 @@
  */
 package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
 
+import static org.opendaylight.yangtools.yang.common.YangConstants.RFC6020_YANG_NAMESPACE;
+import static org.opendaylight.yangtools.yang.common.YangConstants.YANG_XPATH_FUNCTIONS_PREFIX;
 import static org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils.firstAttributeOf;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap.Builder;
 import com.google.common.collect.ImmutableSet;
@@ -29,6 +32,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 import javax.annotation.Nullable;
+import javax.annotation.RegEx;
 import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathExpressionException;
 import javax.xml.xpath.XPathFactory;
@@ -79,6 +83,10 @@ public final class Utils {
     private static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
     private static final Splitter COLON_SPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
     private static final Pattern PATH_ABS = Pattern.compile("/[^/].*");
+    @RegEx
+    private static final String YANG_XPATH_FUNCTIONS_STRING =
+            "(re-match|deref|derived-from(-or-self)?|enum-value|bit-is-set)(\\()";
+    private static final Pattern YANG_XPATH_FUNCTIONS_PATTERN = Pattern.compile(YANG_XPATH_FUNCTIONS_STRING);
     private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
     private static final Set<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
             .add("AegeanNumbers")
@@ -360,12 +368,18 @@ public final class Utils {
 
     static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
         final XPath xPath = XPATH_FACTORY.get().newXPath();
-        xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
+        xPath.setNamespaceContext(StmtNamespaceContext.create(ctx,
+                ImmutableBiMap.of(RFC6020_YANG_NAMESPACE.toString(), YANG_XPATH_FUNCTIONS_PREFIX)));
 
         final String trimmed = trimSingleLastSlashFromXPath(path);
         try {
+            // XPath extension functions have to be prefixed
+            // yang-specific XPath functions are in fact extended functions, therefore we have to add
+            // "yang" prefix to them so that they can be properly validated with the XPath.compile() method
+            // the "yang" prefix is bound to RFC6020 YANG namespace
+            final String prefixedXPath = addPrefixToYangXPathFunctions(trimmed, ctx);
             // TODO: we could capture the result and expose its 'evaluate' method
-            xPath.compile(trimmed);
+            xPath.compile(prefixedXPath);
         } catch (final XPathExpressionException e) {
             LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
         }
@@ -373,6 +387,24 @@ public final class Utils {
         return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
     }
 
+    private static String addPrefixToYangXPathFunctions(final String path, final StmtContext<?, ?, ?> ctx) {
+        if (ctx.getRootVersion() == YangVersion.VERSION_1_1) {
+            // FIXME once Java 9 is available, change this to StringBuilder as Matcher.appendReplacement() and
+            // Matcher.appendTail() will accept StringBuilder parameter in Java 9
+            final StringBuffer result = new StringBuffer();
+            final String prefix = YANG_XPATH_FUNCTIONS_PREFIX + ":";
+            final Matcher matcher = YANG_XPATH_FUNCTIONS_PATTERN.matcher(path);
+            while (matcher.find()) {
+                matcher.appendReplacement(result, prefix + matcher.group());
+            }
+
+            matcher.appendTail(result);
+            return result.toString();
+        }
+
+        return path;
+    }
+
     public static QName trimPrefix(final QName identifier) {
         final String prefixedLocalName = identifier.getLocalName();
         final String[] namesParts = prefixedLocalName.split(":");
diff --git a/yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/stmt/rfc7950/Bug6878Test.java b/yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/stmt/rfc7950/Bug6878Test.java
new file mode 100644 (file)
index 0000000..97b99d0
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2017 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.yangtools.yang.parser.stmt.rfc7950;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.stmt.StmtTestUtils;
+
+public class Bug6878Test {
+
+    @Test
+    public void testParsingXPathWithYang11Functions() throws Exception {
+        final PrintStream stdout = System.out;
+        final ByteArrayOutputStream output = new ByteArrayOutputStream();
+        final String testLog;
+
+        System.setOut(new PrintStream(output, true, "UTF-8"));
+
+        final SchemaContext schemaContext = StmtTestUtils.parseYangSource("/rfc7950/bug6878/foo.yang");
+        assertNotNull(schemaContext);
+
+        testLog = output.toString();
+        assertFalse(testLog.contains("Could not find function: "));
+        System.setOut(stdout);
+    }
+
+    @Test
+    public void shouldLogInvalidYang10XPath() throws Exception {
+        final PrintStream stdout = System.out;
+        final ByteArrayOutputStream output = new ByteArrayOutputStream();
+        final String testLog;
+
+        System.setOut(new PrintStream(output, true, "UTF-8"));
+
+        StmtTestUtils.parseYangSource("/rfc7950/bug6878/foo10-invalid.yang");
+
+        testLog = output.toString();
+        assertTrue(testLog.contains("Could not find function: re-match"));
+        System.setOut(stdout);
+    }
+
+    @Test
+    public void shouldLogInvalidYang10XPath2() throws Exception {
+        final PrintStream stdout = System.out;
+        final ByteArrayOutputStream output = new ByteArrayOutputStream();
+        final String testLog;
+
+        System.setOut(new PrintStream(output, true, "UTF-8"));
+
+        StmtTestUtils.parseYangSource("/rfc7950/bug6878/foo10-invalid-2.yang");
+
+        testLog = output.toString();
+        assertTrue(testLog.contains("Could not find function: deref"));
+        System.setOut(stdout);
+    }
+}
diff --git a/yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo.yang b/yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo.yang
new file mode 100644 (file)
index 0000000..5a6d3c9
--- /dev/null
@@ -0,0 +1,93 @@
+module foo {
+    namespace foo;
+    prefix foo;
+    yang-version 1.1;
+
+    revision 2016-02-24;
+
+    identity interface-type;
+
+    identity ethernet {
+        base interface-type;
+    }
+
+    identity fast-ethernet {
+        base ethernet;
+    }
+
+    identity gigabit-ethernet {
+        base ethernet;
+    }
+
+    list interface {
+        key "name";
+        leaf name {
+            type string;
+        }
+
+        leaf type {
+            type identityref {
+                base interface-type;
+            }
+        }
+
+        leaf enabled {
+            type boolean;
+        }
+    }
+
+    leaf outgoing-interface {
+        type leafref {
+            path "/interface/name";
+        }
+
+        must 'count(/interface[re-match(name, "eth0\.\d+")]) = 3';
+    }
+
+    container mgmt-interface {
+        leaf name {
+            type leafref {
+                path "/interface/name";
+            }
+        }
+        leaf type {
+            type leafref {
+                path "/interface[name=current()/../name]/type";
+            }
+            must 'derived-from-or-self(deref(.), "foo:ethernet")';
+        }
+    }
+
+    container my-cont {
+        leaf enum-value {
+            type enumeration {
+                enum a {
+                    value 1;
+                }
+                enum b {
+                    value 2;
+                }
+            }
+        }
+
+        must "enum-value(current()/enum-value) = 1";
+
+        leaf bit-is-set {
+            type bits {
+                bit x;
+                bit y;
+                bit z;
+            }
+        }
+
+        leaf bits-leaf {
+            type bits {
+                bit a;
+                bit b;
+                bit c;
+            }
+        }
+
+        must 'bit-is-set(current()/bit-is-set, "z") and bit-is-set(current()/bits-leaf, "c")';
+    }
+}
diff --git a/yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo10-invalid-2.yang b/yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo10-invalid-2.yang
new file mode 100644 (file)
index 0000000..08f9d41
--- /dev/null
@@ -0,0 +1,35 @@
+module foo {
+    namespace foo;
+    prefix foo;
+
+    revision 2017-02-10;
+
+    list interface {
+        key "name type";
+        leaf name {
+            type string;
+        }
+        leaf type {
+            type string;
+        }
+        leaf enabled {
+            type boolean;
+        }
+    }
+
+    container mgmt-interface {
+        leaf name {
+            type leafref {
+                path "/interface/name";
+            }
+        }
+        leaf type {
+            type leafref {
+                path "/interface[name=current()/../name]/type";
+            }
+            must "deref(.)/../enabled = 'true'" {
+                error-message "The management interface cannot be disabled.";
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo10-invalid.yang b/yang/yang-parser-impl/src/test/resources/rfc7950/bug6878/foo10-invalid.yang
new file mode 100644 (file)
index 0000000..25ecbdf
--- /dev/null
@@ -0,0 +1,25 @@
+module foo {
+    namespace foo;
+    prefix foo;
+
+    revision 2017-02-10;
+
+    list interface {
+        key "name";
+        leaf name {
+            type string;
+        }
+
+        leaf enabled {
+            type boolean;
+        }
+    }
+
+    leaf outgoing-interface {
+        type leafref {
+            path "/interface/name";
+        }
+
+        must "count(/interface[re-match(name, 'eth0\.\d+')]) = 3";
+    }
+}
\ No newline at end of file