From c37678a3eb62685d32a1581729e2a4e26002bffc Mon Sep 17 00:00:00 2001 From: ToineSiebelink Date: Thu, 20 May 2021 16:44:21 +0100 Subject: Introducing Antlr4 for cpsPath parsing -created new module for cpPathParser -added antlr rule for cpsPathWithSingleLeafCondition -added antlr rule for cpsPathWithDescendant (and with leaf conditions) -added antlr rule for ancestor axis -added unit test (copied from existing CpsPathQuerySpec) -udpated cps-ri to use new cpPathQuery from parser module -'imported' lexer rules from publix xPath grammar -Re-used existing CpsPathException but conversion happens in cps-ri to prevent additional dependency in cps-path-parser module Issue-ID: CPS-376 Change-Id: I2c5df98969402cbf69f6573c52705879450ce606 Signed-off-by: ToineSiebelink --- .../org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 | 123 ++++++++++++++++++++ .../onap/cps/cpspath/parser/CpsPathBuilder.java | 107 ++++++++++++++++++ .../org/onap/cps/cpspath/parser/CpsPathQuery.java | 79 +++++++++++++ .../onap/cps/cpspath/parser/CpsPathQueryType.java | 39 +++++++ .../cps/cpspath/parser/CpsPathQuerySpec.groovy | 124 +++++++++++++++++++++ 5 files changed, 472 insertions(+) create mode 100644 cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 create mode 100644 cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java create mode 100644 cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java create mode 100644 cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java create mode 100644 cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy (limited to 'cps-path-parser/src') diff --git a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 new file mode 100644 index 000000000..86095459e --- /dev/null +++ b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 @@ -0,0 +1,123 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +grammar CpsPath ; + +cpsPath: (cpsPathWithSingleLeafCondition | cpsPathWithDescendant | cpsPathWithDescendantAndLeafConditions) ancestorAxis? ; + +ancestorAxis: SLASH KW_ANCESTOR COLONCOLON ancestorPath ; + +ancestorPath: yangElement (SLASH yangElement)* ; + +cpsPathWithSingleLeafCondition: prefix singleValueCondition ; + +/* +No need to ditinguish between cpsPathWithDescendant | cpsPathWithDescendantAndLeafConditions really! +See https://jira.onap.org/browse/CPS-436 +*/ + +cpsPathWithDescendant: descendant ; + +cpsPathWithDescendantAndLeafConditions: descendant multipleValueConditions ; + +descendant: SLASH prefix ; + +prefix: (SLASH yangElement)* SLASH containerName ; + +yangElement: containerName listElementRef? ; + +containerName: QName ; + +listElementRef: multipleValueConditions ; + +singleValueCondition: '[' leafCondition ']' ; + +multipleValueConditions: '[' leafCondition (' and ' leafCondition)* ']' ; + +leafCondition: '@' leafName '=' (IntegerLiteral | StringLiteral ) ; + +//To Confirm: defintion of Lefname with external xPath grammar +leafName: QName ; + +/* + * Lexer Rules + * Most of the lexer rules below are 'imporetd' from + * https://raw.githubusercontent.com/antlr/grammars-v4/master/xpath/xpath31/XPath31.g4 + */ + +SLASH : '/'; +COLONCOLON : '::' ; + +// KEYWORDS + +KW_ANCESTOR : 'ancestor' ; + +IntegerLiteral : FragDigits ; +// Add below type definitions for leafvalue comparision in https://jira.onap.org/browse/CPS-440 +DecimalLiteral : ('.' FragDigits) | (FragDigits '.' [0-9]*) ; +DoubleLiteral : (('.' FragDigits) | (FragDigits ('.' [0-9]*)?)) [eE] [+-]? FragDigits ; +StringLiteral : ('"' (FragEscapeQuot | ~[^"])*? '"') | ('\'' (FragEscapeApos | ~['])*? '\'') ; +fragment FragEscapeQuot : '""' ; +fragment FragEscapeApos : '\''; +fragment FragDigits : [0-9]+ ; + +QName : FragQName ; +NCName : FragmentNCName ; +fragment FragQName : FragPrefixedName | FragUnprefixedName ; +fragment FragPrefixedName : FragPrefix ':' FragLocalPart ; +fragment FragUnprefixedName : FragLocalPart ; +fragment FragPrefix : FragmentNCName ; +fragment FragLocalPart : FragmentNCName ; +fragment FragNCNameStartChar + : 'A'..'Z' + | '_' + | 'a'..'z' + | '\u00C0'..'\u00D6' + | '\u00D8'..'\u00F6' + | '\u00F8'..'\u02FF' + | '\u0370'..'\u037D' + | '\u037F'..'\u1FFF' + | '\u200C'..'\u200D' + | '\u2070'..'\u218F' + | '\u2C00'..'\u2FEF' + | '\u3001'..'\uD7FF' + | '\uF900'..'\uFDCF' + | '\uFDF0'..'\uFFFD' + | '\u{10000}'..'\u{EFFFF}' + ; +fragment FragNCNameChar + : FragNCNameStartChar | '-' | '.' | '0'..'9' + | '\u00B7' | '\u0300'..'\u036F' + | '\u203F'..'\u2040' + ; +fragment FragmentNCName : FragNCNameStartChar FragNCNameChar* ; + +// https://www.w3.org/TR/REC-xml/#NT-Char + +fragment FragChar : '\u0009' | '\u000a' | '\u000d' + | '\u0020'..'\ud7ff' + | '\ue000'..'\ufffd' + | '\u{10000}'..'\u{10ffff}' + ; + +// Skip all Whitespace +Whitespace : ('\u000d' | '\u000a' | '\u0020' | '\u0009')+ -> skip ; + +// handle characters which failed to match any other token (otherwise Antlr will ignore them) +ErrorCharacter : . ; diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java new file mode 100644 index 000000000..83e076d53 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java @@ -0,0 +1,107 @@ +package org.onap.cps.cpspath.parser; +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +import java.util.HashMap; +import java.util.Map; +import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.AncestorAxisContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.CpsPathWithDescendantAndLeafConditionsContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.CpsPathWithDescendantContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.CpsPathWithSingleLeafConditionContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.LeafConditionContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.MultipleValueConditionsContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.PrefixContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.SingleValueConditionContext; + +public class CpsPathBuilder extends CpsPathBaseListener { + + final CpsPathQuery cpsPathQuery = new CpsPathQuery(); + + final Map leavesData = new HashMap<>(); + + @Override + public void exitPrefix(final PrefixContext ctx) { + cpsPathQuery.setXpathPrefix(ctx.getText()); + } + + @Override + public void exitLeafCondition(final LeafConditionContext ctx) { + Object comparisonValue = null; + if (ctx.IntegerLiteral() != null) { + comparisonValue = Integer.valueOf(ctx.IntegerLiteral().getText()); + } + if (ctx.StringLiteral() != null) { + comparisonValue = stripFirstAndLastCharacter(ctx.StringLiteral().getText()); + } else if (comparisonValue == null) { + throw new IllegalStateException("Unsupported comparison value encountered in expression" + ctx.getText()); + } + leavesData.put(ctx.leafName().getText(), comparisonValue); + } + + @Override + public void enterSingleValueCondition(final SingleValueConditionContext ctx) { + leavesData.clear(); + } + + @Override + public void enterMultipleValueConditions(final MultipleValueConditionsContext ctx) { + leavesData.clear(); + } + + @Override + public void exitSingleValueCondition(final SingleValueConditionContext ctx) { + final String leafName = ctx.leafCondition().leafName().getText(); + cpsPathQuery.setLeafName(leafName); + cpsPathQuery.setLeafValue(leavesData.get(leafName)); + } + + @Override + public void exitCpsPathWithSingleLeafCondition(final CpsPathWithSingleLeafConditionContext ctx) { + cpsPathQuery.setCpsPathQueryType(CpsPathQueryType.XPATH_LEAF_VALUE); + } + + @Override + public void exitCpsPathWithDescendant(final CpsPathWithDescendantContext ctx) { + cpsPathQuery.setCpsPathQueryType(CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE); + cpsPathQuery.setDescendantName(cpsPathQuery.getXpathPrefix().substring(1)); + } + + @Override + public void exitCpsPathWithDescendantAndLeafConditions( + final CpsPathWithDescendantAndLeafConditionsContext ctx) { + cpsPathQuery.setCpsPathQueryType(CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES); + cpsPathQuery.setDescendantName(cpsPathQuery.getXpathPrefix().substring(1)); + cpsPathQuery.setLeavesData(leavesData); + } + + @Override + public void exitAncestorAxis(final AncestorAxisContext ctx) { + cpsPathQuery.setAncestorSchemaNodeIdentifier(ctx.ancestorPath().getText()); + } + + CpsPathQuery build() { + return cpsPathQuery; + } + + private static String stripFirstAndLastCharacter(final String wrappedString) { + return wrappedString.substring(1, wrappedString.length() - 1); + } + +} diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java new file mode 100644 index 000000000..32fe0cbb7 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java @@ -0,0 +1,79 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.cpspath.parser; + + +import java.util.Map; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.onap.cps.cpspath.parser.antlr4.CpsPathLexer; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; + +@Getter +@Setter(AccessLevel.PACKAGE) +public class CpsPathQuery { + + private CpsPathQueryType cpsPathQueryType; + private String xpathPrefix; + private String leafName; + private Object leafValue; + private String descendantName; + private Map leavesData; + private String ancestorSchemaNodeIdentifier = ""; + + /** + * Returns a cps path query. + * + * @param cpsPathSource cps path + * @return a CpsPathQuery object. + */ + public static CpsPathQuery createFrom(final String cpsPathSource) { + final var inputStream = CharStreams.fromString(cpsPathSource); + final var cpsPathLexer = new CpsPathLexer(inputStream); + final var cpsPathParser = new CpsPathParser(new CommonTokenStream(cpsPathLexer)); + cpsPathParser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(final Recognizer recognizer, final Object offendingSymbol, final int line, + final int charPositionInLine, final String msg, final RecognitionException e) { + throw new IllegalStateException("failed to parse at line " + line + " due to " + msg, e); + } + }); + final var cpsPathBuilder = new CpsPathBuilder(); + cpsPathParser.addParseListener(cpsPathBuilder); + cpsPathParser.cpsPath(); + return cpsPathBuilder.build(); + } + + /** + * Has ancestor axis been populated. + * + * @return boolean value. + */ + public boolean hasAncestorAxis() { + return !(ancestorSchemaNodeIdentifier.isEmpty()); + } + +} diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java new file mode 100644 index 000000000..bc6b9f092 --- /dev/null +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2020-2021 Bell Canada. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.cpspath.parser; + +/** + * The enum Cps path query type. + */ +public enum CpsPathQueryType { + /** + * Xpath descendant anywhere type e.g. //nodeName . + */ + XPATH_HAS_DESCENDANT_ANYWHERE, + /** + * Xpath descendant anywhere type e.g. //nodeName[@leafName="value"] . + */ + XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES, + /** + * Xpath leaf value cps path query type e.g. /cps-path[@leaf1="leafValue" and @leaf2=123] . + */ + XPATH_LEAF_VALUE +} diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy new file mode 100644 index 000000000..0e7fc35cf --- /dev/null +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.cpspath.parser + +import spock.lang.Specification + +class CpsPathQuerySpec extends Specification { + + def 'Parse cps path with valid cps path and a filter with #scenario.'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom(cpsPath) + then: 'the query has the right type' + result.cpsPathQueryType == CpsPathQueryType.XPATH_LEAF_VALUE + and: 'the right query parameters are set' + result.xpathPrefix == expectedXpathPrefix + result.leafName == expectedLeafName + result.leafValue == expectedLeafValue + where: 'the following data is used' + scenario | cpsPath || expectedXpathPrefix | expectedLeafName | expectedLeafValue + 'leaf of type String' | '/parent/child[@common-leaf-name="common-leaf-value"]' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' + 'leaf of type String' | '/parent/child[@common-leaf-name=\'common-leaf-value\']' || '/parent/child' | 'common-leaf-name' | 'common-leaf-value' + 'leaf of type Integer' | '/parent/child[@common-leaf-name-int=5]' || '/parent/child' | 'common-leaf-name-int' | 5 + 'spaces around =' | '/parent/child[@common-leaf-name-int = 5]' || '/parent/child' | 'common-leaf-name-int' | 5 + 'key in top container' | '/parent[@common-leaf-name-int=5]' || '/parent' | 'common-leaf-name-int' | 5 + 'parent list' | '/shops/shop[@id=1]/categories[@id=1]/book[@title="Dune"]' || '/shops/shop[@id=1]/categories[@id=1]/book' | 'title' | 'Dune' + } + + def 'Parse cps path of type ends with a #scenario.'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom(cpsPath) + then: 'the query has the right type' + result.cpsPathQueryType == CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE + and: 'the right ends with parameters are set' + result.descendantName == expectedDescendantName + where: 'the following data is used' + scenario | cpsPath || expectedDescendantName + 'yang container' | '//cps-path' || 'cps-path' + 'parent & child' | '//parent/child' || 'parent/child' + } + + def 'Parse cps path that ends with a yang list containing #scenario.'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom(cpsPath) + then: 'the query has the right type' + result.cpsPathQueryType == CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES + and: 'the right ends with parameters are set' + result.descendantName == "child" + result.leavesData.size() == expectedNumberOfLeaves + where: 'the following data is used' + scenario | cpsPath || expectedNumberOfLeaves + 'one attribute' | '//child[@common-leaf-name-int=5]' || 1 + 'more than one attribute' | '//child[@int-leaf=5 and @leaf-name="leaf value"]' || 2 + } + + def 'Parse cps path with error: #scenario.'() { + when: 'the given cps path is parsed' + CpsPathQuery.createFrom(cpsPath) + then: 'a CpsPathException is thrown' + thrown(IllegalStateException) + where: 'the following data is used' + scenario | cpsPath + 'no / at the start' | 'invalid-cps-path/child' + 'additional / after descendant option' | '///cps-path' + 'float value' | '/parent/child[@someFloat=5.0]' + 'unmatched quotes, double quote first ' | '/parent/child[@someString="value with unmatched quotes\']' + 'unmatched quotes, single quote first' | '/parent/child[@someString=\'value with unmatched quotes"]' + 'end with descendant and more than one attribute separated by "or"' | '//child[@int-leaf=5 or @leaf-name="leaf value"]' + 'missing attribute value' | '//child[@int-leaf=5 and @name]' + 'incomplete ancestor value' | '//books/ancestor::' + } + + def 'Parse cps path using ancestor by schema node identifier with a #scenario.'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom('//descendant/ancestor::' + ancestorPath) + then: 'the query has the right type' + result.cpsPathQueryType == CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE + and: 'the result has ancestor axis' + result.hasAncestorAxis() + and: 'the correct ancestor schema node identifier is set' + result.ancestorSchemaNodeIdentifier == ancestorPath + where: + scenario | ancestorPath + 'basic container' | 'someContainer' + 'container with parent' | 'parent/child' + 'ancestor that is a list' | 'categories[@code=1]' + 'parent that is a list' | 'parent[@id=1]/child' + } + + def 'Combinations #scenario.'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom(cpsPath + '/ancestor::someAncestor') + then: 'the query has the right type' + result.cpsPathQueryType == expectedCpsPathQueryType + and: 'the result has ancestor axis' + result.hasAncestorAxis() + and: 'the correct ancestor schema node identifier is set' + result.ancestorSchemaNodeIdentifier == 'someAncestor' + result.descendantName == expectedDescendantName + where: + scenario | cpsPath || expectedDescendantName | expectedCpsPathQueryType + 'basic container' | '//someContainer' || 'someContainer' | CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE + 'container with parent' | '//parent/child' || 'parent/child' | CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE + 'container with list-parent' | '//parent[@id=1]/child' || 'parent[@id=1]/child' | CpsPathQueryType.XPATH_HAS_DESCENDANT_ANYWHERE + 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || 'parent[@id=1]/child' | CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES + } + +} -- cgit 1.2.3-korg