2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2022 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021 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
39 import javax.validation.ConstraintViolationException
41 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
42 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
44 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
47 CpsDataPersistenceService objectUnderTest
49 static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
51 static final String SET_DATA = '/data/fragment.sql'
52 static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
53 static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
54 static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
55 static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
56 static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
57 static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
58 static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
59 static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
60 static final long PARENT_3_FRAGMENT_ID = 4003L
62 static final DataNode newDataNode = new DataNodeBuilder().build()
63 static DataNode existingDataNode
64 static DataNode existingChildDataNode
66 def expectedLeavesByXpathMap = [
67 '/parent-100' : ['parent-leaf': 'parent-leaf value'],
68 '/parent-100/child-001' : ['first-child-leaf': 'first-child-leaf value'],
69 '/parent-100/child-002' : ['second-child-leaf': 'second-child-leaf value'],
70 '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value']
74 existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
75 existingChildDataNode = createDataNodeTree('/parent-1/child-1')
78 @Sql([CLEAR_DATA, SET_DATA])
79 def 'StoreDataNode with descendants.'() {
80 when: 'a fragment with descendants is stored'
81 def parentXpath = "/parent-new"
82 def childXpath = "/parent-new/child-new"
83 def grandChildXpath = "/parent-new/child-new/grandchild-new"
84 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
85 createDataNodeTree(parentXpath, childXpath, grandChildXpath))
86 then: 'it can be retrieved by its xpath'
87 def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
88 and: 'it contains the children'
89 parentFragment.childFragments.size() == 1
90 def childFragment = parentFragment.childFragments[0]
91 childFragment.xpath == childXpath
92 and: "and its children's children"
93 childFragment.childFragments.size() == 1
94 def grandchildFragment = childFragment.childFragments[0]
95 grandchildFragment.xpath == grandChildXpath
98 @Sql([CLEAR_DATA, SET_DATA])
99 def 'Store data node for multiple anchors using the same schema.'() {
100 def xpath = "/parent-new"
101 given: 'a fragment is stored for an anchor'
102 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, createDataNodeTree(xpath))
103 when: 'another fragment is stored for an other anchor, using the same schema set'
104 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME3, createDataNodeTree(xpath))
105 then: 'both fragments can be retrieved by their xpath'
106 def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
107 fragment1.anchor.name == ANCHOR_NAME1
108 fragment1.xpath == xpath
109 def fragment2 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME3, xpath)
110 fragment2.anchor.name == ANCHOR_NAME3
111 fragment2.xpath == xpath
114 @Sql([CLEAR_DATA, SET_DATA])
115 def 'Store datanode error scenario: #scenario.'() {
116 when: 'attempt to store a data node with #scenario'
117 objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
118 then: 'a #expectedException is thrown'
119 thrown(expectedException)
120 where: 'the following data is used'
121 scenario | dataspaceName | anchorName | dataNode || expectedException
122 'dataspace does not exist' | 'unknown' | 'not-relevant' | newDataNode || DataspaceNotFoundException
123 'schema set does not exist' | DATASPACE_NAME | 'unknown' | newDataNode || AnchorNotFoundException
124 'anchor already exists' | DATASPACE_NAME | ANCHOR_NAME1 | newDataNode || ConstraintViolationException
125 'datanode already exists' | DATASPACE_NAME | ANCHOR_NAME1 | existingDataNode || AlreadyDefinedException
128 @Sql([CLEAR_DATA, SET_DATA])
129 def 'Add a child to a Fragment that already has a child.'() {
130 given: ' a new child node'
131 def newChild = createDataNodeTree('xpath for new child')
132 when: 'the child is added to an existing parent with 1 child'
133 objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
134 then: 'the parent is now has to 2 children'
135 def expectedExistingChildPath = '/parent-1/child-1'
136 def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
137 parentFragment.childFragments.size() == 2
138 and: 'it still has the old child'
139 parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath })
140 and: 'it has the new child'
141 parentFragment.childFragments.find({ it.xpath == newChild.xpath })
144 @Sql([CLEAR_DATA, SET_DATA])
145 def 'Add child error scenario: #scenario.'() {
146 when: 'attempt to add a child data node with #scenario'
147 objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
148 then: 'a #expectedException is thrown'
149 thrown(expectedException)
150 where: 'the following data is used'
151 scenario | parentXpath | dataNode || expectedException
152 'parent does not exist' | 'unknown' | newDataNode || DataNodeNotFoundException
153 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
156 @Sql([CLEAR_DATA, SET_DATA])
157 def 'Add multiple list elements including an element with a child datanode.'() {
158 given: 'two new data nodes for an existing list'
159 def listElementXpaths = ['/parent-201/child-204[@key="NEW1"]', '/parent-201/child-204[@key="NEW2"]']
160 def listElements = toDataNodes(listElementXpaths)
161 and: 'a child node for one of the new data nodes'
162 def childDataNode = buildDataNode('/parent-201/child-204[@key="NEW1"]/grand-child-204[@key2="NEW1-CHILD"]', [leave:'value'], [])
163 listElements[0].childDataNodes = [childDataNode]
164 when: 'the data nodes (list elements) are added to existing parent node'
165 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listElements)
166 then: 'new entries successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
167 def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
168 def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
169 assert allChildXpaths.size() == 5
170 assert allChildXpaths.containsAll(listElementXpaths)
171 and: 'the child node of the new list entry is also present'
172 def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
173 def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
174 def listElementChild = fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, childDataNode.xpath)
175 assert listElementChild.isPresent()
178 @Sql([CLEAR_DATA, SET_DATA])
179 def 'Add list element error scenario: #scenario.'() {
180 given: 'list element as a collection of data nodes'
181 def listElementCollection = toDataNodes(listElementXpaths)
182 when: 'attempt to add list elements to parent node'
183 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
184 then: 'a #expectedException is thrown'
185 thrown(expectedException)
186 where: 'following parameters were used'
187 scenario | parentNodeXpath | listElementXpaths || expectedException
188 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException
189 'already existing fragment' | '/parent-201' | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
192 @Sql([CLEAR_DATA, SET_DATA])
193 def 'Get data node by xpath without descendants.'() {
194 when: 'data node is requested'
195 def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
196 inputXPath, OMIT_DESCENDANTS)
197 then: 'data node is returned with no descendants'
198 assert result.xpath == XPATH_DATA_NODE_WITH_LEAVES
199 and: 'expected leaves'
200 assert result.childDataNodes.size() == 0
201 assertLeavesMaps(result.leaves, expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
202 where: 'the following data is used'
203 scenario | inputXPath
204 'some xpath' | '/parent-100'
209 @Sql([CLEAR_DATA, SET_DATA])
210 def 'Get data node by xpath with all descendants.'() {
211 when: 'data node is requested with all descendants'
212 def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
213 inputXPath, INCLUDE_ALL_DESCENDANTS)
214 def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
215 then: 'data node is returned with all the descendants populated'
216 assert mappedResult.size() == 4
217 assert result.childDataNodes.size() == 2
218 assert mappedResult.get('/parent-100/child-001').childDataNodes.size() == 0
219 assert mappedResult.get('/parent-100/child-002').childDataNodes.size() == 1
220 and: 'extracted leaves maps are matching expected'
221 mappedResult.forEach(
222 (xPath, dataNode) -> assertLeavesMaps(dataNode.leaves, expectedLeavesByXpathMap[xPath]))
223 where: 'the following data is used'
224 scenario | inputXPath
225 'some xpath' | '/parent-100'
230 @Sql([CLEAR_DATA, SET_DATA])
231 def 'Get data node error scenario: #scenario.'() {
232 when: 'attempt to get data node with #scenario'
233 objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
234 then: 'a #expectedException is thrown'
235 thrown(expectedException)
236 where: 'the following data is used'
237 scenario | dataspaceName | anchorName | xpath || expectedException
238 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
239 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
240 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException
243 @Sql([CLEAR_DATA, SET_DATA])
244 def 'Update data node leaves.'() {
245 when: 'update is performed for leaves'
246 objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
247 "/parent-200/child-201", ['leaf-value': 'new'])
248 then: 'leaves are updated for selected data node'
249 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
250 def updatedLeaves = getLeavesMap(updatedFragment)
251 assert updatedLeaves.size() == 1
252 assert updatedLeaves.'leaf-value' == 'new'
253 and: 'existing child entry remains as is'
254 def childFragment = updatedFragment.childFragments.iterator().next()
255 def childLeaves = getLeavesMap(childFragment)
256 assert childFragment.id == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
257 assert childLeaves.'leaf-value' == 'original'
260 @Sql([CLEAR_DATA, SET_DATA])
261 def 'Update data leaves error scenario: #scenario.'() {
262 when: 'attempt to update data node for #scenario'
263 objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
264 then: 'a #expectedException is thrown'
265 thrown(expectedException)
266 where: 'the following data is used'
267 scenario | dataspaceName | anchorName | xpath || expectedException
268 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
269 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
270 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
273 @Sql([CLEAR_DATA, SET_DATA])
274 def 'Replace data node tree with descendants removal.'() {
275 given: 'data node object with leaves updated, no children'
276 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
277 when: 'replace data node tree is performed'
278 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
279 then: 'leaves have been updated for selected data node'
280 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
281 def updatedLeaves = getLeavesMap(updatedFragment)
282 assert updatedLeaves.size() == 1
283 assert updatedLeaves.'leaf-value' == 'new'
284 and: 'updated entry has no children'
285 updatedFragment.childFragments.isEmpty()
286 and: 'previously attached child entry is removed from database'
287 fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
290 @Sql([CLEAR_DATA, SET_DATA])
291 def 'Replace data node tree with descendants.'() {
292 given: 'data node object with leaves updated, having child with old content'
293 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
294 buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
296 when: 'update is performed including descendants'
297 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
298 then: 'leaves have been updated for selected data node'
299 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
300 def updatedLeaves = getLeavesMap(updatedFragment)
301 assert updatedLeaves.size() == 1
302 assert updatedLeaves.'leaf-value' == 'new'
303 and: 'existing child entry is not updated as content is same'
304 def childFragment = updatedFragment.childFragments.iterator().next()
305 childFragment.xpath == '/parent-200/child-201/grand-child'
306 def childLeaves = getLeavesMap(childFragment)
307 assert childLeaves.'leaf-value' == 'original'
310 @Sql([CLEAR_DATA, SET_DATA])
311 def 'Replace data node tree with same descendants but changed leaf value.'() {
312 given: 'data node object with leaves updated, having child with old content'
313 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
314 buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'new'], [])
316 when: 'update is performed including descendants'
317 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
318 then: 'leaves have been updated for selected data node'
319 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
320 def updatedLeaves = getLeavesMap(updatedFragment)
321 assert updatedLeaves.size() == 1
322 assert updatedLeaves.'leaf-value' == 'new'
323 and: 'existing child entry is updated with the new content'
324 def childFragment = updatedFragment.childFragments.iterator().next()
325 childFragment.xpath == '/parent-200/child-201/grand-child'
326 def childLeaves = getLeavesMap(childFragment)
327 assert childLeaves.'leaf-value' == 'new'
330 @Sql([CLEAR_DATA, SET_DATA])
331 def 'Replace data node tree with different descendants xpath'() {
332 given: 'data node object with leaves updated, having child with old content'
333 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
334 buildDataNode("/parent-200/child-201/grand-child-new", ['leaf-value': 'new'], [])
336 when: 'update is performed including descendants'
337 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
338 then: 'leaves have been updated for selected data node'
339 def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
340 def updatedLeaves = getLeavesMap(updatedFragment)
341 assert updatedLeaves.size() == 1
342 assert updatedLeaves.'leaf-value' == 'new'
343 and: 'previously attached child entry is removed from database'
344 fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
345 and: 'new child entry is persisted'
346 def childFragment = updatedFragment.childFragments.iterator().next()
347 childFragment.xpath == '/parent-200/child-201/grand-child-new'
348 def childLeaves = getLeavesMap(childFragment)
349 assert childLeaves.'leaf-value' == 'new'
352 @Sql([CLEAR_DATA, SET_DATA])
353 def 'Replace data node tree error scenario: #scenario.'() {
354 given: 'data node object'
355 def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
356 when: 'attempt to update data node for #scenario'
357 objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
358 then: 'a #expectedException is thrown'
359 thrown(expectedException)
360 where: 'the following data is used'
361 scenario | dataspaceName | anchorName | xpath || expectedException
362 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
363 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
364 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
367 @Sql([CLEAR_DATA, SET_DATA])
368 def 'Update existing list with #scenario.'() {
369 given: 'a parent having a list of data nodes containing: #originalKeys (ech list element has a child too)'
370 def parentXpath = '/parent-3'
371 if (originalKeys.size() > 0) {
372 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'original value', originalKeys, true)
373 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
375 and: 'each original list element has one child'
376 def originalParentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
377 originalParentFragment.childFragments.each {assert it.childFragments.size() == 1 }
378 when: 'it is updated with #scenario'
379 def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new value', replacementKeys, false)
380 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
381 then: 'the result list ONLY contains the expected replacement elements'
382 def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
383 def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
384 def expectedListEntriesAfterUpdateAsXpaths = keysToXpaths(parentXpath, replacementKeys)
385 assert allChildXpaths.size() == replacementKeys.size()
386 assert allChildXpaths.containsAll(expectedListEntriesAfterUpdateAsXpaths)
387 and: 'all the list elements have the new values'
388 assert parentFragment.childFragments.stream().allMatch(childFragment -> childFragment.attributes.contains('new value'))
389 and: 'there are no more grandchildren as none of the replacement list entries had a child'
390 parentFragment.childFragments.each {assert it.childFragments.size() == 0 }
391 where: 'the following replacement lists are applied'
392 scenario | originalKeys | replacementKeys
393 'one existing entry only' | [] | ['NEW']
394 'multiple new entries' | [] | ['NEW1', 'NEW2']
395 'one new entry only (existing entries are deleted)' | ['A', 'B'] | ['NEW1', 'NEW2']
396 'one existing on new entry' | ['A', 'B'] | ['A', 'NEW']
397 'one existing entry only' | ['A', 'B'] | ['A']
400 @Sql([CLEAR_DATA, SET_DATA])
401 def 'Replacing existing list element with attributes and (grand)child.'() {
402 given: 'a parent with list elements A and B with attribute and grandchild tagged as "org"'
403 def parentXpath = '/parent-3'
404 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'org', ['A','B'], true)
405 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
406 when: 'A is replaced with an entry with attribute and grandchild tagged tagged as "new" (B is not in replacement list)'
407 def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new', ['A'], true)
408 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
409 then: 'The updated fragment has a child-list with ONLY element "A"'
410 def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
411 parentFragment.childFragments.size() == 1
412 def childListElementA = parentFragment.childFragments[0]
413 childListElementA.xpath == "/parent-3/child-list[@key='A']"
414 and: 'element "A" has an attribute with the "new" (tag) value'
415 childListElementA.attributes == '{"attr1": "new"}'
416 and: 'element "A" has a only one (grand)child'
417 childListElementA.childFragments.size() == 1
418 and: 'the grandchild is the new grandchild (tag)'
419 def grandChild = childListElementA.childFragments[0]
420 grandChild.xpath == "/parent-3/child-list[@key='A']/new-grand-child"
421 and: 'the grandchild has an attribute with the "new" (tag) value'
422 grandChild.attributes == '{"attr1": "new"}'
425 @Sql([CLEAR_DATA, SET_DATA])
426 def 'Replace list element for a parent (parent-1) with existing one (non-list) child'() {
427 when: 'a list element is added under the parent'
428 def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(XPATH_DATA_NODE_WITH_DESCENDANTS, 'new', ['A','B'], false)
429 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, replacementListEntriesAsDataNodes)
430 then: 'the parent will have 3 children after the replacement'
431 def parentFragment = fragmentRepository.getById(ID_DATA_NODE_WITH_DESCENDANTS)
432 parentFragment.childFragments.size() == 3
433 def xpaths = parentFragment.childFragments.collect {it.xpath}
434 and: 'one of the children is the original child fragment'
435 xpaths.contains('/parent-1/child-1')
436 and: 'it has the two new list elements'
437 xpaths.containsAll("/parent-1/child-list[@key='A']", "/parent-1/child-list[@key='B']")
440 @Sql([CLEAR_DATA, SET_DATA])
441 def 'Replace list content using unknown parent'() {
442 given: 'list element as a collection of data nodes'
443 def listElementCollection = toDataNodes(['irrelevant'])
444 when: 'attempt to replace list elements under unknown parent node'
445 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/unknown', listElementCollection)
446 then: 'a datanode not found exception is thrown'
447 thrown(DataNodeNotFoundException)
450 @Sql([CLEAR_DATA, SET_DATA])
451 def 'Replace list content with empty collection is not supported'() {
452 when: 'attempt to replace list elements with empty collection'
453 objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/parent-203', [])
454 then: 'a CPS admin exception is thrown'
455 def thrown = thrown(CpsAdminException)
456 assert thrown.message == 'Invalid list replacement'
459 @Sql([CLEAR_DATA, SET_DATA])
460 def 'Delete list scenario: #scenario.'() {
461 when: 'deleting list is executed for: #scenario.'
462 objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
463 then: 'only the expected children remain'
464 def parentFragment = fragmentRepository.getById(parentFragmentId)
465 def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
466 assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
467 assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
468 where: 'following parameters were used'
469 scenario | targetXpaths | parentFragmentId || expectedRemainingChildXpaths
470 '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"]']
471 '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"]']
472 'whole list' | '/parent-203/child-204' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
473 '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"]']
476 @Sql([CLEAR_DATA, SET_DATA])
477 def 'Delete list error scenario: #scenario.'() {
478 when: 'attempting to delete scenario: #scenario.'
479 objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
480 then: 'a DataNodeNotFoundException is thrown'
481 thrown(DataNodeNotFoundException)
482 where: 'following parameters were used'
483 scenario | targetXpaths
484 'whole list, parent node does not exist' | '/unknown/some-child'
485 'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
486 'whole list does not exist' | '/parent-200/unknown'
487 'list element, list does not exist' | '/parent-200/unknown[@key="C"]'
488 'list element, element does not exist' | '/parent-203/child-204[@key="C"]'
489 'valid datanode but not a list' | '/parent-200/child-202'
492 @Sql([CLEAR_DATA, SET_DATA])
493 def 'Confirm deletion of #scenario.'() {
494 given: 'a valid data node'
497 when: 'data nodes are deleted'
498 objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
499 then: 'verify data nodes are removed'
501 dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, getDataNodesXpaths, INCLUDE_ALL_DESCENDANTS)
502 dataNodeXpath = dataNode.xpath
503 assert dataNodeXpath == expectedXpaths
504 } catch (DataNodeNotFoundException) {
505 assert dataNodeXpath == expectedXpaths
507 where: 'following parameters were used'
508 scenario | xpathForDeletion | getDataNodesXpaths || expectedXpaths
509 'child of target' | '/parent-206/child-206' | '/parent-206/child-206' || null
510 'child data node, parent still exists' | '/parent-206/child-206' | '/parent-206' || '/parent-206'
511 'list element' | '/parent-206/child-206/grand-child-206[@key="A"]' | '/parent-206/child-206/grand-child-206[@key="A"]' || null
512 '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"]'
515 @Sql([CLEAR_DATA, SET_DATA])
516 def 'Delete data node with #scenario.'() {
517 when: 'data node is deleted'
518 objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
519 then: 'a #expectedException is thrown'
520 thrown(DataNodeNotFoundException)
521 where: 'the following parameters were used'
522 scenario | datanodeXpath
523 'valid data node, non existent child node' | '/parent-203/child-non-existent'
524 'invalid list element' | '/parent-206/child-206/grand-child-206@key="A"]'
527 static Collection<DataNode> toDataNodes(xpaths) {
528 return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
531 static DataNode buildDataNode(xpath, leaves, childDataNodes) {
532 return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
535 static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
536 return jsonObjectMapper.convertJsonString(fragmentEntity.attributes, Map<String, Object>.class)
539 def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
540 expectedLeavesMap.forEach((key, value) -> {
541 def actualValue = actualLeavesMap[key]
542 if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
543 assert value.size() == actualValue.size()
544 assert value.containsAll(actualValue)
546 assert value == actualValue
552 def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
553 flatMap.put(dataNodeTree.xpath, dataNodeTree)
554 dataNodeTree.childDataNodes
555 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
559 def keysToXpaths(parent, Collection keys) {
560 return keys.collect { "${parent}/child-list[@key='${it}']".toString() }
563 def static createDataNodeTree(String... xpaths) {
564 def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
565 if (xpaths.length > 1) {
566 def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
567 def childDataNode = createDataNodeTree(xPathsDescendant)
568 dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
570 dataNodeBuilder.build()
573 def getFragmentByXpath(dataspaceName, anchorName, xpath) {
574 def dataspace = dataspaceRepository.getByName(dataspaceName)
575 def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
576 return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
580 def createChildListAllHavingAttributeValue(parentXpath, tag, Collection keys, boolean addGrandChild) {
581 def listElementAsDataNodes = keysToXpaths(parentXpath, keys).collect {
582 new DataNodeBuilder()
584 .withLeaves([attr1: tag])
588 listElementAsDataNodes.each {it.childDataNodes = [createGrandChild(it.xpath, tag)]}
590 return listElementAsDataNodes
593 def createGrandChild(parentXPath, tag) {
594 new DataNodeBuilder()
595 .withXpath("${parentXPath}/${tag}-grand-child")
596 .withLeaves([attr1: tag])