From: Luke Gleeson Date: Thu, 3 Aug 2023 13:13:47 +0000 (+0000) Subject: Merge "Support pagination in query across all anchors(ep4)" X-Git-Tag: 3.3.6~26 X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=478c5dac54ed508f0ce97e18e91aac7b821d814f;hp=2a1e576e6d12456e43c47d4cd81be7f88d1a2a2b;p=cps.git Merge "Support pagination in query across all anchors(ep4)" --- diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml index a72130562..900f663bf 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 2bf29fcb1..fd669b75c 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 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 f904e8bdd..56fbe8cce 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 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 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, @@ -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 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 7d5be13a5..e38fc2f47 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, 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 eb6c4240c..0e2191b67 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 678aa6446..475d3d2fd 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 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() + } }