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 <toine.siebelink@est.tech>
Change-Id: If422d25cafd2850d25c9a28dea16ba7a5f93dddb
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 844ad84..6e12d06 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.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 @@
         } 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()
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 4d7e7ff..c48c79e 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.stereotype.Repository;

 

 @Repository

-public interface FragmentRepository extends JpaRepository<FragmentEntity, Long> {

+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {

 

     Optional<FragmentEntity> 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 @@
     @Query(

         value = "SELECT * FROM FRAGMENT WHERE anchor_id = :anchor AND dataspace_id = :dataspace AND parent_id is NULL",

         nativeQuery = true)

-    List<FragmentEntity> findRootsByDataspaceAndAnchor(

-        @Param("dataspace") int dataspaceId, @Param("anchor") int anchorId);

+    List<FragmentEntity> 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<FragmentEntity> findAllByAnchorAndXpathIn(@NonNull AnchorEntity anchorEntity,

-        @NonNull Collection<String> xpath);

+                                                   @NonNull Collection<String> xpath);

 

     @Modifying

     @Query("DELETE FROM FragmentEntity fe WHERE fe.anchor IN (:anchors)")

     void deleteByAnchorIn(@NotNull @Param("anchors") Collection<AnchorEntity> 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<FragmentEntity> 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<FragmentEntity> 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<FragmentEntity> 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 0000000..04138ec
--- /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<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
index 0000000..4aa3e5f
--- /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<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());
+        }
+    }
+
+}
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 8dc9b7f..ae88d30 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 @@
 
 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 @@
     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 @@
             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 @@
                 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 @@
                 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 @@
             '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 d3908ea..10f8de4 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 @@
 
 /**
  * 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";
diff --git a/cps-ri/src/test/resources/data/anchor.sql b/cps-ri/src/test/resources/data/anchor.sql
index a7d3e67..dbf1a6a 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 @@
     (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 9aee604..8a5e844 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 6755845..8f525df 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 @@
     (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 @@
     (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 1897185..d7109f2 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 @@
     (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 e6306d0..adfcfa1 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');