Support pagination in query across all anchors(ep4) 12/134312/38
authorrajesh.kumar <rk00747546@techmahindra.com>
Tue, 25 Apr 2023 06:28:35 +0000 (11:58 +0530)
committerrajesh.kumar <rk00747546@techmahindra.com>
Wed, 2 Aug 2023 12:45:16 +0000 (18:15 +0530)
Add pagination query parameters in query across all anchors API
pagination parameters (pageIndex and pageSize) are optional
default is to query all fragments
each pageSize represents number of records(number of anchors)
TotalRecords is returned in response header to find number of pages.

- If pagination option is provided in request then query number of
  anchors equal to pageSize. pageIndex is used for setting offset.

- return number of records(one anchor per record) as per pagesize
  and pageSize

Issue-ID: CPS-1605
Change-ID: I73f97f986a817d423f93a8d922dcd9647b2504bc
Signed-off-by: rajesh.kumar <rk00747546@techmahindra.com>
26 files changed:
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsQueryV2.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/impl/utils/CpsValidatorImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.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
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/PaginationOption.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/utils/CpsValidator.java
cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/PaginationOptionSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/DataMapUtilsSpec.groovy
docs/api/swagger/cps/openapi.yaml
integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy

index a7c1300..85e19aa 100644 (file)
@@ -269,6 +269,22 @@ components:
         type: string
         default: none
         example: 3
+    pageIndexInQuery:
+      name: pageIndex
+      in: query
+      description: page index for pagination over anchors. It must be greater then zero if provided.
+      required: false
+      schema:
+        type: integer
+        example: 1
+    pageSizeInQuery:
+      name: pageSize
+      in: query
+      description: number of records (anchors) per page. It must be greater then zero if provided.
+      required: false
+      schema:
+        type: integer
+        example: 10
 
   responses:
     NotFound:
index 9beb0e3..4443fb1 100644 (file)
@@ -53,12 +53,14 @@ nodesByDataspaceAndCpsPath:
     description: Query data nodes for the given dataspace across anchors using CPS path
     tags:
       - cps-query
-    summary: Query data nodes
+    summary: Query data nodes across anchors
     operationId: getNodesByDataspaceAndCpsPath
     parameters:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/cpsPathInQuery'
       - $ref: 'components.yml#/components/parameters/descendantsInQuery'
+      - $ref: 'components.yml#/components/parameters/pageIndexInQuery'
+      - $ref: 'components.yml#/components/parameters/pageSizeInQuery'
     responses:
       '200':
         description: OK
index 1fc13fc..5334b48 100644 (file)
@@ -25,12 +25,14 @@ package org.onap.cps.rest.controller;
 import io.micrometer.core.annotation.Timed;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.api.CpsQueryService;
 import org.onap.cps.rest.api.CpsQueryApi;
 import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.utils.DataMapUtils;
 import org.onap.cps.utils.JsonObjectMapper;
@@ -72,22 +74,55 @@ public class QueryRestController implements CpsQueryApi {
     }
 
     @Override
-    public ResponseEntity<Object> getNodesByDataspaceAndCpsPath(final String dataspaceName,
-        final String cpsPath, final String fetchDescendantsOptionAsString) {
+    @Timed(value = "cps.data.controller.datanode.query.across.anchors",
+            description = "Time taken to query data nodes across anchors")
+    public ResponseEntity<Object> getNodesByDataspaceAndCpsPath(final String dataspaceName, final String cpsPath,
+                                                                final String fetchDescendantsOptionAsString,
+                                                                final Integer pageIndex, final Integer pageSize) {
         final FetchDescendantsOption fetchDescendantsOption =
                 FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
-        final Collection<DataNode> dataNodes =
-                cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption);
-        final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size());
+        final PaginationOption paginationOption = (pageIndex == null || pageSize == null)
+                ? PaginationOption.NO_PAGINATION : new PaginationOption(pageIndex, pageSize);
+        final Collection<DataNode> dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName,
+                cpsPath, fetchDescendantsOption, paginationOption);
+        final List<Map<String, Object>> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size());
         String prefix = null;
-        for (final DataNode dataNode : dataNodes) {
+        final Map<String, List<DataNode>> anchorDataNodeListMap = prepareDataNodesForAnchor(dataNodes);
+        for (final Map.Entry<String, List<DataNode>> anchorDataNodesMapEntry : anchorDataNodeListMap.entrySet()) {
             if (prefix == null) {
-                prefix = prefixResolver.getPrefix(dataspaceName, dataNode.getAnchorName(), dataNode.getXpath());
+                prefix = prefixResolver.getPrefix(dataspaceName, anchorDataNodesMapEntry.getKey(),
+                        anchorDataNodesMapEntry.getValue().get(0).getXpath());
+            }
+            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor(
+                    anchorDataNodesMapEntry.getValue(), anchorDataNodesMapEntry.getKey(), prefix);
+            dataNodesAsListOfMaps.add(dataMap);
+        }
+        final Integer totalPages = getTotalPages(dataspaceName, cpsPath, paginationOption);
+        return ResponseEntity.ok().header("total-pages",
+                totalPages.toString()).body(jsonObjectMapper.asJsonString(dataNodesAsListOfMaps));
+    }
+
+    private Integer getTotalPages(final String dataspaceName, final String cpsPath,
+                                                          final PaginationOption paginationOption) {
+        if (paginationOption == PaginationOption.NO_PAGINATION) {
+            return 1;
+        }
+        final int totalAnchors =  cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath);
+        return totalAnchors <= paginationOption.getPageSize() ? 1
+                : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize());
+    }
+
+    private Map<String, List<DataNode>> prepareDataNodesForAnchor(final Collection<DataNode> dataNodes) {
+        final Map<String, List<DataNode>> dataNodesMapForAnchor = new HashMap<>();
+        for (final DataNode dataNode : dataNodes) {
+            List<DataNode> dataNodesInAnchor = dataNodesMapForAnchor.get(dataNode.getAnchorName());
+            if (dataNodesInAnchor == null) {
+                dataNodesInAnchor = new ArrayList<>();
+                dataNodesMapForAnchor.put(dataNode.getAnchorName(), dataNodesInAnchor);
             }
-            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor(dataNode, prefix);
-            dataMaps.add(dataMap);
+            dataNodesInAnchor.add(dataNode);
         }
-        return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK);
+        return dataNodesMapForAnchor;
     }
 
     private ResponseEntity<Object> executeNodesByDataspaceQueryAndCreateResponse(final String dataspaceName,
index c4bb23c..8ee01c0 100644 (file)
@@ -23,6 +23,7 @@
 
 package org.onap.cps.rest.controller
 
+import org.onap.cps.spi.PaginationOption
 import org.onap.cps.utils.PrefixResolver
 
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
@@ -70,7 +71,7 @@ class QueryRestControllerSpec extends Specification {
 
     def 'Query data node by cps path for the given dataspace and anchor with #scenario.'() {
         given: 'service method returns a list containing a data node'
-             def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
+            def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
                     .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
             mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, expectedCpsDataServiceOption) >> [dataNode1, dataNode1]
         and: 'the query endpoint'
@@ -111,18 +112,24 @@ class QueryRestControllerSpec extends Specification {
     }
 
     def 'Query data node by cps path for the given dataspace across all anchors with #scenario.'() {
-        given: 'service method returns a list containing a data node'
+        given: 'service method returns a list containing a data node from different anchors'
             def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
                 .withAnchor('my_anchor')
                 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
             def dataNode2 = new DataNodeBuilder().withXpath('/xpath')
                 .withAnchor('my_anchor_2')
                 .withLeaves([leaf: 'value', leafList: ['leaveListElement3', 'leaveListElement4']]).build()
+        and: 'second data node for the same anchor'
+            def dataNode3 = new DataNodeBuilder().withXpath('/xpath')
+                .withAnchor('my_anchor_2')
+                .withLeaves([leaf: 'value', leafList: ['leaveListElement5', 'leaveListElement6']]).build()
+        and: 'the query endpoint'
             def dataspaceName = 'my_dataspace'
             def cpsPath = 'some/cps/path'
-            mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, expectedCpsDataServiceOption) >> [dataNode1, dataNode2]
-        and: 'the query endpoint'
             def dataNodeEndpoint = "$basePath/v2/dataspaces/$dataspaceName/nodes/query"
+            mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath,
+                expectedCpsDataServiceOption, PaginationOption.NO_PAGINATION) >> [dataNode1, dataNode2, dataNode3]
+            mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> 2
         when: 'query data nodes API is invoked'
             def response =
                 mvc.perform(
@@ -134,10 +141,46 @@ class QueryRestControllerSpec extends Specification {
             response.status == HttpStatus.OK.value()
             response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
             response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}')
+            response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement5","leaveListElement6"]}}')
         where: 'the following options for include descendants are provided in the request'
             scenario                    | includeDescendantsOptionString || expectedCpsDataServiceOption
             'no descendants by default' | ''                             || OMIT_DESCENDANTS
             'no descendant explicitly'  | 'none'                         || OMIT_DESCENDANTS
             'descendants'               | 'all'                          || INCLUDE_ALL_DESCENDANTS
     }
+
+    def 'Query data node by cps path for the given dataspace across all anchors with pagination #scenario.'() {
+        given: 'service method returns a list containing a data node from different anchors'
+        def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
+                .withAnchor('my_anchor')
+                .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
+        def dataNode2 = new DataNodeBuilder().withXpath('/xpath')
+                .withAnchor('my_anchor_2')
+                .withLeaves([leaf: 'value', leafList: ['leaveListElement3', 'leaveListElement4']]).build()
+        and: 'the query endpoint'
+            def dataspaceName = 'my_dataspace'
+            def cpsPath = 'some/cps/path'
+            def dataNodeEndpoint = "$basePath/v2/dataspaces/$dataspaceName/nodes/query"
+            mockCpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath,
+                INCLUDE_ALL_DESCENDANTS, new PaginationOption(pageIndex,pageSize)) >> [dataNode1, dataNode2]
+            mockCpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath) >> totalAnchors
+        when: 'query data nodes API is invoked'
+            def response =
+                mvc.perform(
+                        get(dataNodeEndpoint)
+                                .param('cps-path', cpsPath)
+                                .param('descendants', "all")
+                                .param('pageIndex', String.valueOf(pageIndex))
+                                .param('pageSize', String.valueOf(pageSize)))
+                        .andReturn().response
+        then: 'the response contains the the datanode in json format'
+            assert response.status == HttpStatus.OK.value()
+            assert Integer.valueOf(response.getHeaderValue("total-pages")) == expectedPageSize
+            assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
+            assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement3","leaveListElement4"]}}')
+        where: 'the following options for include descendants are provided in the request'
+            scenario                     | pageIndex | pageSize | totalAnchors || expectedPageSize
+            '1st page with all anchors'  | 1         | 3        | 3            || 1
+            '1st page with less anchors' | 1         | 2        | 3            || 2
+    }
 }
index 6207393..941d447 100644 (file)
@@ -33,7 +33,7 @@
     <artifactId>cps-ri</artifactId>\r
 \r
     <properties>\r
-        <minimum-coverage>0.30</minimum-coverage>\r
+        <minimum-coverage>0.28</minimum-coverage>\r
         <!-- Additional coverage is provided by integration-test module -->\r
     </properties>\r
 \r
index f4afe3d..19302d6 100644 (file)
@@ -23,7 +23,8 @@
 
 package org.onap.cps.spi.impl;
 
-import com.google.common.base.Strings;
+import static org.onap.cps.spi.PaginationOption.NO_PAGINATION;
+
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
 import io.micrometer.core.annotation.Timed;
@@ -48,6 +49,7 @@ import org.onap.cps.cpspath.parser.CpsPathUtil;
 import org.onap.cps.cpspath.parser.PathParsingException;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.entities.AnchorEntity;
 import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.FragmentEntity;
@@ -79,8 +81,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     private final SessionManager sessionManager;
 
     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@.+?])?)";
-    private static final String QUERY_ACROSS_ANCHORS = null;
-    private static final AnchorEntity ALL_ANCHORS = null;
 
     @Override
     public void addChildDataNodes(final String dataspaceName, final String anchorName,
@@ -288,8 +288,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
                                          final FetchDescendantsOption fetchDescendantsOption) {
         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
-        final AnchorEntity anchorEntity = Strings.isNullOrEmpty(anchorName) ? ALL_ANCHORS
-            : anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+        final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
         final CpsPathQuery cpsPathQuery;
         try {
             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
@@ -298,28 +297,60 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         }
 
         Collection<FragmentEntity> fragmentEntities;
-        if (anchorEntity == ALL_ANCHORS) {
-            fragmentEntities = fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery);
+        fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+        if (cpsPathQuery.hasAncestorAxis()) {
+            final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
+            fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+        }
+        fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
+                fragmentEntities);
+        return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
+    }
+
+    @Override
+    @Timed(value = "cps.data.persistence.service.datanode.query.anchors",
+            description = "Time taken to query data nodes across all anchors or list of anchors")
+    public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
+                                                      final FetchDescendantsOption fetchDescendantsOption,
+                                                      final PaginationOption paginationOption) {
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+        final CpsPathQuery cpsPathQuery;
+        try {
+            cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+        } catch (final PathParsingException e) {
+            throw new CpsPathException(e.getMessage());
+        }
+
+        final List<Long> anchorIds;
+        if (paginationOption == NO_PAGINATION) {
+            anchorIds = Collections.EMPTY_LIST;
         } else {
-            fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+            anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
+            if (anchorIds.isEmpty()) {
+                return Collections.emptyList();
+            }
         }
+        Collection<FragmentEntity> fragmentEntities =
+            fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
+
         if (cpsPathQuery.hasAncestorAxis()) {
             final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
-            if (anchorEntity == ALL_ANCHORS) {
+            if (anchorIds.isEmpty()) {
                 fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
             } else {
-                fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+                fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(
+                        anchorIds.toArray(new Long[0]), ancestorXpaths.toArray(new String[0]));
             }
+
         }
         fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
                 fragmentEntities);
         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
     }
 
-    @Override
-    public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
-                                                      final FetchDescendantsOption fetchDescendantsOption) {
-        return queryDataNodes(dataspaceName, QUERY_ACROSS_ANCHORS, cpsPath, fetchDescendantsOption);
+    private List<Long> getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
+                                                 final PaginationOption paginationOption) {
+        return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
     }
 
     private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
@@ -358,6 +389,19 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
     }
 
+    @Override
+    public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+        final CpsPathQuery cpsPathQuery;
+        try {
+            cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+        } catch (final PathParsingException e) {
+            throw new CpsPathException(e.getMessage());
+        }
+        final List<Long> anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION);
+        return anchorIdList.size();
+    }
+
     private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
                                                     final CpsPathQuery cpsPathQuery) {
         final Set<String> ancestorXpath = new HashSet<>();
index 1f61ee3..c727388 100644 (file)
@@ -25,6 +25,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.utils.CpsValidator;
 import org.springframework.stereotype.Component;
@@ -54,4 +55,16 @@ public class CpsValidatorImpl implements CpsValidator {
             }
         }
     }
+
+    @Override
+    public void validatePaginationOption(final PaginationOption paginationOption) {
+        if (PaginationOption.NO_PAGINATION == paginationOption) {
+            return;
+        }
+
+        if (!paginationOption.isValidPaginationOption()) {
+            throw new DataValidationException("Pagination validation error.",
+                    "Invalid page index or size");
+        }
+    }
 }
index e371035..0c43d62 100644 (file)
 
 package org.onap.cps.spi.repository;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import javax.persistence.EntityManager;
@@ -32,6 +34,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.cpspath.parser.CpsPathPrefixType;
 import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.entities.AnchorEntity;
 import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.FragmentEntity;
@@ -56,7 +59,8 @@ public class FragmentQueryBuilder {
      * @return a executable query object
      */
     public Query getQueryForAnchorAndCpsPath(final AnchorEntity anchorEntity, final CpsPathQuery cpsPathQuery) {
-        return getQueryForDataspaceOrAnchorAndCpsPath(anchorEntity.getDataspace(), anchorEntity, cpsPathQuery);
+        return getQueryForDataspaceOrAnchorAndCpsPath(anchorEntity.getDataspace(),
+                anchorEntity, cpsPathQuery, Collections.EMPTY_LIST);
     }
 
     /**
@@ -67,13 +71,45 @@ public class FragmentQueryBuilder {
      * @return a executable query object
      */
     public Query getQueryForDataspaceAndCpsPath(final DataspaceEntity dataspaceEntity,
-                                                final CpsPathQuery cpsPathQuery) {
-        return getQueryForDataspaceOrAnchorAndCpsPath(dataspaceEntity, ACROSS_ALL_ANCHORS, cpsPathQuery);
+                                                final CpsPathQuery cpsPathQuery,
+                                                final List<Long> anchorIdsForPagination) {
+        return getQueryForDataspaceOrAnchorAndCpsPath(dataspaceEntity, ACROSS_ALL_ANCHORS,
+                cpsPathQuery, anchorIdsForPagination);
+    }
+
+    /**
+     * Get query for dataspace, cps path, page index and page size.
+     * @param dataspaceEntity data space entity
+     * @param cpsPathQuery cps path query
+     * @param paginationOption pagination option
+     * @return query for given dataspace, cps path and pagination parameters
+     */
+    public Query getQueryForAnchorIdsForPagination(final DataspaceEntity dataspaceEntity,
+                                                   final CpsPathQuery cpsPathQuery,
+                                                   final PaginationOption paginationOption) {
+        final StringBuilder sqlStringBuilder = new StringBuilder();
+        final Map<String, Object> queryParameters = new HashMap<>();
+        sqlStringBuilder.append("SELECT distinct(fragment.anchor_id) FROM fragment "
+                + "JOIN anchor ON anchor.id = fragment.anchor_id WHERE dataspace_id = :dataspaceId");
+        queryParameters.put("dataspaceId", dataspaceEntity.getId());
+        addXpathSearch(cpsPathQuery, sqlStringBuilder, queryParameters);
+        addLeafConditions(cpsPathQuery, sqlStringBuilder);
+        addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters);
+        addContainsFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters);
+        if (PaginationOption.NO_PAGINATION != paginationOption) {
+            sqlStringBuilder.append(" ORDER BY fragment.anchor_id");
+            addPaginationCondition(sqlStringBuilder, queryParameters, paginationOption);
+        }
+
+        final Query query = entityManager.createNativeQuery(sqlStringBuilder.toString());
+        setQueryParameters(query, queryParameters);
+        return query;
     }
 
     private Query getQueryForDataspaceOrAnchorAndCpsPath(final DataspaceEntity dataspaceEntity,
                                                          final AnchorEntity anchorEntity,
-                                                         final CpsPathQuery cpsPathQuery) {
+                                                         final CpsPathQuery cpsPathQuery,
+                                                         final List<Long> anchorIdsForPagination) {
         final StringBuilder sqlStringBuilder = new StringBuilder();
         final Map<String, Object> queryParameters = new HashMap<>();
 
@@ -81,6 +117,10 @@ public class FragmentQueryBuilder {
             sqlStringBuilder.append("SELECT fragment.* FROM fragment JOIN anchor ON anchor.id = fragment.anchor_id"
                 + " WHERE dataspace_id = :dataspaceId");
             queryParameters.put("dataspaceId", dataspaceEntity.getId());
+            if (!anchorIdsForPagination.isEmpty()) {
+                sqlStringBuilder.append(" AND anchor_id IN (:anchorIdsForPagination)");
+                queryParameters.put("anchorIdsForPagination", anchorIdsForPagination);
+            }
         } else {
             sqlStringBuilder.append("SELECT * FROM fragment WHERE anchor_id = :anchorId");
             queryParameters.put("anchorId", anchorEntity.getId());
@@ -107,6 +147,15 @@ public class FragmentQueryBuilder {
         }
     }
 
+    private static void addPaginationCondition(final StringBuilder sqlStringBuilder,
+                                               final Map<String, Object> queryParameters,
+                                               final PaginationOption paginationOption) {
+        final Integer offset = (paginationOption.getPageIndex() - 1) * paginationOption.getPageSize();
+        sqlStringBuilder.append(" LIMIT :pageSize OFFSET :offset");
+        queryParameters.put("pageSize", paginationOption.getPageSize());
+        queryParameters.put("offset", offset);
+    }
+
     private static Integer getTextValueAsInt(final CpsPathQuery cpsPathQuery) {
         try {
             return Integer.parseInt(cpsPathQuery.getTextFunctionConditionValue());
index 303af5b..11b2b07 100755 (executable)
@@ -68,6 +68,11 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
         return findByDataspaceIdAndXpathIn(dataspaceEntity.getId(), xpaths.toArray(new String[0]));\r
     }\r
 \r
+    @Query(value = "SELECT * FROM fragment WHERE anchor_id IN (:anchorIds)"\r
+            + " AND xpath = ANY (:xpaths)", nativeQuery = true)\r
+    List<FragmentEntity> findByAnchorIdsAndXpathIn(@Param("anchorIds") Long[] anchorIds,\r
+                                                   @Param("xpaths") String[] xpaths);\r
+\r
     @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId LIMIT 1", nativeQuery = true)\r
     Optional<FragmentEntity> findOneByAnchorId(@Param("anchorId") long anchorId);\r
 \r
index de0c060..9c27961 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.cps.spi.repository;
 
 import java.util.List;
 import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.entities.AnchorEntity;
 import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.FragmentEntity;
@@ -30,5 +31,10 @@ import org.onap.cps.spi.entities.FragmentEntity;
 public interface FragmentRepositoryCpsPathQuery {
     List<FragmentEntity> findByAnchorAndCpsPath(AnchorEntity anchorEntity, CpsPathQuery cpsPathQuery);
 
-    List<FragmentEntity> findByDataspaceAndCpsPath(DataspaceEntity dataspaceEntity, CpsPathQuery cpsPathQuery);
+    List<FragmentEntity> findByDataspaceAndCpsPath(DataspaceEntity dataspaceEntity,
+                                                   CpsPathQuery cpsPathQuery, List<Long> anchorIds);
+
+    List<Long> findAnchorIdsForPagination(DataspaceEntity dataspaceEntity, CpsPathQuery cpsPathQuery,
+                                          PaginationOption paginationOption);
+
 }
index 6cc6495..1ba9d1a 100644 (file)
@@ -29,6 +29,7 @@ import javax.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.cpspath.parser.CpsPathQuery;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.entities.AnchorEntity;
 import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.FragmentEntity;
@@ -55,11 +56,21 @@ public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCps
     @Override
     @Transactional
     public List<FragmentEntity> findByDataspaceAndCpsPath(final DataspaceEntity dataspaceEntity,
-                                                          final CpsPathQuery cpsPathQuery) {
-        final Query query = fragmentQueryBuilder.getQueryForDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery);
+                                                          final CpsPathQuery cpsPathQuery, final List<Long> anchorIds) {
+        final Query query = fragmentQueryBuilder.getQueryForDataspaceAndCpsPath(
+                dataspaceEntity, cpsPathQuery, anchorIds);
         final List<FragmentEntity> fragmentEntities = query.getResultList();
         log.debug("Fetched {} fragment entities by cps path across all anchors.", fragmentEntities.size());
         return fragmentEntities;
     }
 
+    @Override
+    @Transactional
+    public List<Long> findAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
+                                                 final PaginationOption paginationOption) {
+        final Query query = fragmentQueryBuilder.getQueryForAnchorIdsForPagination(
+                dataspaceEntity, cpsPathQuery, paginationOption);
+        return query.getResultList();
+    }
+
 }
index 345089c..8d34844 100644 (file)
@@ -20,6 +20,7 @@
 
 package org.onap.cps.spi.impl.utils
 
+import org.onap.cps.spi.PaginationOption
 import org.onap.cps.spi.exceptions.DataValidationException
 import spock.lang.Specification
 
@@ -64,4 +65,13 @@ class CpsValidatorSpec extends Specification {
         then: 'a data validation exception is thrown'
             thrown(DataValidationException)
     }
+
+    def 'Validate Pagination option with invalid page index and size.'() {
+        when: 'the pagination option is validated using invalid options'
+            objectUnderTest.validatePaginationOption(new PaginationOption(-5, -2))
+        then: 'a data validation exception is thrown'
+            def exceptionThrown = thrown(DataValidationException)
+        and: 'the error was encountered at the following index in #scenario'
+            assert exceptionThrown.getDetails().contains("Invalid page index or size")
+    }
 }
index af54077..edd2d2a 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.cps.api;
 
 import java.util.Collection;
 import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.model.DataNode;
 
 /*
@@ -50,8 +51,18 @@ public interface CpsQueryService {
      * @param cpsPath CPS path
      * @param fetchDescendantsOption defines whether the descendants of the node(s) found by the query should be
      *                               included in the output
+     * @param paginationOption pagination option
      * @return a collection of data nodes
      */
     Collection<DataNode> queryDataNodesAcrossAnchors(String dataspaceName, String cpsPath,
-                                                     FetchDescendantsOption fetchDescendantsOption);
+                                                     FetchDescendantsOption fetchDescendantsOption,
+                                                     PaginationOption paginationOption);
+
+    /**
+     * Query total number of anchors for given dataspace name and cps path.
+     * @param dataspaceName dataspace name
+     * @param cpsPath cps path
+     * @return total number of anchors for given dataspace name and cps path.
+     */
+    Integer countAnchorsForDataspaceAndCpsPath(String dataspaceName, String cpsPath);
 }
index ac018c9..1d7a7ce 100644 (file)
@@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor;
 import org.onap.cps.api.CpsQueryService;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.PaginationOption;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.utils.CpsValidator;
 import org.springframework.stereotype.Service;
@@ -49,8 +50,17 @@ public class CpsQueryServiceImpl implements CpsQueryService {
 
     @Override
     public Collection<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName,
-        final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) {
+        final String cpsPath, final FetchDescendantsOption fetchDescendantsOption,
+        final PaginationOption paginationOption) {
+        cpsValidator.validateNameCharacters(dataspaceName);
+        cpsValidator.validatePaginationOption(paginationOption);
+        return cpsDataPersistenceService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath,
+                fetchDescendantsOption, paginationOption);
+    }
+
+    @Override
+    public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
         cpsValidator.validateNameCharacters(dataspaceName);
-        return cpsDataPersistenceService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption);
+        return cpsDataPersistenceService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath);
     }
 }
index 9674bbe..1baca4e 100644 (file)
@@ -200,11 +200,12 @@ public interface CpsDataPersistenceService {
      * @param cpsPath                cps path
      * @param fetchDescendantsOption defines whether the descendants of the node(s) found by the query should be
      *                               included in the output
+     * @param paginationOption pagination option
      * @return the data nodes found i.e. 0 or more data nodes
      */
     List<DataNode> queryDataNodesAcrossAnchors(String dataspaceName,
-                                  String cpsPath, FetchDescendantsOption fetchDescendantsOption);
-
+                                  String cpsPath, FetchDescendantsOption fetchDescendantsOption,
+                                  PaginationOption paginationOption);
 
     /**
      * Starts a session which allows use of locks and batch interaction with the persistence service.
@@ -230,4 +231,12 @@ public interface CpsDataPersistenceService {
      * @param timeoutInMilliseconds lock attempt timeout in milliseconds
      */
     void lockAnchor(String sessionID, String dataspaceName, String anchorName, Long timeoutInMilliseconds);
+
+    /**
+     * Query total anchors for dataspace name and cps path.
+     * @param dataspaceName datasoace name
+     * @param cpsPath cps path
+     * @return total anchors for dataspace name and cps path
+     */
+    Integer countAnchorsForDataspaceAndCpsPath(String dataspaceName, String cpsPath);
 }
diff --git a/cps-service/src/main/java/org/onap/cps/spi/PaginationOption.java b/cps-service/src/main/java/org/onap/cps/spi/PaginationOption.java
new file mode 100644 (file)
index 0000000..17f025d
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 TechMahindra Ltd.
+ *  ================================================================================
+ *  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;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class PaginationOption {
+
+    private int pageIndex;
+
+    private int pageSize;
+
+    public static final PaginationOption NO_PAGINATION = null;
+
+    public boolean isValidPaginationOption() {
+        return this.pageIndex > 0 && this.pageSize > 0;
+    }
+}
index 231094c..ceb75c0 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.onap.cps.spi.utils;
 
+import org.onap.cps.spi.PaginationOption;
+
 public interface CpsValidator {
 
     /**
@@ -35,4 +37,11 @@ public interface CpsValidator {
      * @param names names of data to be validated
      */
     void validateNameCharacters(final Iterable<String> names);
+
+    /**
+     * Validate pagination option.
+     *
+     * @param paginationOption pagination option
+     */
+    void validatePaginationOption(final PaginationOption paginationOption);
 }
index b4d5a09..1ac2bdd 100644 (file)
@@ -28,8 +28,10 @@ import static java.util.stream.Collectors.toUnmodifiableList;
 import static java.util.stream.Collectors.toUnmodifiableMap;
 
 import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
@@ -52,17 +54,29 @@ public class DataMapUtils {
     }
 
     /**
-     * Converts DataNode structure into a map including the root node identifier for a JSON response.
-     *
-     * @param dataNode data node object
-     * @return a map representing same data with the root node identifier
+     * Converts list of DataNode structure into a map including the root node identifier for a JSON response.
+     * @param dataNodeList list of data nodes for a given anchor name
+     * @param anchorName anchor name
+     * @param prefix prefix
+     * @return a map representing same list of data for given anchor with the root node identifier
      */
-    public static Map<String, Object> toDataMapWithIdentifierAndAnchor(final DataNode dataNode, final String prefix) {
-        final String nodeIdentifierWithPrefix = getNodeIdentifierWithPrefix(dataNode.getXpath(), prefix);
-        final Map<String, Object> dataMap = ImmutableMap.<String, Object>builder()
-                .put(nodeIdentifierWithPrefix, toDataMap(dataNode)).build();
-        return ImmutableMap.<String, Object>builder().put("anchorName", dataNode.getAnchorName())
-                .put("dataNode", dataMap).build();
+    public static Map<String, Object> toDataMapWithIdentifierAndAnchor(final List<DataNode> dataNodeList,
+                                                                       final String anchorName, final String prefix) {
+        final List<Map<String, Object>> dataMaps = toDataNodesWithIdentifier(dataNodeList, prefix);
+        return ImmutableMap.<String, Object>builder().put("anchorName", anchorName)
+                .put("dataNodes", dataMaps).build();
+    }
+
+    private static List<Map<String, Object>> toDataNodesWithIdentifier(final List<DataNode> dataNodeList,
+                                                                       final String prefix) {
+        final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodeList.size());
+        for (final DataNode dataNode: dataNodeList) {
+            final String nodeIdentifierWithPrefix = getNodeIdentifierWithPrefix(dataNode.getXpath(), prefix);
+            final Map<String, Object> dataMap = ImmutableMap.<String, Object>builder()
+                    .put(nodeIdentifierWithPrefix, toDataMap(dataNode)).build();
+            dataMaps.add(dataMap);
+        }
+        return dataMaps;
     }
 
     /**
index 553027a..1ad5017 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.cps.api.impl
 
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.spi.PaginationOption
 import org.onap.cps.spi.utils.CpsValidator
 import spock.lang.Specification
 
@@ -52,14 +53,22 @@ class CpsQueryServiceImplSpec extends Specification {
         given: 'a dataspace name, an anchor name and a cps path'
             def dataspaceName = 'some-dataspace'
             def cpsPath = '/cps-path'
+            def paginationOption = new PaginationOption(1, 2)
         when: 'queryDataNodes is invoked'
-            objectUnderTest.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption)
+            objectUnderTest.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption, paginationOption)
         then: 'the persistence service is called once with the correct parameters'
-            1 * mockCpsDataPersistenceService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption)
+            1 * mockCpsDataPersistenceService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption, paginationOption)
         and: 'the CpsValidator is called on the dataspaceName, schemaSetName and anchorName'
             1 * mockCpsValidator.validateNameCharacters(dataspaceName)
         where: 'all fetch descendants options are supported'
-            fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
+        fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS,
+                                   FetchDescendantsOption.DIRECT_CHILDREN_ONLY, new FetchDescendantsOption(10)]
     }
 
+    def 'Query total anchors for dataspace and cps path.'() {
+        when: 'query total anchors is invoked'
+            objectUnderTest.countAnchorsForDataspaceAndCpsPath("some-dataspace", "/cps-path")
+        then: 'the persistence service is called once with the correct parameters'
+            1 * mockCpsDataPersistenceService.countAnchorsForDataspaceAndCpsPath("some-dataspace", "/cps-path")
+    }
 }
diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/PaginationOptionSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/PaginationOptionSpec.groovy
new file mode 100644 (file)
index 0000000..9d74a17
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
+ *  ================================================================================
+ *  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
+
+import spock.lang.Specification
+
+class PaginationOptionSpec extends Specification {
+
+    def 'Pagination validation with: #scenario'() {
+        given: 'pagination option with pageIndex and pageSize'
+            def paginationOption = new PaginationOption(pageIndex, pageSize)
+        expect: 'validation returns expected result'
+            assert paginationOption.isValidPaginationOption() == expectedIsValidPaginationOption
+        where: 'following parameters are used'
+            scenario           | pageIndex | pageSize || expectedIsValidPaginationOption
+            'valid pagination' | 1         | 1        || true
+            'negative index'   | -1        | 1        || false
+            'negative size'    | 1         | -1       || false
+            'zero index'       | 0         | 1        || false
+            'zero size'        | 1         | 0        || false
+    }
+}
index 29085a9..6b9f9ac 100644 (file)
@@ -74,17 +74,19 @@ class DataMapUtilsSpec extends Specification {
 
     def 'Data node structure with anchor name conversion to map with root node identifier.'() {
         when: 'data node structure is converted to a map with root node identifier'
-            def result = DataMapUtils.toDataMapWithIdentifierAndAnchor(dataNodeWithAnchor, dataNodeWithAnchor.moduleNamePrefix)
+            def result = DataMapUtils.toDataMapWithIdentifierAndAnchor([dataNodeWithAnchor], dataNodeWithAnchor.anchorName, dataNodeWithAnchor.moduleNamePrefix)
         then: 'root node leaves are populated under its node identifier'
-            def parentNode = result.get("dataNode").parent
-            parentNode.parentLeaf == 'parentLeafValue'
-            parentNode.parentLeafList == ['parentLeafListEntry1','parentLeafListEntry2']
+            def dataNodes = result.dataNodes as List
+            assert dataNodes.size() == 1
+            def parentNode = dataNodes[0].parent
+            assert parentNode.parentLeaf == 'parentLeafValue'
+            assert parentNode.parentLeafList == ['parentLeafListEntry1','parentLeafListEntry2']
         and: 'leaves for child element is populated under its node identifier'
             assert parentNode.'child-object'.childLeaf == 'childLeafValue'
         and: 'leaves for grandchild element is populated under its node identifier'
             assert parentNode.'child-object'.'grand-child-object'.grandChildLeaf == 'grandChildLeafValue'
         and: 'data node is associated with anchor name'
-            assert result.get('anchorName') == 'anchor01'
+            assert result.anchorName == 'anchor01'
     }
 
     def 'Data node without leaves and without children.'() {
index ace45f8..12b438a 100644 (file)
@@ -2359,6 +2359,20 @@ paths:
             default: none
             example: "3"
             type: string
+        - description: "page index for pagination over anchors"
+          name: pageIndex
+          in: query
+          required: false
+          schema:
+            type: integer
+            minimum: 1
+        - description: "number of records (anchors) to query per page"
+          name: pageSize
+          in: query
+          required: false
+          schema:
+            type: integer
+            minimum: 1
       responses:
         "200":
           content:
@@ -2370,6 +2384,11 @@ paths:
               schema:
                 type: object
           description: OK
+          headers:
+            total-pages:
+              schema:
+                type: integer
+              description: Total number of pages for given page size
         "400":
           content:
             application/json:
@@ -2749,4 +2768,4 @@ components:
   securitySchemes:
     basicAuth:
       scheme: basic
-      type: http
+      type: http
\ No newline at end of file
index a1e0352..4780e36 100644 (file)
@@ -117,7 +117,7 @@ class CpsIntegrationSpecBase extends Specification {
     def addAnchorsWithData(numberOfAnchors, dataspaceName, schemaSetName, anchorNamePrefix, data) {
         (1..numberOfAnchors).each {
             cpsAdminService.createAnchor(dataspaceName, schemaSetName, anchorNamePrefix + it)
-            cpsDataService.saveData(dataspaceName, anchorNamePrefix + it, data, OffsetDateTime.now())
+            cpsDataService.saveData(dataspaceName, anchorNamePrefix + it, data.replace("Easons", "Easons-"+it.toString()), OffsetDateTime.now())
         }
     }
 }
index 89a5e40..327a39e 100644 (file)
@@ -58,7 +58,7 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase {
         def anchorName = 'bookstoreAnchor' + anchorNumber
         cpsAdminService.deleteAnchor(FUNCTIONAL_TEST_DATASPACE_1, anchorName)
         cpsAdminService.createAnchor(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_SCHEMA_SET, anchorName)
-        cpsDataService.saveData(FUNCTIONAL_TEST_DATASPACE_1, anchorName, bookstoreJsonData, OffsetDateTime.now())
+        cpsDataService.saveData(FUNCTIONAL_TEST_DATASPACE_1, anchorName, bookstoreJsonData.replace("Easons", "Easons-"+anchorNumber.toString()), OffsetDateTime.now())
     }
 
 }
index a3f1439..ebaf909 100644 (file)
@@ -58,7 +58,7 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
         then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
             assert countDataNodesInTree(result) == expectNumberOfDataNodes
         and: 'the top level data node has the expected attribute and value'
-            assert result.leaves['bookstore-name'] == ['Easons']
+            assert result.leaves['bookstore-name'] == ['Easons-1']
         and: 'they are from the correct dataspace'
             assert result.dataspace == [FUNCTIONAL_TEST_DATASPACE_1]
         and: 'they are from the correct anchor'
@@ -74,9 +74,9 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
     def 'Read bookstore top-level container(s) using "root" path variations.'() {
         when: 'get data nodes for bookstore container'
             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, root, OMIT_DESCENDANTS)
-        then: 'the tree consist ouf of one data node'
+        then: 'the tree consist correct number of data nodes'
             assert countDataNodesInTree(result) == 2
-        and: 'the top level data node has the expected attribute and value'
+        and: 'the top level data node has the expected number of leaves'
             assert result.leaves.size() == 2
         where: 'the following variations of "root" are used'
             root << [ '/', '' ]
@@ -324,20 +324,6 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             'new code, new child'           | 'new'        | ', "books" : [ { "title": "New Book" } ]' || 2
     }
 
-    def 'Update multiple data node leaves.'() {
-        given: 'Updated json for bookstore data'
-            def jsonData =  "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
-        when: 'update is performed for leaves'
-            objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
-        then: 'the updated data nodes are retrieved'
-            def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
-        and: 'the leaf values are updated as expected'
-            assert result.leaves['lang'] == ['English/French']
-            assert result.leaves['price'] == [100]
-        cleanup:
-            restoreBookstoreDataAnchor(2)
-    }
-
     def 'Update data node leaves for node that has no leaves (yet).'() {
         given: 'new (webinfo) datanode without leaves'
             def json = '{"webinfo": {} }'
@@ -382,6 +368,20 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             restoreBookstoreDataAnchor(1)
     }
 
+    def 'Update multiple data node leaves.'() {
+        given: 'Updated json for bookstore data'
+            def jsonData =  "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
+        when: 'update is performed for leaves'
+            objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
+        then: 'the updated data nodes are retrieved'
+            def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
+        and: 'the leaf values are updated as expected'
+            assert result.leaves['lang'] == ['English/French']
+            assert result.leaves['price'] == [100]
+        cleanup:
+            restoreBookstoreDataAnchor(2)
+    }
+
     def countDataNodesInBookstore() {
         return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
     }
index 74496d3..146ea95 100644 (file)
@@ -25,11 +25,13 @@ import java.time.OffsetDateTime
 import org.onap.cps.api.CpsQueryService
 import org.onap.cps.integration.base.FunctionalSpecBase
 import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.spi.PaginationOption
 import org.onap.cps.spi.exceptions.CpsPathException
 
 import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+import static org.onap.cps.spi.PaginationOption.NO_PAGINATION
 
 class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
 
@@ -249,7 +251,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Cps Path query across anchors with #scenario.'() {
         when: 'a query is executed to get a data nodes across anchors by the given CpsPath'
-            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, cpsPath, OMIT_DESCENDANTS)
+            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, cpsPath, OMIT_DESCENDANTS, NO_PAGINATION)
         then: 'the correct dataspace is queried'
             assert result.dataspace.toSet() == [FUNCTIONAL_TEST_DATASPACE_1].toSet()
         and: 'correct anchors are queried'
@@ -262,7 +264,6 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             scenario                                    | cpsPath                                               || expectedXpathsPerAnchor
             'container node'                            | '/bookstore'                                          || ["/bookstore"]
             'list node'                                 | '/bookstore/categories'                               || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
-            'string leaf-condition'                     | '/bookstore[@bookstore-name="Easons"]'                || ["/bookstore"]
             'integer leaf-condition'                    | '/bookstore/categories[@code="1"]/books[@price=15]'   || ["/bookstore/categories[@code='1']/books[@title='The Gruffalo']"]
             'multiple list-ancestors'                   | '//books/ancestor::categories'                        || ["/bookstore/categories[@code='1']", "/bookstore/categories[@code='2']", "/bookstore/categories[@code='3']", "/bookstore/categories[@code='4']", "/bookstore/categories[@code='5']"]
             'one ancestor with list value'              | '//books/ancestor::categories[@code="1"]'             || ["/bookstore/categories[@code='1']"]
@@ -274,7 +275,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Cps Path query across anchors with #scenario descendants.'() {
         when: 'a query is executed to get a data node by the given cps path'
-            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '/bookstore', fetchDescendantsOption)
+            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '/bookstore', fetchDescendantsOption, NO_PAGINATION)
         then: 'the correct dataspace was queried'
             assert result.dataspace.toSet() == [FUNCTIONAL_TEST_DATASPACE_1].toSet()
         and: 'correct number of datanodes are returned'
@@ -288,7 +289,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Cps Path query across anchors with ancestors and #scenario descendants.'() {
         when: 'a query is executed to get a data node by the given cps path'
-            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '//books/ancestor::bookstore', fetchDescendantsOption)
+            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '//books/ancestor::bookstore', fetchDescendantsOption, NO_PAGINATION)
         then: 'the correct dataspace was queried'
             assert result.dataspace.toSet() == [FUNCTIONAL_TEST_DATASPACE_1].toSet()
         and: 'correct number of datanodes are returned'
@@ -302,7 +303,7 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Cps Path query across anchors with syntax error throws a CPS Path Exception.'() {
         when: 'trying to execute a query with a syntax (parsing) error'
-            objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, 'cpsPath that cannot be parsed' , OMIT_DESCENDANTS)
+            objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, 'cpsPath that cannot be parsed' , OMIT_DESCENDANTS, NO_PAGINATION)
         then: 'a cps path exception is thrown'
             thrown(CpsPathException)
     }
@@ -375,4 +376,49 @@ class CpsQueryServiceIntegrationSpec extends FunctionalSpecBase {
             'text-condition'     || "/bookstore/categories[@code='1']/books/title[text()='I''m escaping']"
             'contains-condition' || "/bookstore/categories[@code='1']/books[contains(@title, 'I''m escaping')]"
     }
+
+    def 'Cps Path query across anchors using pagination option with #scenario.'() {
+        when: 'a query is executed to get a data nodes across anchors by the given CpsPath and pagination option'
+            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '/bookstore', OMIT_DESCENDANTS, new PaginationOption(pageIndex, pageSize))
+        then: 'correct bookstore names are queried'
+            def bookstoreNames = result.collect { it.getLeaves().get('bookstore-name') }
+            assert bookstoreNames.toList() == expectedBookstoreNames
+        and: 'the correct number of page size is returned'
+            assert result.size() == expectedPageSize
+        and: 'the queried nodes have expected anchor names'
+            assert result.anchorName.toSet() == expectedAnchors.toSet()
+        where: 'the following data is used'
+            scenario                       | pageIndex | pageSize || expectedPageSize || expectedAnchors                          || expectedBookstoreNames
+            '1st page with one anchor'     | 1         | 1        || 1                || [BOOKSTORE_ANCHOR_1]                     || ['Easons-1']
+            '1st page with two anchor'     | 1         | 2        || 2                || [BOOKSTORE_ANCHOR_1, BOOKSTORE_ANCHOR_2] || ['Easons-1', 'Easons-2']
+            '2nd page'                     | 2         | 1        || 1                || [BOOKSTORE_ANCHOR_2]                     || ['Easons-2']
+            'no 2nd page due to page size' | 2         | 2        || 0                || []                                       || []
+    }
+
+    def 'Cps Path query across anchors using pagination option for ancestor axis.'() {
+        when: 'a query is executed to get a data nodes across anchors by the given CpsPath and pagination option'
+            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '//books/ancestor::categories', INCLUDE_ALL_DESCENDANTS, new PaginationOption(1, 2))
+        then: 'correct category codes are queried'
+            def categoryNames = result.collect { it.getLeaves().get('name') }
+            assert categoryNames.toSet() == ['Discount books', 'Computing', 'Comedy', 'Thriller', 'Children'].toSet()
+        and: 'the queried nodes have expected anchors'
+            assert result.anchorName.toSet() == [BOOKSTORE_ANCHOR_1, BOOKSTORE_ANCHOR_2].toSet()
+    }
+
+    def 'Count number of anchors for given dataspace name and cps path'() {
+        expect: '/bookstore is present in two anchors'
+            assert objectUnderTest.countAnchorsForDataspaceAndCpsPath(FUNCTIONAL_TEST_DATASPACE_1, '/bookstore') == 2
+    }
+
+    def 'Cps Path query across anchors using no pagination'() {
+        when: 'a query is executed to get a data nodes across anchors by the given CpsPath and pagination option'
+            def result = objectUnderTest.queryDataNodesAcrossAnchors(FUNCTIONAL_TEST_DATASPACE_1, '/bookstore', OMIT_DESCENDANTS, NO_PAGINATION)
+        then: 'all bookstore names are queried'
+            def bookstoreNames = result.collect { it.getLeaves().get('bookstore-name') }
+            assert bookstoreNames.toSet() == ['Easons-1', 'Easons-2'].toSet()
+        and: 'the correct number of page size is returned'
+            assert result.size() == 2
+        and: 'the queried nodes have expected bookstore names'
+            assert result.anchorName.toSet() == [BOOKSTORE_ANCHOR_1, BOOKSTORE_ANCHOR_2].toSet()
+    }
 }