From 325b4e38dac21c49c3491e820fd213d646429fbe Mon Sep 17 00:00:00 2001 From: Ruslan Kashapov Date: Mon, 24 Oct 2022 16:04:41 +0300 Subject: [PATCH] Allow unqualified node-identifiers in instance-identifier 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 Signed-off-by: Robert Varga (cherry picked from commit 173787948ff24c4098602e351fb097a55a4ffd85) --- xpath/yang-xpath-impl/pom.xml | 9 + .../xpath/antlr/instanceIdentifierParser.g4 | 2 +- .../xpath/impl/InstanceIdentifierParser.java | 63 +++++-- .../impl/InstanceIdentifierParserTest.java | 176 +++++++++++++++++- 4 files changed, 228 insertions(+), 22 deletions(-) diff --git a/xpath/yang-xpath-impl/pom.xml b/xpath/yang-xpath-impl/pom.xml index 8dfd23b0b0..76e7c4a17b 100644 --- a/xpath/yang-xpath-impl/pom.xml +++ b/xpath/yang-xpath-impl/pom.xml @@ -71,6 +71,15 @@ false + + org.apache.maven.plugins + maven-surefire-plugin + + + --add-exports org.opendaylight.yangtools.yang.xpath.impl/org.opendaylight.yangtools.yang.xpath.impl=ALL-UNNAMED + + + diff --git a/xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4 b/xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4 index 223b0b4e8f..21691573c4 100644 --- a/xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4 +++ b/xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4 @@ -20,7 +20,7 @@ instanceIdentifier : (SLASH pathArgument)+ EOF pathArgument : nodeIdentifier predicate? ; -nodeIdentifier : Identifier COLON Identifier +nodeIdentifier : Identifier (COLON Identifier)? ; predicate : keyPredicate+ diff --git a/xpath/yang-xpath-impl/src/main/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParser.java b/xpath/yang-xpath-impl/src/main/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParser.java index 077107063a..eafef29c45 100644 --- a/xpath/yang-xpath-impl/src/main/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParser.java +++ b/xpath/yang-xpath-impl/src/main/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParser.java @@ -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 predicates) { + return YangXPathAxis.CHILD.asStep(UnresolvedQName.unqualified(localName), predicates); + } + @Override QNameStep createChildStep(final String prefix, final String localName, final Collection 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 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 predicates) { + return YangXPathAxis.CHILD.asStep(UnresolvedQName.unqualified(localName), predicates); + } + + @Override + QNameStep createChildStep(final String prefix, final String localName, final Collection 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 predicates); + abstract QNameStep createChildStep(String prefix, String localName, Collection 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 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 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) { diff --git a/xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java b/xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java index 32e0c5f6a9..f26ddff2e9 100644 --- a/xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java +++ b/xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java @@ -7,34 +7,190 @@ */ 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 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 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 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 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; } } -- 2.36.6