diff options
author | 2024-11-07 15:33:39 +0530 | |
---|---|---|
committer | 2025-03-12 09:34:37 +0530 | |
commit | 646caa6ccba0eda7d9a681761ae03df5d2c45f1d (patch) | |
tree | 35e488e6860c6629d32c350b37178859cb8eae24 /cps-service/src | |
parent | ab96e052a6725ad0cc5e56ae69461c352c83551f (diff) |
Refactor buildDataNodes to a separate service
- Moved the code for buildDataNodes from CpsDataServiceImpl.java to a
separate service named DataNodeBuilderService.java
- Renamed the methods to be clear and in-line with their intended use in
DataNodeBuilderService class
- Moved ROOT_NODE_XPATH and NO_PARENT_PATH to CpsPathUtils
Issue-ID: CPS-2487
Change-Id: I46cf843ab79b1e2547d968fbd30528270b95cc16
Signed-off-by: Arpit Singh <AS00745003@techmahindra.com>
Diffstat (limited to 'cps-service/src')
6 files changed, 441 insertions, 145 deletions
diff --git a/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java b/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java new file mode 100644 index 0000000000..1e3410c7f4 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/DataNodeFactory.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.api; + +import java.util.Collection; +import java.util.Map; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.utils.ContentType; + +public interface DataNodeFactory { + + /** + * Create data nodes using an anchor, xpath, and JSON/XML string. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param xpath xpath of the data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorXpathAndNodeData(Anchor anchor, String xpath, + String nodeData, ContentType contentType); + + /** + * Create data nodes using an anchor, parent data node xpath, and JSON/XML string. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param parentNodeXpath xpath of the parent data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorParentXpathAndNodeData(Anchor anchor, + String parentNodeXpath, + String nodeData, + ContentType contentType); + + /** + * Create data nodes using a map of xpath to JSON/XML data, and anchor name. + * + * @param anchor name of Anchor sharing same schema structure as the JSON/XML string + * @param nodesData map of xpath and node JSON/XML data + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithAnchorAndXpathToNodeData(Anchor anchor, + Map<String, String> nodesData, + ContentType contentType); + + /** + * Create data nodes using a map of YANG resource name to content, xpath, and JSON/XML string. + * + * @param yangResourcesNameToContentMap map of YANG resource name to content + * @param xpath xpath of the data node + * @param nodeData JSON/XML data string + * @param contentType JSON or XML content type + * @return a collection of {@link DataNode} + */ + Collection<DataNode> createDataNodesWithYangResourceXpathAndNodeData( + Map<String, String> yangResourcesNameToContentMap, + String xpath, String nodeData, + ContentType contentType); + +} diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java index 9f70ac9132..ab6f7a2df8 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java @@ -3,7 +3,7 @@ * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,9 @@ package org.onap.cps.impl; +import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; + import io.micrometer.core.annotation.Timed; import java.io.Serializable; import java.time.OffsetDateTime; @@ -39,7 +42,7 @@ import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsDeltaService; -import org.onap.cps.api.exceptions.DataValidationException; +import org.onap.cps.api.DataNodeFactory; import org.onap.cps.api.model.Anchor; import org.onap.cps.api.model.DataNode; import org.onap.cps.api.model.DeltaReport; @@ -54,7 +57,6 @@ import org.onap.cps.utils.DataMapUtils; import org.onap.cps.utils.JsonObjectMapper; import org.onap.cps.utils.PrefixResolver; import org.onap.cps.utils.YangParser; -import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.springframework.stereotype.Service; @Service @@ -62,14 +64,12 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class CpsDataServiceImpl implements CpsDataService { - private static final String ROOT_NODE_XPATH = "/"; - private static final String PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH = ""; private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L; - private static final String NO_DATA_NODES = "No data nodes."; private final CpsDataPersistenceService cpsDataPersistenceService; private final CpsDataUpdateEventsService cpsDataUpdateEventsService; private final CpsAnchorService cpsAnchorService; + private final DataNodeFactory dataNodeFactory; private final CpsValidator cpsValidator; private final YangParser yangParser; @@ -90,8 +90,8 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, ROOT_NODE_XPATH, nodeData, contentType); cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.CREATE, observedTimestamp); } @@ -110,8 +110,8 @@ public class CpsDataServiceImpl implements CpsDataService { final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp); } @@ -124,9 +124,9 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> listElementDataNodeCollection = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); - if (isRootNodeXpath(parentNodeXpath)) { + final Collection<DataNode> listElementDataNodeCollection = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection); } else { cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, @@ -163,8 +163,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodesInPatch = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodesInPatch = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); final Map<String, Map<String, Serializable>> xpathToUpdatedLeaves = dataNodesInPatch.stream() .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)); cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves); @@ -180,8 +180,9 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodeUpdates = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); + final Collection<DataNode> dataNodeUpdates = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson, + ContentType.JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(anchor, dataNodeUpdate); } @@ -256,8 +257,8 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp); } @@ -266,13 +267,14 @@ public class CpsDataServiceImpl implements CpsDataService { @Timed(value = "cps.data.service.datanode.descendants.batch.update", description = "Time taken to update a batch of data nodes and descendants") public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, - final Map<String, String> nodeDataPerXPath, + final Map<String, String> nodeDataPerParentNodeXPath, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, nodeDataPerXPath, contentType); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorAndXpathToNodeData(anchor, nodeDataPerParentNodeXPath, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); - nodeDataPerXPath.keySet().forEach(nodeXpath -> + nodeDataPerParentNodeXPath.keySet().forEach(nodeXpath -> sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp)); } @@ -283,8 +285,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection<DataNode> newListElements = - buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); + final Collection<DataNode> newListElements = dataNodeFactory + .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); } @@ -362,7 +364,7 @@ public class CpsDataServiceImpl implements CpsDataService { public void validateData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final ContentType contentType) { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH : + final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? NO_PARENT_PATH : CpsPathUtil.getNormalizedXpath(parentNodeXpath); yangParser.validateData(contentType, nodeData, anchor, xpath); } @@ -373,8 +375,8 @@ public class CpsDataServiceImpl implements CpsDataService { final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>(); if (sourceDataNodes != null) { final String sourceDataNodesAsJson = getDataNodesAsJson(sourceAnchor, sourceDataNodes); - sourceDataNodesRebuilt.addAll( - buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON)); + sourceDataNodesRebuilt.addAll(dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData( + sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON)); } return sourceDataNodesRebuilt; } @@ -383,10 +385,12 @@ public class CpsDataServiceImpl implements CpsDataService { final Map<String, String> yangResourceContentPerName, final String targetData) { if (yangResourceContentPerName.isEmpty()) { - return buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, targetData, ContentType.JSON); + return dataNodeFactory + .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, ContentType.JSON); } else { - return buildDataNodesWithYangResourceAndXpath(yangResourceContentPerName, xpath, - targetData, ContentType.JSON); + return dataNodeFactory + .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, + targetData, ContentType.JSON); } } @@ -415,105 +419,6 @@ public class CpsDataServiceImpl implements CpsDataService { return prefixToDataNodes; } - private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, - final Map<String, String> nodesJsonData, - final ContentType contentType) { - final Collection<DataNode> dataNodes = new ArrayList<>(); - for (final Map.Entry<String, String> nodeJsonData : nodesJsonData.entrySet()) { - dataNodes.addAll(buildDataNodesWithParentNodeXpath(anchor, nodeJsonData.getKey(), - nodeJsonData.getValue(), contentType)); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, final String parentNodeXpath, - final String nodeData, final ContentType contentType) { - - if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - anchor, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "No data nodes provided"); - } - return dataNodes; - } - final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); - final ContainerNode containerNode = - yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withParentNodeXpath(normalizedParentNodeXpath) - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "No data nodes provided"); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithParentNodeXpath( - final Map<String, String> yangResourceContentPerName, final String xpath, - final String nodeData, final ContentType contentType) { - - if (isRootNodeXpath(xpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - yangResourceContentPerName, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "Data nodes were not found under the xpath " + xpath); - } - return dataNodes; - } - final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(xpath); - final ContainerNode containerNode = - yangParser.parseData(contentType, nodeData, yangResourceContentPerName, normalizedParentNodeXpath); - final Collection<DataNode> dataNodes = new DataNodeBuilder() - .withParentNodeXpath(normalizedParentNodeXpath) - .withContainerNode(containerNode) - .buildCollection(); - if (dataNodes.isEmpty()) { - throw new DataValidationException(NO_DATA_NODES, "Data nodes were not found under the xpath " + xpath); - } - return dataNodes; - } - - private Collection<DataNode> buildDataNodesWithAnchorAndXpath(final Anchor anchor, final String xpath, - final String nodeData, - final ContentType contentType) { - - if (!isRootNodeXpath(xpath)) { - final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); - if (parentNodeXpath.isEmpty()) { - return buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(anchor, xpath, nodeData, contentType); - } - - private Collection<DataNode> buildDataNodesWithYangResourceAndXpath( - final Map<String, String> yangResourceContentPerName, final String xpath, - final String nodeData, final ContentType contentType) { - if (!isRootNodeXpath(xpath)) { - final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); - if (parentNodeXpath.isEmpty()) { - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, ROOT_NODE_XPATH, - nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, parentNodeXpath, - nodeData, contentType); - } - return buildDataNodesWithParentNodeXpath(yangResourceContentPerName, xpath, nodeData, contentType); - } - - private static boolean isRootNodeXpath(final String xpath) { - return ROOT_NODE_XPATH.equals(xpath); - } - private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) { cpsDataPersistenceService.batchUpdateDataLeaves(anchor.getDataspaceName(), anchor.getName(), Collections.singletonMap(dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves())); diff --git a/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java b/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java new file mode 100644 index 0000000000..76db887c8e --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/impl/DataNodeFactoryImpl.java @@ -0,0 +1,107 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.impl; + +import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.DataNodeFactory; +import org.onap.cps.api.exceptions.DataValidationException; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.utils.ContentType; +import org.onap.cps.utils.YangParser; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DataNodeFactoryImpl implements DataNodeFactory { + + private final YangParser yangParser; + + @Override + public Collection<DataNode> createDataNodesWithAnchorAndXpathToNodeData(final Anchor anchor, + final Map<String, String> nodesDataPerParentNodeXpath, + final ContentType contentType) { + final Collection<DataNode> dataNodes = new ArrayList<>(); + for (final Map.Entry<String, String> nodeDataToParentNodeXpath : nodesDataPerParentNodeXpath.entrySet()) { + dataNodes.addAll(createDataNodesWithAnchorParentXpathAndNodeData(anchor, nodeDataToParentNodeXpath.getKey(), + nodeDataToParentNodeXpath.getValue(), contentType)); + } + return dataNodes; + } + + @Override + public Collection<DataNode> createDataNodesWithAnchorXpathAndNodeData(final Anchor anchor, final String xpath, + final String nodeData, + final ContentType contentType) { + final String xpathToBuildNodes = isRootNodeXpath(xpath) ? NO_PARENT_PATH : + CpsPathUtil.getNormalizedParentXpath(xpath); + final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, xpathToBuildNodes); + return convertToDataNodes(xpathToBuildNodes, containerNode); + } + + @Override + public Collection<DataNode> createDataNodesWithAnchorParentXpathAndNodeData(final Anchor anchor, + final String parentNodeXpath, + final String nodeData, + final ContentType contentType) { + + final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); + return convertToDataNodes(normalizedParentNodeXpath, containerNode); + } + + @Override + public Collection<DataNode> createDataNodesWithYangResourceXpathAndNodeData( + final Map<String, String> yangResourceContentPerName, + final String xpath, final String nodeData, + final ContentType contentType) { + final String normalizedParentNodeXpath = isRootNodeXpath(xpath) ? NO_PARENT_PATH : + CpsPathUtil.getNormalizedParentXpath(xpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, yangResourceContentPerName, normalizedParentNodeXpath); + return convertToDataNodes(normalizedParentNodeXpath, containerNode); + } + + private static Collection<DataNode> convertToDataNodes(final String normalizedParentNodeXpath, + final ContainerNode containerNode) { + final Collection<DataNode> dataNodes = new DataNodeBuilder() + .withParentNodeXpath(normalizedParentNodeXpath) + .withContainerNode(containerNode) + .buildCollection(); + if (dataNodes.isEmpty()) { + throw new DataValidationException("No Data Nodes", "The request did not return any data nodes for xpath " + + normalizedParentNodeXpath); + } + return dataNodes; + } + + private static boolean isRootNodeXpath(final String xpath) { + return ROOT_NODE_XPATH.equals(xpath); + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy index abcda6c696..6b90e557dc 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,9 +68,10 @@ class CpsDataServiceImplSpec extends Specification { def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def mockPrefixResolver = Mock(PrefixResolver) + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, - mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) + dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) def loggingListAppender @@ -107,8 +108,9 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving #scenario data.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') - when: 'save data method is invoked with test-tree #scenario data' + and: 'JSON/XML data is fetched from resource file' def data = TestUtils.getResourceFileContent(dataFile) + when: 'save data method is invoked with test-tree #scenario data' objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -131,7 +133,7 @@ class CpsDataServiceImplSpec extends Specification { assert exceptionThrown.message.startsWith(expectedMessage) where: 'given parameters' scenario | invalidData | contentType || expectedMessage - 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes' + 'no data nodes' | '{}' | ContentType.JSON || 'No Data Nodes' 'invalid json' | '{invalid json' | ContentType.JSON || 'Data Validation Failed' 'invalid xml' | '<invalid xml' | ContentType.XML || 'Data Validation Failed' } @@ -139,8 +141,9 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving list element data fragment under Root node.'() { given: 'schema set for given anchor and dataspace references bookstore model' setupSchemaSetMocks('bookstore.yang') - when: 'save data method is invoked with list element json data' + and: 'JSON data associated with bookstore model' def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}' + when: 'save data method is invoked with list element json data' objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp, ContentType.JSON) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -159,8 +162,8 @@ class CpsDataServiceImplSpec extends Specification { def 'Saving child data fragment under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') - when: 'save data method is invoked with test-tree json data' def jsonData = '{"branch": [{"name": "New"}]}' + when: 'save data method is invoked with test-tree json data' objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree', @@ -169,7 +172,7 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) } - def 'Saving list element data fragment under existing JSON/XML node.'() { + def 'Saving list element data fragment under existing #scenario .'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with list element data' @@ -187,12 +190,13 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) where: - data | contentType - '{"branch": [{"name": "A"}, {"name": "B"}]}' | ContentType.JSON - '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch><branch><name>B</name></branch></test-tree>' | ContentType.XML + scenario | data | contentType + 'JSON data' | '{"branch": [{"name": "A"}, {"name": "B"}]}' | ContentType.JSON + 'XML data' | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch><branch><name>B</name></branch></test-tree>' | ContentType.XML + } - def 'Saving empty list element data fragment for JSON/XML data.'() { + def 'Saving empty list element data fragment for #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with an empty list' @@ -200,9 +204,9 @@ class CpsDataServiceImplSpec extends Specification { then: 'invalid data exception is thrown' thrown(DataValidationException) where: - data | contentType - '{"branch": []}' | ContentType.JSON - '<test-tree><branch></branch></test-tree>' | ContentType.XML + scenario | data | contentType + 'JSON data' | '{"branch": []}' | ContentType.JSON + 'XML data' | '<test-tree><branch></branch></test-tree>' | ContentType.XML } def 'Get all data nodes #scenario.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy new file mode 100644 index 0000000000..082fb33a61 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/impl/DataNodeFactorySpec.groovy @@ -0,0 +1,196 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.impl + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import org.onap.cps.TestUtils +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.exceptions.DataValidationException +import org.onap.cps.api.model.Anchor +import org.onap.cps.utils.ContentType +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper +import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder +import org.onap.cps.yang.YangTextSchemaSourceSet +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import spock.lang.Specification + +class DataNodeFactorySpec extends Specification { + + def mockCpsAnchorService = Mock(CpsAnchorService) + def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) + def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) + def objectUnderTest = new DataNodeFactoryImpl(yangParser) + + def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) + def loggingListAppender + def applicationContext = new AnnotationConfigApplicationContext() + + def dataspaceName = 'some-dataspace' + def anchorName = 'some-anchor' + def schemaSetName = 'some-schema-set' + def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build() + + def setup() { + mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor + logger.setLevel(Level.DEBUG) + loggingListAppender = new ListAppender() + logger.addAppender(loggingListAppender) + loggingListAppender.start() + applicationContext.refresh() + } + + void cleanup() { + ((Logger) LoggerFactory.getLogger(DataNodeFactoryImpl.class)).detachAndStopAllAppenders() + applicationContext.close() + } + + def 'Create data nodes using anchor and map of xpath to #scenario'() { + given:'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorAndXpathToNodeData(anchor, xpathToNodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == expectedDataNodes + and: 'data nodes have expected xpaths' + dataNodes.stream().map { it.getXpath() }.toList().containsAll(expectedXpaths) + where: 'the following data was used' + scenario | xpathToNodeData | contentType || expectedDataNodes | expectedXpaths + 'JSON Data' | ['/' : "{'test-tree': {'branch': []}}", '/test-tree' : "{'branch': [{'name':'Name'}]}"] | ContentType.JSON || 2 | ['/test-tree', "/test-tree/branch[@name='Name']"] + 'XML Data' | ['/test-tree' : '<branch><name>Name</name></branch>'] | ContentType.XML || 1 | ["/test-tree/branch[@name='Name']"] + } + + def 'Create data nodes using anchor, xpath and #scenario string'() { + given:'xpath, json string and schema set for given anchor and dataspace references test-tree model' + def xpath = '/' + def nodeData = TestUtils.getResourceFileContent(data) + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorXpathAndNodeData(anchor, xpath, nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpaths' + dataNodes[0].getXpath() == '/test-tree' + where: 'the following data was used' + scenario | data | contentType + 'JSON' | 'test-tree.json' | ContentType.JSON + 'XML' | 'test-tree.xml' | ContentType.XML + } + + def 'Building data nodes using anchor, xpath and #scenario'() { + given:'xpath, invalid json string and schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithAnchorXpathAndNodeData(anchor, '/test-tree', invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no data nodes' | '{}' | ContentType.JSON || 'No Data Nodes' + 'invalid json' | '{invalid json' | ContentType.JSON || 'Data Validation Failed' + 'invalid xml' | '<invalid xml' | ContentType.XML || 'Data Validation Failed' + } + + def 'Create data nodes using anchor, parent node xpath and #scenario string'() { + given:'parent node xpath, json string and schema set for given anchor and dataspace references test-tree model' + def parentXpath = '/test-tree' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentXpath, nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpaths' + dataNodes[0].getXpath() == "/test-tree/branch[@name='A']" + where: 'the following data was used' + scenario | nodeData | contentType + 'JSON' | '{"branch": [{"name": "A"}]}' | ContentType.JSON + 'XML' | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>A</name></branch></test-tree>' | ContentType.XML + } + + def 'Create data nodes using anchor, parent node xpath and invalid #scenario string'() { + given:'parent node xpath, invalid json string and schema set for given anchor and dataspace references test-tree model' + def parentXpath = '/test-tree' + setupSchemaSetMocks('test-tree.yang') + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentXpath, invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no data nodes' | '{"branch": []}' | ContentType.JSON || 'No Data Nodes' + 'invalid json' | '<test-tree><branch></branch></test-tree>' | ContentType.JSON || 'Data Validation Failed' + } + + def 'Create data nodes using schema, xpath and #scenario string'() { + given:'xpath, json string and schema set for given anchor and dataspace references bookstore model' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + setupSchemaSetMocksForDelta(yangResourcesNameToContentMap) + when: 'attempt to create data nodes' + def dataNodes = objectUnderTest.createDataNodesWithYangResourceXpathAndNodeData(yangResourcesNameToContentMap, '/', nodeData, contentType) + then: 'expected number of data nodes are created' + dataNodes.size() == 1 + and: 'data nodes have expected xpath' + dataNodes[0].getXpath() == '/bookstore' + where: 'the following data was used' + scenario | nodeData | contentType + 'JSON' | '{"bookstore":{"bookstore-name":"Easons"}}' | ContentType.JSON + 'XML' | "<bookstore xmlns=\"org:onap:ccsdk:sample\"><bookstore-name>Easons</bookstore-name></bookstore>" | ContentType.XML + } + + def 'Create data nodes using schema, xpath and invalid #scenario string'() { + given:'xpath, invalid json string and schema set for given anchor and dataspace references bookstore model' + def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + setupSchemaSetMocksForDelta(yangResourcesNameToContentMap) + when: 'attempt to create data nodes' + objectUnderTest.createDataNodesWithYangResourceXpathAndNodeData(yangResourcesNameToContentMap, '/', invalidData, contentType) + then: 'expected number of data nodes are created' + def exceptionThrown = thrown(DataValidationException) + assert exceptionThrown.message.startsWith(expectedMessage) + where: + scenario | invalidData | contentType || expectedMessage + 'no json nodes' | '{}' | ContentType.JSON || 'No Data Nodes' + 'no xml nodes' | '"<bookstore xmlns=\"org:onap:ccsdk:sample\"/>' | ContentType.XML || 'Data Validation Failed' + 'invalid json' | '{invalid' | ContentType.JSON || 'Data Validation Failed' + 'invalid xml' | '<invalid' | ContentType.XML || 'Data Validation Failed' + } + + def setupSchemaSetMocks(String... yangResources) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } + + def setupSchemaSetMocksForDelta(Map<String, String> yangResourcesNameToContentMap) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourcesNameToContentMap) >> mockYangTextSchemaSourceSet + mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy index db5b4f104e..f91570136c 100755 --- a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2021-2025 Nordix Foundation. * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022-2024 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,8 @@ class E2ENetworkSliceSpec extends Specification { mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) - def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) + def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' |