descendantsInQuery:
name: descendants
in: query
- description: Number of descendants to query. Allowed values are 'none', 'all', -1 (for all), 0 (for none) and any positive number.
+ description: Number of descendants to query. Allowed values are 'none', 'all', 'direct', 1 (for direct), -1 (for all), 0 (for none) and any positive number.
required: false
schema:
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:
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
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
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'
def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
.withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption -> {
- assert descendantsOption.depth == 2}}) >> [dataNode1, dataNode1]
+ assert descendantsOption.depth == expectedDepth}}) >> [dataNode1, dataNode1]
when: 'query data nodes API is invoked'
def response =
mvc.perform(
get(dataNodeEndpointV2)
.param('cps-path', cpsPath)
- .param('descendants', '2'))
+ .param('descendants', includeDescendantsOptionString))
.andReturn().response
then: 'the response contains the the datanode in json format'
assert response.status == HttpStatus.OK.value()
assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
+ where: 'the following options for include descendants are provided in the request'
+ scenario | includeDescendantsOptionString || expectedDepth
+ 'direct children' | 'direct' || 1
+ 'descendants' | '2' || 2
}
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(
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
+ '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
+ }
}
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;
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;
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,
newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
try {
fragmentRepository.save(newChildAsFragmentEntity);
- } catch (final DataIntegrityViolationException e) {
+ } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
throw AlreadyDefinedException.forDataNodes(Collections.singletonList(newChild.getXpath()),
anchorEntity.getName());
}
fragmentEntities.add(newChildAsFragmentEntity);
}
fragmentRepository.saveAll(fragmentEntities);
- } catch (final DataIntegrityViolationException e) {
+ } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
- e, fragmentEntities.size());
+ dataIntegrityViolationException, fragmentEntities.size());
retrySavingEachChildIndividually(anchorEntity, parentNodeXpath, newChildren);
}
}
for (final DataNode newChild : newChildren) {
try {
addNewChildDataNode(anchorEntity, parentNodeXpath, newChild);
- } catch (final AlreadyDefinedException e) {
+ } catch (final AlreadyDefinedException alreadyDefinedException) {
failedXpaths.add(newChild.getXpath());
}
}
try {
final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
fragmentRepository.save(fragmentEntity);
- } catch (final DataIntegrityViolationException e) {
+ } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
failedXpaths.add(dataNode.getXpath());
}
}
private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
final Collection<String> xpaths) {
- final Collection<String> nonRootXpaths = new HashSet<>(xpaths);
- final boolean haveRootXpath = nonRootXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
+ final Collection<String> normalizedXpaths = getNormalizedXpaths(xpaths);
- final Collection<String> normalizedXpaths = new HashSet<>(nonRootXpaths.size());
- for (final String xpath : nonRootXpaths) {
- try {
- normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath));
- } catch (final PathParsingException e) {
- log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
+ final boolean haveRootXpath = normalizedXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
+
+ final List<FragmentEntity> fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity,
+ normalizedXpaths);
+
+ for (final FragmentEntity fragmentEntity : fragmentEntities) {
+ normalizedXpaths.remove(fragmentEntity.getXpath());
+ }
+
+ for (final String xpath : normalizedXpaths) {
+ if (!CpsPathUtil.isPathToListElement(xpath)) {
+ fragmentEntities.addAll(fragmentRepository.findListByAnchorAndXpath(anchorEntity, xpath));
}
}
+
if (haveRootXpath) {
- normalizedXpaths.addAll(fragmentRepository.findAllXpathByAnchorAndParentIdIsNull(anchorEntity));
+ fragmentEntities.addAll(fragmentRepository.findRootsByAnchorId(anchorEntity.getId()));
}
- return fragmentRepository.findByAnchorAndXpathIn(anchorEntity, normalizedXpaths);
+ return fragmentEntities;
}
private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
final FetchDescendantsOption fetchDescendantsOption) {
final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final AnchorEntity anchorEntity = Strings.isNullOrEmpty(anchorName) ? ALL_ANCHORS
- : anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
final CpsPathQuery cpsPathQuery;
try {
cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
- } catch (final PathParsingException e) {
- throw new CpsPathException(e.getMessage());
+ } catch (final PathParsingException pathParsingException) {
+ throw new CpsPathException(pathParsingException.getMessage());
}
Collection<FragmentEntity> fragmentEntities;
- if (anchorEntity == ALL_ANCHORS) {
- fragmentEntities = fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery);
+ fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+ if (cpsPathQuery.hasAncestorAxis()) {
+ final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
+ fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+ }
+ fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
+ fragmentEntities);
+ return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
+ }
+
+ @Override
+ @Timed(value = "cps.data.persistence.service.datanode.query.anchors",
+ description = "Time taken to query data nodes across all anchors or list of anchors")
+ public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
+ final FetchDescendantsOption fetchDescendantsOption,
+ final PaginationOption paginationOption) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final CpsPathQuery cpsPathQuery;
+ try {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
+
+ final List<Long> anchorIds;
+ if (paginationOption == NO_PAGINATION) {
+ anchorIds = Collections.EMPTY_LIST;
} else {
- fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity, cpsPathQuery);
+ anchorIds = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
+ if (anchorIds.isEmpty()) {
+ return Collections.emptyList();
+ }
}
+ Collection<FragmentEntity> fragmentEntities =
+ fragmentRepository.findByDataspaceAndCpsPath(dataspaceEntity, cpsPathQuery, anchorIds);
+
if (cpsPathQuery.hasAncestorAxis()) {
final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery);
- if (anchorEntity == ALL_ANCHORS) {
+ if (anchorIds.isEmpty()) {
fragmentEntities = fragmentRepository.findByDataspaceAndXpathIn(dataspaceEntity, ancestorXpaths);
} else {
- fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
+ fragmentEntities = fragmentRepository.findByAnchorIdsAndXpathIn(
+ anchorIds.toArray(new Long[0]), ancestorXpaths.toArray(new String[0]));
}
+
}
fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
fragmentEntities);
return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
}
- @Override
- public List<DataNode> queryDataNodesAcrossAnchors(final String dataspaceName, final String cpsPath,
- final FetchDescendantsOption fetchDescendantsOption) {
- return queryDataNodes(dataspaceName, QUERY_ACROSS_ANCHORS, cpsPath, fetchDescendantsOption);
+ private List<Long> getAnchorIdsForPagination(final DataspaceEntity dataspaceEntity, final CpsPathQuery cpsPathQuery,
+ final PaginationOption paginationOption) {
+ return fragmentRepository.findAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, paginationOption);
}
private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
}
try {
return CpsPathUtil.getNormalizedXpath(xpathSource);
- } catch (final PathParsingException e) {
- throw new CpsPathException(e.getMessage());
+ } catch (final PathParsingException pathParsingException) {
+ throw new CpsPathException(pathParsingException.getMessage());
+ }
+ }
+
+ private static Collection<String> getNormalizedXpaths(final Collection<String> xpaths) {
+ final Collection<String> normalizedXpaths = new HashSet<>(xpaths.size());
+ for (final String xpath : xpaths) {
+ try {
+ normalizedXpaths.add(getNormalizedXpath(xpath));
+ } catch (final CpsPathException cpsPathException) {
+ log.warn("Error parsing xpath \"{}\": {}", xpath, cpsPathException.getMessage());
+ }
}
+ return normalizedXpaths;
}
@Override
sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds);
}
+ @Override
+ public Integer countAnchorsForDataspaceAndCpsPath(final String dataspaceName, final String cpsPath) {
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final CpsPathQuery cpsPathQuery;
+ try {
+ cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
+ } catch (final PathParsingException e) {
+ throw new CpsPathException(e.getMessage());
+ }
+ final List<Long> anchorIdList = getAnchorIdsForPagination(dataspaceEntity, cpsPathQuery, NO_PAGINATION);
+ return anchorIdList.size();
+ }
+
private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities,
final CpsPathQuery cpsPathQuery) {
final Set<String> ancestorXpath = new HashSet<>();
for (final FragmentEntity dataNodeFragment : fragmentEntities) {
try {
fragmentRepository.save(dataNodeFragment);
- } catch (final StaleStateException e) {
+ } catch (final StaleStateException staleStateException) {
failedXpaths.add(dataNodeFragment.getXpath());
}
}
final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
- final Collection<String> deleteChecklist = new HashSet<>(xpathsToDelete.size());
- for (final String xpath : xpathsToDelete) {
- try {
- deleteChecklist.add(CpsPathUtil.getNormalizedXpath(xpath));
- } catch (final PathParsingException e) {
- log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
- }
- }
-
+ final Collection<String> deleteChecklist = getNormalizedXpaths(xpathsToDelete);
final Collection<String> xpathsToExistingContainers =
fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist);
if (onlySupportListDeletion) {
return findByAnchorIdAndXpathIn(anchorEntity.getId(), xpaths.toArray(new String[0]));\r
}\r
\r
+ @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId \n"\r
+ + "AND xpath LIKE :escapedXpath||'[@%]' AND xpath NOT LIKE :escapedXpath||'[@%]/%[@%]'",\r
+ nativeQuery = true)\r
+ List<FragmentEntity> findListByAnchorIdAndEscapedXpath(@Param("anchorId") long anchorId,\r
+ @Param("escapedXpath") String escapedXpath);\r
+\r
+ default List<FragmentEntity> findListByAnchorAndXpath(final AnchorEntity anchorEntity, final String xpath) {\r
+ final String escapedXpath = EscapeUtils.escapeForSqlLike(xpath);\r
+ return findListByAnchorIdAndEscapedXpath(anchorEntity.getId(), escapedXpath);\r
+ }\r
+\r
@Query(value = "SELECT fragment.* FROM fragment JOIN anchor ON anchor.id = fragment.anchor_id "\r
+ "WHERE dataspace_id = :dataspaceId AND xpath = ANY (:xpaths)", nativeQuery = true)\r
List<FragmentEntity> findByDataspaceIdAndXpathIn(@Param("dataspaceId") int dataspaceId,\r
return findByDataspaceIdAndXpathIn(dataspaceEntity.getId(), xpaths.toArray(new String[0]));\r
}\r
\r
+ @Query(value = "SELECT * FROM fragment WHERE anchor_id IN (:anchorIds)"\r
+ + " AND xpath = ANY (:xpaths)", nativeQuery = true)\r
+ List<FragmentEntity> findByAnchorIdsAndXpathIn(@Param("anchorIds") Long[] anchorIds,\r
+ @Param("xpaths") String[] xpaths);\r
+ \r
@Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId LIMIT 1", nativeQuery = true)\r
Optional<FragmentEntity> findOneByAnchorId(@Param("anchorId") long anchorId);\r
\r
\r
boolean existsByAnchorAndXpathStartsWith(AnchorEntity anchorEntity, String xpath);\r
\r
- @Query("SELECT xpath FROM FragmentEntity WHERE anchor = :anchor AND parentId IS NULL")\r
- List<String> findAllXpathByAnchorAndParentIdIsNull(@Param("anchor") AnchorEntity anchorEntity);\r
+ @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId AND parent_id IS NULL", nativeQuery = true)\r
+ List<FragmentEntity> findRootsByAnchorId(@Param("anchorId") long anchorId);\r
\r
}\r
schema:
default: /
type: string
- - description: "Number of descendants to query. Allowed values are 'none', 'all',\
- \ -1 (for all), 0 (for none) and any positive number."
+ - description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+ \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
in: query
name: descendants
required: false
schema:
default: /
type: string
- - description: "Number of descendants to query. Allowed values are 'none', 'all',\
- \ -1 (for all), 0 (for none) and any positive number."
+ - description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+ \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
in: query
name: descendants
required: false
schema:
default: /
type: string
- - description: "Number of descendants to query. Allowed values are 'none', 'all',\
- \ -1 (for all), 0 (for none) and any positive number."
+ - description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+ \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
in: query
name: descendants
required: false
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:
schema:
type: object
description: OK
+ headers:
+ total-pages:
+ schema:
+ type: integer
+ description: Total number of pages for given page size
"400":
content:
application/json:
example: false
type: boolean
descendantsInQuery:
- description: "Number of descendants to query. Allowed values are 'none', 'all',\
- \ -1 (for all), 0 (for none) and any positive number."
+ description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+ \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
in: query
name: descendants
required: false
securitySchemes:
basicAuth:
scheme: basic
- type: http
+ type: http
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'
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 << [ '/', '' ]
restoreBookstoreDataAnchor(1)
}
+ def 'Get whole list data' () {
+ def xpathForWholeList = "/bookstore/categories"
+ when: 'get data nodes for bookstore container'
+ def dataNodes = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpathForWholeList, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+ then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
+ assert dataNodes.size() == 5
+ and: 'each datanode contains the list node xpath partially in its xpath'
+ dataNodes.each {dataNode ->
+ assert dataNode.xpath.contains(xpathForWholeList)
+ }
+ }
+
+ def 'Read (multiple) data nodes with #scenario' () {
+ when: 'attempt to get data nodes using multiple valid xpaths'
+ def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpath, OMIT_DESCENDANTS)
+ then: 'expected numer of data nodes are returned'
+ dataNodes.size() == expectedNumberOfDataNodes
+ where: 'the following data was used'
+ scenario | xpath | expectedNumberOfDataNodes
+ 'container-node xpath' | ['/bookstore'] | 1
+ 'list-item' | ['/bookstore/categories[@code=1]'] | 1
+ 'parent-list xpath' | ['/bookstore/categories'] | 5
+ 'child-list xpath' | ['/bookstore/categories[@code=1]/books'] | 2
+ 'both parent and child list xpath' | ['/bookstore/categories', '/bookstore/categories[@code=1]/books'] | 7
+ }
+
def 'Add and Delete a (container) data node using #scenario.'() {
- when: 'the new datanode is saved'
- objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
- then: 'it can be retrieved by its normalized xpath'
- def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
- assert result.size() == 1
- assert result[0].xpath == normalizedXpathToNode
- and: 'there is now one extra datanode'
- assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
- when: 'the new datanode is deleted'
- objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
- then: 'the original number of data nodes is restored'
- assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
- where:
- scenario | parentXpath | json || normalizedXpathToNode
- 'normalized parent xpath' | '/bookstore' | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
- 'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}' || "/bookstore/categories[@code='1']/books[@title='new']"
+ when: 'the new datanode is saved'
+ objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
+ then: 'it can be retrieved by its normalized xpath'
+ def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
+ assert result.size() == 1
+ assert result[0].xpath == normalizedXpathToNode
+ and: 'there is now one extra datanode'
+ assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
+ when: 'the new datanode is deleted'
+ objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
+ then: 'the original number of data nodes is restored'
+ assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
+ where:
+ scenario | parentXpath | json || normalizedXpathToNode
+ 'normalized parent xpath' | '/bookstore' | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
+ 'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}' || "/bookstore/categories[@code='1']/books[@title='new']"
}
def 'Attempt to create a top level data node using root.'() {
def 'Add and Delete top-level list (element) data nodes with root node.'() {
given: 'a new (multiple-data-tree:invoice) datanodes'
- def json = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Mango","price": "150","stock": true}]}'
+ def json = '{"bookstore-address":[{"bookstore-name":"Scholastic","address":"Bangalore,India","postal-code":"560043"}]}'
when: 'the new list elements are saved'
objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/', json, now)
then: 'they can be retrieved by their xpaths'
- objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', INCLUDE_ALL_DESCENDANTS)
+ objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', INCLUDE_ALL_DESCENDANTS)
and: 'there is one extra datanode'
assert originalCountBookstoreTopLevelListNodes + 1 == countTopLevelListDataNodesInBookstore()
when: 'the new elements are deleted'
- objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', now)
+ objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', now)
then: 'the original number of datanodes is restored'
assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore()
}
'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": {} }'
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))
}