Fetching data node by xpath - persistence layer 94/117294/5
authorRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Mon, 1 Feb 2021 08:47:25 +0000 (10:47 +0200)
committerRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Thu, 4 Feb 2021 05:50:59 +0000 (07:50 +0200)
IssueID: CPS-71
Change-Id: I88f76cf36ef8a1e4ccbd4f1eac8867e93ed5be82
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsPersistenceSpecBase.groovy
cps-ri/src/test/resources/data/fragment.sql
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java

index c73b65d..d8f3df1 100644 (file)
@@ -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.
 
 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();
+    }
 }
index 03e352a..e3fa885 100644 (file)
@@ -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<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'
@@ -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<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
+    }
 }
index 54807ef..c8a8b9b 100644 (file)
@@ -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'
 
 }
index 05f5bfe..e652703 100644 (file)
@@ -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
index d59fa47..97aecaa 100644 (file)
@@ -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 (file)
index 0000000..0c994d8
--- /dev/null
@@ -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
+}
index d187f62..67e93dd 100644 (file)
@@ -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<String, Object> leaves = Collections.emptyMap();
     private Collection<DataNode> 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<String, Object> 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;
     }