diff options
author | rajesh.kumar <rk00747546@techmahindra.com> | 2023-04-25 11:58:35 +0530 |
---|---|---|
committer | rajesh.kumar <rk00747546@techmahindra.com> | 2023-08-02 18:15:16 +0530 |
commit | f248b5d9b794d5bdff59145406e0398d6fdcafa4 (patch) | |
tree | f99670f0911b4c6e5f13ec5590fe15eb205f0dc3 /cps-rest | |
parent | 7fcffe5ae6753bfb6746d18d41ec536092603a76 (diff) |
Support pagination in query across all anchors(ep4)
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>
Diffstat (limited to 'cps-rest')
4 files changed, 111 insertions, 15 deletions
diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml index a7c13002bd..85e19aa883 100644 --- a/cps-rest/docs/openapi/components.yml +++ b/cps-rest/docs/openapi/components.yml @@ -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: diff --git a/cps-rest/docs/openapi/cpsQueryV2.yml b/cps-rest/docs/openapi/cpsQueryV2.yml index 9beb0e3330..4443fb17ec 100644 --- a/cps-rest/docs/openapi/cpsQueryV2.yml +++ b/cps-rest/docs/openapi/cpsQueryV2.yml @@ -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 diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java index 1fc13fc522..5334b48143 100644 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java @@ -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, diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy index c4bb23ce4e..8ee01c089c 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy @@ -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 + } } |