2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2022 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021-2022 Bell Canada.
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
22 package org.onap.cps.spi.impl
24 import com.fasterxml.jackson.databind.ObjectMapper
25 import com.google.common.collect.ImmutableSet
26 import org.onap.cps.spi.CpsDataPersistenceService
27 import org.onap.cps.spi.entities.FragmentEntity
28 import org.onap.cps.spi.exceptions.AlreadyDefinedException
29 import org.onap.cps.spi.exceptions.AnchorNotFoundException
30 import org.onap.cps.spi.exceptions.CpsAdminException
31 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
32 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
33 import org.onap.cps.spi.model.DataNode
34 import org.onap.cps.spi.model.DataNodeBuilder
35 import org.onap.cps.utils.JsonObjectMapper
36 import org.springframework.beans.factory.annotation.Autowired
37 import org.springframework.test.context.jdbc.Sql
38 import javax.validation.ConstraintViolationException
40 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
41 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
43 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
46 CpsDataPersistenceService objectUnderTest
48 static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
50 static final String SET_DATA = '/data/fragment.sql'
51 static final int DATASPACE_1001_ID = 1001L
52 static final int ANCHOR_3003_ID = 3003L
53 static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
54 static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
55 static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
56 static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
57 static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
58 static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
59 static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
60 static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
61 static final long PARENT_3_FRAGMENT_ID = 4003L
63 static final DataNode newDataNode = new DataNodeBuilder().build()
64 static DataNode existingDataNode
65 static DataNode existingChildDataNode
67 def expectedLeavesByXpathMap = [
68 '/parent-100' : ['parent-leaf': 'parent-leaf value'],
69 '/parent-100/child-001' : ['first-child-leaf': 'first-child-leaf value'],
70 '/parent-100/child-002' : ['second-child-leaf': 'second-child-leaf value'],
71 '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value']
75 existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
76 existingChildDataNode = createDataNodeTree('/parent-1/child-1')
79 @Sql([CLEAR_DATA, SET_DATA])
80 def 'StoreDataNode with descendants.'() {
81 when: 'a fragment with descendants is stored'
82 def parentXpath = "/parent-new"
83 def childXpath = "/parent-new/child-new"
84 def grandChildXpath = "/parent-new/child-new/grandchild-new"
85 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
86 createDataNodeTree(parentXpath, childXpath, grandChildXpath))
87 then: 'it can be retrieved by its xpath'
88 def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
89 and: 'it contains the children'
90 parentFragment.childFragments.size() == 1
91 def childFragment = parentFragment.childFragments[0]
92 childFragment.xpath == childXpath
93 and: "and its children's children"
94 childFragment.childFragments.size() == 1
95 def grandchildFragment = childFragment.childFragments[0]
96 grandchildFragment.xpath == grandChildXpath
99 @Sql([CLEAR_DATA, SET_DATA])
100 def 'Store data node for multiple anchors using the same schema.'() {
101 def xpath = "/parent-new"
102 given: 'a fragment is stored for an anchor'
103 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, createDataNodeTree(xpath))
104 when: 'another fragment is stored for an other anchor, using the same schema set'
105 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME3, createDataNodeTree(xpath))
106 then: 'both fragments can be retrieved by their xpath'
107 def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
108 fragment1.anchor.name == ANCHOR_NAME1
109 fragment1.xpath == xpath
110 def fragment2 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME3, xpath)
111 fragment2.anchor.name == ANCHOR_NAME3
112 fragment2.xpath == xpath
115 @Sql([CLEAR_DATA, SET_DATA])
116 def 'Store datanode error scenario: #scenario.'() {
117 when: 'attempt to store a data node with #scenario'
118 objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
119 then: 'a #expectedException is thrown'
120 thrown(expectedException)
121 where: 'the following data is used'
122 scenario | dataspaceName | anchorName | dataNode || expectedException
123 'dataspace does not exist' | 'unknown' | 'not-relevant' | newDataNode || DataspaceNotFoundException
124 'schema set does not exist' | DATASPACE_NAME | 'unknown' | newDataNode || AnchorNotFoundException
125 'anchor already exists' | DATASPACE_NAME | ANCHOR_NAME1 | newDataNode || ConstraintViolationException
126 'datanode already exists' | DATASPACE_NAME | ANCHOR_NAME1 | existingDataNode || AlreadyDefinedException
129 @Sql([CLEAR_DATA, SET_DATA])
130 def 'Add a child to a Fragment that already has a child.'() {
131 given: ' a new child node'
132 def newChild = createDataNodeTree('xpath for new child')
133 when: 'the child is added to an existing parent with 1 child'
134 objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
135 then: 'the parent is now has to 2 children'
136 def expectedExistingChildPath = '/parent-1/child-1'
137 def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
138 parentFragment.childFragments.size() == 2
139 and: 'it still has the old child'
140 parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath })
141 and: 'it has the new child'
142 parentFragment.childFragments.find({ it.xpath == newChild.xpath })
145 @Sql([CLEAR_DATA, SET_DATA])
146 def 'Add child error scenario: #scenario.'() {
147 when: 'attempt to add a child data node with #scenario'
148 objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
149 then: 'a #expectedException is thrown'
150 thrown(expectedException)
151 where: 'the following data is used'
152 scenario | parentXpath | dataNode || expectedException
153 'parent does not exist' | 'unknown' | newDataNode || DataNodeNotFoundException
154 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
157 @Sql([CLEAR_DATA, SET_DATA])
158 def 'Add multiple new list elements including an element with a child datanode.'() {
159 given: 'two new child list elements for an existing parent'
160 def listElementXpaths = ['/parent-201/child-204[@key="NEW1"]', '/parent-201/child-204[@key="NEW2"]']
161 def listElements = toDataNodes(listElementXpaths)
162 and: 'a (grand)child data node for one of the new list elements'
163 def grandChild = buildDataNode('/parent-201/child-204[@key="NEW1"]/grand-child-204[@key2="NEW1-CHILD"]', [leave:'value'], [])
164 listElements[0].childDataNodes = [grandChild]
165 when: 'the new data node (list elements) are added to an existing parent node'
166 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listElements)
167 then: 'new entries are successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
168 def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
169 def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
170 assert allChildXpaths.size() == 5
171 assert allChildXpaths.containsAll(listElementXpaths)
172 and: 'the (grand)child node of the new list entry is also present'
173 def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
174 def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
175 def grandChildFragmentEntity = fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, grandChild.xpath)
176 assert grandChildFragmentEntity.isPresent()
179 @Sql([CLEAR_DATA, SET_DATA])
180 def 'Add list element error scenario: #scenario.'() {
181 given: 'list element as a collection of data nodes'
182 def listElementCollection = toDataNodes(listElementXpaths)
183 when: 'attempt to add list elements to parent node'
184 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
185 then: 'a #expectedException is thrown'
186 thrown(expectedException)
187 where: 'following parameters were used'
188 scenario | parentNodeXpath | listElementXpaths || expectedException
189 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException
190 'already existing fragment' | '/parent-201' | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
193 @Sql([CLEAR_DATA, SET_DATA])
194 def 'Get data node by xpath without descendants.'() {
195 when: 'data node is requested'
196 def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
197 inputXPath, OMIT_DESCENDANTS)
198 then: 'data node is returned with no descendants'
199 assert result.xpath == XPATH_DATA_NODE_WITH_LEAVES
200 and: 'expected leaves'
201 assert result.childDataNodes.size() == 0
202 assertLeavesMaps(result.leaves, expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
203 where: 'the following data is used'
204 scenario | inputXPath
205 'some xpath' | '/parent-100'
210 @Sql([CLEAR_DATA, SET_DATA])
211 def 'Get data node by xpath with all descendants.'() {
212 when: 'data node is requested with all descendants'
213 def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
214 inputXPath, INCLUDE_ALL_DESCENDANTS)
215 def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
216 then: 'data node is returned with all the descendants populated'
217 assert mappedResult.size() == 4
218 assert result.childDataNodes.size() == 2
219 assert mappedResult.get('/parent-100/child-001').childDataNodes.size() == 0
220 assert mappedResult.get('/parent-100/child-002').childDataNodes.size() == 1
221 and: 'extracted leaves maps are matching expected'
222 mappedResult.forEach(
223 (xPath, dataNode) -> assertLeavesMaps(dataNode.leaves, expectedLeavesByXpathMap[xPath]))
224 where: 'the following data is used'
225 scenario | inputXPath
226 'some xpath' | '/parent-100'
231 @Sql([CLEAR_DATA, SET_DATA])
232 def 'Get data node error scenario: #scenario.'() {
233 when: 'attempt to get data node with #scenario'
234 objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
235 then: 'a #expectedException is thrown'
236 thrown(expectedException)
237 where: 'the following data is used'
238 scenario | dataspaceName | anchorName | xpath || expectedException
239 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
240 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
241 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException
244 @Sql([CLEAR_DATA, SET_DATA])
245 def 'Update data node leaves.'() {
246 when: 'update is performed for leaves'
247 objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
248 "/parent-200/child-201", ['leaf-value': 'new'])
249 then: 'leaves are updated for selected data node'
250 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
251 def updatedLeaves = getLeavesMap(updatedFragment)
252 assert updatedLeaves.size() == 1
253 assert updatedLeaves.'leaf-value' == 'new'
254 and: 'existing child entry remains as is'
255 def childFragment = updatedFragment.childFragments.iterator().next()
256 def childLeaves = getLeavesMap(childFragment)
257 assert childFragment.id == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
258 assert childLeaves.'leaf-value' == 'original'
261 @Sql([CLEAR_DATA, SET_DATA])
262 def 'Update data leaves error scenario: #scenario.'() {
263 when: 'attempt to update data node for #scenario'
264 objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
265 then: 'a #expectedException is thrown'
266 thrown(expectedException)
267 where: 'the following data is used'
268 scenario | dataspaceName | anchorName | xpath || expectedException
269 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
270 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
271 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
274 @Sql([CLEAR_DATA, SET_DATA])
275 def 'Replace data node tree with descendants removal.'() {
276 given: 'data node object with leaves updated, no children'
277 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
278 when: 'replace data node tree is performed'
279 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
280 then: 'leaves have been updated for selected data node'
281 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
282 def updatedLeaves = getLeavesMap(updatedFragment)
283 assert updatedLeaves.size() == 1
284 assert updatedLeaves.'leaf-value' == 'new'
285 and: 'updated entry has no children'
286 updatedFragment.childFragments.isEmpty()
287 and: 'previously attached child entry is removed from database'
288 fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
291 @Sql([CLEAR_DATA, SET_DATA])
292 def 'Replace data node tree with descendants.'() {
293 given: 'data node object with leaves updated, having child with old content'
294 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
295 buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
297 when: 'update is performed including descendants'
298 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
299 then: 'leaves have been updated for selected data node'
300 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
301 def updatedLeaves = getLeavesMap(updatedFragment)
302 assert updatedLeaves.size() == 1
303 assert updatedLeaves.'leaf-value' == 'new'
304 and: 'existing child entry is not updated as content is same'
305 def childFragment = updatedFragment.childFragments.iterator().next()
306 childFragment.xpath == '/parent-200/child-201/grand-child'
307 def childLeaves = getLeavesMap(childFragment)
308 assert childLeaves.'leaf-value' == 'original'
311 @Sql([CLEAR_DATA, SET_DATA])
312 def 'Replace data node tree with same descendants but changed leaf value.'() {
313 given: 'data node object with leaves updated, having child with old content'
314 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
315 buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'new'], [])
317 when: 'update is performed including descendants'
318 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
319 then: 'leaves have been updated for selected data node'
320 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
321 def updatedLeaves = getLeavesMap(updatedFragment)
322 assert updatedLeaves.size() == 1
323 assert updatedLeaves.'leaf-value' == 'new'
324 and: 'existing child entry is updated with the new content'
325 def childFragment = updatedFragment.childFragments.iterator().next()
326 childFragment.xpath == '/parent-200/child-201/grand-child'
327 def childLeaves = getLeavesMap(childFragment)
328 assert childLeaves.'leaf-value' == 'new'
331 @Sql([CLEAR_DATA, SET_DATA])
332 def 'Replace data node tree with different descendants xpath'() {
333 given: 'data node object with leaves updated, having child with old content'
334 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
335 buildDataNode("/parent-200/child-201/grand-child-new", ['leaf-value': 'new'], [])
337 when: 'update is performed including descendants'
338 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
339 then: 'leaves have been updated for selected data node'
340 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
341 def updatedLeaves = getLeavesMap(updatedFragment)
342 assert updatedLeaves.size() == 1
343 assert updatedLeaves.'leaf-value' == 'new'
344 and: 'previously attached child entry is removed from database'
345 fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
346 and: 'new child entry is persisted'
347 def childFragment = updatedFragment.childFragments.iterator().next()
348 childFragment.xpath == '/parent-200/child-201/grand-child-new'
349 def childLeaves = getLeavesMap(childFragment)
350 assert childLeaves.'leaf-value' == 'new'
353 @Sql([CLEAR_DATA, SET_DATA])
354 def 'Replace data node tree error scenario: #scenario.'() {
355 given: 'data node object'
356 def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
357 when: 'attempt to update data node for #scenario'
358 objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
359 then: 'a #expectedException is thrown'
360 thrown(expectedException)
361 where: 'the following data is used'
362 scenario | dataspaceName | anchorName | xpath || expectedException
363 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
364 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
365 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
368 @Sql([CLEAR_DATA, SET_DATA])
369 def 'Update existing list with #scenario.'() {
370 given: 'a parent having a list of data nodes containing: #originalKeys (ech list element has a child too)'
371 def parentXpath = '/parent-3'
372 if (originalKeys.size() > 0) {
373 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'original value', originalKeys, true)
374 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
376 and: 'each original list element has one child'
377 def originalParentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
378 originalParentFragment.childFragments.each {assert it.childFragments.size() == 1 }
379 when: 'it is updated with #scenario'
380 def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new value', replacementKeys, false)
381 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
382 then: 'the result list ONLY contains the expected replacement elements'
383 def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
384 def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
385 def expectedListEntriesAfterUpdateAsXpaths = keysToXpaths(parentXpath, replacementKeys)
386 assert allChildXpaths.size() == replacementKeys.size()
387 assert allChildXpaths.containsAll(expectedListEntriesAfterUpdateAsXpaths)
388 and: 'all the list elements have the new values'
389 assert parentFragment.childFragments.stream().allMatch(childFragment -> childFragment.attributes.contains('new value'))
390 and: 'there are no more grandchildren as none of the replacement list entries had a child'
391 parentFragment.childFragments.each {assert it.childFragments.size() == 0 }
392 where: 'the following replacement lists are applied'
393 scenario | originalKeys | replacementKeys
394 'one existing entry only' | [] | ['NEW']
395 'multiple new entries' | [] | ['NEW1', 'NEW2']
396 'one new entry only (existing entries are deleted)' | ['A', 'B'] | ['NEW1', 'NEW2']
397 'one existing on new entry' | ['A', 'B'] | ['A', 'NEW']
398 'one existing entry only' | ['A', 'B'] | ['A']
401 @Sql([CLEAR_DATA, SET_DATA])
402 def 'Replacing existing list element with attributes and (grand)child.'() {
403 given: 'a parent with list elements A and B with attribute and grandchild tagged as "org"'
404 def parentXpath = '/parent-3'
405 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'org', ['A','B'], true)
406 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
407 when: 'A is replaced with an entry with attribute and grandchild tagged tagged as "new" (B is not in replacement list)'
408 def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new', ['A'], true)
409 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
410 then: 'The updated fragment has a child-list with ONLY element "A"'
411 def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
412 parentFragment.childFragments.size() == 1
413 def childListElementA = parentFragment.childFragments[0]
414 childListElementA.xpath == "/parent-3/child-list[@key='A']"
415 and: 'element "A" has an attribute with the "new" (tag) value'
416 childListElementA.attributes == '{"attr1": "new"}'
417 and: 'element "A" has a only one (grand)child'
418 childListElementA.childFragments.size() == 1
419 and: 'the grandchild is the new grandchild (tag)'
420 def grandChild = childListElementA.childFragments[0]
421 grandChild.xpath == "/parent-3/child-list[@key='A']/new-grand-child"
422 and: 'the grandchild has an attribute with the "new" (tag) value'
423 grandChild.attributes == '{"attr1": "new"}'
426 @Sql([CLEAR_DATA, SET_DATA])
427 def 'Replace list element for a parent (parent-1) with existing one (non-list) child'() {
428 when: 'a list element is added under the parent'
429 def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(XPATH_DATA_NODE_WITH_DESCENDANTS, 'new', ['A','B'], false)
430 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, replacementListEntriesAsDataNodes)
431 then: 'the parent will have 3 children after the replacement'
432 def parentFragment = fragmentRepository.getById(ID_DATA_NODE_WITH_DESCENDANTS)
433 parentFragment.childFragments.size() == 3
434 def xpaths = parentFragment.childFragments.collect {it.xpath}
435 and: 'one of the children is the original child fragment'
436 xpaths.contains('/parent-1/child-1')
437 and: 'it has the two new list elements'
438 xpaths.containsAll("/parent-1/child-list[@key='A']", "/parent-1/child-list[@key='B']")
441 @Sql([CLEAR_DATA, SET_DATA])
442 def 'Replace list content using unknown parent'() {
443 given: 'list element as a collection of data nodes'
444 def listElementCollection = toDataNodes(['irrelevant'])
445 when: 'attempt to replace list elements under unknown parent node'
446 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/unknown', listElementCollection)
447 then: 'a datanode not found exception is thrown'
448 thrown(DataNodeNotFoundException)
451 @Sql([CLEAR_DATA, SET_DATA])
452 def 'Replace list content with empty collection is not supported'() {
453 when: 'attempt to replace list elements with empty collection'
454 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/parent-203', [])
455 then: 'a CPS admin exception is thrown'
456 def thrown = thrown(CpsAdminException)
457 assert thrown.message == 'Invalid list replacement'
460 @Sql([CLEAR_DATA, SET_DATA])
461 def 'Delete list scenario: #scenario.'() {
462 when: 'deleting list is executed for: #scenario.'
463 objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
464 then: 'only the expected children remain'
465 def parentFragment = fragmentRepository.getById(parentFragmentId)
466 def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
467 assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
468 assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
469 where: 'following parameters were used'
470 scenario | targetXpaths | parentFragmentId || expectedRemainingChildXpaths
471 'list element with key' | '/parent-203/child-204[@key="A"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="B"]']
472 'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]' | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
473 'whole list' | '/parent-203/child-204' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
474 'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]']
477 @Sql([CLEAR_DATA, SET_DATA])
478 def 'Delete list error scenario: #scenario.'() {
479 when: 'attempting to delete scenario: #scenario.'
480 objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
481 then: 'a DataNodeNotFoundException is thrown'
482 thrown(DataNodeNotFoundException)
483 where: 'following parameters were used'
484 scenario | targetXpaths
485 'whole list, parent node does not exist' | '/unknown/some-child'
486 'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
487 'whole list does not exist' | '/parent-200/unknown'
488 'list element, list does not exist' | '/parent-200/unknown[@key="C"]'
489 'list element, element does not exist' | '/parent-203/child-204[@key="C"]'
490 'valid datanode but not a list' | '/parent-200/child-202'
493 @Sql([CLEAR_DATA, SET_DATA])
494 def 'Confirm deletion of #scenario.'() {
495 given: 'a valid data node'
498 when: 'data nodes are deleted'
499 objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
500 then: 'verify data nodes are removed'
502 dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, getDataNodesXpaths, INCLUDE_ALL_DESCENDANTS)
503 dataNodeXpath = dataNode.xpath
504 assert dataNodeXpath == expectedXpaths
505 } catch (DataNodeNotFoundException) {
506 assert dataNodeXpath == expectedXpaths
508 where: 'following parameters were used'
509 scenario | xpathForDeletion | getDataNodesXpaths || expectedXpaths
510 'child of target' | '/parent-206/child-206' | '/parent-206/child-206' || null
511 'child data node, parent still exists' | '/parent-206/child-206' | '/parent-206' || '/parent-206'
512 'list element' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="A"]' || null
513 'list element, sibling still exists' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="X"]' || '/parent-206/child-206/grand-child-206[@key="X"]'
514 'container node' | '/parent-206' | '/parent-206' || null
515 'container list node' | '/parent-206[@key="A"]' | '/parent-206[@key="B"]' || '/parent-206[@key="B"]'
516 'root node with xpath /' | '/' | '/' || null
517 'root node with xpath passed as blank' | '' | '' || null
521 @Sql([CLEAR_DATA, SET_DATA])
522 def 'Delete data node with #scenario.'() {
523 when: 'data node is deleted'
524 objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
525 then: 'a #expectedException is thrown'
526 thrown(DataNodeNotFoundException)
527 where: 'the following parameters were used'
528 scenario | datanodeXpath
529 'valid data node, non existent child node' | '/parent-203/child-non-existent'
530 'invalid list element' | '/parent-206/child-206/grand-child-206@key="A"]'
533 @Sql([CLEAR_DATA, SET_DATA])
534 def 'Delete data node for an anchor.'() {
535 given: 'a data-node exists for an anchor'
536 assert fragmentsExistInDB(DATASPACE_1001_ID, ANCHOR_3003_ID)
537 when: 'data nodes are deleted '
538 objectUnderTest.deleteDataNodes(DATASPACE_NAME, ANCHOR_NAME3)
539 then: 'all data-nodes are deleted successfully'
540 assert !fragmentsExistInDB(DATASPACE_1001_ID, ANCHOR_3003_ID)
543 def fragmentsExistInDB(dataSpaceId, anchorId) {
544 !fragmentRepository.findRootsByDataspaceAndAnchor(dataSpaceId, anchorId).isEmpty()
547 static Collection<DataNode> toDataNodes(xpaths) {
548 return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
551 static DataNode buildDataNode(xpath, leaves, childDataNodes) {
552 return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
555 static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
556 return jsonObjectMapper.convertJsonString(fragmentEntity.attributes, Map<String, Object>.class)
559 def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
560 expectedLeavesMap.forEach((key, value) -> {
561 def actualValue = actualLeavesMap[key]
562 if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
563 assert value.size() == actualValue.size()
564 assert value.containsAll(actualValue)
566 assert value == actualValue
572 def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
573 flatMap.put(dataNodeTree.xpath, dataNodeTree)
574 dataNodeTree.childDataNodes
575 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
579 def keysToXpaths(parent, Collection keys) {
580 return keys.collect { "${parent}/child-list[@key='${it}']".toString() }
583 def static createDataNodeTree(String... xpaths) {
584 def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
585 if (xpaths.length > 1) {
586 def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
587 def childDataNode = createDataNodeTree(xPathsDescendant)
588 dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
590 dataNodeBuilder.build()
593 def getFragmentByXpath(dataspaceName, anchorName, xpath) {
594 def dataspace = dataspaceRepository.getByName(dataspaceName)
595 def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
596 return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
600 def createChildListAllHavingAttributeValue(parentXpath, tag, Collection keys, boolean addGrandChild) {
601 def listElementAsDataNodes = keysToXpaths(parentXpath, keys).collect {
602 new DataNodeBuilder()
604 .withLeaves([attr1: tag])
608 listElementAsDataNodes.each {it.childDataNodes = [createGrandChild(it.xpath, tag)]}
610 return listElementAsDataNodes
613 def createGrandChild(parentXPath, tag) {
614 new DataNodeBuilder()
615 .withXpath("${parentXPath}/${tag}-grand-child")
616 .withLeaves([attr1: tag])