summaryrefslogtreecommitdiffstats
path: root/cps-rest
diff options
context:
space:
mode:
authorLuke Gleeson <luke.gleeson@est.tech>2023-08-03 13:13:47 +0000
committerGerrit Code Review <gerrit@onap.org>2023-08-03 13:13:47 +0000
commit478c5dac54ed508f0ce97e18e91aac7b821d814f (patch)
tree6b8d3d25972ebacf2d429ede2fff601cce15bc2e /cps-rest
parent2a1e576e6d12456e43c47d4cd81be7f88d1a2a2b (diff)
parentf248b5d9b794d5bdff59145406e0398d6fdcafa4 (diff)
Merge "Support pagination in query across all anchors(ep4)"
Diffstat (limited to 'cps-rest')
-rw-r--r--cps-rest/docs/openapi/components.yml16
-rw-r--r--cps-rest/docs/openapi/cpsQueryV2.yml4
-rw-r--r--cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java55
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy51
4 files changed, 111 insertions, 15 deletions
diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml
index a72130562e..900f663bfc 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 2bf29fcb14..fd669b75c3 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.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
+ }
}