summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrajesh.kumar <rk00747546@techmahindra.com>2023-04-25 11:58:35 +0530
committerrajesh.kumar <rk00747546@techmahindra.com>2023-08-02 18:15:16 +0530
commitf248b5d9b794d5bdff59145406e0398d6fdcafa4 (patch)
treef99670f0911b4c6e5f13ec5590fe15eb205f0dc3
parent7fcffe5ae6753bfb6746d18d41ec536092603a76 (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>
-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
-rw-r--r--cps-ri/pom.xml2
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java72
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/utils/CpsValidatorImpl.java13
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java57
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java5
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java8
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java15
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy10
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java13
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java14
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java13
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/PaginationOption.java38
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/utils/CpsValidator.java9
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java34
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy15
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/spi/PaginationOptionSpec.groovy41
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/DataMapUtilsSpec.groovy12
-rw-r--r--docs/api/swagger/cps/openapi.yaml21
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy2
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy2
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy34
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy56
26 files changed, 527 insertions, 85 deletions
diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml
index a7c13002b..85e19aa88 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 9beb0e333..4443fb17e 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 1fc13fc52..5334b4814 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 c4bb23ce4..8ee01c089 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
+ }
}
diff --git a/cps-ri/pom.xml b/cps-ri/pom.xml
index 620739374..941d44734 100644
--- a/cps-ri/pom.xml
+++ b/cps-ri/pom.xml
@@ -33,7 +33,7 @@
<artifactId>cps-ri</artifactId>
<properties>
- <minimum-coverage>0.30</minimum-coverage>
+ <minimum-coverage>0.28</minimum-coverage>
<!-- Additional coverage is provided by integration-test module -->
</properties>
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
index f4afe3d73..19302d67a 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
@@ -23,7 +23,8 @@
package org.onap.cps.spi.impl;
-import com.google.common.base.Strings;
+import static org.onap.cps.spi.PaginationOption.NO_PAGINATION;
+
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import io.micrometer.core.annotation.Timed;
@@ -48,6 +49,7 @@ import org.onap.cps.cpspath.parser.CpsPathUtil;
import org.onap.cps.cpspath.parser.PathParsingException;
import org.onap.cps.spi.CpsDataPersistenceService;
import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.PaginationOption;
import org.onap.cps.spi.entities.AnchorEntity;
import org.onap.cps.spi.entities.DataspaceEntity;
import org.onap.cps.spi.entities.FragmentEntity;
@@ -79,8 +81,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
private final SessionManager sessionManager;
private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@.+?])?)";
- private static final String QUERY_ACROSS_ANCHORS = null;
- private static final AnchorEntity ALL_ANCHORS = null;
@Override
public void addChildDataNodes(final String dataspaceName, final String anchorName,
@@ -288,8 +288,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
final FetchDescendantsOption fetchDescendantsOption) {
final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final AnchorEntity anchorEntity = Strings.isNullOrEmpty(anchorName) ? ALL_ANCHORS
- : anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
final CpsPathQuery cpsPathQuery;
try {
cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
@@ -298,28 +297,60 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
}
Collection<FragmentEntity> fragmentEntities;
- if (anchorEntity == ALL_ANCHORS) {
- fragmentEntities = fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery);
+ fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+ if (cpsPathQuery.hasAncestorAxis()) {
+ final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
+ fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+ }
+ fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
+ fragmentEntities);
+ return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
+ }
+
+ @Override
+ @Timed(value = "cps.data.persistence.service.datanode.query.anchors",
+ description = "Time taken to query data nodes across all anchors or list of anchors")
+ public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
+ final FetchDescendantsOption fetchDescendantsOption,
+ final PaginationOption paginationOption) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final CpsPathQuery cpsPathQuery;
+ try {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
+
+ final List<Long> anchorIds;
+ if (paginationOption == NO_PAGINATION) {
+ anchorIds = Collections.EMPTY_LIST;
} else {
- fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+ anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
+ if (anchorIds.isEmpty()) {
+ return Collections.emptyList();
+ }
}
+ Collection<FragmentEntity> fragmentEntities =
+ fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
+
if (cpsPathQuery.hasAncestorAxis()) {
final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
- if (anchorEntity == ALL_ANCHORS) {
+ if (anchorIds.isEmpty()) {
fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
} else {
- fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+ fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(
+ anchorIds.toArray(new Long[0]), ancestorXpaths.toArray(new String[0]));
}
+
}
fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
fragmentEntities);
return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
}
- @Override
- public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
- final FetchDescendantsOption fetchDescendantsOption) {
- return queryDataNodes(dataspaceName, QUERY_ACROSS_ANCHORS, cpsPath, fetchDescendantsOption);
+ private List<Long> getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
+ final PaginationOption paginationOption) {
+ return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
}
private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
@@ -358,6 +389,19 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
}
+ @Override
+ public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final CpsPathQuery cpsPathQuery;
+ try {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
+ final List<Long> anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION);
+ return anchorIdList.size();
+ }
+
private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
final CpsPathQuery cpsPathQuery) {
final Set<String> ancestorXpath = new HashSet<>();
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/utils/CpsValidatorImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/utils/CpsValidatorImpl.java
index 1f61ee3c5..c727388b2 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/utils/CpsValidatorImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/utils/CpsValidatorImpl.java
@@ -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");
+ }
+ }
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java
index e371035ba..0c43d62bc 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java
@@ -21,8 +21,10 @@
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());
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
index 303af5bc4..11b2b0773 100755
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
@@ -68,6 +68,11 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
return findByDataspaceIdAndXpathIn(dataspaceEntity.getId(), xpaths.toArray(new String[0]));
}
+ @Query(value = "SELECT * FROM fragment WHERE anchor_id IN (:anchorIds)"
+ + " AND xpath = ANY (:xpaths)", nativeQuery = true)
+ List<FragmentEntity> findByAnchorIdsAndXpathIn(@Param("anchorIds") Long[] anchorIds,
+ @Param("xpaths") String[] xpaths);
+
@Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId LIMIT 1", nativeQuery = true)
Optional<FragmentEntity> findOneByAnchorId(@Param("anchorId") long anchorId);
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java
index de0c06014..9c279618b 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQuery.java
@@ -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);
+
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
index 6cc6495b8..1ba9d1a4c 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
@@ -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();
+ }
+
}
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy
index 345089c93..8d348443c 100644
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/utils/CpsValidatorSpec.groovy
@@ -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")
+ }
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
index af54077fe..edd2d2ad3 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java
@@ -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);
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
index ac018c9e8..1d7a7ceeb 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsQueryServiceImpl.java
@@ -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);
}
}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
index 9674bbe8c..1baca4ea7 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
@@ -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
index 000000000..17f025dba
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/spi/PaginationOption.java
@@ -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;
+ }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/utils/CpsValidator.java b/cps-service/src/main/java/org/onap/cps/spi/utils/CpsValidator.java
index 231094cf1..ceb75c09b 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/utils/CpsValidator.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/utils/CpsValidator.java
@@ -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);
}
diff --git a/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java b/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java
index b4d5a0944..1ac2bddf8 100644
--- a/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java
+++ b/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java
@@ -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;
}
/**
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
index 553027a4b..1ad501791 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsQueryServiceImplSpec.groovy
@@ -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
index 000000000..9d74a1722
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/spi/PaginationOptionSpec.groovy
@@ -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
+ }
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/DataMapUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/DataMapUtilsSpec.groovy
index 29085a9c7..6b9f9acb3 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/DataMapUtilsSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/DataMapUtilsSpec.groovy
@@ -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.'() {
diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml
index ace45f844..12b438a3e 100644
--- a/docs/api/swagger/cps/openapi.yaml
+++ b/docs/api/swagger/cps/openapi.yaml
@@ -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
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
index a1e03529c..4780e3642 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
@@ -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())
}
}
}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
index 89a5e4074..327a39ee4 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
@@ -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())
}
}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
index a3f14397c..ebaf9093c 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
@@ -58,7 +58,7 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
assert countDataNodesInTree(result) == expectNumberOfDataNodes
and: 'the top level data node has the expected attribute and value'
- assert result.leaves['bookstore-name'] == ['Easons']
+ assert result.leaves['bookstore-name'] == ['Easons-1']
and: 'they are from the correct dataspace'
assert result.dataspace == [FUNCTIONAL_TEST_DATASPACE_1]
and: 'they are from the correct anchor'
@@ -74,9 +74,9 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
def 'Read bookstore top-level container(s) using "root" path variations.'() {
when: 'get data nodes for bookstore container'
def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, root, OMIT_DESCENDANTS)
- then: 'the tree consist ouf of one data node'
+ then: 'the tree consist correct number of data nodes'
assert countDataNodesInTree(result) == 2
- and: 'the top level data node has the expected attribute and value'
+ and: 'the top level data node has the expected number of leaves'
assert result.leaves.size() == 2
where: 'the following variations of "root" are used'
root << [ '/', '' ]
@@ -324,20 +324,6 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
'new code, new child' | 'new' | ', "books" : [ { "title": "New Book" } ]' || 2
}
- def 'Update multiple data node leaves.'() {
- given: 'Updated json for bookstore data'
- def jsonData = "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
- when: 'update is performed for leaves'
- objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
- then: 'the updated data nodes are retrieved'
- def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
- and: 'the leaf values are updated as expected'
- assert result.leaves['lang'] == ['English/French']
- assert result.leaves['price'] == [100]
- cleanup:
- restoreBookstoreDataAnchor(2)
- }
-
def 'Update data node leaves for node that has no leaves (yet).'() {
given: 'new (webinfo) datanode without leaves'
def json = '{"webinfo": {} }'
@@ -382,6 +368,20 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
restoreBookstoreDataAnchor(1)
}
+ def 'Update multiple data node leaves.'() {
+ given: 'Updated json for bookstore data'
+ def jsonData = "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
+ when: 'update is performed for leaves'
+ objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
+ then: 'the updated data nodes are retrieved'
+ def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
+ and: 'the leaf values are updated as expected'
+ assert result.leaves['lang'] == ['English/French']
+ assert result.leaves['price'] == [100]
+ cleanup:
+ restoreBookstoreDataAnchor(2)
+ }
+
def countDataNodesInBookstore() {
return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy
index 74496d301..146ea95e8 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsQueryServiceIntegrationSpec.groovy
@@ -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()
+ }
}