Make single deleteDataNode use plural deleteDataNodes
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceIntegrationSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2023 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021-2022 Bell Canada.
6  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
7  *  ================================================================================
8  *  Licensed under the Apache License, Version 2.0 (the "License");
9  *  you may not use this file except in compliance with the License.
10  *  You may obtain a copy of the License at
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
14  *  Unless required by applicable law or agreed to in writing, software
15  *  distributed under the License is distributed on an "AS IS" BASIS,
16  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  *  See the License for the specific language governing permissions and
18  *  limitations under the License.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.spi.impl
25
26 import com.fasterxml.jackson.databind.ObjectMapper
27 import com.google.common.collect.ImmutableSet
28 import org.onap.cps.spi.CpsDataPersistenceService
29 import org.onap.cps.spi.entities.FragmentEntity
30 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch
31 import org.onap.cps.spi.exceptions.AnchorNotFoundException
32 import org.onap.cps.spi.exceptions.CpsAdminException
33 import org.onap.cps.spi.exceptions.CpsPathException
34 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
35 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
36 import org.onap.cps.spi.model.DataNode
37 import org.onap.cps.spi.model.DataNodeBuilder
38 import org.onap.cps.utils.JsonObjectMapper
39 import org.springframework.beans.factory.annotation.Autowired
40 import org.springframework.test.context.jdbc.Sql
41
42 import javax.validation.ConstraintViolationException
43
44 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
45 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
46
47 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
48
49     @Autowired
50     CpsDataPersistenceService objectUnderTest
51
52     static JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
53     static DataNodeBuilder dataNodeBuilder = new DataNodeBuilder()
54
55     static final String SET_DATA = '/data/fragment.sql'
56     static long ID_DATA_NODE_WITH_DESCENDANTS = 4001
57     static String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
58     static long DATA_NODE_202_FRAGMENT_ID = 4202L
59     static long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L
60     static long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
61     static long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
62     static long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
63     static long PARENT_3_FRAGMENT_ID = 4003L
64
65     static Collection<DataNode> newDataNodes = [new DataNodeBuilder().build()]
66     static Collection<DataNode> existingDataNodes = [createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)]
67     static Collection<DataNode> existingChildDataNodes = [createDataNodeTree('/parent-1/child-1')]
68
69     def static deleteTestParentXPath = '/parent-200'
70     def static deleteTestChildXpath = "${deleteTestParentXPath}/child-with-slash[@key='a/b']"
71     def static deleteTestGrandChildXPath = "${deleteTestChildXpath}/grandChild"
72
73     def expectedLeavesByXpathMap = [
74             '/parent-207'                      : ['parent-leaf': 'parent-leaf value'],
75             '/parent-207/child-001'            : ['first-child-leaf': 'first-child-leaf value'],
76             '/parent-207/child-002'            : ['second-child-leaf': 'second-child-leaf value'],
77             '/parent-207/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value']
78     ]
79
80     @Sql([CLEAR_DATA, SET_DATA])
81     def 'Get all datanodes with descendants .'() {
82         when: 'data nodes are retrieved by their xpath'
83             def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME1, ['/parent-1'], INCLUDE_ALL_DESCENDANTS)
84         then: 'same data nodes are returned by getDataNodesForMultipleXpaths method'
85             assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) == dataNodes
86         and: 'the dataNodes have no prefix (to be addressed by CPS-1301)'
87             assert dataNodes[0].moduleNamePrefix == null
88     }
89
90     @Sql([CLEAR_DATA, SET_DATA])
91     def 'Storing and Retrieving a new DataNodes with descendants.'() {
92         when: 'a fragment with descendants is stored'
93             def parentXpath = '/parent-new'
94             def childXpath = '/parent-new/child-new'
95             def grandChildXpath = '/parent-new/child-new/grandchild-new'
96             def dataNodes = [createDataNodeTree(parentXpath, childXpath, grandChildXpath)]
97             objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, dataNodes)
98         then: 'it can be retrieved by its xpath'
99             def dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS)
100             assert dataNode[0].xpath == parentXpath
101         and: 'it has the correct child'
102             assert dataNode[0].childDataNodes.size() == 1
103             def childDataNode = dataNode[0].childDataNodes[0]
104             assert childDataNode.xpath == childXpath
105         and: 'and its grandchild'
106             assert childDataNode.childDataNodes.size() == 1
107             def grandChildDataNode = childDataNode.childDataNodes[0]
108             assert grandChildDataNode.xpath == grandChildXpath
109     }
110
111     @Sql([CLEAR_DATA, SET_DATA])
112     def 'Store data node for multiple anchors using the same schema.'() {
113         def xpath = '/parent-new'
114         given: 'a fragment is stored for an anchor'
115             objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, [createDataNodeTree(xpath)])
116         when: 'another fragment is stored for an other anchor, using the same schema set'
117             objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME3, [createDataNodeTree(xpath)])
118         then: 'both fragments can be retrieved by their xpath'
119             def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
120             fragment1.anchor.name == ANCHOR_NAME1
121             fragment1.xpath == xpath
122             def fragment2 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME3, xpath)
123             fragment2.anchor.name == ANCHOR_NAME3
124             fragment2.xpath == xpath
125     }
126
127     @Sql([CLEAR_DATA, SET_DATA])
128     def 'Store datanodes error scenario: #scenario.'() {
129         when: 'attempt to store a data node with #scenario'
130             objectUnderTest.storeDataNodes(dataspaceName, anchorName, dataNodes)
131         then: 'a #expectedException is thrown'
132             thrown(expectedException)
133         where: 'the following data is used'
134             scenario                    | dataspaceName  | anchorName     | dataNodes          || expectedException
135             'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNodes       || DataspaceNotFoundException
136             'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNodes       || AnchorNotFoundException
137             'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNodes       || ConstraintViolationException
138             'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNodes  || AlreadyDefinedExceptionBatch
139     }
140
141     @Sql([CLEAR_DATA, SET_DATA])
142     def 'Add children to a Fragment that already has a child.'() {
143         given: 'collection of new child data nodes'
144             def newChild1 = createDataNodeTree('/parent-1/child-2')
145             def newChild2 = createDataNodeTree('/parent-1/child-3')
146             def newChildrenCollection = [newChild1, newChild2]
147         when: 'the child is added to an existing parent with 1 child'
148             objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChildrenCollection)
149         then: 'the parent is now has to 3 children'
150             def expectedExistingChildPath = '/parent-1/child-1'
151             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
152             parentFragment.childFragments.size() == 3
153         and: 'it still has the old child'
154             parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath })
155         and: 'it has the new children'
156             parentFragment.childFragments.find({ it.xpath == newChildrenCollection[0].xpath })
157             parentFragment.childFragments.find({ it.xpath == newChildrenCollection[1].xpath })
158     }
159
160     @Sql([CLEAR_DATA, SET_DATA])
161     def 'Add child error scenario: #scenario.'() {
162         when: 'attempt to add a child data node with #scenario'
163             objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNodes)
164         then: 'a #expectedException is thrown'
165             thrown(expectedException)
166         where: 'the following data is used'
167             scenario                 | parentXpath                      | dataNodes               || expectedException
168             'parent does not exist'  | '/unknown'                       | newDataNodes            || DataNodeNotFoundException
169             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNodes  || AlreadyDefinedExceptionBatch
170     }
171
172     @Sql([CLEAR_DATA, SET_DATA])
173     def 'Add collection of multiple new list elements including an element with a child datanode.'() {
174         given: 'two new child list elements for an existing parent'
175             def listElementXpaths = ['/parent-201/child-204[@key="NEW1"]', '/parent-201/child-204[@key="NEW2"]']
176             def listElements = toDataNodes(listElementXpaths)
177         and: 'a (grand)child data node for one of the new list elements'
178             def grandChild = buildDataNode('/parent-201/child-204[@key="NEW1"]/grand-child-204[@key2="NEW1-CHILD"]', [leave:'value'], [])
179             listElements[0].childDataNodes = [grandChild]
180         when: 'the new data node (list elements) are added to an existing parent node'
181             objectUnderTest.addMultipleLists(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', [listElements])
182         then: 'new entries are successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
183             def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
184             def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
185             assert allChildXpaths.size() == 5
186             assert allChildXpaths.containsAll(listElementXpaths)
187         and: 'the (grand)child node of the new list entry is also present'
188             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
189             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
190             def grandChildFragmentEntity = fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, grandChild.xpath)
191             assert grandChildFragmentEntity.isPresent()
192     }
193
194     @Sql([CLEAR_DATA, SET_DATA])
195     def 'Add multiple list with a mix of existing and new elements'() {
196         given: 'two new child list elements for an existing parent'
197             def existingDataNode = dataNodeBuilder.withXpath('/parent-207/child-001').withLeaves(['id': '001']).build()
198             def newDataNode1 = dataNodeBuilder.withXpath('/parent-207/child-new1').withLeaves(['id': 'new1']).build()
199             def newDataNode2 = dataNodeBuilder.withXpath('/parent-200/child-new2').withLeaves(['id': 'new2']).build()
200             def dataNodeList1 = [existingDataNode, newDataNode1]
201             def dataNodeList2 = [newDataNode2]
202         when: 'duplicate data node is requested to be added'
203             objectUnderTest.addMultipleLists(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT, '/', [dataNodeList1, dataNodeList2])
204         then: 'already defined batch exception is thrown'
205             def thrown = thrown(AlreadyDefinedExceptionBatch)
206         and: 'it only contains the xpath(s) of the duplicated elements'
207             assert thrown.alreadyDefinedXpaths.size() == 1
208             assert thrown.alreadyDefinedXpaths.contains('/parent-207/child-001')
209         and: 'it does NOT contains the xpaths of the new element that were not combined with existing elements'
210             assert !thrown.alreadyDefinedXpaths.contains('/parent-207/child-new1')
211             assert !thrown.alreadyDefinedXpaths.contains('/parent-207/child-new1')
212         and: 'the new entity is inserted correctly'
213             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
214             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT)
215             fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, '/parent-200/child-new2').isPresent()
216     }
217
218     @Sql([CLEAR_DATA, SET_DATA])
219     def 'Add list element error scenario: #scenario.'() {
220         given: 'list element as a collection of data nodes'
221             def listElements = toDataNodes(listElementXpaths)
222         when: 'attempt to add list elements to parent node'
223             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElements)
224         then: 'a #expectedException is thrown'
225             thrown(expectedException)
226         where: 'following parameters were used'
227             scenario                        | parentNodeXpath | listElementXpaths                   || expectedException
228             'parent node does not exist'    | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
229             'data fragment already exists'  | '/parent-201'   | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedExceptionBatch
230     }
231
232     @Sql([CLEAR_DATA, SET_DATA])
233     def 'Get all data nodes by single xpath without descendants : #scenario'() {
234         when: 'data nodes are requested'
235             def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
236                 [inputXPath], OMIT_DESCENDANTS)
237         then: 'data nodes under root are returned'
238             assert result.childDataNodes.size() == 2
239         and: 'no descendants of parent nodes are returned'
240             result.each {assert it.childDataNodes.size() == 0}
241         and: 'same data nodes are returned when V2 of get Data Nodes API is executed'
242             assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
243                 inputXPath, OMIT_DESCENDANTS) == result
244         where: 'the following xpath is used'
245             scenario      | inputXPath
246             'root xpath'  | '/'
247             'empty xpath' | ''
248     }
249
250     @Sql([CLEAR_DATA, SET_DATA])
251     def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
252         when: 'trying to execute a query with a syntax (parsing) error'
253             objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
254         then: 'exception is thrown'
255             def exceptionThrown = thrown(CpsPathException)
256             assert exceptionThrown.getDetails() == "failed to parse at line 1 due to extraneous input 'invalid-cps-path' expecting '/'"
257     }
258
259     @Sql([CLEAR_DATA, SET_DATA])
260     def 'Get all data nodes by single xpath with all descendants : #scenario'() {
261         when: 'data nodes are requested with all descendants'
262             def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
263                 [inputXPath], INCLUDE_ALL_DESCENDANTS)
264             def mappedResult = multipleTreesToFlatMapByXpath(new HashMap<>(), result)
265         then: 'data nodes are returned with all the descendants populated'
266             assert mappedResult.size() == 8
267             assert result.childDataNodes.size() == 2
268             assert mappedResult.get('/parent-208/child-001').childDataNodes.size() == 0
269             assert mappedResult.get('/parent-208/child-002').childDataNodes.size() == 1
270             assert mappedResult.get('/parent-209/child-001').childDataNodes.size() == 0
271             assert mappedResult.get('/parent-209/child-002').childDataNodes.size() == 1
272         and: 'same data nodes are returned when V2 of Get Data Nodes API is executed'
273             assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
274                 inputXPath, INCLUDE_ALL_DESCENDANTS) == result
275         where: 'the following data is used'
276             scenario      | inputXPath
277             'root xpath'  | '/'
278             'empty xpath' | ''
279     }
280
281     @Sql([CLEAR_DATA, SET_DATA])
282     def 'Get data nodes error scenario : #scenario.'() {
283         when: 'attempt to get data nodes with #scenario'
284             objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
285         then: 'an #expectedException is thrown'
286             thrown(expectedException)
287         where: 'the following data is used'
288             scenario             | dataspaceName  | anchorName                        | xpath           || expectedException
289             'non existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH'     || DataNodeNotFoundException
290             'invalid Xpath'      | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException
291     }
292
293     @Sql([CLEAR_DATA, SET_DATA])
294     def 'Get data nodes for multiple xpaths.'() {
295         when: 'fetch #scenario.'
296             def results = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS)
297         then: 'the expected number of data nodes are returned'
298             assert results.size() == expectedResultSize
299         where: 'following parameters were used'
300             scenario                               | inputXpaths                                     || expectedResultSize
301             '0 nodes'                              | []                                              || 0
302             '1 node'                               | ["/parent-200"]                                 || 1
303             '2 unique nodes'                       | ["/parent-200", "/parent-201"]                  || 2
304             '3 unique nodes'                       | ["/parent-200", "/parent-201", "/parent-202"]   || 3
305             '1 unique node with duplicate xpath'   | ["/parent-200", "/parent-200"]                  || 1
306             '2 unique nodes with duplicate xpath'  | ["/parent-200", "/parent-202", "/parent-200"]   || 2
307             'list element with key (single quote)' | ["/parent-201/child-204[@key='A']"]             || 1
308             'list element with key (double quote)' | ['/parent-201/child-204[@key="A"]']             || 1
309             'whole list (not implemented)'         | ["/parent-201/child-204"]                       || 0
310             'non-existing xpath'                   | ["/NO-XPATH"]                                   || 0
311             'existing and non-existing xpaths'     | ["/parent-200", "/NO-XPATH", "/parent-201"]     || 2
312             'invalid xpath'                        | ["INVALID XPATH"]                               || 0
313             'valid and invalid xpaths'             | ["/parent-200", "INVALID XPATH", "/parent-201"] || 2
314             'root xpath'                           | ["/"]                                           || 7
315             'empty (root) xpath'                   | [""]                                            || 7
316             'root and top-level xpaths'            | ["/", "/parent-200", "/parent-201"]             || 7
317             'root and child xpaths'                | ["/", "/parent-200/child-201"]                  || 8
318     }
319
320     @Sql([CLEAR_DATA, SET_DATA])
321     def 'Get data nodes for collection of xpath error scenario : #scenario.'() {
322         when: 'attempt to get data nodes with #scenario'
323             objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS)
324         then: 'a #expectedException is thrown'
325             thrown(expectedException)
326         where: 'the following data is used'
327             scenario                 | dataspaceName  | anchorName     || expectedException
328             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' || DataspaceNotFoundException
329             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'    || AnchorNotFoundException
330     }
331
332     @Sql([CLEAR_DATA, SET_DATA])
333     def 'Update data node leaves.'() {
334         when: 'update is performed for leaves'
335             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
336                     '/parent-200/child-201', ['leaf-value': 'new'])
337         then: 'leaves are updated for selected data node'
338             def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
339             def updatedLeaves = getLeavesMap(updatedFragment)
340             assert updatedLeaves.size() == 1
341             assert updatedLeaves.'leaf-value' == 'new'
342         and: 'existing child entry remains as is'
343             def childFragment = updatedFragment.childFragments.iterator().next()
344             def childLeaves = getLeavesMap(childFragment)
345             assert childFragment.id == CHILD_OF_DATA_NODE_202_FRAGMENT_ID
346             assert childLeaves.'leaf-value' == 'original'
347     }
348
349     @Sql([CLEAR_DATA, SET_DATA])
350     def 'Update data leaves error scenario: #scenario.'() {
351         when: 'attempt to update data node for #scenario'
352             objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
353         then: 'a #expectedException is thrown'
354             thrown(expectedException)
355         where: 'the following data is used'
356             scenario                 | dataspaceName  | anchorName                        | xpath                 || expectedException
357             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | '/not relevant'       || DataspaceNotFoundException
358             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | '/not relevant'       || AnchorNotFoundException
359             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING-XPATH' || DataNodeNotFoundException
360     }
361
362     @Sql([CLEAR_DATA, SET_DATA])
363     def 'Update data node and descendants by removing descendants.'() {
364         given: 'data node object with leaves updated, no children'
365             def submittedDataNode = buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [])
366         when: 'update data nodes and descendants is performed'
367             objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
368         then: 'leaves have been updated for selected data node'
369             def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
370             def updatedLeaves = getLeavesMap(updatedFragment)
371             assert updatedLeaves.size() == 1
372             assert updatedLeaves.'leaf-value' == 'new'
373         and: 'updated entry has no children'
374             updatedFragment.childFragments.isEmpty()
375         and: 'previously attached child entry is removed from database'
376             fragmentRepository.findById(CHILD_OF_DATA_NODE_202_FRAGMENT_ID).isEmpty()
377     }
378
379     @Sql([CLEAR_DATA, SET_DATA])
380     def 'Update data node and descendants with new descendants'() {
381         given: 'data node object with leaves updated, having child with old content'
382             def submittedDataNode = buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [
383                   buildDataNode('/parent-200/child-201/grand-child', ['leaf-value': 'original'], [])
384             ])
385         when: 'update is performed including descendants'
386             objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
387         then: 'leaves have been updated for selected data node'
388             def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
389             def updatedLeaves = getLeavesMap(updatedFragment)
390             assert updatedLeaves.size() == 1
391             assert updatedLeaves.'leaf-value' == 'new'
392         and: 'existing child entry is not updated as content is same'
393             def childFragment = updatedFragment.childFragments.iterator().next()
394             childFragment.xpath == '/parent-200/child-201/grand-child'
395             def childLeaves = getLeavesMap(childFragment)
396             assert childLeaves.'leaf-value' == 'original'
397     }
398
399     @Sql([CLEAR_DATA, SET_DATA])
400     def 'Update data node and descendants with same descendants but changed leaf value.'() {
401         given: 'data node object with leaves updated, having child with old content'
402             def submittedDataNode = buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [
403                     buildDataNode('/parent-200/child-201/grand-child', ['leaf-value': 'new'], [])
404             ])
405         when: 'update is performed including descendants'
406             objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
407         then: 'leaves have been updated for selected data node'
408             def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
409             def updatedLeaves = getLeavesMap(updatedFragment)
410             assert updatedLeaves.size() == 1
411             assert updatedLeaves.'leaf-value' == 'new'
412         and: 'existing child entry is updated with the new content'
413             def childFragment = updatedFragment.childFragments.iterator().next()
414             childFragment.xpath == '/parent-200/child-201/grand-child'
415             def childLeaves = getLeavesMap(childFragment)
416             assert childLeaves.'leaf-value' == 'new'
417     }
418
419     @Sql([CLEAR_DATA, SET_DATA])
420     def 'Update data node and descendants with different descendants xpath'() {
421         given: 'data node object with leaves updated, having child with old content'
422             def submittedDataNode = buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [
423                     buildDataNode('/parent-200/child-201/grand-child-new', ['leaf-value': 'new'], [])
424             ])
425         when: 'update is performed including descendants'
426             objectUnderTest.updateDataNodeAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
427         then: 'leaves have been updated for selected data node'
428             def updatedFragment = fragmentRepository.getById(DATA_NODE_202_FRAGMENT_ID)
429             def updatedLeaves = getLeavesMap(updatedFragment)
430             assert updatedLeaves.size() == 1
431             assert updatedLeaves.'leaf-value' == 'new'
432         and: 'previously attached child entry is removed from database'
433             fragmentRepository.findById(CHILD_OF_DATA_NODE_202_FRAGMENT_ID).isEmpty()
434         and: 'new child entry is persisted'
435             def childFragment = updatedFragment.childFragments.iterator().next()
436             childFragment.xpath == '/parent-200/child-201/grand-child-new'
437             def childLeaves = getLeavesMap(childFragment)
438             assert childLeaves.'leaf-value' == 'new'
439     }
440
441     @Sql([CLEAR_DATA, SET_DATA])
442     def 'Update data node and descendants error scenario: #scenario.'() {
443         given: 'data node object'
444             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
445         when: 'attempt to update data node for #scenario'
446             objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, submittedDataNode)
447         then: 'a #expectedException is thrown'
448             thrown(expectedException)
449         where: 'the following data is used'
450             scenario                 | dataspaceName  | anchorName                        | xpath                 || expectedException
451             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | '/not relevant'       || DataspaceNotFoundException
452             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | '/not relevant'       || AnchorNotFoundException
453             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING-XPATH' || DataNodeNotFoundException
454             'invalid xpath'          | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH'       || CpsPathException
455     }
456
457     @Sql([CLEAR_DATA, SET_DATA])
458     def 'Update existing list with #scenario.'() {
459         given: 'a parent having a list of data nodes containing: #originalKeys (ech list element has a child too)'
460             def parentXpath = '/parent-3'
461             if (originalKeys.size() > 0) {
462                 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'original value', originalKeys, true)
463                 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
464             }
465         and: 'each original list element has one child'
466             def originalParentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
467             originalParentFragment.childFragments.each {assert it.childFragments.size() == 1 }
468         when: 'it is updated with #scenario'
469             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new value', replacementKeys, false)
470             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
471         then: 'the result list ONLY contains the expected replacement elements'
472             def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
473             def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
474             def expectedListEntriesAfterUpdateAsXpaths = keysToXpaths(parentXpath, replacementKeys)
475             assert allChildXpaths.size() == replacementKeys.size()
476             assert allChildXpaths.containsAll(expectedListEntriesAfterUpdateAsXpaths)
477         and: 'all the list elements have the new values'
478             assert parentFragment.childFragments.stream().allMatch(childFragment -> childFragment.attributes.contains('new value'))
479         and: 'there are no more grandchildren as none of the replacement list entries had a child'
480             parentFragment.childFragments.each {assert it.childFragments.size() == 0 }
481         where: 'the following replacement lists are applied'
482             scenario                                            | originalKeys | replacementKeys
483             'one existing entry only'                           | []           | ['NEW']
484             'multiple new entries'                              | []           | ['NEW1', 'NEW2']
485             'one new entry only (existing entries are deleted)' | ['A', 'B']   | ['NEW1', 'NEW2']
486             'one existing on new entry'                         | ['A', 'B']   | ['A', 'NEW']
487             'one existing entry only'                           | ['A', 'B']   | ['A']
488     }
489
490     @Sql([CLEAR_DATA, SET_DATA])
491     def 'Replacing existing list element with attributes and (grand)child.'() {
492         given: 'a parent with list elements A and B with attribute and grandchild tagged as "org"'
493             def parentXpath = '/parent-3'
494             def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'org', ['A','B'], true)
495             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
496         when: 'A is replaced with an entry with attribute and grandchild tagged tagged as "new" (B is not in replacement list)'
497             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new', ['A'], true)
498             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
499         then: 'The updated fragment has a child-list with ONLY element "A"'
500             def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
501             parentFragment.childFragments.size() == 1
502             def childListElementA = parentFragment.childFragments[0]
503             childListElementA.xpath == "/parent-3/child-list[@key='A']"
504         and: 'element "A" has an attribute with the "new" (tag) value'
505             childListElementA.attributes == '{"attr1": "new"}'
506         and: 'element "A" has a only one (grand)child'
507             childListElementA.childFragments.size() == 1
508         and: 'the grandchild is the new grandchild (tag)'
509             def grandChild = childListElementA.childFragments[0]
510             grandChild.xpath == "/parent-3/child-list[@key='A']/new-grand-child"
511         and: 'the grandchild has an attribute with the "new" (tag) value'
512             grandChild.attributes == '{"attr1": "new"}'
513     }
514
515     @Sql([CLEAR_DATA, SET_DATA])
516     def 'Replace list element for a parent (parent-1) with existing one (non-list) child'() {
517         when: 'a list element is added under the parent'
518             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(XPATH_DATA_NODE_WITH_DESCENDANTS, 'new', ['A','B'], false)
519             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, replacementListEntriesAsDataNodes)
520         then: 'the parent will have 3 children after the replacement'
521             def parentFragment = fragmentRepository.getById(ID_DATA_NODE_WITH_DESCENDANTS)
522             parentFragment.childFragments.size() == 3
523             def xpaths = parentFragment.childFragments.collect {it.xpath}
524         and: 'one of the children is the original child fragment'
525             xpaths.contains('/parent-1/child-1')
526         and: 'it has the two new list elements'
527             xpaths.containsAll("/parent-1/child-list[@key='A']", "/parent-1/child-list[@key='B']")
528     }
529
530     @Sql([CLEAR_DATA, SET_DATA])
531     def 'Replace list content using unknown parent'() {
532         given: 'list element as a collection of data nodes'
533             def listElementCollection = toDataNodes(['irrelevant'])
534         when: 'attempt to replace list elements under unknown parent node'
535             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/unknown', listElementCollection)
536         then: 'a datanode not found exception is thrown'
537             thrown(DataNodeNotFoundException)
538     }
539
540     @Sql([CLEAR_DATA, SET_DATA])
541     def 'Replace list content with empty collection is not supported'() {
542         when: 'attempt to replace list elements with empty collection'
543             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/parent-203', [])
544         then: 'a CPS admin exception is thrown'
545             def thrown = thrown(CpsAdminException)
546             assert thrown.message == 'Invalid list replacement'
547     }
548
549     @Sql([CLEAR_DATA, SET_DATA])
550     def 'Delete list scenario: #scenario.'() {
551         when: 'deleting list is executed for: #scenario.'
552             objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
553         and: 'remaining children are fetched'
554             def parentFragment = fragmentRepository.getById(parentFragmentId)
555             def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
556         then: 'only the expected children remain'
557             assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
558             assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
559         where: 'following parameters were used'
560             scenario                          | targetXpaths                                                 | parentFragmentId                     || expectedRemainingChildXpaths
561             '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']"]
562             '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']"]
563             'whole list'                      | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
564             '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']"]
565     }
566
567     @Sql([CLEAR_DATA, SET_DATA])
568     def 'Delete multiple data nodes using scenario: #scenario.'() {
569         when: 'deleting nodes is executed for: #scenario.'
570             objectUnderTest.deleteDataNodes(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
571         and: 'remaining children are fetched'
572             def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT203_FRAGMENT_ID)
573             def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
574         then: 'only the expected children remain'
575             assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
576             assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
577         where: 'following parameters were used'
578             scenario                          | targetXpaths                                                           || expectedRemainingChildXpaths
579             'delete nothing'                  | []                                                                     || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
580             'datanode'                        | ['/parent-203/child-203']                                              || ["/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
581             '1 list element'                  | ['/parent-203/child-204[@key="A"]']                                    || ["/parent-203/child-203", "/parent-203/child-204[@key='B']"]
582             '2 list elements'                 | ['/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]'] || ["/parent-203/child-203"]
583             'whole list'                      | ['/parent-203/child-204']                                              || ['/parent-203/child-203']
584             'list and element in same list'   | ['/parent-203/child-204', '/parent-203/child-204[@key="A"]']           || ['/parent-203/child-203']
585             'list element under list element' | ['/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]']         || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
586             'invalid xpath'                   | ['INVALID XPATH', '/parent-203/child-204']                             || ['/parent-203/child-203']
587     }
588
589     @Sql([CLEAR_DATA, SET_DATA])
590     def 'Delete multiple data nodes error scenario: #scenario.'() {
591         when: 'deleting nodes is executed for: #scenario.'
592             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, targetXpaths)
593         then: 'a #expectedException is thrown'
594             thrown(expectedException)
595         where: 'the following data is used'
596             scenario                 | dataspaceName  | anchorName     | targetXpaths            || expectedException
597             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | ['/not relevant']       || DataspaceNotFoundException
598             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'    | ['/not relevant']       || AnchorNotFoundException
599             'non-existing datanode'  | DATASPACE_NAME | ANCHOR_NAME3   | ['/NON-EXISTING-XPATH'] || DataNodeNotFoundException
600     }
601
602     @Sql([CLEAR_DATA, SET_DATA])
603     def 'Delete data nodes with "/"-token in list key value: #scenario. (CPS-1409)'() {
604         given: 'a data nodes with list-element child with "/" in index value (and grandchild)'
605             def grandChild = new DataNodeBuilder().withXpath(deleteTestGrandChildXPath).build()
606             def child = new DataNodeBuilder().withXpath(deleteTestChildXpath).withChildDataNodes([grandChild]).build()
607             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTestParentXPath, child)
608         and: 'number of children before delete is stored'
609             def numberOfChildrenBeforeDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size()
610         when: 'target node is deleted'
611             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTarget)
612         then: 'one child has been deleted'
613             def numberOfChildrenAfterDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size()
614             assert numberOfChildrenAfterDelete == numberOfChildrenBeforeDelete - 1
615         where:
616             scenario                | deleteTarget              | pathToParentOfDeletedNode
617             'list element with /'   | deleteTestChildXpath      | deleteTestParentXPath
618             'child of list element' | deleteTestGrandChildXPath | deleteTestChildXpath
619     }
620
621     @Sql([CLEAR_DATA, SET_DATA])
622     def 'Delete list error scenario: #scenario.'() {
623         when: 'attempting to delete scenario: #scenario.'
624             objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
625         then: 'a DataNodeNotFoundException is thrown'
626             thrown(DataNodeNotFoundException)
627         where: 'following parameters were used'
628             scenario                                   | targetXpaths
629             'whole list, parent node does not exist'   | '/unknown/some-child'
630             'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
631             'whole list does not exist'                | '/parent-200/unknown'
632             'list element, list does not exist'        | '/parent-200/unknown[@key="C"]'
633             'list element, element does not exist'     | '/parent-203/child-204[@key="C"]'
634             'valid datanode but not a list'            | '/parent-200/child-202'
635     }
636
637     @Sql([CLEAR_DATA, SET_DATA])
638     def 'Delete data node by xpath #scenario.'() {
639         given: 'a valid data node'
640             def dataNode
641         and: 'data nodes are deleted'
642             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
643         when: 'verify data nodes are removed'
644             objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS)
645         then:
646             thrown(DataNodeNotFoundException)
647         and: 'some related object is not deleted'
648             if (xpathSurvivor!=null) {
649                 dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS)
650                 assert dataNode[0].xpath == xpathSurvivor
651             }
652         where: 'following parameters were used'
653             scenario                               | xpathForDeletion                                  || xpathSurvivor
654             'child data node, parent still exists' | '/parent-206/child-206'                           || '/parent-206'
655             'list element, sibling still exists'   | '/parent-206/child-206/grand-child-206[@key="A"]' || "/parent-206/child-206/grand-child-206[@key='X']"
656             'container node'                       | '/parent-206'                                     || null
657             'container list node'                  | '/parent-206[@key="A"]'                           || "/parent-206[@key='B']"
658             'root node with xpath /'               | '/'                                               || null
659             'root node with xpath passed as blank' | ''                                                || null
660     }
661
662     @Sql([CLEAR_DATA, SET_DATA])
663     def 'Delete data node error scenario: #scenario.'() {
664         when: 'data node is deleted'
665             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
666         then: 'a #expectedException is thrown'
667             thrown(expectedException)
668         where: 'the following parameters were used'
669             scenario                                   | datanodeXpath                                    | expectedException
670             'valid data node, non existent child node' | '/parent-203/child-non-existent'                 | DataNodeNotFoundException
671             'invalid list element'                     | '/parent-206/child-206/grand-child-206@key="A"]' | CpsPathException
672     }
673
674     @Sql([CLEAR_DATA, SET_DATA])
675     def 'Delete data node for an anchor.'() {
676         given: 'a data-node exists for an anchor'
677             assert fragmentsExistInDB(1001, 3003)
678         when: 'data nodes are deleted '
679             objectUnderTest.deleteDataNodes(DATASPACE_NAME, ANCHOR_NAME3)
680         then: 'all data-nodes are deleted successfully'
681             assert !fragmentsExistInDB(1001, 3003)
682     }
683
684     @Sql([CLEAR_DATA, SET_DATA])
685     def 'Delete data node for multiple anchors.'() {
686         given: 'a data-node exists for an anchor'
687             assert fragmentsExistInDB(1001, 3001)
688             assert fragmentsExistInDB(1001, 3003)
689         when: 'data nodes are deleted '
690             objectUnderTest.deleteDataNodes(DATASPACE_NAME, ['ANCHOR-001', 'ANCHOR-003'])
691         then: 'all data-nodes are deleted successfully'
692             assert !fragmentsExistInDB(1001, 3001)
693             assert !fragmentsExistInDB(1001, 3003)
694     }
695
696     def fragmentsExistInDB(dataSpaceId, anchorId) {
697         !fragmentRepository.findRootsByDataspaceAndAnchor(dataSpaceId, anchorId).isEmpty()
698     }
699
700     static Collection<DataNode> toDataNodes(xpaths) {
701         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
702     }
703
704
705     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
706         return dataNodeBuilder.withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
707     }
708
709     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
710         return jsonObjectMapper.convertJsonString(fragmentEntity.attributes, Map<String, Object>.class)
711     }
712
713     def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
714         expectedLeavesMap.forEach((key, value) -> {
715             def actualValue = actualLeavesMap[key]
716             if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
717                 assert value.size() == actualValue.size()
718                 assert value.containsAll(actualValue)
719             } else {
720                 assert value == actualValue
721             }
722         })
723         return true
724     }
725
726     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
727         flatMap.put(dataNodeTree.xpath, dataNodeTree)
728         dataNodeTree.childDataNodes
729                 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
730         return flatMap
731     }
732
733     def static multipleTreesToFlatMapByXpath(Map<String, DataNode> flatMap, Collection<DataNode> dataNodeTrees) {
734         for (DataNode dataNodeTree: dataNodeTrees){
735             flatMap.put(dataNodeTree.xpath, dataNodeTree)
736             dataNodeTree.childDataNodes
737                 .forEach(childDataNode -> multipleTreesToFlatMapByXpath(flatMap, [childDataNode]))
738         }
739         return flatMap
740     }
741
742     def keysToXpaths(parent, Collection keys) {
743         return keys.collect { "${parent}/child-list[@key='${it}']".toString() }
744     }
745
746     def static createDataNodeTree(String... xpaths) {
747         def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
748         if (xpaths.length > 1) {
749             def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
750             def childDataNode = createDataNodeTree(xPathsDescendant)
751             dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
752         }
753         dataNodeBuilder.build()
754     }
755
756     def getFragmentByXpath(dataspaceName, anchorName, xpath) {
757         def dataspace = dataspaceRepository.getByName(dataspaceName)
758         def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
759         return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
760     }
761
762
763     def createChildListAllHavingAttributeValue(parentXpath, tag, Collection keys, boolean addGrandChild) {
764         def listElementAsDataNodes = keysToXpaths(parentXpath, keys).collect {
765                 new DataNodeBuilder()
766                     .withXpath(it)
767                     .withLeaves([attr1: tag])
768                     .build()
769         }
770         if (addGrandChild) {
771             listElementAsDataNodes.each {it.childDataNodes = [createGrandChild(it.xpath, tag)]}
772         }
773         return listElementAsDataNodes
774     }
775
776     def createGrandChild(parentXPath, tag) {
777         new DataNodeBuilder()
778             .withXpath("${parentXPath}/${tag}-grand-child")
779             .withLeaves([attr1: tag])
780             .build()
781     }
782
783 }