diff options
author | Ruslan Kashapov <ruslan.kashapov@pantheon.tech> | 2021-02-01 10:47:25 +0200 |
---|---|---|
committer | Ruslan Kashapov <ruslan.kashapov@pantheon.tech> | 2021-02-04 07:50:59 +0200 |
commit | 1f77f638a8da07d766c8c6d276e7170b24828f85 (patch) | |
tree | a04c1dbbab701c719d0092357aa852bf4b49926a /cps-ri/src | |
parent | e7f94478a241f8ee7618da08f6bc8495e9f5a504 (diff) |
Fetching data node by xpath - persistence layer
IssueID: CPS-71
Change-Id: I88f76cf36ef8a1e4ccbd4f1eac8867e93ed5be82
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
Diffstat (limited to 'cps-ri/src')
4 files changed, 144 insertions, 39 deletions
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index c73b65ddd8..d8f3df112f 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,15 +20,23 @@ package org.onap.cps.spi.impl; +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; + import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.onap.cps.spi.CpsDataPersistenceService; +import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.entities.AnchorEntity; import org.onap.cps.spi.entities.DataspaceEntity; import org.onap.cps.spi.entities.FragmentEntity; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.spi.model.DataNodeBuilder; import org.onap.cps.spi.repository.AnchorRepository; import org.onap.cps.spi.repository.DataspaceRepository; import org.onap.cps.spi.repository.FragmentRepository; @@ -93,8 +102,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } private static FragmentEntity toFragmentEntity(final DataspaceEntity dataspaceEntity, - final AnchorEntity anchorEntity, - final DataNode dataNode) { + final AnchorEntity anchorEntity, final DataNode dataNode) { return FragmentEntity.builder() .dataspace(dataspaceEntity) .anchor(anchorEntity) @@ -102,4 +110,34 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService .attributes(GSON.toJson(dataNode.getLeaves())) .build(); } + + @Override + public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final FragmentEntity fragmentEntity = + fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath); + return toDataNode(fragmentEntity, fetchDescendantsOption); + } + + private static DataNode toDataNode(final FragmentEntity fragmentEntity, + final FetchDescendantsOption fetchDescendantsOption) { + final Map<String, Object> leaves = GSON.fromJson(fragmentEntity.getAttributes(), Map.class); + final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption); + return new DataNodeBuilder() + .withXpath(fragmentEntity.getXpath()) + .withLeaves(leaves) + .withChildDataNodes(childDataNodes).build(); + } + + private static List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity, + final FetchDescendantsOption fetchDescendantsOption) { + if (fetchDescendantsOption == INCLUDE_ALL_DESCENDANTS) { + return fragmentEntity.getChildFragments().stream() + .map(childFragmentEntity -> toDataNode(childFragmentEntity, fetchDescendantsOption)) + .collect(Collectors.toUnmodifiableList()); + } + return Collections.emptyList(); + } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index 03e352a874..e3fa885301 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +29,10 @@ import org.onap.cps.spi.model.DataNodeBuilder import org.springframework.beans.factory.annotation.Autowired import org.springframework.dao.DataIntegrityViolationException import org.springframework.test.context.jdbc.Sql +import spock.lang.Unroll + +import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { @@ -37,49 +42,34 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { static final String SET_DATA = '/data/fragment.sql' static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001 static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1' + static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100' static final DataNode newDataNode = new DataNodeBuilder().build() static DataNode existingDataNode static DataNode existingChildDataNode + static Map<String, Map<String, Object>> expectedLeavesByXpathMap = [ + '/parent-100' : ["x": "y"], + '/parent-100/child-001' : ["a": "b", "c": ["d", "e", "f"]], + '/parent-100/child-002' : ["g": "h", "i": ["j", "k"]], + '/parent-100/child-002/grand-child': ["l": "m", "n": ["o", "p"]] + ] + static { existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS) existingChildDataNode = createDataNodeTree('/parent-1/child-1') } @Sql([CLEAR_DATA, SET_DATA]) - def 'Get fragment with descendants.'() { - /* - TODO: This test is not really testing the object under test! Needs to be updated as part of CPS-71 - Actually I think this test will become redundant once th store data node tests is asserted using - a new getByXpath() method in the service (object under test) - A lot of preloaded dat will become redundant then too - */ - // - when: 'a fragment is retrieved from the repository' - def fragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow() - then: 'it has the correct xpath' - fragment.xpath == '/parent-1' - and: 'it contains the children' - fragment.childFragments.size() == 1 - def childFragment = fragment.childFragments[0] - childFragment.xpath == '/parent-1/child-1' - and: "and its children's children" - childFragment.childFragments.size() == 1 - def grandchildFragment = childFragment.childFragments[0] - grandchildFragment.xpath == '/parent-1/child-1/grandchild-1' - } - - @Sql([CLEAR_DATA, SET_DATA]) def 'StoreDataNode with descendants.'() { when: 'a fragment with descendants is stored' def parentXpath = "/parent-new" def childXpath = "/parent-new/child-new" def grandChildXpath = "/parent-new/child-new/grandchild-new" objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, - createDataNodeTree(parentXpath, childXpath, grandChildXpath)) + createDataNodeTree(parentXpath, childXpath, grandChildXpath)) then: 'it can be retrieved by its xpath' - def parentFragment = getFragmentByXpath(parentXpath) + def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath) and: 'it contains the children' parentFragment.childFragments.size() == 1 def childFragment = parentFragment.childFragments[0] @@ -91,7 +81,7 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Store datanode error scenario: #scenario.'() { + def 'Store datanode error scenario: #scenario.'() { when: 'attempt to store a data node with #scenario' objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode) then: 'a #expectedException is thrown' @@ -113,14 +103,14 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { def expectedExistingChildPath = '/parent-1/child-1' def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow() parentFragment.getChildFragments().size() == 2 - and : 'it still has the old child' - parentFragment.getChildFragments().find( {it.xpath == expectedExistingChildPath}) - and : 'it has the new child' - parentFragment.getChildFragments().find( {it.xpath == newChild.xpath}) + and: 'it still has the old child' + parentFragment.getChildFragments().find({ it.xpath == expectedExistingChildPath }) + and: 'it has the new child' + parentFragment.getChildFragments().find({ it.xpath == newChild.xpath }) } @Sql([CLEAR_DATA, SET_DATA]) - def 'Add child error scenario: #scenario.'() { + def 'Add child error scenario: #scenario.'() { when: 'attempt to add a child data node with #scenario' objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode) then: 'a #expectedException is thrown' @@ -141,10 +131,74 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { dataNodeBuilder.build() } - def getFragmentByXpath = xpath -> { - //TODO: Remove this method when CPS-71 gets implemented - fragmentRepository.findAll().stream() - .filter(fragment -> fragment.getXpath().contains(xpath)).findAny().orElseThrow() + def getFragmentByXpath(dataspaceName, anchorName, xpath) { + def dataspace = dataspaceRepository.getByName(dataspaceName) + def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName) + return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow() + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Get data node by xpath without descendants.'() { + when: 'data node is requested' + def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, + XPATH_DATA_NODE_WITH_LEAVES, OMIT_DESCENDANTS) + then: 'data node is returned with no descendants' + assert result.getXpath() == XPATH_DATA_NODE_WITH_LEAVES + and: 'expected leaves' + assert result.getChildDataNodes().size() == 0 + assertLeavesMaps(result.getLeaves(), expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES]) } + @Sql([CLEAR_DATA, SET_DATA]) + def 'Get data node by xpath with all descendants.'() { + when: 'data node is requested with all descendants' + def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, + XPATH_DATA_NODE_WITH_LEAVES, INCLUDE_ALL_DESCENDANTS) + def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result) + then: 'data node is returned with all the descendants populated' + assert mappedResult.size() == 4 + assert result.getChildDataNodes().size() == 2 + assert mappedResult.get('/parent-100/child-001').getChildDataNodes().size() == 0 + assert mappedResult.get('/parent-100/child-002').getChildDataNodes().size() == 1 + and: 'extracted leaves maps are matching expected' + mappedResult.forEach( + (xpath, dataNode) -> + assertLeavesMaps(dataNode.getLeaves(), expectedLeavesByXpathMap[xpath]) + ) + } + + def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) { + expectedLeavesMap.forEach((key, value) -> { + def actualValue = actualLeavesMap[key] + if (value instanceof Collection<?> && actualValue instanceof Collection<?>) { + assert value.size() == actualValue.size() + assert value.containsAll(actualValue) + } else { + assert value == actualValue + } + } + ) + return true + } + + def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) { + flatMap.put(dataNodeTree.getXpath(), dataNodeTree) + dataNodeTree.getChildDataNodes() + .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode)) + return flatMap + } + + @Unroll + @Sql([CLEAR_DATA, SET_DATA]) + def 'Get data node error scenario: #scenario.'() { + when: 'attempt to get data node with #scenario' + objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) + then: 'a #expectedException is thrown' + thrown(expectedException) + where: 'the following data is used' + scenario | dataspaceName | anchorName | xpath || expectedException + 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | 'not relevant' || DataspaceNotFoundException + 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException + } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy index 54807efd2b..c8a8b9bf1e 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Nordix Foundation + * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -20,6 +21,7 @@ package org.onap.cps.spi.impl import org.onap.cps.DatabaseTestContainer +import org.onap.cps.spi.repository.AnchorRepository import org.onap.cps.spi.repository.DataspaceRepository import org.onap.cps.spi.repository.FragmentRepository import org.onap.cps.spi.repository.YangResourceRepository @@ -43,6 +45,9 @@ class CpsPersistenceSpecBase extends Specification { YangResourceRepository yangResourceRepository @Autowired + AnchorRepository anchorRepository + + @Autowired FragmentRepository fragmentRepository static final String CLEAR_DATA = '/data/clear-all.sql' @@ -52,5 +57,6 @@ class CpsPersistenceSpecBase extends Specification { static final String SCHEMA_SET_NAME2 = 'SCHEMA-SET-002' static final String ANCHOR_NAME1 = 'ANCHOR-001' static final String ANCHOR_NAME2 = 'ANCHOR-002' + static final String ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003' } diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index 05f5bfe455..e652703268 100644 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -5,7 +5,8 @@ INSERT INTO SCHEMA_SET (ID, NAME, DATASPACE_ID) VALUES (2001, 'SCHEMA-SET-001', 1001); INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES - (3001, 'ANCHOR-001', 1001, 2001); + (3001, 'ANCHOR-001', 1001, 2001), + (3003, 'ANCHOR-003', 1001, 2001); INSERT INTO FRAGMENT (ID, XPATH, ANCHOR_ID, PARENT_ID, DATASPACE_ID) VALUES (4001, '/parent-1', 3001, null, 1001), @@ -13,4 +14,10 @@ INSERT INTO FRAGMENT (ID, XPATH, ANCHOR_ID, PARENT_ID, DATASPACE_ID) VALUES (4003, '/parent-3', 3001, null, 1001), (4004, '/parent-1/child-1', 3001, 4001, 1001), (4005, '/parent-2/child-2', 3001, 4002, 1001), - (4006, '/parent-1/child-1/grandchild-1', 3001, 4004, 1001);
\ No newline at end of file + (4006, '/parent-1/child-1/grandchild-1', 3001, 4004, 1001); + +INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES + (4101, 1001, 3003, null, '/parent-100', '{"x": "y"}'), + (4102, 1001, 3003, 4101, '/parent-100/child-001', '{"a": "b", "c": ["d", "e", "f"]}'), + (4103, 1001, 3003, 4101, '/parent-100/child-002', '{"g": "h", "i": ["j", "k"]}'), + (4104, 1001, 3003, 4103, '/parent-100/child-002/grand-child', '{"l": "m", "n": ["o", "p"]}');
\ No newline at end of file |