diff options
6 files changed, 64 insertions, 1 deletions
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 index d4718111f6..86c1705617 100644 --- 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 @@ -21,7 +21,7 @@ grammar CpsPath ; -cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? invalidPostFix?; +cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? containsFunctionCondition? ancestorAxis? invalidPostFix?; ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ; @@ -29,6 +29,8 @@ ancestorPath : yangElement ( SLASH yangElement)* ; textFunctionCondition : SLASH leafName OB KW_TEXT_FUNCTION EQ StringLiteral CB ; +containsFunctionCondition : OB KW_CONTAINS_FUNCTION OP AT leafName COMMA StringLiteral CP CB ; + parent : ( SLASH yangElement)* ; prefix : parent SLASH containerName ; @@ -65,6 +67,9 @@ COLONCOLON : '::' ; EQ : '=' ; OB : '[' ; SLASH : '/' ; +COMMA : ',' ; +OP : '(' ; +CP : ')' ; // KEYWORDS @@ -72,6 +77,7 @@ KW_ANCESTOR : 'ancestor' ; KW_AND : 'and' ; KW_TEXT_FUNCTION: 'text()' ; KW_OR : 'or' ; +KW_CONTAINS_FUNCTION: 'contains' ; IntegerLiteral : FragDigits ; // Add below type definitions for leafvalue comparision in https://jira.onap.org/browse/CPS-440 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 index 4299d13081..854450c8bb 100644 --- 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 @@ -150,6 +150,12 @@ public class CpsPathBuilder extends CpsPathBaseListener { } @Override + public void exitContainsFunctionCondition(final CpsPathParser.ContainsFunctionConditionContext ctx) { + cpsPathQuery.setContainsFunctionConditionLeafName(ctx.leafName().getText()); + cpsPathQuery.setContainsFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText())); + } + + @Override public void enterListElementRef(final CpsPathParser.ListElementRefContext ctx) { normalizedXpathBuilder.append(OPEN_BRACKET); if (processingAncestorAxis) { 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 index 2c96d91051..418b5ec55b 100644 --- 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 @@ -44,6 +44,8 @@ public class CpsPathQuery { private String textFunctionConditionLeafName; private String textFunctionConditionValue; private List<String> booleanOperatorsType; + private String containsFunctionConditionLeafName; + private String containsFunctionConditionValue; /** * Returns a cps path query. @@ -83,6 +85,15 @@ public class CpsPathQuery { } /** + * Has contains function condition been included in cpsPath. + * + * @return boolean value. + */ + public boolean hasContainsFunctionCondition() { + return containsFunctionConditionLeafName != null; + } + + /** * Returns boolean indicating xpath is an absolute path to a list element. * * @return true if xpath is an absolute path to a list element 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 index 153dfbe9ed..96fdf88cf6 100644 --- 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 @@ -136,6 +136,17 @@ class CpsPathQuerySpec extends Specification { 'descendant with leaf value and ancestor' | '//child[@other-leaf=1]/leaf-name[text()="search"]/ancestor::parent' || true | true } + def 'Parse #scenario cps path with contains function condition'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom('//someContainer[contains(@lang,"en")]') + then: 'the query has the right xpath type' + result.cpsPathPrefixType == DESCENDANT + and: 'the right contains function condition is set' + result.hasContainsFunctionCondition() + result.containsFunctionConditionLeafName == 'lang' + result.containsFunctionConditionValue == 'en' + } + def 'Parse cps path with error: #scenario.'() { when: 'the given cps path is parsed' CpsPathQuery.createFrom(cpsPath) diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java index 1643ca078a..212eb93cb2 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java @@ -97,6 +97,7 @@ public class FragmentQueryBuilder { sqlStringBuilder.append(")"); } addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters); + addContainsFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters); final Query query = entityManager.createNativeQuery(sqlStringBuilder.toString(), FragmentEntity.class); setQueryParameters(query, queryParameters); return query; @@ -159,6 +160,16 @@ public class FragmentQueryBuilder { } } + private static void addContainsFunctionCondition(final CpsPathQuery cpsPathQuery, + final StringBuilder sqlStringBuilder, + final Map<String, Object> queryParameters) { + if (cpsPathQuery.hasContainsFunctionCondition()) { + sqlStringBuilder.append(" AND attributes ->> :containsLeafName LIKE CONCAT('%',:containsValue,'%') "); + queryParameters.put("containsLeafName", cpsPathQuery.getContainsFunctionConditionLeafName()); + queryParameters.put("containsValue", cpsPathQuery.getContainsFunctionConditionValue()); + } + } + private static void setQueryParameters(final Query query, final Map<String, Object> queryParameters) { for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) { query.setParameter(queryParameter.getKey(), queryParameter.getValue()); diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy index bd39605de3..38bb4de573 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy @@ -147,6 +147,22 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { 'text condition on key containing /' | '//books/title[text()="Debian GNU/Linux"]' || ["Debian GNU/Linux"] } + def 'Query for attribute by cps path using contains condition #scenario.'() { + when: 'a query is executed to get response by the given cps path' + def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS) + then: 'the cps-path of queryDataNodes has expected number of nodes' + assert result.size() == expectedResultsize + and: 'xpaths of the retrieved data nodes are as expected' + def bookTitles = result.collect { it.getLeaves().get('title') } + assert bookTitles.sort() == expectedBookTitles.sort() + where: 'the following data is used' + scenario | cpsPath | expectedResultsize || expectedBookTitles + 'contains condition with leaf' | '//books[contains(@title,"Mat")]' | 1 || ["Matilda"] + 'contains condition with case-sensitive' | '//books[contains(@title,"Ti")]' | 0 || [] + 'contains condition with Integer Value' | '//books[contains(@price,"15")]' | 2 || ["Annihilation", "The Gruffalo"] + 'contains condition with No-value' | '//books[contains(@title,"")]' | 9 || ["A Book with No Language", "Annihilation", "Debian GNU/Linux", "Good Omens", "Logarithm tables", "Matilda", "The Colour of Magic", "The Gruffalo", "The Light Fantastic"] + } + def 'Cps Path query using descendant anywhere with #scenario condition for a container element.'() { when: 'a query is executed to get a data node by the given cps path' def result = objectUnderTest.queryDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, OMIT_DESCENDANTS) @@ -164,6 +180,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { 'more than one leaf has multiple OR' | '//books[ @title="Matilda" or @price=15 or @edition=2006]' || ['Annihilation', 'Matilda', 'The Gruffalo'] 'leaves reversed in order' | '//books[@lang="English" and @price=12]' || ['The Colour of Magic'] 'leaf and text' | '//books[@price=14]/authors[text()="Terry Pratchett"]' || ['The Light Fantastic'] + 'leaf and contains' | '//books[contains(@price,"13")]' || ['Good Omens'] } def 'Cps Path query using descendant anywhere with #scenario condition(s) for a list element.'() { @@ -195,6 +212,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase { 'ancestor combined with text condition' | '//books/title[text()="Matilda"]/ancestor::bookstore' || ["/bookstore"] 'ancestor with parent that does not exist' | '//books/ancestor::parentDoesNoExist/categories' || [] 'ancestor does not exist' | '//books/ancestor::ancestorDoesNotExist' || [] + 'ancestor combined with contains condition' | '//books[contains(@title,"Mat")]/ancestor::bookstore' || ["/bookstore"] } def 'Query for attribute by cps path of type ancestor with #scenario descendants.'() { |