From d1ea2af79952a41264552247441410fc24cd48f5 Mon Sep 17 00:00:00 2001 From: Ruslan Kashapov Date: Mon, 8 Feb 2021 11:02:39 +0200 Subject: Data fragment update by xpath #2 - persistence layer Issue-ID: CPS-58 Change-Id: Ifc4580936d06c6907d6b5ab20657063b6707ccbe Signed-off-by: Ruslan Kashapov --- .../org/onap/cps/spi/entities/FragmentEntity.java | 3 +- .../spi/impl/CpsDataPersistenceServiceImpl.java | 49 +++++++-- .../spi/impl/CpsDataPersistenceServiceSpec.groovy | 117 +++++++++++++++++++-- cps-ri/src/test/resources/data/fragment.sql | 13 ++- .../onap/cps/spi/CpsDataPersistenceService.java | 22 ++++ .../java/org/onap/cps/spi/FetchChildrenOption.java | 25 ----- 6 files changed, 182 insertions(+), 47 deletions(-) delete mode 100644 cps-service/src/main/java/org/onap/cps/spi/FetchChildrenOption.java diff --git a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java index 677652899d..53ceaaae1f 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntity.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. @@ -83,7 +84,7 @@ public class FragmentEntity implements Serializable { @JoinColumn(name = "anchor_id") private AnchorEntity anchor; - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Set childFragments; } 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 d8f3df112f..fd4e768cab 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 @@ -29,6 +29,7 @@ import com.google.gson.GsonBuilder; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; @@ -60,12 +61,10 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath, final DataNode dataNode) { - final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); - final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); - final FragmentEntity parentFragment = - fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, parentXpath); - final FragmentEntity childFragment = toFragmentEntity(dataspaceEntity, anchorEntity, dataNode); - parentFragment.getChildFragments().add(childFragment); + final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentXpath); + final FragmentEntity fragmentEntity = + toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode); + parentFragment.getChildFragments().add(fragmentEntity); fragmentRepository.save(parentFragment); } @@ -114,11 +113,15 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath, final FetchDescendantsOption fetchDescendantsOption) { + final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath); + return toDataNode(fragmentEntity, fetchDescendantsOption); + } + + private FragmentEntity getFragmentByXpath(final String dataspaceName, final String anchorName, + final String xpath) { 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); + return fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, xpath); } private static DataNode toDataNode(final FragmentEntity fragmentEntity, @@ -140,4 +143,32 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } return Collections.emptyList(); } + + @Override + public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath, + final Map leaves) { + final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath); + fragmentEntity.setAttributes(GSON.toJson(leaves)); + fragmentRepository.save(fragmentEntity); + } + + @Override + public void replaceDataNodeTree(final String dataspaceName, final String anchorName, final DataNode dataNode) { + final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, dataNode.getXpath()); + removeExistingDescendants(fragmentEntity); + + fragmentEntity.setAttributes(GSON.toJson(dataNode.getLeaves())); + final Set childFragmentEntities = dataNode.getChildDataNodes().stream().map( + childDataNode -> convertToFragmentWithAllDescendants( + fragmentEntity.getDataspace(), fragmentEntity.getAnchor(), childDataNode) + ).collect(Collectors.toUnmodifiableSet()); + fragmentEntity.setChildFragments(childFragmentEntities); + + fragmentRepository.save(fragmentEntity); + } + + private void removeExistingDescendants(final FragmentEntity fragmentEntity) { + fragmentEntity.setChildFragments(Collections.emptySet()); + fragmentRepository.save(fragmentEntity); + } } 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 e3fa885301..a6e4701366 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 @@ -19,8 +19,14 @@ */ package org.onap.cps.spi.impl +import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS +import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS + import com.google.common.collect.ImmutableSet +import com.google.gson.Gson +import com.google.gson.GsonBuilder import org.onap.cps.spi.CpsDataPersistenceService +import org.onap.cps.spi.entities.FragmentEntity import org.onap.cps.spi.exceptions.AnchorNotFoundException import org.onap.cps.spi.exceptions.DataNodeNotFoundException import org.onap.cps.spi.exceptions.DataspaceNotFoundException @@ -31,28 +37,29 @@ 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 { @Autowired CpsDataPersistenceService objectUnderTest + static final Gson GSON = new GsonBuilder().create() + 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 long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L + static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L 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"]] + def expectedLeavesByXpathMap = [ + '/parent-100' : ['parent-leaf': 'parent-leaf-value'], + '/parent-100/child-001' : ['first-child-leaf': 'first-child-leaf-value'], + '/parent-100/child-002' : ['second-child-leaf': 'second-child-leaf-value'], + '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf-value'] ] static { @@ -201,4 +208,98 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | 'not relevant' || AnchorNotFoundException 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH' || DataNodeNotFoundException } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Update data node leaves.'() { + when: 'update is performed for leaves' + objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, + "/parent-200/child-201", ['leaf-value': 'new']) + then: 'leaves are updated for selected data node' + def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID) + def updatedLeaves = getLeavesMap(updatedFragment) + assert updatedLeaves.size() == 1 + assert updatedLeaves.'leaf-value' == 'new' + and: 'existing child entry remains as is' + def childFragment = updatedFragment.getChildFragments().iterator().next() + def childLeaves = getLeavesMap(childFragment) + assert childFragment.getId() == UPDATE_DATA_NODE_SUB_FRAGMENT_ID + assert childLeaves.'leaf-value' == 'original' + } + + @Unroll + @Sql([CLEAR_DATA, SET_DATA]) + def 'Update data leaves error scenario: #scenario.'() { + when: 'attempt to update data node for #scenario' + objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value']) + 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 | 'NON-EXISTING XPATH' || DataNodeNotFoundException + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Replace data node tree with descendants removal.'() { + given: 'data node object with leaves updated, no children' + def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], []) + when: 'replace data node tree is performed' + objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode) + then: 'leaves have been updated for selected data node' + def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID) + def updatedLeaves = getLeavesMap(updatedFragment) + assert updatedLeaves.size() == 1 + assert updatedLeaves.'leaf-value' == 'new' + and: 'updated entry has no children' + updatedFragment.getChildFragments().isEmpty() + and: 'previously attached child entry is removed from database' + fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty() + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Replace data node tree with descendants.'() { + given: 'data node object with leaves updated, having child with old content' + def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [ + buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], []) + ]) + when: 'update is performed including descendants' + objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode) + then: 'leaves have been updated for selected data node' + def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID) + def updatedLeaves = getLeavesMap(updatedFragment) + assert updatedLeaves.size() == 1 + assert updatedLeaves.'leaf-value' == 'new' + and: 'previously attached child entry is removed from database' + fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty() + and: 'new child entry with same content is created' + def childFragment = updatedFragment.getChildFragments().iterator().next() + def childLeaves = getLeavesMap(childFragment) + assert childFragment.getId() != UPDATE_DATA_NODE_SUB_FRAGMENT_ID + assert childLeaves.'leaf-value' == 'original' + } + + @Unroll + @Sql([CLEAR_DATA, SET_DATA]) + def 'Replace data node tree error scenario: #scenario.'() { + given: 'data node object' + def submittedDataNode = buildDataNode(xpath, ['leaf-name':'leaf-value'], []) + when: 'attempt to update data node for #scenario' + objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode) + 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 | 'NON-EXISTING XPATH' || DataNodeNotFoundException + } + + static DataNode buildDataNode(xpath, leaves, childDataNodes) { + return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build() + } + + static Map getLeavesMap(FragmentEntity fragmentEntity) { + return GSON.fromJson(fragmentEntity.getAttributes(), Map.class) + } } diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index e652703268..95991462a3 100644 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -17,7 +17,12 @@ INSERT INTO FRAGMENT (ID, XPATH, ANCHOR_ID, PARENT_ID, DATASPACE_ID) VALUES (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 + (4101, 1001, 3003, null, '/parent-100', '{"parent-leaf": "parent-leaf-value"}'), + (4102, 1001, 3003, 4101, '/parent-100/child-001', '{"first-child-leaf": "first-child-leaf-value"}'), + (4103, 1001, 3003, 4101, '/parent-100/child-002', '{"second-child-leaf": "second-child-leaf-value"}'), + (4104, 1001, 3003, 4103, '/parent-100/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf-value"}'); + +INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES + (4201, 1001, 3003, null, '/parent-200', '{"leaf-value": "original"}'), + (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'), + (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'); \ 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 97aecaafd1..9a0c180a8f 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 @@ -21,6 +21,7 @@ package org.onap.cps.spi; +import java.util.Map; import org.checkerframework.checker.nullness.qual.NonNull; import org.onap.cps.spi.model.DataNode; @@ -64,4 +65,25 @@ public interface CpsDataPersistenceService { */ DataNode getDataNode(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String xpath, @NonNull FetchDescendantsOption fetchDescendantsOption); + + + /** + * Updates leaves for existing data node. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param xpath xpath + * @param leaves the leaves as a map where key is a leaf name and a value is a leaf value + */ + void updateDataLeaves(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String xpath, + @NonNull Map leaves); + + /** + * Replaces existing data node content including descendants. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param dataNode data node + */ + void replaceDataNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull DataNode dataNode); } diff --git a/cps-service/src/main/java/org/onap/cps/spi/FetchChildrenOption.java b/cps-service/src/main/java/org/onap/cps/spi/FetchChildrenOption.java deleted file mode 100644 index 97712c128c..0000000000 --- a/cps-service/src/main/java/org/onap/cps/spi/FetchChildrenOption.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * ============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 FetchChildrenOption { - OMIT_CHILDREN, - INCLUDE_ALL_CHILDREN -} -- cgit 1.2.3-korg