Support text() condition 52/122452/9
authorToineSiebelink <toine.siebelink@est.tech>
Tue, 6 Jul 2021 12:03:03 +0000 (13:03 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Tue, 20 Jul 2021 15:19:37 +0000 (16:19 +0100)
- 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 <toine.siebelink@est.tech>
Change-Id: If422d25cafd2850d25c9a28dea16ba7a5f93dddb

22 files changed:
cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathPrefixType.java [moved from cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQueryType.java with 72% similarity]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathQuerySpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceQueryDataNodeSpec.groovy
cps-ri/src/test/java/org/onap/cps/DatabaseTestContainer.java
cps-ri/src/test/resources/data/anchor.sql
cps-ri/src/test/resources/data/clear-all.sql
cps-ri/src/test/resources/data/cps-path-query.sql
cps-ri/src/test/resources/data/fragment.sql
cps-ri/src/test/resources/data/schemaset.sql
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
cps-service/src/test/resources/test-tree.json
cps-service/src/test/resources/test-tree.yang
docs/cps-path.rst
docs/release-notes.rst

index 198cede..cefeac4 100644 (file)
 
 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 ;
index b9d0c25..ebf6fd3 100644 (file)
 
 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;
     }
 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
 }
index 107bfa3..de7adf2 100644 (file)
@@ -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<String, Object> 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;
+    }
+
 }
index b7826f6..bfec574 100644 (file)
@@ -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
     }
 
 }
index 844ad84..6e12d06 100644 (file)
@@ -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<FragmentEntity> 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<FragmentEntity> fragmentEntities =
+            fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery);
         if (cpsPathQuery.hasAncestorAxis()) {
             final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
             fragmentEntities = ancestorXpaths.isEmpty()
index 4d7e7ff..c48c79e 100755 (executable)
@@ -1,6 +1,6 @@
 /*\r
  * ============LICENSE_START=======================================================\r
- * Copyright (C) 2020-201 Nordix Foundation.\r
+ * Copyright (C) 2020-2021 Nordix Foundation.\r
  * Modifications Copyright (C) 2020-2021 Bell Canada.\r
  * Modifications Copyright (C) 2020-2021 Pantheon.tech.\r
  * ================================================================================\r
@@ -38,13 +38,15 @@ import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;\r
 \r
 @Repository\r
-public interface FragmentRepository extends JpaRepository<FragmentEntity, Long> {\r
+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {\r
 \r
     Optional<FragmentEntity> findByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,\r
-        @NonNull AnchorEntity anchorEntity, @NonNull String xpath);\r
+                                                              @NonNull AnchorEntity anchorEntity,\r
+                                                              @NonNull String xpath);\r
 \r
     default FragmentEntity getByDataspaceAndAnchorAndXpath(@NonNull DataspaceEntity dataspaceEntity,\r
-        @NonNull AnchorEntity anchorEntity, @NonNull String xpath) {\r
+                                                           @NonNull AnchorEntity anchorEntity,\r
+                                                           @NonNull String xpath) {\r
         return findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath)\r
             .orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath));\r
     }\r
@@ -52,42 +54,19 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>
     @Query(\r
         value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND dataspace_id = :dataspace AND parent_id is NULL",\r
         nativeQuery = true)\r
-    List<FragmentEntity> findRootsByDataspaceAndAnchor(\r
-        @Param("dataspace") int dataspaceId, @Param("anchor") int anchorId);\r
+    List<FragmentEntity> findRootsByDataspaceAndAnchor(@Param("dataspace") int dataspaceId,\r
+                                                       @Param("anchor") int anchorId);\r
 \r
     default FragmentEntity findFirstRootByDataspaceAndAnchor(@NonNull DataspaceEntity dataspaceEntity,\r
-        @NonNull AnchorEntity anchorEntity) {\r
+                                                             @NonNull AnchorEntity anchorEntity) {\r
         return findRootsByDataspaceAndAnchor(dataspaceEntity.getId(), anchorEntity.getId()).stream().findFirst()\r
             .orElseThrow(() -> new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName()));\r
     }\r
 \r
     List<FragmentEntity> findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity,\r
-        @NonNull Collection<String> xpath);\r
+                                                   @NonNull Collection<String> xpath);\r
 \r
     @Modifying\r
     @Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")\r
     void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> anchorEntities);\r
-\r
-    @Query(value =\r
-        "SELECT * FROM FRAGMENT WHERE (anchor_id = :anchor) AND (xpath = (:xpath) OR xpath LIKE "\r
-            + "CONCAT(:xpath,'\\[@%]')) AND attributes @> jsonb_build_object(:leafName , :leafValue)",\r
-        nativeQuery = true)\r
-    // Above query will match an xpath with or without the index for a list [@key=value] and match anchor id,\r
-    // leaf name and leaf value\r
-    List<FragmentEntity> getByAnchorAndXpathAndLeafAttributes(@Param("anchor") int anchorId, @Param("xpath")\r
-        String xpathPrefix, @Param("leafName") String leafName, @Param("leafValue") Object leafValue);\r
-\r
-    @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND xpath LIKE CONCAT('%/',:descendantName)",\r
-        nativeQuery = true)\r
-    // Above query will match the anchor id and last descendant name\r
-    List<FragmentEntity> getByAnchorAndXpathEndsInDescendantName(@Param("anchor") int anchorId,\r
-                                                                 @Param("descendantName") String descendantName);\r
-\r
-    @Query(value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND (xpath LIKE CONCAT('%/',:descendantName) OR "\r
-        + "xpath LIKE CONCAT('%/', :descendantName,'\\[@%]')) AND attributes @> :leafDataAsJson\\:\\:jsonb",\r
-        nativeQuery = true)\r
-    // Above query will match the anchor id, last descendant name and all parameters passed into leafDataASJson with the\r
-    // attribute values of the requested data node eg: {"leaf_name":"value", "another_leaf_name":"another value"}​​​​​​\r
-    List<FragmentEntity> getByAnchorAndDescendentNameAndLeafValues(@Param("anchor") int anchorId,\r
-        @Param("descendantName") String descendantName, @Param("leafDataAsJson") String leafDataAsJson);\r
 }\r
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 (file)
index 0000000..04138ec
--- /dev/null
@@ -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<FragmentEntity> 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 (file)
index 0000000..4aa3e5f
--- /dev/null
@@ -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<FragmentEntity> findByAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) {
+        final var sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId");
+        final Map<String, Object> 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<String, Object> 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<String, Object> queryParameters) {
+        for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) {
+            query.setParameter(queryParameter.getKey(), queryParameter.getValue());
+        }
+    }
+
+}
index 8dc9b7f..ae88d30 100644 (file)
@@ -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'              || []
     }
index d3908ea..10f8de4 100755 (executable)
@@ -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<DatabaseTestContainer> {
     private static final String IMAGE_VERSION = "postgres:13.2";
index a7d3e67..dbf1a6a 100644 (file)
@@ -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', '{}');
index 9aee604..8a5e844 100644 (file)
@@ -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;
index 6755845..8f525df 100644 (file)
@@ -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"}');
index 1897185..d7109f2 100755 (executable)
@@ -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"}');
index e6306d0..adfcfa1 100644 (file)
@@ -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');
 
index 532f442..be213c0 100755 (executable)
@@ -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.'() {
index 7f50f7f..2751d55 100644 (file)
@@ -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<String, Map<String, Object>> 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'
index bc9cbd7..e1789ab 100644 (file)
           ]
         }
       }
+    ],
+    "fruit": [
+      {
+        "name": "Apple",
+        "color": "Green"
+      }
     ]
   }
-}
\ No newline at end of file
+}
index faba8a1..6310065 100644 (file)
@@ -20,5 +20,17 @@ module test-tree {
                 }
             }
         }
+        list fruit {
+            key "name color";
+
+            leaf name {
+                type string;
+            }
+
+            leaf color {
+                type string;
+            }
+
+        }
     }
 }
index aec87cd..0271d07 100644 (file)
@@ -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. <https://www.w3.org/TR/2017/REC-xpath-31-20170321/>`_
+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. <https://www.w3.org/TR/2017/REC-xpath-31-20170321/>`_
 
 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
 
     <shops>
        <bookstore name="Chapters">
-         <bookstore-name>Chapters</bookstore-name>
-         <categories code="1" name="SciFi" numberOfBooks="2">
-           <books>
-             <book name="Space Odyssee"/>
-             <book name="Dune"/>
-           </books>
-        </categories>
-        <categories code="2" name="Kids" numberOfBooks="1">
-           <books>
-             <book name="Matilda"/>
-           </books>
-        </categories>
+          <bookstore-name>Chapters</bookstore-name>
+          <categories code="1" name="SciFi" numberOfBooks="2">
+             <books>
+                <book title="2001: A Space Odyssey" price="5">
+                   <label>sale</label>
+                   <label>classic</label>
+                   <edition>1968</edition>
+                   <edition>2018</edition>
+              </book>
+                <book title="Dune" price="5">
+                   <label>classic</label>
+                   <edition>1965</edition>
+                </book>
+             </books>
+          </categories>
+          <categories code="2" name="Kids" numberOfBooks="1">
+             <books>
+                <book title="Matilda" />
+             </books>
+          </categories>
        </bookstore>
     </shops>
 
-**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 <https://jira.onap.org/browse/CPS-500>`_ Special Character Limitations of cpsPath Queries
 
-Supported Queries
-=================
+Query Syntax
+============
 
-Get List Elements by Any Attribute Value
-----------------------------------------
+``( <absolute-path> | <descendant-path> ) [ <leaf-conditions> ] [ <text()-condition> ] [ <ancestor-axis> ]``
 
-**Syntax**: ``<xpath>/<target-node>[@<leaf-name>=<leaf-value>]``
-  - ``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> ( '[' <list-key> ']' )? ( '/' <containerName> ( '[' <list-key> ']' )? )*``
+
+  - ``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**: ``'//' <container-name> ( '[' <list-key> ']' )? ( '/' <containerName> ( '[' <list-key> ']' )? )*``
 
-**Syntax**: ``//<direct-ancestors><target-node>``
-  - ``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><target-node>[@<leaf-name>=<leaf-value>]``
-  - ``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**: ``<xpath> '[' @<leaf-name1> '=' <leaf-value1> ( ' and ' @<leaf-name> '=' <leaf-value> )* ']'``
+  - ``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**: ``<cps-path> ( '/' <leaf-name> '[text()=' <string-value> ']' )?``
+  - ``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 @<leaf-name> 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**: ``//<cps-path>/ancestor::<ancestor-path>``
+**Syntax**: ``<cps-path> ( '/ancestor::' <ancestor-path> )?``
   - ``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.
index 8d8bbe3..f213c7e 100755 (executable)
@@ -16,9 +16,9 @@ CPS Release Notes
 
 
 
-..      =========================
-..      * * *   HONOULULU   * * *
-..      =========================
+..      ========================
+..      * * *   HONOLULU   * * *
+..      ========================
 
 Version: 1.0.1
 ==============