2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
20 package org.onap.cps.spi.impl
22 import com.google.common.collect.ImmutableSet
23 import com.google.gson.Gson
24 import com.google.gson.GsonBuilder
25 import org.onap.cps.spi.CpsDataPersistenceService
26 import org.onap.cps.spi.entities.FragmentEntity
27 import org.onap.cps.spi.exceptions.AnchorNotFoundException
28 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
29 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
30 import org.onap.cps.spi.model.DataNode
31 import org.onap.cps.spi.model.DataNodeBuilder
32 import org.springframework.beans.factory.annotation.Autowired
33 import org.springframework.dao.DataIntegrityViolationException
34 import org.springframework.test.context.jdbc.Sql
35 import spock.lang.Unroll
37 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
38 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
40 class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
43 CpsDataPersistenceService objectUnderTest
45 static final Gson GSON = new GsonBuilder().create()
47 static final String SET_DATA = '/data/fragment.sql'
48 static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
49 static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
50 static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
51 static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
52 static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
54 static final DataNode newDataNode = new DataNodeBuilder().build()
55 static DataNode existingDataNode
56 static DataNode existingChildDataNode
58 def expectedLeavesByXpathMap = [
59 '/parent-100' : ['parent-leaf': 'parent-leaf-value'],
60 '/parent-100/child-001' : ['first-child-leaf': 'first-child-leaf-value'],
61 '/parent-100/child-002' : ['second-child-leaf': 'second-child-leaf-value'],
62 '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf-value']
66 existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
67 existingChildDataNode = createDataNodeTree('/parent-1/child-1')
70 @Sql([CLEAR_DATA, SET_DATA])
71 def 'StoreDataNode with descendants.'() {
72 when: 'a fragment with descendants is stored'
73 def parentXpath = "/parent-new"
74 def childXpath = "/parent-new/child-new"
75 def grandChildXpath = "/parent-new/child-new/grandchild-new"
76 objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
77 createDataNodeTree(parentXpath, childXpath, grandChildXpath))
78 then: 'it can be retrieved by its xpath'
79 def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
80 and: 'it contains the children'
81 parentFragment.childFragments.size() == 1
82 def childFragment = parentFragment.childFragments[0]
83 childFragment.xpath == childXpath
84 and: "and its children's children"
85 childFragment.childFragments.size() == 1
86 def grandchildFragment = childFragment.childFragments[0]
87 grandchildFragment.xpath == grandChildXpath
91 @Sql([CLEAR_DATA, SET_DATA])
92 def 'Store datanode error scenario: #scenario.'() {
93 when: 'attempt to store a data node with #scenario'
94 objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
95 then: 'a #expectedException is thrown'
96 thrown(expectedException)
97 where: 'the following data is used'
98 scenario | dataspaceName | anchorName | dataNode || expectedException
99 'dataspace does not exist' | 'unknown' | 'not-relevant' | newDataNode || DataspaceNotFoundException
100 'schema set does not exist' | DATASPACE_NAME | 'unknown' | newDataNode || AnchorNotFoundException
101 'anchor already exists' | DATASPACE_NAME | ANCHOR_NAME1 | existingDataNode || DataIntegrityViolationException
104 @Sql([CLEAR_DATA, SET_DATA])
105 def 'Add a child to a Fragment that already has a child.'() {
106 given: ' a new child node'
107 def newChild = createDataNodeTree('xpath for new child')
108 when: 'the child is added to an existing parent with 1 child'
109 objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
110 then: 'the parent is now has to 2 children'
111 def expectedExistingChildPath = '/parent-1/child-1'
112 def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
113 parentFragment.getChildFragments().size() == 2
114 and: 'it still has the old child'
115 parentFragment.getChildFragments().find({ it.xpath == expectedExistingChildPath })
116 and: 'it has the new child'
117 parentFragment.getChildFragments().find({ it.xpath == newChild.xpath })
121 @Sql([CLEAR_DATA, SET_DATA])
122 def 'Add child error scenario: #scenario.'() {
123 when: 'attempt to add a child data node with #scenario'
124 objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
125 then: 'a #expectedException is thrown'
126 thrown(expectedException)
127 where: 'the following data is used'
128 scenario | parentXpath | dataNode || expectedException
129 'parent does not exist' | 'unknown' | newDataNode || DataNodeNotFoundException
130 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || DataIntegrityViolationException
133 static def createDataNodeTree(String... xpaths) {
134 def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
135 if (xpaths.length > 1) {
136 def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
137 def childDataNode = createDataNodeTree(xPathsDescendant)
138 dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
140 dataNodeBuilder.build()
143 def getFragmentByXpath(dataspaceName, anchorName, xpath) {
144 def dataspace = dataspaceRepository.getByName(dataspaceName)
145 def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
146 return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
149 @Sql([CLEAR_DATA, SET_DATA])
150 def 'Get data node by xpath without descendants.'() {
151 when: 'data node is requested'
152 def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
153 XPATH_DATA_NODE_WITH_LEAVES, OMIT_DESCENDANTS)
154 then: 'data node is returned with no descendants'
155 assert result.getXpath() == XPATH_DATA_NODE_WITH_LEAVES
156 and: 'expected leaves'
157 assert result.getChildDataNodes().size() == 0
158 assertLeavesMaps(result.getLeaves(), expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
161 @Sql([CLEAR_DATA, SET_DATA])
162 def 'Get data node by xpath with all descendants.'() {
163 when: 'data node is requested with all descendants'
164 def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
165 XPATH_DATA_NODE_WITH_LEAVES, INCLUDE_ALL_DESCENDANTS)
166 def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
167 then: 'data node is returned with all the descendants populated'
168 assert mappedResult.size() == 4
169 assert result.getChildDataNodes().size() == 2
170 assert mappedResult.get('/parent-100/child-001').getChildDataNodes().size() == 0
171 assert mappedResult.get('/parent-100/child-002').getChildDataNodes().size() == 1
172 and: 'extracted leaves maps are matching expected'
173 mappedResult.forEach(
175 assertLeavesMaps(dataNode.getLeaves(), expectedLeavesByXpathMap[xpath])
179 def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
180 expectedLeavesMap.forEach((key, value) -> {
181 def actualValue = actualLeavesMap[key]
182 if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
183 assert value.size() == actualValue.size()
184 assert value.containsAll(actualValue)
186 assert value == actualValue
193 def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
194 flatMap.put(dataNodeTree.getXpath(), dataNodeTree)
195 dataNodeTree.getChildDataNodes()
196 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
201 @Sql([CLEAR_DATA, SET_DATA])
202 def 'Get data node error scenario: #scenario.'() {
203 when: 'attempt to get data node with #scenario'
204 objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
205 then: 'a #expectedException is thrown'
206 thrown(expectedException)
207 where: 'the following data is used'
208 scenario | dataspaceName | anchorName | xpath || expectedException
209 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
210 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
211 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException
214 @Sql([CLEAR_DATA, SET_DATA])
215 def 'Update data node leaves.'() {
216 when: 'update is performed for leaves'
217 objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
218 "/parent-200/child-201", ['leaf-value': 'new'])
219 then: 'leaves are updated for selected data node'
220 def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
221 def updatedLeaves = getLeavesMap(updatedFragment)
222 assert updatedLeaves.size() == 1
223 assert updatedLeaves.'leaf-value' == 'new'
224 and: 'existing child entry remains as is'
225 def childFragment = updatedFragment.getChildFragments().iterator().next()
226 def childLeaves = getLeavesMap(childFragment)
227 assert childFragment.getId() == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
228 assert childLeaves.'leaf-value' == 'original'
232 @Sql([CLEAR_DATA, SET_DATA])
233 def 'Update data leaves error scenario: #scenario.'() {
234 when: 'attempt to update data node for #scenario'
235 objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
236 then: 'a #expectedException is thrown'
237 thrown(expectedException)
238 where: 'the following data is used'
239 scenario | dataspaceName | anchorName | xpath || expectedException
240 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
241 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
242 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
245 @Sql([CLEAR_DATA, SET_DATA])
246 def 'Replace data node tree with descendants removal.'() {
247 given: 'data node object with leaves updated, no children'
248 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
249 when: 'replace data node tree is performed'
250 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
251 then: 'leaves have been updated for selected data node'
252 def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
253 def updatedLeaves = getLeavesMap(updatedFragment)
254 assert updatedLeaves.size() == 1
255 assert updatedLeaves.'leaf-value' == 'new'
256 and: 'updated entry has no children'
257 updatedFragment.getChildFragments().isEmpty()
258 and: 'previously attached child entry is removed from database'
259 fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
262 @Sql([CLEAR_DATA, SET_DATA])
263 def 'Replace data node tree with descendants.'() {
264 given: 'data node object with leaves updated, having child with old content'
265 def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
266 buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
268 when: 'update is performed including descendants'
269 objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
270 then: 'leaves have been updated for selected data node'
271 def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
272 def updatedLeaves = getLeavesMap(updatedFragment)
273 assert updatedLeaves.size() == 1
274 assert updatedLeaves.'leaf-value' == 'new'
275 and: 'previously attached child entry is removed from database'
276 fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
277 and: 'new child entry with same content is created'
278 def childFragment = updatedFragment.getChildFragments().iterator().next()
279 def childLeaves = getLeavesMap(childFragment)
280 assert childFragment.getId() != UPDATE_DATA_NODE_SUB_FRAGMENT_ID
281 assert childLeaves.'leaf-value' == 'original'
285 @Sql([CLEAR_DATA, SET_DATA])
286 def 'Replace data node tree error scenario: #scenario.'() {
287 given: 'data node object'
288 def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
289 when: 'attempt to update data node for #scenario'
290 objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
291 then: 'a #expectedException is thrown'
292 thrown(expectedException)
293 where: 'the following data is used'
294 scenario | dataspaceName | anchorName | xpath || expectedException
295 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException
296 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException
297 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
300 static DataNode buildDataNode(xpath, leaves, childDataNodes) {
301 return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
304 static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
305 return GSON.fromJson(fragmentEntity.getAttributes(), Map<String, Object>.class)
309 @Sql([CLEAR_DATA, SET_DATA])
310 def 'Cps Path query for single leaf value with type: #type.'() {
311 when: 'a query is executed to get a data node by the given cps path'
312 def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath)
313 then: 'the correct data is returned'
314 def leaves ='[common-leaf-name:common-leaf-value, common-leaf-name-int:5.0]'
316 def dataNode = result.stream().findFirst().get()
317 dataNode.getLeaves().toString() == leaves
318 where: 'the following data is used'
320 'String' | '/parent-200/child-202[@common-leaf-name=\'common-leaf-value\']'
321 'Integer' | '/parent-200/child-202[@common-leaf-name-int=5]'
325 @Sql([CLEAR_DATA, SET_DATA])
326 def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() {
327 when: 'a query is executed to get datanodes for the given cps path'
328 def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath)
329 then: 'no data is returned'
331 where: 'following cps queries are performed'
333 'cps path is incomplete' | '/parent-200[@common-leaf-name-int=5]'
334 'missing / at beginning of path' | 'parent-200/child-202[@common-leaf-name-int=5]'
335 'leaf value does not exist' | '/parent-200/child-202[@common-leaf-name=\'does not exist\']'
336 'incomplete end of xpath prefix' | '/parent-200/child-20[@common-leaf-name-int=5]'