summaryrefslogtreecommitdiffstats
path: root/cps-ri
diff options
context:
space:
mode:
authorDylanB95EST <dylan.byrne@est.tech>2021-09-29 13:44:53 +0100
committerDylanB95EST <dylan.byrne@est.tech>2021-09-29 13:44:53 +0100
commit8bf710c85d26ddd4586554868473b070ca68a70c (patch)
tree28e9dd1dce1410e46618a7e7379f948bee3084c7 /cps-ri
parentfdcb36fa9db642df3c47637d32f4e7ac5dd347e8 (diff)
CPS-594: Exception when updating list node PATCH
SQL ConstraintViolationException updating the list node element using PATCH List node API - Took advantage of replaceDataNodeTree to replace list node children recursively - Added functionality to exclude the updated dataNodes when removing the list node descendants Issue-ID: CPS-594 Signed-off-by: lukegleeson <luke.gleeson@est.tech> Signed-off-by: DylanB95EST <dylan.byrne@est.tech> Change-Id: Idbf580fab05581513b52327d6895b9e39b4cf470 Signed-off-by: DylanB95EST <dylan.byrne@est.tech>
Diffstat (limited to 'cps-ri')
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java127
-rwxr-xr-xcps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy74
-rwxr-xr-xcps-ri/src/test/resources/data/fragment.sql9
3 files changed, 167 insertions, 43 deletions
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 c57723d322..f924c70453 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
@@ -86,7 +86,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
this.dataspaceRepository = dataspaceRepository;
this.anchorRepository = anchorRepository;
this.fragmentRepository = fragmentRepository;
- this.objectMapper = new ObjectMapper();
+ objectMapper = new ObjectMapper();
}
private static final Gson GSON = new GsonBuilder().create();
@@ -97,7 +97,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath,
final DataNode dataNode) {
final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentXpath);
- final var fragmentEntity =
+ final FragmentEntity fragmentEntity =
toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode);
parentFragment.getChildFragments().add(fragmentEntity);
try {
@@ -131,9 +131,9 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
@Override
public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
- final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final var anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
- final var fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
dataNode);
try {
fragmentRepository.save(fragmentEntity);
@@ -153,7 +153,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
*/
private static FragmentEntity convertToFragmentWithAllDescendants(final DataspaceEntity dataspaceEntity,
final AnchorEntity anchorEntity, final DataNode dataNodeToBeConverted) {
- final var parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
+ final FragmentEntity parentFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNodeToBeConverted);
final Builder<FragmentEntity> childFragmentsImmutableSetBuilder = ImmutableSet.builder();
for (final DataNode childDataNode : dataNodeToBeConverted.getChildDataNodes()) {
final FragmentEntity childFragment =
@@ -189,14 +189,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
@Override
public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
final FetchDescendantsOption fetchDescendantsOption) {
- final var fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
+ final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
return toDataNode(fragmentEntity, fetchDescendantsOption);
}
private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName,
final String xpath) {
- final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final var anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
if (isRootXpath(xpath)) {
return fragmentRepository.findFirstRootByDataspaceAndAnchor(dataspaceEntity, anchorEntity);
} else {
@@ -208,8 +208,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
@Override
public List<DataNode> queryDataNodes(final String dataspaceName, final String anchorName, final String cpsPath,
final FetchDescendantsOption fetchDescendantsOption) {
- final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
- final var anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+ final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+ final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
final CpsPathQuery cpsPathQuery;
try {
cpsPathQuery = CpsPathQuery.createFrom(cpsPath);
@@ -231,11 +231,11 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities,
final CpsPathQuery cpsPathQuery) {
final Set<String> ancestorXpath = new HashSet<>();
- final var pattern =
+ final Pattern pattern =
Pattern.compile("([\\s\\S]*\\/" + Pattern.quote(cpsPathQuery.getAncestorSchemaNodeIdentifier())
+ REG_EX_FOR_OPTIONAL_LIST_INDEX + "\\/[\\s\\S]*");
for (final FragmentEntity fragmentEntity : fragmentEntities) {
- final var matcher = pattern.matcher(fragmentEntity.getXpath());
+ final Matcher matcher = pattern.matcher(fragmentEntity.getXpath());
if (matcher.matches()) {
ancestorXpath.add(matcher.group(1));
}
@@ -276,15 +276,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
@Override
public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
final Map<String, Object> leaves) {
- final var fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
+ final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath);
fragmentEntity.setAttributes(GSON.toJson(leaves));
fragmentRepository.save(fragmentEntity);
}
@Override
- public void replaceDataNodeTree(final String dataspaceName, final String anchorName,
- final DataNode dataNode) {
- final var fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
+ public void replaceDataNodeTree(final String dataspaceName, final String anchorName, final DataNode dataNode) {
+ final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath());
replaceDataNodeTree(fragmentEntity, dataNode);
try {
fragmentRepository.save(fragmentEntity);
@@ -296,51 +295,105 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
}
}
- private void replaceDataNodeTree(final FragmentEntity existingFragmentEntity, final DataNode submittedDataNode) {
+ private static void replaceDataNodeTree(final FragmentEntity existingFragmentEntity,
+ final DataNode submittedDataNode) {
existingFragmentEntity.setAttributes(GSON.toJson(submittedDataNode.getLeaves()));
final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments()
.stream().collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
- final var updatedChildFragments = new HashSet<FragmentEntity>();
+ final Collection updatedChildFragments = new HashSet<FragmentEntity>();
for (final DataNode submittedChildDataNode : submittedDataNode.getChildDataNodes()) {
final FragmentEntity childFragment;
- if (existingChildrenByXpath.containsKey(submittedChildDataNode.getXpath())) {
- childFragment = existingChildrenByXpath.get(submittedChildDataNode.getXpath());
- replaceDataNodeTree(childFragment, submittedChildDataNode);
- } else {
+ if (isNewDataNode(submittedChildDataNode, existingChildrenByXpath)) {
childFragment = convertToFragmentWithAllDescendants(
existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), submittedChildDataNode);
+ } else {
+ childFragment = existingChildrenByXpath.get(submittedChildDataNode.getXpath());
+ replaceDataNodeTree(childFragment, submittedChildDataNode);
}
updatedChildFragments.add(childFragment);
}
- existingFragmentEntity.setChildFragments(updatedChildFragments);
+ existingFragmentEntity.getChildFragments().clear();
+ existingFragmentEntity.getChildFragments().addAll(updatedChildFragments);
}
@Override
@Transactional
public void replaceListDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
- final Collection<DataNode> dataNodes) {
- final var parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
- final var firstChildNodeXpath = dataNodes.iterator().next().getXpath();
- final var listNodeXpath = firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf("["));
- removeListNodeDescendants(parentEntity, listNodeXpath);
- final Set<FragmentEntity> childFragmentEntities = dataNodes.stream().map(
- dataNode -> convertToFragmentWithAllDescendants(
- parentEntity.getDataspace(), parentEntity.getAnchor(), dataNode)
- ).collect(Collectors.toUnmodifiableSet());
- parentEntity.getChildFragments().addAll(childFragmentEntities);
+ final Collection<DataNode> replacementDataNodes) {
+ final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+ final String listNodeXpathPrefix = getListNodeXpathPrefix(replacementDataNodes);
+ final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
+ extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listNodeXpathPrefix);
+ removeExistingListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
+ final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
+ for (final DataNode replacementDataNode : replacementDataNodes) {
+ final FragmentEntity existingListNodeElementEntity =
+ existingListElementFragmentEntitiesByXPath.get(replacementDataNode.getXpath());
+ final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, replacementDataNode,
+ existingListNodeElementEntity);
+
+ updatedChildFragmentEntities.add(entityToBeAdded);
+ }
+ parentEntity.getChildFragments().addAll(updatedChildFragmentEntities);
fragmentRepository.save(parentEntity);
}
+ private static void removeExistingListElements(
+ final Collection<FragmentEntity> fragmentEntities,
+ final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
+ fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
+ }
+
+ private static String getListNodeXpathPrefix(final Collection<DataNode> replacementDataNodes) {
+ final String firstChildNodeXpath = replacementDataNodes.iterator().next().getXpath();
+ return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf("[") + 1);
+ }
+
+ private static FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
+ final DataNode replacementDataNode,
+ final FragmentEntity existingListNodeElementEntity) {
+ if (existingListNodeElementEntity == null) {
+ return convertToFragmentWithAllDescendants(
+ parentEntity.getDataspace(), parentEntity.getAnchor(), replacementDataNode);
+ }
+ if (replacementDataNode.getChildDataNodes().isEmpty()) {
+ copyAttributesFromReplacementDataNode(existingListNodeElementEntity, replacementDataNode);
+ existingListNodeElementEntity.getChildFragments().clear();
+ } else {
+ replaceDataNodeTree(existingListNodeElementEntity, replacementDataNode);
+ }
+ return existingListNodeElementEntity;
+ }
+
+ private static boolean isNewDataNode(final DataNode replacementDataNode,
+ final Map<String, FragmentEntity> existingListNodeElementsByXpath) {
+ return !existingListNodeElementsByXpath.containsKey(replacementDataNode.getXpath());
+ }
+
+ private static void copyAttributesFromReplacementDataNode(final FragmentEntity existingListNodeElementEntity,
+ final DataNode replacementDataNode) {
+ final FragmentEntity replacementFragmentEntity =
+ FragmentEntity.builder().attributes(GSON.toJson(replacementDataNode.getLeaves())).build();
+ existingListNodeElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
+ }
+
+ private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
+ final Set<FragmentEntity> childEntities, final String listNodeXpathPrefix) {
+ return childEntities.stream()
+ .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listNodeXpathPrefix))
+ .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
+ }
+
@Override
@Transactional
public void deleteListDataNodes(final String dataspaceName, final String anchorName, final String listNodeXpath) {
- final var parentNodeXpath = listNodeXpath.substring(0, listNodeXpath.lastIndexOf('/'));
- final var parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
- final var descendantNode = listNodeXpath.substring(listNodeXpath.lastIndexOf('/'));
+ final String parentNodeXpath = listNodeXpath.substring(0, listNodeXpath.lastIndexOf('/'));
+ final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+ final String descendantNode = listNodeXpath.substring(listNodeXpath.lastIndexOf('/'));
final Matcher descendantNodeHasListNodeKey = Pattern.compile(REG_EX_FOR_LIST_NODE_KEY).matcher(descendantNode);
final boolean xpathPointsToAValidChildNodeWithKey = parentEntity.getChildFragments().stream().anyMatch(
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
index e2316e8636..144b18b535 100755
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
@@ -32,10 +32,12 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException
import org.onap.cps.spi.exceptions.DataspaceNotFoundException
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
+import org.spockframework.util.CollectionUtil
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.jdbc.Sql
import javax.validation.ConstraintViolationException
+import java.util.stream.Collectors
import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
@@ -55,6 +57,8 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
+ static final long LIST_DATA_NODE_PARENT204_FRAGMENT_ID = 4219L
+ static final long LIST_DATA_NODE_PARENT205_FRAGMENT_ID = 4221L
static final long LIST_DATA_NODE_CHILD202_FRAGMENT_ID = 4204L
static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
@@ -385,19 +389,79 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
given: 'list node data fragment as a collection of data nodes'
def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
when: 'list-node elements replaced within the existing parent node'
- objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
+ objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, listNodeCollection)
then: 'child list elements are updated as expected, non-list element remains as is'
- def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
+ def parentFragment = fragmentRepository.getById(listNodeFragmentID)
def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
assert allChildXpaths.size() == expectedChildXpaths.size()
assert allChildXpaths.containsAll(expectedChildXpaths)
where: 'following parameters were used'
- scenario | listNodeXpaths || expectedChildXpaths
- 'existing list-node' | ['/parent-201/child-204[@key="B"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
- 'non-existing list-node' | ['/parent-201/child-205[@key="1"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]']
+ scenario | listNodeXpaths |parentXpath |listNodeFragmentID || expectedChildXpaths
+ 'existing list node with non existing key' | ['/parent-201/child-204[@key="B"]'] | '/parent-201' | LIST_DATA_NODE_PARENT201_FRAGMENT_ID || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
+ 'non existing list node with non existing key' | ['/parent-201/child-205[@key="1"]'] | '/parent-201' | LIST_DATA_NODE_PARENT201_FRAGMENT_ID || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]']
+ 'existing list node with 1 existing key' | ['/parent-201/child-204[@key="X"]'] | '/parent-201' | LIST_DATA_NODE_PARENT201_FRAGMENT_ID || ['/parent-201/child-203', '/parent-201/child-204[@key="X"]']
+ 'existing list-node with combined keys' | ['/parent-202/child-205[@key="A"]'] | '/parent-202' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A"]']
+ 'existing grandchild list-node' | ['/parent-200/child-202/grand-child-202[@key="E"]'] | '/parent-200/child-202' | LIST_DATA_NODE_CHILD202_FRAGMENT_ID || ['/parent-200/child-202/grand-child-202[@key="E"]']
+ 'existing list node with two list nodes' | ['/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]'] | '/parent-201' | LIST_DATA_NODE_PARENT201_FRAGMENT_ID || ['/parent-201/child-203', '/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]']
+ 'existing list node with compounded list node' | ['/parent-202/child-205[@key="A" and @key2="B"]'] | '/parent-202' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A" and @key2="B"]']
+ 'existing list node with list node with parent with key value' | ['/parent-204[@key="L"]/child-210[@key="N"]'] | '/parent-204[@key="L"]' | LIST_DATA_NODE_PARENT204_FRAGMENT_ID || ['/parent-204[@key="L"]/child-210[@key="N"]']
}
@Sql([CLEAR_DATA, SET_DATA])
+ def 'Replace list-node that has children with #scenario'() {
+ given: 'list node data fragment with child data node fragments'
+ def grandChildDataNodes = buildDataNodeCollection(grandChildXpaths)
+ def listNode = new DataNodeBuilder().withXpath(childXpath).withChildDataNodes(grandChildDataNodes).build()
+ when: 'list-node elements replaced within the existing parent node'
+ objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, [ listNode ])
+ then: 'child list elements are updated as expected with non-list elements remaining as is'
+ def parentFragment = fragmentRepository.getById(listNodeFragmentId)
+ def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+ assert allChildXpaths.size() == expectedChildXpaths.size()
+ assert allChildXpaths.containsAll(expectedChildXpaths)
+ and: 'grandchild list elements are updated as expected'
+ def allGrandChildXpaths = parentFragment.getChildFragments().collect(){
+ it.getChildFragments().collect(){
+ it.getXpath()}}
+ allGrandChildXpaths.removeIf(list -> list.isEmpty())
+ def grandChildXpathsToList = allGrandChildXpaths.stream().flatMap(List::stream).collect(Collectors.toList())
+ def expectedGrandChildXpaths = grandChildXpaths
+ assert grandChildXpathsToList.size() == expectedGrandChildXpaths.size()
+ assert grandChildXpathsToList.containsAll(expectedGrandChildXpaths)
+ where: 'the following parameters are used'
+ scenario | parentXpath | childXpath | grandChildXpaths | expectedChildXpaths | listNodeFragmentId
+ 'existing grandchild of list node' | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]'] | ['/parent-203/child-203', '/parent-203/child-204[@key="X"]'] | LIST_DATA_NODE_PARENT203_FRAGMENT_ID
+ 'existing grandchild of list node with two new nodes' | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]' , '/parent-203/child-204/grandchild[@key="3"]'] | ['/parent-203/child-203', '/parent-203/child-204[@key="X"]'] | LIST_DATA_NODE_PARENT203_FRAGMENT_ID
+ 'existing grandchild with compound list node' | '/parent-205' | '/parent-205/child-205[@key="X"]' | ['/parent-205/child-205/grand-child-206[@key="Y" and @key2="Z"]'] | ['/parent-205/child-205', '/parent-205/child-205[@key="X"]'] | LIST_DATA_NODE_PARENT205_FRAGMENT_ID
+ 'two existing list node with a new node' | '/parent-205' | '/parent-205/child-205[@key="X"]' | ['/parent-205/child-205/grandchild[@key="A"]'] | ['/parent-205/child-205', '/parent-205/child-205[@key="X"]'] | LIST_DATA_NODE_PARENT205_FRAGMENT_ID
+ }
+
+ @Sql([CLEAR_DATA, SET_DATA])
+ def 'Replace list-node content of #scenario with grandchildren.'() {
+ given: 'list node data fragment as a collection of data nodes'
+ def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
+ when: 'list-node elements replaced within the existing parent node'
+ objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, listNodeCollection)
+ then: 'child list elements are updated as expected with non-list elements remaining as is'
+ def parentFragment = fragmentRepository.getById(listNodeFragmentID)
+ def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+ assert allChildXpaths.size() == expectedChildXpaths.size()
+ assert allChildXpaths.containsAll(expectedChildXpaths)
+ and: 'grandchild list elements are updated as expected'
+ def allGrandChildXpaths = parentFragment.getChildFragments().collect {
+ it.getChildFragments().collect {
+ it.getXpath()}}
+ allGrandChildXpaths.removeIf(list -> list.isEmpty())
+ assert allGrandChildXpaths.size() == expectedGrandChildXpaths.size()
+ assert allGrandChildXpaths.containsAll(expectedGrandChildXpaths)
+ where: 'following parameters were used'
+ scenario | listNodeXpaths | parentXpath | listNodeFragmentID || expectedChildXpaths | expectedGrandChildXpaths
+ 'existing list node with existing keys' | ['/parent-203/child-204[@key="X"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]'] | []
+ 'non existing list node with existing keys' | ['/parent-203/child-204[@key="V"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="V"]'] | []
+ }
+
+
+ @Sql([CLEAR_DATA, SET_DATA])
def 'Replace list-node fragment error scenario: #scenario.'() {
given: 'list node data fragment as a collection of data nodes'
def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql
index 886e6e1751..f6e887e017 100755
--- a/cps-ri/src/test/resources/data/fragment.sql
+++ b/cps-ri/src/test/resources/data/fragment.sql
@@ -61,4 +61,11 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
(4215, 1001, 3003, 4214, '/parent-203/child-203', '{}'),
(4216, 1001, 3003, 4214, '/parent-203/child-204[@key="A"]', '{"key": "A"}'),
(4217, 1001, 3003, 4214, '/parent-203/child-204[@key="X"]', '{"key": "X"}'),
- (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="X"]/grand-child-204[@key2="Y"]', '{"key": "X", "key2": "Y"}'); \ No newline at end of file
+ (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="X"]/grand-child-204[@key2="Y"]', '{"key": "X", "key2": "Y"}'),
+ (4219, 1001, 3003, null, '/parent-204[@key="L"]', '{"key": "L"}'),
+ (4220, 1001, 3003, 4219, '/parent-204[@key="L"]/child-210[@key="M"]', '{"key": "M"}'),
+ (4221, 1001, 3003, null, '/parent-205', '{"leaf-value": "original"}'),
+ (4222, 1001, 3003, 4221, '/parent-205/child-205', '{}'),
+ (4223, 1001, 3003, 4221, '/parent-205/child-205[@key="X"]', '{"key": "X"}'),
+ (4224, 1001, 3003, 4223, '/parent-205/child-205[@key="X"]/grand-child-206[@key="Y"]', '{"key": "Y", "key2": "Z"}'),
+ (4225, 1001, 3003, 4223, '/parent-205/child-205[@key="X"]/grand-child-206[@key="Y" and @key2="Z"]', '{"key": "Y", "key2": "Z"}'); \ No newline at end of file