From 1f77f638a8da07d766c8c6d276e7170b24828f85 Mon Sep 17 00:00:00 2001 From: Ruslan Kashapov Date: Mon, 1 Feb 2021 10:47:25 +0200 Subject: [PATCH] Fetching data node by xpath - persistence layer IssueID: CPS-71 Change-Id: I88f76cf36ef8a1e4ccbd4f1eac8867e93ed5be82 Signed-off-by: Ruslan Kashapov --- .../spi/impl/CpsDataPersistenceServiceImpl.java | 42 ++++++- .../spi/impl/CpsDataPersistenceServiceSpec.groovy | 124 +++++++++++++++------ .../cps/spi/impl/CpsPersistenceSpecBase.groovy | 6 + cps-ri/src/test/resources/data/fragment.sql | 11 +- .../onap/cps/spi/CpsDataPersistenceService.java | 14 +++ .../org/onap/cps/spi/FetchDescendantsOption.java | 25 +++++ .../org/onap/cps/spi/model/DataNodeBuilder.java | 14 +++ 7 files changed, 197 insertions(+), 39 deletions(-) create mode 100644 cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java 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 c73b65ddd..d8f3df112 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 leaves = GSON.fromJson(fragmentEntity.getAttributes(), Map.class); + final List childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption); + return new DataNodeBuilder() + .withXpath(fragmentEntity.getXpath()) + .withLeaves(leaves) + .withChildDataNodes(childDataNodes).build(); + } + + private static List 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 03e352a87..e3fa88530 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,39 +42,24 @@ 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> 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' @@ -77,9 +67,9 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { 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 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 54807efd2..c8a8b9bf1 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 @@ -42,6 +44,9 @@ class CpsPersistenceSpecBase extends Specification { @Autowired YangResourceRepository yangResourceRepository + @Autowired + AnchorRepository anchorRepository + @Autowired FragmentRepository fragmentRepository @@ -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 05f5bfe45..e65270326 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 diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index d59fa4746..97aecaafd 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -1,6 +1,7 @@ /*- * ============LICENSE_START======================================================= * Copyright (C) 2020 Nordix Foundation. All rights reserved. + * 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. @@ -50,4 +51,17 @@ public interface CpsDataPersistenceService { */ void addChildDataNode(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentXpath, @NonNull DataNode dataNode); + + /** + * Retrieves datanode by XPath for given dataspace and anchor. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param xpath xpath + * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes + * (recursively) as well + * @return data node object + */ + DataNode getDataNode(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String xpath, + @NonNull FetchDescendantsOption fetchDescendantsOption); } diff --git a/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java b/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java new file mode 100644 index 000000000..0c994d8d7 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java @@ -0,0 +1,25 @@ +/* + * ============LICENSE_START======================================================= + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.spi; + +public enum FetchDescendantsOption { + OMIT_DESCENDANTS, + INCLUDE_ALL_DESCENDANTS +} diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java index d187f62e0..67e93dd82 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Bell Canada. All rights reserved. + * 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. @@ -42,6 +43,7 @@ public class DataNodeBuilder { private NormalizedNode normalizedNodeTree; private String xpath; + private Map leaves = Collections.emptyMap(); private Collection childDataNodes = Collections.emptySet(); @@ -67,6 +69,17 @@ public class DataNodeBuilder { return this; } + /** + * To use attributes for creating {@link DataNode}. + * + * @param leaves for the data node + * @return DataNodeBuilder + */ + public DataNodeBuilder withLeaves(final Map leaves) { + this.leaves = leaves; + return this; + } + /** * To specify child nodes needs to be used while creating {@link DataNode}. * @@ -96,6 +109,7 @@ public class DataNodeBuilder { private DataNode buildFromAttributes() { final DataNode dataNode = new DataNode(); dataNode.setXpath(xpath); + dataNode.setLeaves(leaves); dataNode.setChildDataNodes(childDataNodes); return dataNode; } -- 2.16.6