From f248b5d9b794d5bdff59145406e0398d6fdcafa4 Mon Sep 17 00:00:00 2001 From: "rajesh.kumar" Date: Tue, 25 Apr 2023 11:58:35 +0530 Subject: [PATCH] Support pagination in query across all anchors(ep4) Add pagination query parameters in query across all anchors API pagination parameters (pageIndex and pageSize) are optional default is to query all fragments each pageSize represents number of records(number of anchors) TotalRecords is returned in response header to find number of pages. - If pagination option is provided in request then query number of anchors equal to pageSize. pageIndex is used for setting offset. - return number of records(one anchor per record) as per pagesize and pageSize Issue-ID: CPS-1605 Change-ID: I73f97f986a817d423f93a8d922dcd9647b2504bc Signed-off-by: rajesh.kumar --- cps-rest/docs/openapi/components.yml | 16 +++++ cps-rest/docs/openapi/cpsQueryV2.yml | 4 +- .../cps/rest/controller/QueryRestController.java | 55 ++++++++++++++--- .../rest/controller/QueryRestControllerSpec.groovy | 51 +++++++++++++-- cps-ri/pom.xml | 2 +- .../spi/impl/CpsDataPersistenceServiceImpl.java | 72 +++++++++++++++++----- .../onap/cps/spi/impl/utils/CpsValidatorImpl.java | 13 ++++ .../cps/spi/repository/FragmentQueryBuilder.java | 57 +++++++++++++++-- .../cps/spi/repository/FragmentRepository.java | 5 ++ .../repository/FragmentRepositoryCpsPathQuery.java | 8 ++- .../FragmentRepositoryCpsPathQueryImpl.java | 15 ++++- .../cps/spi/impl/utils/CpsValidatorSpec.groovy | 10 +++ .../java/org/onap/cps/api/CpsQueryService.java | 13 +++- .../org/onap/cps/api/impl/CpsQueryServiceImpl.java | 14 ++++- .../onap/cps/spi/CpsDataPersistenceService.java | 13 +++- .../java/org/onap/cps/spi/PaginationOption.java | 38 ++++++++++++ .../java/org/onap/cps/spi/utils/CpsValidator.java | 9 +++ .../main/java/org/onap/cps/utils/DataMapUtils.java | 34 +++++++--- .../cps/api/impl/CpsQueryServiceImplSpec.groovy | 15 ++++- .../org/onap/cps/spi/PaginationOptionSpec.groovy | 41 ++++++++++++ .../org/onap/cps/utils/DataMapUtilsSpec.groovy | 12 ++-- docs/api/swagger/cps/openapi.yaml | 21 ++++++- .../integration/base/CpsIntegrationSpecBase.groovy | 2 +- .../cps/integration/base/FunctionalSpecBase.groovy | 2 +- .../CpsDataServiceIntegrationSpec.groovy | 34 +++++----- .../CpsQueryServiceIntegrationSpec.groovy | 56 +++++++++++++++-- 26 files changed, 527 insertions(+), 85 deletions(-) create mode 100644 cps-service/src/main/java/org/onap/cps/spi/PaginationOption.java create mode 100644 cps-service/src/test/groovy/org/onap/cps/spi/PaginationOptionSpec.groovy 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 getNodesByDataspaceAndCpsPath(final String dataspaceName, - final String cpsPath, final String fetchDescendantsOptionAsString) { + @Timed(value = "cps.data.controller.datanode.query.across.anchors", + description = "Time taken to query data nodes across anchors") + public ResponseEntity getNodesByDataspaceAndCpsPath(final String dataspaceName, final String cpsPath, + final String fetchDescendantsOptionAsString, + final Integer pageIndex, final Integer pageSize) { final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString); - final Collection dataNodes = - cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, cpsPath, fetchDescendantsOption); - final List> dataMaps = new ArrayList<>(dataNodes.size()); + final PaginationOption paginationOption = (pageIndex == null || pageSize == null) + ? PaginationOption.NO_PAGINATION : new PaginationOption(pageIndex, pageSize); + final Collection dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, + cpsPath, fetchDescendantsOption, paginationOption); + final List> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size()); String prefix = null; - for (final DataNode dataNode : dataNodes) { + final Map> anchorDataNodeListMap = prepareDataNodesForAnchor(dataNodes); + for (final Map.Entry> anchorDataNodesMapEntry : anchorDataNodeListMap.entrySet()) { if (prefix == null) { - prefix = prefixResolver.getPrefix(dataspaceName, dataNode.getAnchorName(), dataNode.getXpath()); + prefix = prefixResolver.getPrefix(dataspaceName, anchorDataNodesMapEntry.getKey(), + anchorDataNodesMapEntry.getValue().get(0).getXpath()); + } + final Map dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor( + anchorDataNodesMapEntry.getValue(), anchorDataNodesMapEntry.getKey(), prefix); + dataNodesAsListOfMaps.add(dataMap); + } + final Integer totalPages = getTotalPages(dataspaceName, cpsPath, paginationOption); + return ResponseEntity.ok().header("total-pages", + totalPages.toString()).body(jsonObjectMapper.asJsonString(dataNodesAsListOfMaps)); + } + + private Integer getTotalPages(final String dataspaceName, final String cpsPath, + final PaginationOption paginationOption) { + if (paginationOption == PaginationOption.NO_PAGINATION) { + return 1; + } + final int totalAnchors = cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath); + return totalAnchors <= paginationOption.getPageSize() ? 1 + : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize()); + } + + private Map> prepareDataNodesForAnchor(final Collection dataNodes) { + final Map> dataNodesMapForAnchor = new HashMap<>(); + for (final DataNode dataNode : dataNodes) { + List dataNodesInAnchor = dataNodesMapForAnchor.get(dataNode.getAnchorName()); + if (dataNodesInAnchor == null) { + dataNodesInAnchor = new ArrayList<>(); + dataNodesMapForAnchor.put(dataNode.getAnchorName(), dataNodesInAnchor); } - final Map dataMap = DataMapUtils.toDataMapWithIdentifierAndAnchor(dataNode, prefix); - dataMaps.add(dataMap); + dataNodesInAnchor.add(dataNode); } - return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK); + return dataNodesMapForAnchor; } private ResponseEntity executeNodesByDataspaceQueryAndCreateResponse(final String dataspaceName, diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy index 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 @@ cps-ri - 0.30 + 0.28 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 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 fragmentEntities; - if (anchorEntity == ALL_ANCHORS) { - fragmentEntities = fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery); + fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery); + if (cpsPathQuery.hasAncestorAxis()) { + final Collection 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 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 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 fragmentEntities = + fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds); + if (cpsPathQuery.hasAncestorAxis()) { final Collection 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 queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath, - final FetchDescendantsOption fetchDescendantsOption) { - return queryDataNodes(dataspaceName, QUERY_ACROSS_ANCHORS, cpsPath, fetchDescendantsOption); + private List getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery, + final PaginationOption paginationOption) { + return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption); } private List 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 anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION); + return anchorIdList.size(); + } + private static Set processAncestorXpath(final Collection fragmentEntities, final CpsPathQuery cpsPathQuery) { final Set 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 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 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 anchorIdsForPagination) { final StringBuilder sqlStringBuilder = new StringBuilder(); final Map 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 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, 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 findByAnchorIdsAndXpathIn(@Param("anchorIds") Long[] anchorIds, + @Param("xpaths") String[] xpaths); + @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId LIMIT 1", nativeQuery = true) Optional 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 findByAnchorAndCpsPath(AnchorEntity anchorEntity, CpsPathQuery cpsPathQuery); - List findByDataspaceAndCpsPath(DataspaceEntity dataspaceEntity, CpsPathQuery cpsPathQuery); + List findByDataspaceAndCpsPath(DataspaceEntity dataspaceEntity, + CpsPathQuery cpsPathQuery, List anchorIds); + + List 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 findByDataspaceAndCpsPath(final DataspaceEntity dataspaceEntity, - final CpsPathQuery cpsPathQuery) { - final Query query = fragmentQueryBuilder.getQueryForDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery); + final CpsPathQuery cpsPathQuery, final List anchorIds) { + final Query query = fragmentQueryBuilder.getQueryForDataspaceAndCpsPath( + dataspaceEntity, cpsPathQuery, anchorIds); final List fragmentEntities = query.getResultList(); log.debug("Fetched {} fragment entities by cps path across all anchors.", fragmentEntities.size()); return fragmentEntities; } + @Override + @Transactional + public List 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 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 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 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 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 toDataMapWithIdentifierAndAnchor(final DataNode dataNode, final String prefix) { - final String nodeIdentifierWithPrefix = getNodeIdentifierWithPrefix(dataNode.getXpath(), prefix); - final Map dataMap = ImmutableMap.builder() - .put(nodeIdentifierWithPrefix, toDataMap(dataNode)).build(); - return ImmutableMap.builder().put("anchorName", dataNode.getAnchorName()) - .put("dataNode", dataMap).build(); + public static Map toDataMapWithIdentifierAndAnchor(final List dataNodeList, + final String anchorName, final String prefix) { + final List> dataMaps = toDataNodesWithIdentifier(dataNodeList, prefix); + return ImmutableMap.builder().put("anchorName", anchorName) + .put("dataNodes", dataMaps).build(); + } + + private static List> toDataNodesWithIdentifier(final List dataNodeList, + final String prefix) { + final List> dataMaps = new ArrayList<>(dataNodeList.size()); + for (final DataNode dataNode: dataNodeList) { + final String nodeIdentifierWithPrefix = getNodeIdentifierWithPrefix(dataNode.getXpath(), prefix); + final Map dataMap = ImmutableMap.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() + } } -- 2.16.6