From f0527c58c17963d940535d0ce0eb934c2b4c635c Mon Sep 17 00:00:00 2001 From: ToineSiebelink Date: Tue, 6 Jul 2021 13:03:03 +0100 Subject: [PATCH] Support text() condition - Added Antlr parsing of text() condition (as an optional additional to any query) - Implemented text-condition combined with descendants - Refactor descendants queries into using one more flexible Custom (native) Query builder - Refactor ALL cpsPath queries to now use FragmentRepositoryCpsPathQuery (custom query builder) - Refactor Antrl code to simply parsing of cpsPath and allow all combinations (no more query types, addresses CPS-436) - Minor clean up of some minor convention issues in CpsAdminServiceImplSpec.groovy (found during groovy demo) - Update .rst documentation of xPaths - Fixed incorrect matching of additional list indexes using more precise SIMILAR-TO regex in postgreSQL - Documented special chararter limitation (CPS-500) - Checked for consistent use of term 'CPS path' in documentation and error message - Included (updated) copyright in all .SQL test files Issue-ID: CPS-452 Issue-ID: CPS-436 Issue-ID: CPS-500 Signed-off-by: ToineSiebelink Change-Id: If422d25cafd2850d25c9a28dea16ba7a5f93dddb --- .../org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 | 54 ++++---- .../onap/cps/cpspath/parser/CpsPathBuilder.java | 52 +++---- ...psPathQueryType.java => CpsPathPrefixType.java} | 17 +-- .../org/onap/cps/cpspath/parser/CpsPathQuery.java | 28 +++- .../cps/cpspath/parser/CpsPathQuerySpec.groovy | 63 ++++++--- .../spi/impl/CpsDataPersistenceServiceImpl.java | 17 +-- .../cps/spi/repository/FragmentRepository.java | 41 ++---- .../repository/FragmentRepositoryCpsPathQuery.java | 29 ++++ .../FragmentRepositoryCpsPathQueryImpl.java | 120 ++++++++++++++++ .../CpsDataPersistenceQueryDataNodeSpec.groovy | 46 ++++--- .../java/org/onap/cps/DatabaseTestContainer.java | 6 +- cps-ri/src/test/resources/data/anchor.sql | 24 +++- cps-ri/src/test/resources/data/clear-all.sql | 22 +++ cps-ri/src/test/resources/data/cps-path-query.sql | 27 +++- cps-ri/src/test/resources/data/fragment.sql | 24 +++- cps-ri/src/test/resources/data/schemaset.sql | 22 +++ .../cps/api/impl/CpsAdminServiceImplSpec.groovy | 10 +- .../org/onap/cps/model/DataNodeBuilderSpec.groovy | 14 +- cps-service/src/test/resources/test-tree.json | 8 +- cps-service/src/test/resources/test-tree.yang | 12 ++ docs/cps-path.rst | 153 +++++++++++++-------- docs/release-notes.rst | 6 +- 22 files changed, 555 insertions(+), 240 deletions(-) rename cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/{CpsPathQueryType.java => CpsPathPrefixType.java} (72%) create mode 100644 cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java create mode 100644 cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java 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 198cede0d..cefeac438 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 @@ -20,56 +20,50 @@ grammar CpsPath ; -cpsPath: (cpsPathWithSingleLeafCondition | cpsPathWithDescendant | cpsPathWithDescendantAndLeafConditions) ancestorAxis? ; +cpsPath : ( prefix | descendant | incorrectPrefix ) multipleLeafConditions? textFunctionCondition? ancestorAxis? ; -ancestorAxis: SLASH KW_ANCESTOR COLONCOLON ancestorPath ; +ancestorAxis : SLASH KW_ANCESTOR COLONCOLON ancestorPath ; -ancestorPath: yangElement (SLASH yangElement)* ; +ancestorPath : yangElement ( SLASH yangElement)* ; -cpsPathWithSingleLeafCondition: prefix singleValueCondition postfix? ; +textFunctionCondition : SLASH leafName OB KW_TEXT_FUNCTION EQ StringLiteral CB ; -/* -No need to ditinguish between cpsPathWithDescendant | cpsPathWithDescendantAndLeafConditions really! -See https://jira.onap.org/browse/CPS-436 -*/ - -cpsPathWithDescendant: descendant ; - -cpsPathWithDescendantAndLeafConditions: descendant multipleValueConditions ; +prefix : ( SLASH yangElement)* SLASH containerName ; -descendant: SLASH prefix ; +descendant : SLASH prefix ; -prefix: (SLASH yangElement)* SLASH containerName ; +incorrectPrefix : SLASH SLASH SLASH+ ; -postfix: (SLASH yangElement)+ ; +yangElement : containerName listElementRef? ; -yangElement: containerName listElementRef? ; +containerName : QName ; -containerName: QName ; +listElementRef : OB leafCondition ( KW_AND leafCondition)* CB ; -listElementRef: multipleValueConditions ; +multipleLeafConditions : OB leafCondition ( KW_AND leafCondition)* CB ; -singleValueCondition: '[' leafCondition ']' ; +leafCondition : AT leafName EQ ( IntegerLiteral | StringLiteral) ; -multipleValueConditions: '[' leafCondition (' and ' leafCondition)* ']' ; - -leafCondition: '@' leafName '=' (IntegerLiteral | StringLiteral ) ; - -//To Confirm: defintion of Lefname with external xPath grammar -leafName: QName ; +leafName : QName ; /* * Lexer Rules - * Most of the lexer rules below are 'imporetd' from + * Most of the lexer rules below are inspired by * https://raw.githubusercontent.com/antlr/grammars-v4/master/xpath/xpath31/XPath31.g4 */ -SLASH : '/'; +AT : '@' ; +CB : ']' ; COLONCOLON : '::' ; +EQ : '=' ; +OB : '[' ; +SLASH : '/' ; // KEYWORDS KW_ANCESTOR : 'ancestor' ; +KW_AND : 'and' ; +KW_TEXT_FUNCTION: 'text()' ; IntegerLiteral : FragDigits ; // Add below type definitions for leafvalue comparision in https://jira.onap.org/browse/CPS-440 @@ -77,7 +71,7 @@ DecimalLiteral : ('.' FragDigits) | (FragDigits '.' [0-9]*) ; DoubleLiteral : (('.' FragDigits) | (FragDigits ('.' [0-9]*)?)) [eE] [+-]? FragDigits ; StringLiteral : ('"' (FragEscapeQuot | ~[^"])*? '"') | ('\'' (FragEscapeApos | ~['])*? '\'') ; fragment FragEscapeQuot : '""' ; -fragment FragEscapeApos : '\''; +fragment FragEscapeApos : '\'' ; fragment FragDigits : [0-9]+ ; QName : FragQName ; @@ -109,7 +103,7 @@ fragment FragNCNameChar | '\u00B7' | '\u0300'..'\u036F' | '\u203F'..'\u2040' ; -fragment FragmentNCName : FragNCNameStartChar FragNCNameChar* ; +fragment FragmentNCName : FragNCNameStartChar FragNCNameChar* ; // https://www.w3.org/TR/REC-xml/#NT-Char @@ -117,7 +111,7 @@ fragment FragChar : '\u0009' | '\u000a' | '\u000d' | '\u0020'..'\ud7ff' | '\ue000'..'\ufffd' | '\u{10000}'..'\u{10ffff}' - ; + ; // Skip all Whitespace Whitespace : ('\u000d' | '\u000a' | '\u0020' | '\u0009')+ -> skip ; 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 b9d0c25b1..ebf6fd3c9 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 @@ -20,18 +20,18 @@ package org.onap.cps.cpspath.parser; +import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT; + 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.DescendantContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.IncorrectPrefixContext; 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.PostfixContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.MultipleLeafConditionsContext; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.PrefixContext; -import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.SingleValueConditionContext; +import org.onap.cps.cpspath.parser.antlr4.CpsPathParser.TextFunctionConditionContext; public class CpsPathBuilder extends CpsPathBaseListener { @@ -45,8 +45,8 @@ public class CpsPathBuilder extends CpsPathBaseListener { } @Override - public void exitPostfix(final PostfixContext ctx) { - throw new IllegalStateException(String.format("Unsupported postfix %s encountered in CpsPath.", ctx.getText())); + public void exitIncorrectPrefix(final IncorrectPrefixContext ctx) { + throw new IllegalStateException("CPS path can only start with one or two slashes (/)"); } @Override @@ -64,38 +64,18 @@ public class CpsPathBuilder extends CpsPathBaseListener { } @Override - public void enterSingleValueCondition(final SingleValueConditionContext ctx) { - leavesData.clear(); + public void exitDescendant(final DescendantContext ctx) { + cpsPathQuery.setCpsPathPrefixType(DESCENDANT); + cpsPathQuery.setDescendantName(ctx.getText().substring(2)); } @Override - public void enterMultipleValueConditions(final MultipleValueConditionsContext ctx) { + public void enterMultipleLeafConditions(final MultipleLeafConditionsContext 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)); + public void exitMultipleLeafConditions(final MultipleLeafConditionsContext ctx) { cpsPathQuery.setLeavesData(leavesData); } @@ -104,6 +84,12 @@ public class CpsPathBuilder extends CpsPathBaseListener { cpsPathQuery.setAncestorSchemaNodeIdentifier(ctx.ancestorPath().getText()); } + @Override + public void exitTextFunctionCondition(final TextFunctionConditionContext ctx) { + cpsPathQuery.setTextFunctionConditionLeafName(ctx.leafName().getText()); + cpsPathQuery.setTextFunctionConditionValue(stripFirstAndLastCharacter(ctx.StringLiteral().getText())); + } + CpsPathQuery build() { return cpsPathQuery; } 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/CpsPathPrefixType.java similarity index 72% rename from cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java rename to cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathPrefixType.java index ac3e31334..dfac9b0c8 100644 --- 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/CpsPathPrefixType.java @@ -22,19 +22,16 @@ package org.onap.cps.cpspath.parser; /** - * The enum Cps path query type. + * The enum Cps path prefix type. */ -public enum CpsPathQueryType { +public enum CpsPathPrefixType { /** - * Xpath descendant anywhere type e.g. //nodeName . + * Fully qualified Xpath starting from root with single slash e.g. /parent/child . */ - XPATH_HAS_DESCENDANT_ANYWHERE, - /** - * Xpath descendant anywhere type e.g. //nodeName[@leafName="value"] . - */ - XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES, + ABSOLUTE, + /** - * Xpath leaf value cps path query type e.g. /cps-path[@leaf1="leafValue" and @leaf2=123] . + * Xpath descendant anywhere starting with double slash type e.g. //child/grandchild . */ - XPATH_LEAF_VALUE + DESCENDANT } 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 107bfa3e3..de7adf2b7 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 @@ -20,6 +20,8 @@ package org.onap.cps.cpspath.parser; +import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE; + import java.util.Map; import lombok.AccessLevel; import lombok.Getter; @@ -36,13 +38,13 @@ import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; @Setter(AccessLevel.PACKAGE) public class CpsPathQuery { - private CpsPathQueryType cpsPathQueryType; private String xpathPrefix; - private String leafName; - private Object leafValue; + private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE; private String descendantName; private Map leavesData; private String ancestorSchemaNodeIdentifier = ""; + private String textFunctionConditionLeafName; + private String textFunctionConditionValue; /** * Returns a cps path query. @@ -68,7 +70,7 @@ public class CpsPathQuery { } /** - * Has ancestor axis been populated. + * Has ancestor axis been included in cpsPath. * * @return boolean value. */ @@ -76,4 +78,22 @@ public class CpsPathQuery { return !(ancestorSchemaNodeIdentifier.isEmpty()); } + /** + * Have leaf value conditions been included in cpsPath. + * + * @return boolean value. + */ + public boolean hasLeafConditions() { + return leavesData != null; + } + + /** + * Has text function condition been included in cpsPath. + * + * @return boolean value. + */ + public boolean hasTextFunctionCondition() { + return textFunctionConditionLeafName != null; + } + } 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 b7826f67b..bfec574eb 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 @@ -22,17 +22,21 @@ package org.onap.cps.cpspath.parser import spock.lang.Specification +import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE +import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT + 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 + then: 'the query has the right xpath type' + result.cpsPathPrefixType == ABSOLUTE and: 'the right query parameters are set' result.xpathPrefix == expectedXpathPrefix - result.leafName == expectedLeafName - result.leafValue == expectedLeafValue + result.hasLeafConditions() == true + result.leavesData.containsKey(expectedLeafName) == true + result.leavesData.get(expectedLeafName) == 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' @@ -46,8 +50,8 @@ class CpsPathQuerySpec extends Specification { 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 + then: 'the query has the right xpath type' + result.cpsPathPrefixType == DESCENDANT and: 'the right ends with parameters are set' result.descendantName == expectedDescendantName where: 'the following data is used' @@ -59,9 +63,9 @@ class CpsPathQuerySpec extends Specification { 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' + then: 'the query has the right xpath type' + result.cpsPathPrefixType == DESCENDANT + and: 'the right parameters are set' result.descendantName == "child" result.leavesData.size() == expectedNumberOfLeaves where: 'the following data is used' @@ -70,6 +74,27 @@ class CpsPathQuerySpec extends Specification { 'more than one attribute' | '//child[@int-leaf=5 and @leaf-name="leaf value"]' || 2 } + def 'Parse #scenario cps path with text function condition'() { + when: 'the given cps path is parsed' + def result = CpsPathQuery.createFrom(cpsPath) + then: 'the query has the right xpath type' + result.cpsPathPrefixType == DESCENDANT + and: 'leaf conditions are only present when expected' + result.hasLeafConditions() == expectLeafConditions + and: 'the right text function condition is set' + result.hasTextFunctionCondition() + result.textFunctionConditionLeafName == 'leaf-name' + result.textFunctionConditionValue == 'search' + and: 'the ancestor is only present when expected' + assert result.hasAncestorAxis() == expectHasAncestorAxis + where: 'the following data is used' + scenario | cpsPath || expectLeafConditions | expectHasAncestorAxis + 'descendant anywhere' | '//someContainer/leaf-name[text()="search"]' || false | false + 'descendant with leaf value' | '//child[@other-leaf=1]/leaf-name[text()="search"]' || true | false + 'descendant anywhere and ancestor' | '//someContainer/leaf-name[text()="search"]/ancestor::parent' || false | true + 'descendant with leaf value and ancestor' | '//child[@other-leaf=1]/leaf-name[text()="search"]/ancestor::parent' || true | true + } + def 'Parse cps path with error: #scenario.'() { when: 'the given cps path is parsed' CpsPathQuery.createFrom(cpsPath) @@ -85,18 +110,20 @@ class CpsPathQuerySpec extends Specification { '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::' - 'unsupported postfix after single value condition (JIRA CPS-450)' | '/parent/child[@id=1]/somePostFix' +// DISCUSS WITH TEAM : 'unsupported postfix after value condition (JIRA CPS-450)' | '/parent/child[@id=1]/somePostFix' } 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 + result.cpsPathPrefixType == DESCENDANT and: 'the result has ancestor axis' result.hasAncestorAxis() and: 'the correct ancestor schema node identifier is set' result.ancestorSchemaNodeIdentifier == ancestorPath + and: 'there are no leaves conditions' + result.hasLeafConditions() == false where: scenario | ancestorPath 'basic container' | 'someContainer' @@ -109,18 +136,20 @@ class CpsPathQuerySpec extends Specification { when: 'the given cps path is parsed' def result = CpsPathQuery.createFrom(cpsPath + '/ancestor::someAncestor') then: 'the query has the right type' - result.cpsPathQueryType == expectedCpsPathQueryType + result.cpsPathPrefixType == DESCENDANT + and: 'leaf conditions are only present when expected' + result.hasLeafConditions() == expectLeafConditions 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 + scenario | cpsPath || expectedDescendantName | expectLeafConditions + 'basic container' | '//someContainer' || 'someContainer' | false + 'container with parent' | '//parent/child' || 'parent/child' | false + 'container with list-parent' | '//parent[@id=1]/child' || 'parent[@id=1]/child' | false + 'container with list-parent' | '//parent[@id=1]/child[@name="test"]' || 'parent[@id=1]/child' | true } } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index 844ad8474..6e12d0601 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -38,7 +38,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.transaction.Transactional; import org.onap.cps.cpspath.parser.CpsPathQuery; -import org.onap.cps.cpspath.parser.CpsPathQueryType; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.entities.AnchorEntity; @@ -179,20 +178,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } catch (final IllegalStateException e) { throw new CpsPathException(e.getMessage()); } - List fragmentEntities; - if (CpsPathQueryType.XPATH_LEAF_VALUE.equals(cpsPathQuery.getCpsPathQueryType())) { - fragmentEntities = fragmentRepository - .getByAnchorAndXpathAndLeafAttributes(anchorEntity.getId(), cpsPathQuery.getXpathPrefix(), - cpsPathQuery.getLeafName(), cpsPathQuery.getLeafValue()); - } else if (CpsPathQueryType.XPATH_HAS_DESCENDANT_WITH_LEAF_VALUES.equals(cpsPathQuery.getCpsPathQueryType())) { - final String leafDataAsJson = GSON.toJson(cpsPathQuery.getLeavesData()); - fragmentEntities = fragmentRepository - .getByAnchorAndDescendentNameAndLeafValues(anchorEntity.getId(), cpsPathQuery.getDescendantName(), - leafDataAsJson); - } else { - fragmentEntities = fragmentRepository - .getByAnchorAndXpathEndsInDescendantName(anchorEntity.getId(), cpsPathQuery.getDescendantName()); - } + List fragmentEntities = + fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); if (cpsPathQuery.hasAncestorAxis()) { final Set ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); fragmentEntities = ancestorXpaths.isEmpty() diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java index 4d7e7ff54..c48c79ef6 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-201 Nordix Foundation. + * Copyright (C) 2020-2021 Nordix Foundation. * Modifications Copyright (C) 2020-2021 Bell Canada. * Modifications Copyright (C) 2020-2021 Pantheon.tech. * ================================================================================ @@ -38,13 +38,15 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository -public interface FragmentRepository extends JpaRepository { +public interface FragmentRepository extends JpaRepository, FragmentRepositoryCpsPathQuery { Optional findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity, - @NonNull AnchorEntity anchorEntity, @NonNull String xpath); + @NonNull AnchorEntity anchorEntity, + @NonNull String xpath); default FragmentEntity getByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity, - @NonNull AnchorEntity anchorEntity, @NonNull String xpath) { + @NonNull AnchorEntity anchorEntity, + @NonNull String xpath) { return findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath) .orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath)); } @@ -52,42 +54,19 @@ public interface FragmentRepository extends JpaRepository @Query( value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND dataspace_id = :dataspace AND parent_id is NULL", nativeQuery = true) - List findRootsByDataspaceAndAnchor( - @Param("dataspace") int dataspaceId, @Param("anchor") int anchorId); + List findRootsByDataspaceAndAnchor(@Param("dataspace") int dataspaceId, + @Param("anchor") int anchorId); default FragmentEntity findFirstRootByDataspaceAndAnchor(@NonNull DataspaceEntity dataspaceEntity, - @NonNull AnchorEntity anchorEntity) { + @NonNull AnchorEntity anchorEntity) { return findRootsByDataspaceAndAnchor(dataspaceEntity.getId(), anchorEntity.getId()).stream().findFirst() .orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName())); } List findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity, - @NonNull Collection xpath); + @NonNull Collection xpath); @Modifying @Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)") void deleteByAnchorIn(@NotNull @Param("anchors") Collection anchorEntities); - - @Query(value = - "SELECT * FROM FRAGMENT WHERE (anchor_id = :anchor) AND (xpath = (:xpath) OR xpath LIKE " - + "CONCAT(:xpath,'\\[@%]')) AND attributes @> jsonb_build_object(:leafName , :leafValue)", - nativeQuery = true) - // Above query will match an xpath with or without the index for a list [@key=value] and match anchor id, - // leaf name and leaf value - List getByAnchorAndXpathAndLeafAttributes(@Param("anchor") int anchorId, @Param("xpath") - String xpathPrefix, @Param("leafName") String leafName, @Param("leafValue") Object leafValue); - - @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND xpath LIKE CONCAT('%/',:descendantName)", - nativeQuery = true) - // Above query will match the anchor id and last descendant name - List getByAnchorAndXpathEndsInDescendantName(@Param("anchor") int anchorId, - @Param("descendantName") String descendantName); - - @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND (xpath LIKE CONCAT('%/',:descendantName) OR " - + "xpath LIKE CONCAT('%/', :descendantName,'\\[@%]')) AND attributes @> :leafDataAsJson\\:\\:jsonb", - nativeQuery = true) - // Above query will match the anchor id, last descendant name and all parameters passed into leafDataASJson with the - // attribute values of the requested data node eg: {"leaf_name":"value", "another_leaf_name":"another value"}​​​​​​ - List getByAnchorAndDescendentNameAndLeafValues(@Param("anchor") int anchorId, - @Param("descendantName") String descendantName, @Param("leafDataAsJson") String leafDataAsJson); } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java new file mode 100644 index 000000000..04138ecc6 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java @@ -0,0 +1,29 @@ +/*- + * ============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.spi.repository; + +import java.util.List; +import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.spi.entities.FragmentEntity; + +public interface FragmentRepositoryCpsPathQuery { + List findByAnchorAndCpsPath(int anchorId, CpsPathQuery cpsPathQuery); +} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java new file mode 100644 index 000000000..4aa3e5fb3 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java @@ -0,0 +1,120 @@ +/*- + * ============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.spi.repository; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.onap.cps.cpspath.parser.CpsPathPrefixType; +import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.spi.entities.FragmentEntity; + +public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery { + + public static final String SIMILAR_TO_ABSOLUTE_PATH_PREFIX = "%/"; + public static final String SIMILAR_TO_OPTIONAL_LIST_INDEX_POSTFIX = "(\\[[^/]*])?"; + + @PersistenceContext + private EntityManager entityManager; + + private static final Gson GSON = new GsonBuilder().create(); + + @Override + public List findByAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) { + final var sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId"); + final Map queryParameters = new HashMap<>(); + queryParameters.put("anchorId", anchorId); + sqlStringBuilder.append(" AND xpath SIMILAR TO :xpathRegex"); + final String xpathRegex = getSimilarToXpathSqlRegex(cpsPathQuery); + queryParameters.put("xpathRegex", xpathRegex); + if (cpsPathQuery.hasLeafConditions()) { + sqlStringBuilder.append(" AND attributes @> :leafDataAsJson\\:\\:jsonb"); + queryParameters.put("leafDataAsJson", GSON.toJson(cpsPathQuery.getLeavesData())); + } + + addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters); + final var query = entityManager.createNativeQuery(sqlStringBuilder.toString(), FragmentEntity.class); + setQueryParameters(query, queryParameters); + return query.getResultList(); + } + + @NotNull + private static String getSimilarToXpathSqlRegex(final CpsPathQuery cpsPathQuery) { + final var xpathRegexBuilder = new StringBuilder(); + if (CpsPathPrefixType.ABSOLUTE.equals(cpsPathQuery.getCpsPathPrefixType())) { + xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getXpathPrefix())); + } else { + xpathRegexBuilder.append(SIMILAR_TO_ABSOLUTE_PATH_PREFIX); + xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getDescendantName())); + } + xpathRegexBuilder.append(SIMILAR_TO_OPTIONAL_LIST_INDEX_POSTFIX); + return xpathRegexBuilder.toString(); + } + + @NotNull + private static String escapeXpath(final String xpath) { + // See https://jira.onap.org/browse/CPS-500 for limitations of this basic escape mechanism + return xpath.replace("[@", "\\[@"); + } + + @Nullable + private static Integer getTextValueAsInt(final CpsPathQuery cpsPathQuery) { + try { + return Integer.parseInt(cpsPathQuery.getTextFunctionConditionValue()); + } catch (final NumberFormatException e) { + return null; + } + } + + private static void addTextFunctionCondition(final CpsPathQuery cpsPathQuery, final StringBuilder sqlStringBuilder, + final Map queryParameters) { + if (cpsPathQuery.hasTextFunctionCondition()) { + sqlStringBuilder.append(" AND ("); + sqlStringBuilder.append("attributes @> jsonb_build_object(:textLeafName, :textValue)"); + sqlStringBuilder + .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValue))"); + queryParameters.put("textLeafName", cpsPathQuery.getTextFunctionConditionLeafName()); + queryParameters.put("textValue", cpsPathQuery.getTextFunctionConditionValue()); + final var textValueAsInt = getTextValueAsInt(cpsPathQuery); + if (textValueAsInt != null) { + sqlStringBuilder.append(" OR attributes @> jsonb_build_object(:textLeafName, :textValueAsInt)"); + sqlStringBuilder + .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValueAsInt))"); + queryParameters.put("textValueAsInt", textValueAsInt); + } + sqlStringBuilder.append(")"); + } + } + + private static void setQueryParameters(final Query query, final Map queryParameters) { + for (final Map.Entry queryParameter : queryParameters.entrySet()) { + query.setParameter(queryParameter.getKey(), queryParameter.getValue()); + } + } + +} diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy index 8dc9b7f3c..ae88d302b 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy @@ -23,7 +23,6 @@ package org.onap.cps.spi.impl import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.exceptions.CpsPathException -import org.onap.cps.spi.model.DataNode import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.jdbc.Sql @@ -38,23 +37,25 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { static final String SET_DATA = '/data/cps-path-query.sql' @Sql([CLEAR_DATA, SET_DATA]) - def 'Cps Path query for single leaf value with type: #type.'() { + def 'Cps Path query for leaf value(s) with : #scenario.'() { when: 'a query is executed to get a data node by the given cps path' def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, includeDescendantsOption) + then: 'the correct number of parent nodes are returned' + result.size() == expectedNumberOfParentNodes then: 'the correct data is returned' - def leaves = '[price:15.0, title:Dune]' - DataNode dataNode = result.stream().findFirst().get() - dataNode.getLeaves().toString() == leaves - dataNode.getChildDataNodes().size() == expectedNumberOfChidlNodes + result.each { + assert it.getChildDataNodes().size() == expectedNumberOfChildNodes + } where: 'the following data is used' - type | cpsPath | includeDescendantsOption || expectedNumberOfChidlNodes - 'String and no descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@title="Dune"]' | OMIT_DESCENDANTS || 0 - 'Integer and descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@price=15]' | INCLUDE_ALL_DESCENDANTS || 1 + scenario | cpsPath | includeDescendantsOption || expectedNumberOfParentNodes | expectedNumberOfChildNodes + 'String and no descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@title="Dune"]' | OMIT_DESCENDANTS || 1 | 0 + 'Integer and descendants' | '/shops/shop[@id=1]/categories[@code=1]/book[@price=5]' | INCLUDE_ALL_DESCENDANTS || 1 | 1 + 'No condition no descendants' | '/shops/shop[@id=1]/categories' | OMIT_DESCENDANTS || 2 | 0 } @Sql([CLEAR_DATA, SET_DATA]) def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() { - when: 'a query is executed to get datanodes for the given cps path' + when: 'a query is executed to get data nodes for the given cps path' def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, OMIT_DESCENDANTS) then: 'no data is returned' result.isEmpty() @@ -71,7 +72,7 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { def cpsPath = '//categories[@code=1]' def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_SHOP_EXAMPLE, cpsPath, includeDescendantsOption) then: 'the data node has the correct number of children' - DataNode dataNode = result.stream().findFirst().get() + def dataNode = result.stream().findFirst().get() dataNode.getChildDataNodes().size() == expectedNumberOfChildNodes where: 'the following data is used' type | includeDescendantsOption || expectedNumberOfChildNodes @@ -90,9 +91,16 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { assert result[i].getXpath() == expectedXPaths[i] } where: 'the following data is used' - scenario | cpsPath || expectedXPaths - 'fully unique descendant name' | '//categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]', '/shops/shop[@id=2]/categories[@code=1]', '/shops/shop[@id=2]/categories[@code=2]'] - 'descendant name match end of other node' | '//book' || ['/shops/shop[@id=1]/categories[@code=1]/book', '/shops/shop[@id=1]/categories[@code=2]/book'] + scenario | cpsPath || expectedXPaths + 'fully unique descendant name' | '//categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]', '/shops/shop[@id=2]/categories[@code=1]', '/shops/shop[@id=2]/categories[@code=2]'] + 'descendant name match end of other node' | '//book' || ['/shops/shop[@id=1]/categories[@code=1]/book', '/shops/shop[@id=1]/categories[@code=2]/book'] + 'descendant with text condition on leaf' | '//book/title[text()="Chapters"]' || ['/shops/shop[@id=1]/categories[@code=2]/book'] + 'descendant with text condition case mismatch' | '//book/title[text()="chapters"]' || [] + 'descendant with text condition on int leaf' | '//book/price[text()="5"]' || ['/shops/shop[@id=1]/categories[@code=1]/book'] + 'descendant with text condition on leaf-list' | '//book/labels[text()="special offer"]' || ['/shops/shop[@id=1]/categories[@code=1]/book'] + 'descendant with text condition partial match' | '//book/labels[text()="special"]' || [] + 'descendant with text condition (existing) empty string' | '//book/labels[text()=""]' || ['/shops/shop[@id=1]/categories[@code=1]/book'] + 'descendant with text condition on int leaf-list' | '//book/editions[text()="2000"]' || ['/shops/shop[@id=1]/categories[@code=2]/book'] } @Sql([CLEAR_DATA, SET_DATA]) @@ -106,10 +114,11 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { assert result[i].getXpath() == expectedXPaths[i] } where: 'the following data is used' - scenario | cpsPath || expectedXPaths - 'one leaf' | '//author[@FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]'] - 'more than one leaf' | '//author[@FirstName="Joe" and @Surname="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] - 'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] + scenario | cpsPath || expectedXPaths + 'one leaf' | '//author[@FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]'] + 'more than one leaf' | '//author[@FirstName="Joe" and @Surname="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] + 'leaves reversed in order' | '//author[@Surname="Bloggs" and @FirstName="Joe"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] + 'leaf and text condition' | '//author[@FirstName="Joe"]/Surname[text()="Bloggs"]' || ['/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]'] } @Sql([CLEAR_DATA, SET_DATA]) @@ -146,6 +155,7 @@ class CpsDataPersistenceQueryDataNodeSpec extends CpsPersistenceSpecBase { 'list with index value in the xpath prefix' | '//categories[@code=1]/book/ancestor::shop[@id=1]' || ['/shops/shop[@id=1]'] 'ancestor with parent list' | '//book/ancestor::shop[@id=1]/categories[@code=2]' || ['/shops/shop[@id=1]/categories[@code=2]'] 'ancestor with parent' | '//phonenumbers[@type="mob"]/ancestor::info/contact' || ['/shops/shop[@id=3]/info/contact'] + 'ancestor combined with text condition' | '//book/title[text()="Dune"]/ancestor::shop' || ['/shops/shop[@id=1]'] 'ancestor with parent that does not exist' | '//book/ancestor::parentDoesNoExist/categories' || [] 'ancestor does not exist' | '//book/ancestor::ancestorDoesNotExist' || [] } diff --git a/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java b/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java index d3908ea80..10f8de422 100755 --- a/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java +++ b/cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java @@ -23,8 +23,10 @@ import org.testcontainers.containers.PostgreSQLContainer; /** * The Postgresql database test container wrapper. - * Singleton implementation allows saving time on database initialization which - * otherwise would occur on each test. + * Singleton implementation allows saving time on database initialization which otherwise would occur on each test. + * for debugging/developing purposes you can suspend any test and connect to this database: + * docker exec -it {container-id} sh + * psql -d test -U test */ public class DatabaseTestContainer extends PostgreSQLContainer { private static final String IMAGE_VERSION = "postgres:13.2"; diff --git a/cps-ri/src/test/resources/data/anchor.sql b/cps-ri/src/test/resources/data/anchor.sql index a7d3e6715..dbf1a6a47 100644 --- a/cps-ri/src/test/resources/data/anchor.sql +++ b/cps-ri/src/test/resources/data/anchor.sql @@ -1,3 +1,25 @@ +/* + ============LICENSE_START======================================================= + Copyright (C) 2020 Pantheon.tech + Modifications Copyright (C) 2020 Nordix Foundation. + Modifications Copyright (C) 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========================================================= +*/ + INSERT INTO DATASPACE (ID, NAME) VALUES (1001, 'DATASPACE-001'), (1002, 'DATASPACE-002'); @@ -9,4 +31,4 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES (3002, 'ANCHOR-002', 1001, 2002); INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES - (4001, 1001, 3001, null, '/xpath', '{}'); \ No newline at end of file + (4001, 1001, 3001, null, '/xpath', '{}'); diff --git a/cps-ri/src/test/resources/data/clear-all.sql b/cps-ri/src/test/resources/data/clear-all.sql index 9aee604a3..8a5e8444e 100644 --- a/cps-ri/src/test/resources/data/clear-all.sql +++ b/cps-ri/src/test/resources/data/clear-all.sql @@ -1,3 +1,25 @@ +/* + ============LICENSE_START======================================================= + Copyright (C) 2020-2021 Pantheon.tech + Modifications Copyright (C) 2020 Nordix Foundation. + Modifications Copyright (C) 2020 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========================================================= +*/ + DELETE FROM FRAGMENT; DELETE FROM ANCHOR; DELETE FROM DATASPACE; diff --git a/cps-ri/src/test/resources/data/cps-path-query.sql b/cps-ri/src/test/resources/data/cps-path-query.sql index 67558456e..8f525df6b 100644 --- a/cps-ri/src/test/resources/data/cps-path-query.sql +++ b/cps-ri/src/test/resources/data/cps-path-query.sql @@ -1,3 +1,24 @@ +/* + ============LICENSE_START======================================================= + Copyright (C) 2021 Nordix Foundation. + Modifications Copyright (C) 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========================================================= +*/ + INSERT INTO DATASPACE (ID, NAME) VALUES (1001, 'DATASPACE-001'); @@ -12,8 +33,8 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (2, 1001, 1003, 1, '/shops/shop[@id=1]', '{"id" : 1, "type" : "bookstore"}'), (3, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=1]', '{"code" : 1, "type" : "bookstore", "name": "SciFi"}'), (4, 1001, 1003, 2, '/shops/shop[@id=1]/categories[@code=2]', '{"code" : 2, "type" : "bookstore", "name": "Fiction"}'), - (5, 1001, 1003, 3, '/shops/shop[@id=1]/categories[@code=1]/book', '{"price" : 15, "title": "Dune"}'), - (6, 1001, 1003, 4, '/shops/shop[@id=1]/categories[@code=2]/book', '{"price" : 15, "title": "Chapters"}'), + (5, 1001, 1003, 3, '/shops/shop[@id=1]/categories[@code=1]/book', '{"price" : 5, "title" : "Dune", "labels" : ["special offer","classics",""]}'), + (6, 1001, 1003, 4, '/shops/shop[@id=1]/categories[@code=2]/book', '{"price" : 15, "title" : "Chapters", "editions" : [2000,2010,2020]}'), (7, 1001, 1003, 5, '/shops/shop[@id=1]/categories[@code=1]/book/author[@FirstName="Joe" and @Surname="Bloggs"]', '{"FirstName" : "Joe", "Surname": "Bloggs","title": "Dune"}'), (8, 1001, 1003, 6, '/shops/shop[@id=1]/categories[@code=2]/book/author[@FirstName="Joe" and @Surname="Smith"]', '{"FirstName" : "Joe", "Surname": "Smith","title": "Chapters"}'); @@ -30,4 +51,4 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (17, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact', null), (18, 1001, 1003, 1, '/shops/shop[@id=3]/info/contact/website', '{"address" : "myshop.ie"}'), (19, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="mob"]', '{"type" : "mob", "number" : "123123456"}'), - (20, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="landline"]', '{"type" : "landline", "number" : "012123456"}'); \ No newline at end of file + (20, 1001, 1003, 12, '/shops/shop[@id=3]/info/contact/phonenumbers[@type="landline"]', '{"type" : "landline", "number" : "012123456"}'); diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index 1897185fa..d7109f20b 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -1,3 +1,25 @@ +/* + ============LICENSE_START======================================================= + Copyright (C) 2021 Nordix Foundation. + Modifications Copyright (C) 2021 Pantheon.tech + Modifications Copyright (C) 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========================================================= +*/ + INSERT INTO DATASPACE (ID, NAME) VALUES (1001, 'DATASPACE-001'); @@ -31,4 +53,4 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) (4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'), (4207, 1001, 3003, 4206, '/parent-201/child-203', '{}'), (4208, 1001, 3003, 4206, '/parent-201/child-204[@key="A"]', '{"key": "A"}'), - (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}'); \ No newline at end of file + (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}'); diff --git a/cps-ri/src/test/resources/data/schemaset.sql b/cps-ri/src/test/resources/data/schemaset.sql index e6306d0d0..adfcfa172 100644 --- a/cps-ri/src/test/resources/data/schemaset.sql +++ b/cps-ri/src/test/resources/data/schemaset.sql @@ -1,3 +1,25 @@ +/* + ============LICENSE_START======================================================= + Copyright (C) 2020-2021 Pantheon.tech + Modifications Copyright (C) 2020 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========================================================= +*/ + INSERT INTO DATASPACE (ID, NAME) VALUES (1001, 'DATASPACE-001'), (1002, 'DATASPACE-002'); diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy index 532f44203..be213c0fa 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy @@ -35,16 +35,16 @@ class CpsAdminServiceImplSpec extends Specification { } def 'Create dataspace method invokes persistence service.'() { - when: 'Create dataspace method is invoked' + when: 'create dataspace method is invoked' objectUnderTest.createDataspace('someDataspace') - then: 'The persistence service method is invoked with same parameters' + then: 'the persistence service method is invoked with same parameters' 1 * mockCpsAdminPersistenceService.createDataspace('someDataspace') } def 'Create anchor method invokes persistence service.'() { - when: 'Create anchor method is invoked' + when: 'create anchor method is invoked' objectUnderTest.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName') - then: 'The persistence service method is invoked with same parameters' + then: 'the persistence service method is invoked with same parameters' 1 * mockCpsAdminPersistenceService.createAnchor('someDataspace', 'someSchemaSet', 'someAnchorName') } @@ -61,7 +61,7 @@ class CpsAdminServiceImplSpec extends Specification { Anchor anchor = new Anchor() mockCpsAdminPersistenceService.getAnchor('someDataspace','someAnchor') >> anchor expect: 'the anchor provided by persistence service is returned as result' - objectUnderTest.getAnchor('someDataspace','someAnchor') == anchor + assert objectUnderTest.getAnchor('someDataspace','someAnchor') == anchor } def 'Delete anchor.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy index 7f50f7f52..2751d5507 100644 --- a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech + * Modifications 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. @@ -28,11 +29,12 @@ import spock.lang.Specification class DataNodeBuilderSpec extends Specification { Map> expectedLeavesByXpathMap = [ - '/test-tree' : [], - '/test-tree/branch[@name=\'Left\']' : [name: 'Left'], - '/test-tree/branch[@name=\'Left\']/nest' : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']], - '/test-tree/branch[@name=\'Right\']' : [name: 'Right'], - '/test-tree/branch[@name=\'Right\']/nest': [name: 'Big', birds: ['Owl', 'Raven', 'Crow']] + '/test-tree' : [], + '/test-tree/branch[@name=\'Left\']' : [name: 'Left'], + '/test-tree/branch[@name=\'Left\']/nest' : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']], + '/test-tree/branch[@name=\'Right\']' : [name: 'Right'], + '/test-tree/branch[@name=\'Right\']/nest' : [name: 'Big', birds: ['Owl', 'Raven', 'Crow']], + '/test-tree/fruit[@color=\'Green\' and @name=\'Apple\']': [color: 'Green', name: 'Apple'] ] String[] networkTopologyModelRfc8345 = [ @@ -55,7 +57,7 @@ class DataNodeBuilderSpec extends Specification { def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: '5 DataNode objects with unique xpath were created in total' - mappedResult.size() == 5 + mappedResult.size() == 6 and: 'all expected xpaths were built' mappedResult.keySet().containsAll(expectedLeavesByXpathMap.keySet()) and: 'each data node contains the expected attributes' diff --git a/cps-service/src/test/resources/test-tree.json b/cps-service/src/test/resources/test-tree.json index bc9cbd7ce..e1789abc1 100644 --- a/cps-service/src/test/resources/test-tree.json +++ b/cps-service/src/test/resources/test-tree.json @@ -23,6 +23,12 @@ ] } } + ], + "fruit": [ + { + "name": "Apple", + "color": "Green" + } ] } -} \ No newline at end of file +} diff --git a/cps-service/src/test/resources/test-tree.yang b/cps-service/src/test/resources/test-tree.yang index faba8a11d..63100657e 100644 --- a/cps-service/src/test/resources/test-tree.yang +++ b/cps-service/src/test/resources/test-tree.yang @@ -20,5 +20,17 @@ module test-tree { } } } + list fruit { + key "name color"; + + leaf name { + type string; + } + + leaf color { + type string; + } + + } } } diff --git a/docs/cps-path.rst b/docs/cps-path.rst index aec87cd43..0271d07e1 100644 --- a/docs/cps-path.rst +++ b/docs/cps-path.rst @@ -14,8 +14,8 @@ CPS Path Introduction ============ -Several CPS APIs use the cps-path (or cpsPath in Java API) parameter. -The CPS Path parameter is used for querying xpaths. CPS Path is insprired by the `XML Path Language (XPath) 3.1. `_ +Several CPS APIs use the CPS path (or cpsPath in Java API) parameter. +The CPS path parameter is used for querying xpaths. CPS path is inspired by the `XML Path Language (XPath) 3.1. `_ This section describes the functionality currently supported by CPS Path. @@ -28,104 +28,137 @@ The xml below describes some basic data to be used to illustrate the CPS Path fu - Chapters - - - - - - - - - - - + Chapters + + + + + + 1968 + 2018 + + + + 1965 + + + + + + + + -**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers +**Note.** 'categories' is a Yang List and 'code' is its key leaf. All other data nodes are Yang Containers. 'label' and 'edition' are both leaf-lists. General Notes ============= -- String values must be wrapped in quotation marks (U+0022) or apostrophes (U+0027). +- String values must be wrapped in quotation marks ``"`` (U+0022) or apostrophes ``'`` (U+0027). - String comparisons are case sensitive. +- List key-fields containing ``\`` or ``@[`` will not be processed correctly when being referenced with such key values in absolute or descendant paths. This means such entries will be omitted from any query result. See `CPS-500 `_ Special Character Limitations of cpsPath Queries -Supported Queries -================= +Query Syntax +============ -Get List Elements by Any Attribute Value ----------------------------------------- +``( | ) [ ] [ ] [ ]`` -**Syntax**: ``/[@=]`` - - ``xpath``: The xpath to the parent of the target node including all ancestors. - - ``target-node``: The name of the (list) node which elements will queried. - - ``leaf-name``: The name of the leaf which value needs to be compared. - - ``leaf-value``: The required value of the leaf. +Each CPS path expression need to start with an 'absolute' or 'descendant' xpath. + +absolute-path +------------- + +**Syntax**: ``'/' ( '[' ']' )? ( '/' ( '[' ']' )? )*`` + + - ``container name``: Any yang container or list. + - ``list-key``: One or more key-value pairs, each preceded by the ``@`` symbol, combined using the ``and`` keyword. + - The above van repeated any number of times. **Examples** - - ``/shops/bookstore/categories[@numberOfBooks=1]`` - - ``/shops/bookstore/categories[@name="Kids"]`` - - ``/shops/bookstore/categories[@name='Kids']`` + - ``/shops/bookstore`` + - ``/shops/bookstore/categories[@code=1]`` + - ``/shops/bookstore/categories[@code=1]/book`` **Limitations** - - Only one list (last descendant) can be queried for a non-key value. Any ancestor list will have to be referenced by its key name-value pair(s). - - Only one attribute can be queried. - - Only string and integer values are supported (boolean and float values are not supported). + - Absolute paths must start with the top element (data node) as per the model tree. + - Each list reference must include a valid instance reference to the key for that list. Except when it is the last element. -**Notes** - - For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath. +descendant-path +--------------- -Get Any Descendant ------------------- +**Syntax**: ``'//' ( '[' ']' )? ( '/' ( '[' ']' )? )*`` -**Syntax**: ``//`` - - ``direct-ancestors``: Optional path to direct ancestors of the target node. This can contain zero to many ancestor nodes separated by a /. - - ``target-node``: The name of the (list) node from which element will be selected. If the target node is a Yang List he element needs to be specified using the key as normal e.g. ``categories[@code=1]``. + - The syntax of a descendant path is identical to a absolute path except that it is preceded by a double slash ``//``. **Examples** - - ``//book`` - - ``//books/book`` - - ``//categories[@code=1]`` - - ``//categories[@code=1]/books`` + - ``//bookstore`` + - ``//categories[@code=1]/book`` + - ``//bookstore/categories`` **Limitations** - - List elements can only be addressed using the list key leaf. + - Each list reference must include a valid instance reference to the key for that list. Except when it is the last element. -Get Any Descendant by Any Attribute Value ------------------------------------------ +leaf-conditions +--------------- -**Syntax**: ``//[@=]`` - - ``direct-ancestors``: Optional path to direct ancestors of the target node. This can contain zero to many ancestor nodes separated by a /. - - ``target-node``: The name of the (list) node which elements will queried. - - ``leaf1-name .. leafN-name:``: One or more leaves whose value needs to be compared. - - ``leaf1-value .. leafN-value:``: One or more required leaf values (multiple condition can be combined using the 'and' keyword). +**Syntax**: `` '[' @ '=' ( ' and ' @ '=' )* ']'`` + - ``xpath``: Absolute or descendant or xpath to the (list) node which elements will be queried. + - ``leaf-name``: The name of the leaf which value needs to be compared. + - ``leaf-value``: The required value of the leaf. **Examples** + - ``/shops/bookstore/categories[@numberOfBooks=1]`` + - ``//categories[@name="Kids"]`` - ``//categories[@name='Kids']`` - - ``//categories[@name='Kids' and @numberOfBooks=1]`` + - ``//categories[@code=1]/book[@title='Dune' and price=5]`` **Limitations** - - Only string and integer values are supported (boolean and float values are not supported). - - Multiple attributes can only be combined using 'and'. 'or' and bracketing is not supported. + - Only the last list or container can be queried leaf values. Any ancestor list will have to be referenced by its key name-value pair(s). + - Multiple attributes can only be combined using ``and``. ``or`` and bracketing is not supported. + - Only leaves can be used, leaf-list are not supported. + - Only string and integer values are supported, boolean and float values are not supported. + +**Notes** + - For performance reasons it does not make sense to query using key leaf as attribute. If the key value is known it is better to execute a get request with the complete xpath. + +text()-condition +---------------- + +The text()-condition can be added to any CPS path query. -Query Extensions -================ +**Syntax**: `` ( '/' '[text()=' ']' )?`` + - ``cps-path``: Any CPS path query. + - ``leaf-name``: The name of the leaf or leaf-list which value needs to be compared. + - ``string-value``: The required value of the leaf or leaf-list element as a string wrapped in quotation marks (U+0022) or apostrophes (U+0027). This wil still match integer values. + +**Examples** + - ``//book/label[text()="classic"]`` + - ``//book/edition[text()="1965"]`` + +**Limitations** + - Only the last list or container can be queried for leaf values with a text() condition. Any ancestor list will have to be referenced by its key name-value pair(s). + - Only one leaf or leaf-list can be tested. + - Only string and integer values are supported, boolean and float values are not supported. + - Since CPS cannot return individual leaves it will always return the container with all its leaves. Ancestor-axis can be used to specify a parent higher up the tree. + - When querying a leaf value (instead of leaf-list) it is better, more performant to use a text value condition use @ as described above. -Ancestor Axis +ancestor-axis ------------- -The ancestor axis can be added to any CPS path query. +The ancestor axis can be added to any CPS path query but has to be the last part. -**Syntax**: ``///ancestor::`` +**Syntax**: `` ( '/ancestor::' )?`` - ``cps-path``: Any CPS path query. - - ``ancestor-path``: Partial path to ancestors of the target node. This can contain one or more ancestor nodes separated by a /. + - ``ancestor-path``: Partial path to ancestors of the target node. This can contain one or more ancestor nodes separated by a ``/``. **Examples** - ``//book/ancestor::categories`` - ``//categories[@genre="SciFi"]/book/ancestor::bookstore`` - ``book/ancestor::categories[@code=1]/books`` + - ``//book/label[text()="classic"]/ancestor::shop`` **Limitations** - Ancestor list elements can only be addressed using the list key leaf. - - List elements with compound keys are not supported. \ No newline at end of file + - List elements with compound keys are not supported. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 8d8bbe333..f213c7e4d 100755 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -16,9 +16,9 @@ CPS Release Notes -.. ========================= -.. * * * HONOULULU * * * -.. ========================= +.. ======================== +.. * * * HONOLULU * * * +.. ======================== Version: 1.0.1 ============== -- 2.16.6