Merge "Support pagination in query across all anchors(ep4)"
authorLuke Gleeson <luke.gleeson@est.tech>
Thu, 3 Aug 2023 13:13:47 +0000 (13:13 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 3 Aug 2023 13:13:47 +0000 (13:13 +0000)
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 a721305..900f663 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 2bf29fc..fd669b7 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.DIRECT_CHILDREN_ONLY
@@ -71,7 +72,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'
@@ -116,18 +117,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(
@@ -139,6 +146,7 @@ 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
@@ -146,4 +154,39 @@ class QueryRestControllerSpec extends Specification {
             'descendants'               | 'all'                          || INCLUDE_ALL_DESCENDANTS
             'direct children'           | 'direct'                       || DIRECT_CHILDREN_ONLY
     }
+
+    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 f904e8b..56fbe8c 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,
@@ -294,8 +294,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);
@@ -304,28 +303,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,
@@ -376,6 +407,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 7d5be13..e38fc2f 100755 (executable)
@@ -79,6 +79,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 eb6c424..0e2191b 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 678aa64..475d3d2 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 << [ '/', '' ]
@@ -350,20 +350,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": {} }'
@@ -408,6 +394,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()
+    }
 }