Allow unqualified node-identifiers in instance-identifier 58/103958/5
authorRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Mon, 24 Oct 2022 13:04:41 +0000 (16:04 +0300)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 9 Jan 2023 15:11:07 +0000 (16:11 +0100)
node-identifier has an optional prefix, make sure we reflect this in our
class design.

JIRA: YANGTOOLS-1361
Change-Id: I928eabf5d2508d9be1390f7662998829cb06af81
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 173787948ff24c4098602e351fb097a55a4ffd85)

xpath/yang-xpath-impl/pom.xml
xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4
xpath/yang-xpath-impl/src/main/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParser.java
xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java

index 8dfd23b0b06c17a1ec7eae27dd986efaa89c64dc..76e7c4a17b04ef5beb4fe166d3996bf736745b43 100644 (file)
                     <listener>false</listener>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <argLine>
+                        --add-exports org.opendaylight.yangtools.yang.xpath.impl/org.opendaylight.yangtools.yang.xpath.impl=ALL-UNNAMED
+                    </argLine>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
index 223b0b4e8fa7f533da151826659f1f66e26981c5..21691573c453c5a06f0dd141132f7e7d06cc3f0f 100644 (file)
@@ -20,7 +20,7 @@ instanceIdentifier : (SLASH pathArgument)+ EOF
 pathArgument : nodeIdentifier predicate?
   ;
 
-nodeIdentifier : Identifier COLON Identifier
+nodeIdentifier : Identifier (COLON Identifier)?
   ;
 
 predicate : keyPredicate+
index 077107063a153e3fe9e839e2a008eb5938056d6e..eafef29c45d05bd4321b606f0f4669a627df2349 100644 (file)
@@ -54,11 +54,21 @@ abstract class InstanceIdentifierParser {
             super(mathMode);
         }
 
+        @Override
+        YangQNameExpr createExpr(final String localName) {
+            return YangQNameExpr.of(UnresolvedQName.unqualified(localName));
+        }
+
         @Override
         YangQNameExpr createExpr(final String prefix, final String localName) {
             return YangQNameExpr.of(UnresolvedQName.qualified(prefix, localName));
         }
 
+        @Override
+        QNameStep createChildStep(final String localName, final Collection<YangExpr> predicates) {
+            return YangXPathAxis.CHILD.asStep(UnresolvedQName.unqualified(localName), predicates);
+        }
+
         @Override
         QNameStep createChildStep(final String prefix, final String localName, final Collection<YangExpr> predicates) {
             return YangXPathAxis.CHILD.asStep(UnresolvedQName.qualified(prefix, localName), predicates);
@@ -66,7 +76,7 @@ abstract class InstanceIdentifierParser {
     }
 
     static final class Qualified extends InstanceIdentifierParser {
-        final YangNamespaceContext namespaceContext;
+        private final YangNamespaceContext namespaceContext;
 
         Qualified(final YangXPathMathMode mathMode, final YangNamespaceContext namespaceContext) {
             super(mathMode);
@@ -74,16 +84,24 @@ abstract class InstanceIdentifierParser {
         }
 
         @Override
-        QNameStep createChildStep(final String prefix, final String localName, final Collection<YangExpr> predicates) {
-            return YangXPathAxis.CHILD.asStep(namespaceContext.createQName(prefix, localName), predicates);
+        YangQNameExpr createExpr(final String localName) {
+            return YangQNameExpr.of(UnresolvedQName.unqualified(localName));
         }
 
-
         @Override
         YangQNameExpr createExpr(final String prefix, final String localName) {
             return YangQNameExpr.of(namespaceContext.createQName(prefix, localName));
         }
 
+        @Override
+        QNameStep createChildStep(final String localName, final Collection<YangExpr> predicates) {
+            return YangXPathAxis.CHILD.asStep(UnresolvedQName.unqualified(localName), predicates);
+        }
+
+        @Override
+        QNameStep createChildStep(final String prefix, final String localName, final Collection<YangExpr> predicates) {
+            return YangXPathAxis.CHILD.asStep(namespaceContext.createQName(prefix, localName), predicates);
+        }
     }
 
     private final YangXPathMathSupport mathSupport;
@@ -113,23 +131,38 @@ abstract class InstanceIdentifierParser {
         return YangLocationPath.absolute(steps);
     }
 
+    abstract YangQNameExpr createExpr(String localName);
+
     abstract YangQNameExpr createExpr(String prefix, String localName);
 
+    abstract QNameStep createChildStep(String localName, Collection<YangExpr> predicates);
+
     abstract QNameStep createChildStep(String prefix, String localName, Collection<YangExpr> predicates);
 
     private QNameStep parsePathArgument(final PathArgumentContext expr) {
-        final NodeIdentifierContext childExpr = getChild(expr, NodeIdentifierContext.class, 0);
-        final String prefix = verifyIdentifier(childExpr, 0);
-        final String localName = verifyIdentifier(childExpr, 2);
-
+        final Collection<YangExpr> predicates;
         switch (expr.getChildCount()) {
             case 1:
-                return createChildStep(prefix, localName, ImmutableSet.of());
+                predicates = ImmutableSet.of();
+                break;
             case 2:
-                return createChildStep(prefix, localName, parsePredicate(getChild(expr, PredicateContext.class, 1)));
+                predicates = parsePredicate(getChild(expr, PredicateContext.class, 1));
+                break;
             default:
                 throw illegalShape(expr);
         }
+
+        final NodeIdentifierContext childExpr = getChild(expr, NodeIdentifierContext.class, 0);
+        final String first = verifyIdentifier(childExpr, 0);
+
+        switch (childExpr.getChildCount()) {
+            case 1:
+                return createChildStep(first, predicates);
+            case 3:
+                return createChildStep(first, verifyIdentifier(childExpr, 2), predicates);
+            default:
+                throw illegalShape(childExpr);
+        }
     }
 
     private Collection<YangExpr> parsePredicate(final PredicateContext expr) {
@@ -159,7 +192,15 @@ abstract class InstanceIdentifierParser {
     }
 
     private YangQNameExpr createChildExpr(final NodeIdentifierContext expr) {
-        return createExpr(verifyIdentifier(expr, 0), verifyIdentifier(expr, 2));
+        final var first = verifyIdentifier(expr, 0);
+        switch (expr.getChildCount()) {
+            case 1:
+                return createExpr(first);
+            case 3:
+                return createExpr(first, verifyIdentifier(expr, 2));
+            default:
+                throw illegalShape(expr);
+        }
     }
 
     private static String verifyIdentifier(final NodeIdentifierContext expr, final int child) {
index 32e0c5f6a9318fe37025a9b10773778c8fabe3f8..f26ddff2e98f46fedd1dc3ff652e22c6181ba536 100644 (file)
  */
 package org.opendaylight.yangtools.yang.xpath.impl;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
 import com.google.common.collect.ImmutableBiMap;
-import org.junit.Test;
+import java.util.List;
+import java.util.stream.Stream;
+import javax.xml.xpath.XPathExpressionException;
+import org.eclipse.jdt.annotation.Nullable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.opendaylight.yangtools.yang.common.AbstractQName;
 import org.opendaylight.yangtools.yang.common.BiMapYangNamespaceContext;
+import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.UnresolvedQName;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
 import org.opendaylight.yangtools.yang.common.YangNamespaceContext;
+import org.opendaylight.yangtools.yang.xpath.api.YangBinaryExpr;
+import org.opendaylight.yangtools.yang.xpath.api.YangBinaryOperator;
+import org.opendaylight.yangtools.yang.xpath.api.YangExpr;
 import org.opendaylight.yangtools.yang.xpath.api.YangLiteralExpr;
+import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath;
 import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.Absolute;
+import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.QNameStep;
+import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.Step;
+import org.opendaylight.yangtools.yang.xpath.api.YangQNameExpr;
 import org.opendaylight.yangtools.yang.xpath.api.YangXPathMathMode;
 
-class InstanceIdentifierParserTest {
+public class InstanceIdentifierParserTest {
     private static final QNameModule DEFNS = QNameModule.create(XMLNamespace.of("defaultns"));
     private static final YangNamespaceContext CONTEXT = new BiMapYangNamespaceContext(ImmutableBiMap.of(
         "def", DEFNS,
         "foo", QNameModule.create(XMLNamespace.of("foo")),
         "bar", QNameModule.create(XMLNamespace.of("bar"))));
 
-    private static final InstanceIdentifierParser PARSER =
+    private static final QName FOO_FOO_QUALIFIED = CONTEXT.createQName("foo", "foo");
+    private static final QName FOO_X_QUALIFIED = CONTEXT.createQName("foo", "x");
+    private static final UnresolvedQName FOO_FOO_UNRESOLVED = UnresolvedQName.qualified("foo", "foo");
+    private static final UnresolvedQName FOO_X_UNRESOLVED = UnresolvedQName.qualified("foo", "x");
+    private static final UnresolvedQName FOO_UNRESOLVED = UnresolvedQName.unqualified("foo");
+    private static final UnresolvedQName X_UNRESOLVED = UnresolvedQName.unqualified("x");
+
+    private static final YangExpr POSITION_EXPR = FunctionSupport.POSITION;
+    private static final YangExpr SELF_EXPR = YangLocationPath.self();
+
+    private static final InstanceIdentifierParser BASE = new InstanceIdentifierParser.Base(YangXPathMathMode.IEEE754);
+    private static final InstanceIdentifierParser QUALIFIED =
         new InstanceIdentifierParser.Qualified(YangXPathMathMode.IEEE754, CONTEXT);
 
-    @Test
-    void smokeTests() throws Exception {
-        assertPath("/foo:foo");
-        assertPath("/foo:foo[.='abcd']");
-        assertPath("/foo:foo[.=\"abcd\"]");
+    @ParameterizedTest(name = "Smoke test: {0}")
+    @MethodSource("smokeTestArgs")
+    public void smokeTest(final String input) throws Exception {
+        parseBase(input);
+        parseQualified(input);
+    }
+
+    public static List<String> smokeTestArgs() {
+        return List.of("/foo:foo/bar:bar", "/foo/bar", "/foo/bar[ 1 ]", "/foo/bar[11]", "/foo:foo[.='abc']",
+                "/foo[ . = \"abc\" ]", "/bar:bar[foo:x = 'a'][foo:y = 'b']", "/a/b/c");
+    }
+
+    @ParameterizedTest(name = "Qualified parsing: {0}")
+    @MethodSource("qualifiedParsingArgs")
+    public void qualifiedParsing(final String input, final AbstractQName expectedQName,
+            final @Nullable YangExpr expectedLeft, final @Nullable YangExpr expectedRight) throws Exception {
+        assertParsed(parseQualified(input), expectedQName, expectedLeft, expectedRight);
+    }
+
+    public static Stream<Arguments> qualifiedParsingArgs() {
+        return Stream.of(
+                // input, expected QName, expected left expr of predicate, expected right expr of predicate
+                Arguments.of("/foo:foo", FOO_FOO_QUALIFIED, null, null),
+                Arguments.of("/foo", FOO_UNRESOLVED, null, null),
+                Arguments.of("/foo:foo[1]", FOO_FOO_QUALIFIED, POSITION_EXPR, toNumberExpr(1)),
+                Arguments.of("/foo[ 101 ]", FOO_UNRESOLVED, POSITION_EXPR, toNumberExpr(101)),
+                Arguments.of("/foo:foo[foo:x='abc']", FOO_FOO_QUALIFIED,
+                        YangQNameExpr.of(FOO_X_QUALIFIED), YangLiteralExpr.of("abc")),
+                Arguments.of("/foo[ x = \"xyz\" ]", FOO_UNRESOLVED,
+                        YangQNameExpr.of(X_UNRESOLVED), YangLiteralExpr.of("xyz")),
+                Arguments.of("/foo:foo[.=\"\"]", FOO_FOO_QUALIFIED, SELF_EXPR, YangLiteralExpr.of("")),
+                Arguments.of("/foo[ . = \"value\" ]", FOO_UNRESOLVED, SELF_EXPR, YangLiteralExpr.of("value"))
+        );
+    }
+
+    @ParameterizedTest(name = "Base parsing: {0}")
+    @MethodSource("baseParsingArgs")
+    public void baseParsing(final String input, final AbstractQName expectedQName,
+            @Nullable final YangExpr expectedLeft, @Nullable final YangExpr expectedRight) throws Exception {
+        assertParsed(parseBase(input), expectedQName, expectedLeft, expectedRight);
+    }
+
+    public static Stream<Arguments> baseParsingArgs() {
+        return Stream.of(
+                // input, expected QName, expected left expr of predicate, expected right expr of predicate
+                Arguments.of("/foo:foo", FOO_FOO_UNRESOLVED, null, null),
+                Arguments.of("/foo", FOO_UNRESOLVED, null, null),
+                Arguments.of("/foo:foo[1]", FOO_FOO_UNRESOLVED, POSITION_EXPR, toNumberExpr(1)),
+                Arguments.of("/foo[ 101 ]", FOO_UNRESOLVED, POSITION_EXPR, toNumberExpr(101)),
+                Arguments.of("/foo:foo[foo:x='abc']", FOO_FOO_UNRESOLVED,
+                        YangQNameExpr.of(FOO_X_UNRESOLVED), YangLiteralExpr.of("abc")),
+                Arguments.of("/foo[ x = \"xyz\" ]", FOO_UNRESOLVED,
+                        YangQNameExpr.of(X_UNRESOLVED), YangLiteralExpr.of("xyz")),
+                Arguments.of("/foo:foo[.=\"\"]", FOO_FOO_UNRESOLVED, SELF_EXPR, YangLiteralExpr.of("")),
+                Arguments.of("/foo[ . = \"value\" ]", FOO_UNRESOLVED, SELF_EXPR, YangLiteralExpr.of("value"))
+        );
+    }
+
+    private static void assertParsed(final Absolute parsed, final AbstractQName expectedQName,
+            final @Nullable YangExpr expectedLeft, final @Nullable YangExpr expectedRight) throws Exception {
+        final var step = assertInstanceOf(QNameStep.class, extractFirstStep(parsed));
+        assertEquals(expectedQName, step.getQName());
+
+        if (expectedLeft == null) {
+            return;
+        }
+
+        final var predicate = assertInstanceOf(YangBinaryExpr.class, extractFirstPredicate(step));
+        assertEquals(YangBinaryOperator.EQUALS, predicate.getOperator());
+        assertEquals(expectedLeft, predicate.getLeftExpr());
+        assertEquals(expectedRight, predicate.getRightExpr());
+    }
+
+    @ParameterizedTest(name = "Unrecognized prefix: {0}")
+    @ValueSource(strings = {"/x:foo", "/foo:foo[x:bar = 'a']", "/foo[x:bar = 'b']"})
+    public void unrecognizedPrefix(final String input) {
+        assertThrows(IllegalArgumentException.class, () -> parseQualified(input));
+    }
+
+    @ParameterizedTest(name = "Literals with escaped quotes: {0}")
+    @MethodSource("escapedQuotesLiteralArgs")
+    public void escapedQuotesLiteral(final String input, final String expectedText) throws Exception {
+
+        // expected 1 step with predicate having literal
+        final var step = extractFirstStep(parseQualified(input));
+        final var predicate = assertInstanceOf(YangBinaryExpr.class, extractFirstPredicate(step));
+        final var right = assertInstanceOf(YangLiteralExpr.class, predicate.getRightExpr());
+
+        // ensure the string literal value unquoted properly
+        assertEquals(expectedText, right.getLiteral());
+    }
+
+    public static Stream<Arguments> escapedQuotesLiteralArgs() {
+        return Stream.of(
+                Arguments.of("/foo[.= \"quo\\\"tes\\\\\"]", "quo\"tes\\"),
+                Arguments.of("/foo[.= \"quo\\ntes\\t\"]", "quo\ntes\t"),
+                Arguments.of("/foo[.= 'quo\"tes']", "quo\"tes"),
+                Arguments.of("/foo[.= 'quo\\tes']", "quo\\tes"),
+                Arguments.of("/foo[.= \"\"]", ""),
+                Arguments.of("/foo[.= '']", ""),
+                Arguments.of("/foo[.= \"\"]", "")
+        );
+    }
+
+    private static Absolute parseQualified(final String literal) throws XPathExpressionException {
+        return QUALIFIED.interpretAsInstanceIdentifier(YangLiteralExpr.of(literal));
+    }
+
+    private static Absolute parseBase(final String literal) throws XPathExpressionException {
+        return BASE.interpretAsInstanceIdentifier(YangLiteralExpr.of(literal));
+    }
+
+    private static YangExpr toNumberExpr(final int number) {
+        return YangXPathMathMode.IEEE754.getSupport().createNumber(number);
+    }
+
+    private static Step extractFirstStep(final Absolute parsed) {
+        assertNotNull(parsed);
+        assertNotNull(parsed.getSteps());
+        assertFalse(parsed.getSteps().isEmpty());
+        final var step = parsed.getSteps().get(0);
+        assertNotNull(step);
+        return step;
     }
 
-    private static Absolute assertPath(final String literal) throws Exception {
-        return PARSER.interpretAsInstanceIdentifier(YangLiteralExpr.of(literal));
+    private static YangExpr extractFirstPredicate(final Step step) {
+        assertNotNull(step.getPredicates());
+        assertFalse(step.getPredicates().isEmpty());
+        final var predicate = step.getPredicates().iterator().next();
+        assertNotNull(predicate);
+        return predicate;
     }
 }