From f248b5d9b794d5bdff59145406e0398d6fdcafa4 Mon Sep 17 00:00:00 2001 From: "rajesh.kumar" Date: Tue, 25 Apr 2023 11:58:35 +0530 Subject: 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 --- .../cps/rest/controller/QueryRestController.java | 55 ++++++++++++++++++---- .../rest/controller/QueryRestControllerSpec.groovy | 51 ++++++++++++++++++-- 2 files changed, 92 insertions(+), 14 deletions(-) (limited to 'cps-rest/src') 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 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 getNodesByDataspaceAndCpsPath(final String dataspaceName, final String cpsPath, + final String fetchDescendantsOptionAsString, + final Integer pageIndex, final Integer pageSize) { final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); - final Collection dataNodes = - cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption); - final List> dataMaps = new ArrayList<>(dataNodes.size()); + final PaginationOption paginationOption = (pageIndex == null || pageSize == null) + ? PaginationOption.NO_PAGINATION : new PaginationOption(pageIndex, pageSize); + final Collection dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, + cpsPath, fetchDescendantsOption, paginationOption); + final List> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size()); String prefix = null; - for (final DataNode dataNode : dataNodes) { + final Map> anchorDataNodeListMap = prepareDataNodesForAnchor(dataNodes); + for (final Map.Entry> 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 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> prepareDataNodesForAnchor(final Collection dataNodes) { + final Map> dataNodesMapForAnchor = new HashMap<>(); + for (final DataNode dataNode : dataNodes) { + List dataNodesInAnchor = dataNodesMapForAnchor.get(dataNode.getAnchorName()); + if (dataNodesInAnchor == null) { + dataNodesInAnchor = new ArrayList<>(); + dataNodesMapForAnchor.put(dataNode.getAnchorName(), dataNodesInAnchor); } - final Map dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor(dataNode, prefix); - dataMaps.add(dataMap); + dataNodesInAnchor.add(dataNode); } - return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK); + return dataNodesMapForAnchor; } private ResponseEntity 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 + } } -- cgit 1.2.3-korg