aboutsummaryrefslogtreecommitdiffstats
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
parent2a1e576e6d12456e43c47d4cd81be7f88d1a2a2b (diff)
parentf248b5d9b794d5bdff59145406e0398d6fdcafa4 (diff)
Merge "Support pagination in query across all anchors(ep4)"
-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 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
+ }
}
diff --git a/cps-ri/pom.xml b/cps-ri/pom.xml
index 6207393740..941d44734a 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 f904e8bddd..56fbe8cceb 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,
@@ -294,8 +294,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
final FetchDescendantsOption fetchDescendantsOption) {
final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final AnchorEntity anchorEntity = Strings.isNullOrEmpty(anchorName) ? ALL_ANCHORS
- : anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
final CpsPathQuery cpsPathQuery;
try {
cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
@@ -304,28 +303,60 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
}
Collection<FragmentEntity> fragmentEntities;
- if (anchorEntity == ALL_ANCHORS) {
- fragmentEntities = fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery);
+ fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+ if (cpsPathQuery.hasAncestorAxis()) {
+ final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
+ fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+ }
+ fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
+ fragmentEntities);
+ return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
+ }
+
+ @Override
+ @Timed(value = "cps.data.persistence.service.datanode.query.anchors",
+ description = "Time taken to query data nodes across all anchors or list of anchors")
+ public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
+ final FetchDescendantsOption fetchDescendantsOption,
+ final PaginationOption paginationOption) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final CpsPathQuery cpsPathQuery;
+ try {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
+
+ final List<Long> anchorIds;
+ if (paginationOption == NO_PAGINATION) {
+ anchorIds = Collections.EMPTY_LIST;
} else {
- fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+ anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
+ if (anchorIds.isEmpty()) {
+ return Collections.emptyList();
+ }
}
+ Collection<FragmentEntity> fragmentEntities =
+ fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
+
if (cpsPathQuery.hasAncestorAxis()) {
final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
- if (anchorEntity == ALL_ANCHORS) {
+ if (anchorIds.isEmpty()) {
fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
} else {
- fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+ fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(
+ anchorIds.toArray(new Long[0]), ancestorXpaths.toArray(new String[0]));
}
+
}
fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
fragmentEntities);
return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
}
- @Override
- public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
- final FetchDescendantsOption fetchDescendantsOption) {
- return queryDataNodes(dataspaceName, QUERY_ACROSS_ANCHORS, cpsPath, fetchDescendantsOption);
+ private List<Long> getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
+ final PaginationOption paginationOption) {
+ return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
}
private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
@@ -376,6 +407,19 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
}
+ @Override
+ public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final CpsPathQuery cpsPathQuery;
+ try {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
+ final List<Long> anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION);
+ return anchorIdList.size();
+ }
+
private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
final CpsPathQuery cpsPathQuery) {
final Set<String> ancestorXpath = new HashSet<>();
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 1f61ee3c51..c727388b25 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 e371035ba5..0c43d62bc7 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 7d5be13a5d..e38fc2f47e 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
@@ -79,6 +79,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 de0c060148..9c279618b0 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 6cc6495b8d..1ba9d1a4c5 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 345089c931..8d348443c7 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 af54077fea..edd2d2ad32 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 ac018c9e8f..1d7a7ceeb0 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 9674bbe8c1..1baca4ea7d 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 0000000000..17f025dba6
--- /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 231094cf16..ceb75c09b2 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 b4d5a09447..1ac2bddf88 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 553027a4b8..1ad5017919 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 0000000000..9d74a17222
--- /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 29085a9c7e..6b9f9acb3f 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 eb6c4240cd..0e2191b675 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 a1e03529c3..4780e36428 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 89a5e4074b..327a39ee4f 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 678aa64460..475d3d2fdb 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 << [ '/', '' ]
@@ -350,20 +350,6 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
'new code, new child' | 'new' | ', "books" : [ { "title": "New Book" } ]' || 2
}
- def 'Update multiple data node leaves.'() {
- given: 'Updated json for bookstore data'
- def jsonData = "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
- when: 'update is performed for leaves'
- objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
- then: 'the updated data nodes are retrieved'
- def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
- and: 'the leaf values are updated as expected'
- assert result.leaves['lang'] == ['English/French']
- assert result.leaves['price'] == [100]
- cleanup:
- restoreBookstoreDataAnchor(2)
- }
-
def 'Update data node leaves for node that has no leaves (yet).'() {
given: 'new (webinfo) datanode without leaves'
def json = '{"webinfo": {} }'
@@ -408,6 +394,20 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
restoreBookstoreDataAnchor(1)
}
+ def 'Update multiple data node leaves.'() {
+ given: 'Updated json for bookstore data'
+ def jsonData = "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
+ when: 'update is performed for leaves'
+ objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now)
+ then: 'the updated data nodes are retrieved'
+ def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
+ and: 'the leaf values are updated as expected'
+ assert result.leaves['lang'] == ['English/French']
+ assert result.leaves['price'] == [100]
+ cleanup:
+ restoreBookstoreDataAnchor(2)
+ }
+
def countDataNodesInBookstore() {
return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
}
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 74496d3016..146ea95e8b 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()
+ }
}