diff options
Diffstat (limited to 'cps-service')
20 files changed, 1042 insertions, 301 deletions
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java b/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java new file mode 100644 index 0000000000..8933f02cb4 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/CpsFacade.java @@ -0,0 +1,96 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * 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.List; +import java.util.Map; +import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.api.parameters.PaginationOption; + +public interface CpsFacade { + + /** + * Get the first data node for a given dataspace, anchor and xpath. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param xpath the xpath + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data node and its descendants + */ + Map<String, Object> getFirstDataNodeByAnchor(String dataspaceName, + String anchorName, + String xpath, + FetchDescendantsOption fetchDescendantsOption); + + /** + * Get data nodes for a given dataspace, anchor and xpath. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param xpath the xpath + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data nodes and their descendants + */ + List<Map<String, Object>> getDataNodesByAnchor(String dataspaceName, + String anchorName, + String xpath, + FetchDescendantsOption fetchDescendantsOption); + + /** + * Query the given anchor using a cps path expression. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param cpsPath the xpath i.e. query + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data nodes and their descendants + */ + List<Map<String, Object>> executeAnchorQuery(String dataspaceName, + String anchorName, + String cpsPath, + FetchDescendantsOption fetchDescendantsOption); + + /** + * Query the given dataspace (all anchors) using a cps path expression. + * + * @param dataspaceName the name of the dataspace + * @param cpsPath the xpath i.e. query + * @param fetchDescendantsOption control what level of descendants should be returned + * @return a map representing the data nodes and their descendants + */ + List<Map<String, Object>> executeDataspaceQuery(String dataspaceName, + String cpsPath, + FetchDescendantsOption fetchDescendantsOption, + PaginationOption paginationOption); + + /** + * Query how many anchors wil be returned for the given dataspace and a cps path query. + * + * @param dataspaceName the name of the dataspace + * @param cpsPath the xpath i.e. query + * @param paginationOption the options for pagination + * @return the number of anchors involved in the output + */ + int countAnchorsInDataspaceQuery(String dataspaceName, + String cpsPath, + PaginationOption paginationOption); +} diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java index 30c8bbbdf3..d6c1f7fc60 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsQueryService.java @@ -74,6 +74,19 @@ public interface CpsQueryService { <T> Set<T> queryDataLeaf(String dataspaceName, String anchorName, String cpsPath, Class<T> targetClass); /** + * Get data leaf for the given dataspace and anchor by cps path. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param cpsPath cps path + * @param queryResultLimit the maximum number of data nodes to return; if less than 1, returns all matching nodes + * @param targetClass class of the expected data type + * @return a collection of data objects of expected type + */ + <T> Set<T> queryDataLeaf(String dataspaceName, String anchorName, String cpsPath, int queryResultLimit, + Class<T> targetClass); + + /** * Get data nodes for the given dataspace across all anchors by cps path. * * @param dataspaceName dataspace name 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/api/parameters/FetchDescendantsOption.java b/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java index 46022ba46b..05fa366239 100644 --- a/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java +++ b/cps-service/src/main/java/org/onap/cps/api/parameters/FetchDescendantsOption.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2025 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ package org.onap.cps.api.parameters; import com.google.common.base.Strings; import java.util.regex.Matcher; import java.util.regex.Pattern; +import lombok.Getter; import lombok.RequiredArgsConstructor; import org.onap.cps.api.exceptions.DataValidationException; @@ -44,6 +45,12 @@ public class FetchDescendantsOption { private static final Pattern FETCH_DESCENDANTS_OPTION_PATTERN = Pattern.compile("^$|^all$|^none$|^direct$|^[0-9]+$|^-1$|^1$"); + /** + * Get depth. + * + * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants + */ + @Getter private final int depth; private final String optionName; @@ -76,15 +83,7 @@ public class FetchDescendantsOption { } /** - * Get depth. - * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants - */ - public int getDepth() { - return depth; - } - - /** - * get fetch descendants option for given descendant. + * Convert fetch descendants option from string to enum with depth. * * @param fetchDescendantsOptionAsString fetch descendants option string * @return fetch descendants option for given descendant @@ -99,11 +98,22 @@ public class FetchDescendantsOption { } else if ("1".equals(fetchDescendantsOptionAsString) || "direct".equals(fetchDescendantsOptionAsString)) { return FetchDescendantsOption.DIRECT_CHILDREN_ONLY; } else { - final Integer depth = Integer.valueOf(fetchDescendantsOptionAsString); + final int depth = Integer.parseInt(fetchDescendantsOptionAsString); return new FetchDescendantsOption(depth); } } + /** + * Convert include all-descendants boolean parameter to FetchDescendantsOption enum. + * + * @param includedDescendantsOptionAsBoolean fetch descendants option as Boolean + * @return fetch descendants option for given descendant + */ + public static FetchDescendantsOption getFetchDescendantsOption(final Boolean includedDescendantsOptionAsBoolean) { + return Boolean.TRUE.equals(includedDescendantsOptionAsBoolean) + ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS; + } + @Override public String toString() { return optionName; 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..a93bf9ac82 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 @@ -1,9 +1,9 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 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,13 +24,16 @@ 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 static org.onap.cps.utils.ContentType.JSON; + import io.micrometer.core.annotation.Timed; import java.io.Serializable; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -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; @@ -50,11 +53,9 @@ import org.onap.cps.events.model.Data.Operation; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.utils.ContentType; import org.onap.cps.utils.CpsValidator; -import org.onap.cps.utils.DataMapUtils; +import org.onap.cps.utils.DataMapper; 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,36 +63,33 @@ 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; private final CpsDeltaService cpsDeltaService; + private final DataMapper dataMapper; private final JsonObjectMapper jsonObjectMapper; - private final PrefixResolver prefixResolver; @Override public void saveData(final String dataspaceName, final String anchorName, final String nodeData, final OffsetDateTime observedTimestamp) { - saveData(dataspaceName, anchorName, nodeData, observedTimestamp, ContentType.JSON); + saveData(dataspaceName, anchorName, nodeData, observedTimestamp, JSON); } @Override - @Timed(value = "cps.data.service.datanode.root.save", - description = "Time taken to save a root data node") + @Timed(value = "cps.data.service.datanode.root.save", description = "Time taken to save a root data node") public void saveData(final String dataspaceName, final String anchorName, final String nodeData, 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); } @@ -99,34 +97,32 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, final OffsetDateTime observedTimestamp) { - saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, ContentType.JSON); + saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, JSON); } @Override - @Timed(value = "cps.data.service.datanode.child.save", - description = "Time taken to save a child data node") + @Timed(value = "cps.data.service.datanode.child.save", description = "Time taken to save a child data node") public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, 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.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp); } @Override - @Timed(value = "cps.data.service.list.element.save", - description = "Time taken to save list elements") + @Timed(value = "cps.data.service.list.element.save", description = "Time taken to save list elements") public void saveListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String nodeData, 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, @@ -136,8 +132,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.get", - description = "Time taken to get data nodes for an xpath") + @Timed(value = "cps.data.service.datanode.get", description = "Time taken to get data nodes for an xpath") public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName, final String xpath, final FetchDescendantsOption fetchDescendantsOption) { @@ -146,8 +141,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.batch.get", - description = "Time taken to get a batch of data nodes") + @Timed(value = "cps.data.service.datanode.batch.get", description = "Time taken to get a batch of data nodes") public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName, final Collection<String> xpaths, final FetchDescendantsOption fetchDescendantsOption) { @@ -163,8 +157,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 +174,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, + JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(anchor, dataNodeUpdate); } @@ -210,8 +205,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.get.delta", - description = "Time taken to get delta between anchors") + @Timed(value = "cps.data.service.get.delta", description = "Time taken to get delta between anchors") public List<DeltaReport> getDeltaByDataspaceAndAnchors(final String dataspaceName, final String sourceAnchorName, final String targetAnchorName, final String xpath, @@ -225,9 +219,9 @@ public class CpsDataServiceImpl implements CpsDataService { return cpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes); } + @Override @Timed(value = "cps.data.service.get.deltaBetweenAnchorAndPayload", description = "Time taken to get delta between anchor and a payload") - @Override public List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName, final String sourceAnchorName, final String xpath, final Map<String, String> yangResourceContentPerName, @@ -256,8 +250,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,31 +260,30 @@ 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)); } @Override - @Timed(value = "cps.data.service.list.update", - description = "Time taken to update a list") + @Timed(value = "cps.data.service.list.update", description = "Time taken to update a list") public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, 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); } @Override - @Timed(value = "cps.data.service.list.batch.update", - description = "Time taken to update a batch of lists") + @Timed(value = "cps.data.service.list.batch.update", description = "Time taken to update a batch of lists") public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -300,8 +293,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.delete", - description = "Time taken to delete a datanode") + @Timed(value = "cps.data.service.datanode.delete", description = "Time taken to delete a datanode") public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -311,8 +303,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.datanode.batch.delete", - description = "Time taken to delete a batch of datanodes") + @Timed(value = "cps.data.service.datanode.batch.delete", description = "Time taken to delete a batch of datanodes") public void deleteDataNodes(final String dataspaceName, final String anchorName, final Collection<String> dataNodeXpaths, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -348,8 +339,7 @@ public class CpsDataServiceImpl implements CpsDataService { } @Override - @Timed(value = "cps.data.service.list.delete", - description = "Time taken to delete a list or list element") + @Timed(value = "cps.data.service.list.delete", description = "Time taken to delete a list or list element") public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); @@ -362,156 +352,34 @@ 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); } - private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor, + private Collection<DataNode> rebuildSourceDataNodes(final String xpath, + final Anchor sourceAnchor, final Collection<DataNode> sourceDataNodes) { - final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>(); if (sourceDataNodes != null) { - final String sourceDataNodesAsJson = getDataNodesAsJson(sourceAnchor, sourceDataNodes); - sourceDataNodesRebuilt.addAll( - buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON)); + final Map<String, Object> sourceDataNodesAsMap = dataMapper.toFlatDataMap(sourceAnchor, sourceDataNodes); + final String sourceDataNodesAsJson = jsonObjectMapper.asJsonString(sourceDataNodesAsMap); + final Collection<DataNode> dataNodes = dataNodeFactory + .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, sourceDataNodesAsJson, JSON); + sourceDataNodesRebuilt.addAll(dataNodes); } return sourceDataNodesRebuilt; } - private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, final String xpath, + private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, + final String xpath, final Map<String, String> yangResourceContentPerName, final String targetData) { if (yangResourceContentPerName.isEmpty()) { - return buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, targetData, ContentType.JSON); - } else { - return buildDataNodesWithYangResourceAndXpath(yangResourceContentPerName, xpath, - targetData, ContentType.JSON); + return dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, JSON); } - } - - private String getDataNodesAsJson(final Anchor anchor, final Collection<DataNode> dataNodes) { - - final List<Map<String, Object>> prefixToDataNodes = prefixResolver(anchor, dataNodes); - final Map<String, Object> targetDataAsJsonObject = getNodeDataAsJsonString(prefixToDataNodes); - return jsonObjectMapper.asJsonString(targetDataAsJsonObject); - } - - private Map<String, Object> getNodeDataAsJsonString(final List<Map<String, Object>> prefixToDataNodes) { - final Map<String, Object> nodeDataAsJson = new HashMap<>(); - for (final Map<String, Object> prefixToDataNode : prefixToDataNodes) { - nodeDataAsJson.putAll(prefixToDataNode); - } - return nodeDataAsJson; - } - - private List<Map<String, Object>> prefixResolver(final Anchor anchor, final Collection<DataNode> dataNodes) { - final List<Map<String, Object>> prefixToDataNodes = new ArrayList<>(dataNodes.size()); - for (final DataNode dataNode: dataNodes) { - final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - final Map<String, Object> prefixToDataNode = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); - prefixToDataNodes.add(prefixToDataNode); - } - 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); + return dataNodeFactory + .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON); } private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) { @@ -523,8 +391,10 @@ public class CpsDataServiceImpl implements CpsDataService { } } - private void sendDataUpdatedEvent(final Anchor anchor, final String xpath, - final Operation operation, final OffsetDateTime observedTimestamp) { + private void sendDataUpdatedEvent(final Anchor anchor, + final String xpath, + final Operation operation, + final OffsetDateTime observedTimestamp) { try { cpsDataUpdateEventsService.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp); } catch (final Exception exception) { diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java new file mode 100644 index 0000000000..4ac0d5d8e8 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsFacadeImpl.java @@ -0,0 +1,97 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * 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 java.util.Collection; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.CpsDataService; +import org.onap.cps.api.CpsFacade; +import org.onap.cps.api.CpsQueryService; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.api.parameters.PaginationOption; +import org.onap.cps.utils.DataMapper; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class CpsFacadeImpl implements CpsFacade { + + private final CpsDataService cpsDataService; + private final CpsQueryService cpsQueryService; + private final DataMapper dataMapper; + + @Override + public Map<String, Object> getFirstDataNodeByAnchor(final String dataspaceName, + final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, + fetchDescendantsOption).iterator().next(); + return dataMapper.toDataMap(dataspaceName, anchorName, dataNode); + } + + @Override + public List<Map<String, Object>> getDataNodesByAnchor(final String dataspaceName, + final String anchorName, + final String xpath, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath, + fetchDescendantsOption); + return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes); + } + + @Override + public List<Map<String, Object>> executeAnchorQuery(final String dataspaceName, + final String anchorName, + final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption) { + final Collection<DataNode> dataNodes = + cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); + return dataMapper.toDataMaps(dataspaceName, anchorName, dataNodes); + } + + @Override + public List<Map<String, Object>> executeDataspaceQuery(final String dataspaceName, + final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption, + final PaginationOption paginationOption) { + final Collection<DataNode> dataNodes = cpsQueryService.queryDataNodesAcrossAnchors(dataspaceName, + cpsPath, fetchDescendantsOption, paginationOption); + return dataMapper.toDataMaps(dataspaceName, dataNodes); + } + + @Override + public int countAnchorsInDataspaceQuery(final String dataspaceName, + final String cpsPath, + final PaginationOption paginationOption) { + if (paginationOption == PaginationOption.NO_PAGINATION) { + return 1; + } + final int totalAnchors = cpsQueryService.countAnchorsForDataspaceAndCpsPath(dataspaceName, cpsPath); + return totalAnchors <= paginationOption.getPageSize() ? 1 + : (int) Math.ceil((double) totalAnchors / paginationOption.getPageSize()); + } + +} + diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java index 09ef637965..dc293b26e2 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2025 TechMahindra Ltd. + * Modifications Copyright (C) 2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +23,6 @@ package org.onap.cps.impl; import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -36,8 +36,7 @@ import org.onap.cps.api.model.DataNode; import org.onap.cps.cpspath.parser.CpsPathUtil; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.utils.ContentType; -import org.onap.cps.utils.DataMapUtils; -import org.onap.cps.utils.PrefixResolver; +import org.onap.cps.utils.DataMapper; import org.onap.cps.utils.YangParser; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.springframework.stereotype.Service; @@ -53,10 +52,10 @@ public class CpsNotificationServiceImpl implements CpsNotificationService { private final YangParser yangParser; - private final PrefixResolver prefixResolver; + private final DataMapper dataMapper; private static final String ADMIN_DATASPACE = "CPS-Admin"; - private static final String ANCHOR_NAME = "cps-notification-subscriptions"; + private static final String CPS_SUBSCRIPTION_ANCHOR_NAME = "cps-notification-subscriptions"; private static final String DATASPACE_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']"; private static final String ANCHORS_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']/anchors"; private static final String ANCHOR_SUBSCRIPTION_XPATH_FORMAT = @@ -65,29 +64,22 @@ public class CpsNotificationServiceImpl implements CpsNotificationService { @Override public void createNotificationSubscription(final String notificationSubscriptionAsJson, final String xpath) { - final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME); + final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME); final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, xpath, notificationSubscriptionAsJson); - cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, ANCHOR_NAME, xpath, dataNodes); + cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, dataNodes); } @Override public void deleteNotificationSubscription(final String xpath) { - cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, ANCHOR_NAME, xpath); + cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath); } @Override public List<Map<String, Object>> getNotificationSubscription(final String xpath) { - final Collection<DataNode> dataNodes = - cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); - final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size()); - final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME); - for (final DataNode dataNode: dataNodes) { - final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); - final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); - dataMaps.add(dataMap); - } - return dataMaps; + final Collection<DataNode> dataNodes = cpsDataPersistenceService + .getDataNodes(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); + return dataMapper.toDataMaps(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, dataNodes); } @Override @@ -103,7 +95,8 @@ public class CpsNotificationServiceImpl implements CpsNotificationService { private boolean isNotificationEnabledForXpath(final String xpath) { try { - cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); + cpsDataPersistenceService + .getDataNodes(ADMIN_DATASPACE, CPS_SUBSCRIPTION_ANCHOR_NAME, xpath, INCLUDE_ALL_DESCENDANTS); } catch (final DataNodeNotFoundException e) { return false; } diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsQueryServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsQueryServiceImpl.java index a3884820c7..5abdd0fe2b 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsQueryServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsQueryServiceImpl.java @@ -67,8 +67,15 @@ public class CpsQueryServiceImpl implements CpsQueryService { @Override public <T> Set<T> queryDataLeaf(final String dataspaceName, final String anchorName, final String cpsPath, final Class<T> targetClass) { + return queryDataLeaf(dataspaceName, anchorName, cpsPath, NO_LIMIT, targetClass); + } + + @Override + public <T> Set<T> queryDataLeaf(final String dataspaceName, final String anchorName, final String cpsPath, + final int queryResultLimit, final Class<T> targetClass) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - return cpsDataPersistenceService.queryDataLeaf(dataspaceName, anchorName, cpsPath, targetClass); + return cpsDataPersistenceService.queryDataLeaf(dataspaceName, anchorName, cpsPath, queryResultLimit, + targetClass); } @Override 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/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index 5be5fb0481..b319929e47 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 @@ -208,10 +208,12 @@ public interface CpsDataPersistenceService { * @param dataspaceName dataspace name * @param anchorName anchor name * @param cpsPath cps path + * @param queryResultLimit limits the number of returned entities (if less than 1 returns all) * @param targetClass class of the expected data type * @return a collection of data objects of expected type */ - <T> Set<T> queryDataLeaf(String dataspaceName, String anchorName, String cpsPath, Class<T> targetClass); + <T> Set<T> queryDataLeaf(String dataspaceName, String anchorName, String cpsPath, int queryResultLimit, + Class<T> targetClass); /** * Get a datanode by dataspace name and cps path across all anchors. diff --git a/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java b/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java new file mode 100644 index 0000000000..6e7eff9132 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/DataMapper.java @@ -0,0 +1,133 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation. + * ================================================================================ + * 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.utils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.CpsAnchorService; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DataMapper { + + private final CpsAnchorService cpsAnchorService; + private final PrefixResolver prefixResolver; + + /** + * Convert a data node to a data map. + * + * @param dataspaceName the name of the dataspace + * @param anchorName the name of the anchor + * @param dataNode the data node to convert + * @return the data node represented as a map of key value pairs + */ + public Map<String, Object> toDataMap(final String dataspaceName, final String anchorName, final DataNode dataNode) { + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + return DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); + } + + /** + * Convert a collection of data nodes to a list of data maps. + * + * @param dataspaceName the name dataspace name + * @param anchorName the name of the anchor + * @param dataNodes the data nodes to convert + * @return a list of maps representing the data nodes + */ + public List<Map<String, Object>> toDataMaps(final String dataspaceName, final String anchorName, + final Collection<DataNode> dataNodes) { + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + return toDataMaps(anchor, dataNodes); + } + + /** + * Convert a collection of data nodes to a list of data maps. + * + * @param anchor the anchor + * @param dataNodes the data nodes to convert + * @return a list of maps representing the data nodes + */ + public List<Map<String, Object>> toDataMaps(final Anchor anchor, final Collection<DataNode> dataNodes) { + final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size()); + for (final DataNode dataNode : dataNodes) { + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); + dataMaps.add(dataMap); + } + return dataMaps; + } + + /** + * Convert a collection of data nodes (belonging to multiple anchors) to a list of data maps. + * + * @param dataspaceName the name dataspace name + * @param dataNodes the data nodes to convert + * @return a list of maps representing the data nodes + */ + public List<Map<String, Object>> toDataMaps(final String dataspaceName, final Collection<DataNode> dataNodes) { + final List<Map<String, Object>> dataNodesAsMaps = new ArrayList<>(dataNodes.size()); + final Map<String, List<DataNode>> dataNodesPerAnchor = groupDataNodesPerAnchor(dataNodes); + for (final Map.Entry<String, List<DataNode>> dataNodesPerAnchorEntry : dataNodesPerAnchor.entrySet()) { + final String anchorName = dataNodesPerAnchorEntry.getKey(); + final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + final DataNode dataNode = dataNodesPerAnchorEntry.getValue().get(0); + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + final Map<String, Object> dataNodeAsMap = DataMapUtils.toDataMapWithIdentifierAndAnchor( + dataNodesPerAnchorEntry.getValue(), anchorName, prefix); + dataNodesAsMaps.add(dataNodeAsMap); + } + return dataNodesAsMaps; + } + + /** + * Convert a collection of data nodes to a data map. + * + * @param anchor the anchor + * @param dataNodes the data nodes to convert + * @return a map representing the data nodes + */ + public Map<String, Object> toFlatDataMap(final Anchor anchor, final Collection<DataNode> dataNodes) { + final List<Map<String, Object>> dataNodesAsMaps = toDataMaps(anchor, dataNodes); + return flattenDataNodesMaps(dataNodesAsMaps); + } + + private Map<String, Object> flattenDataNodesMaps(final List<Map<String, Object>> dataNodesAsMaps) { + final Map<String, Object> dataNodesAsFlatMap = new HashMap<>(); + for (final Map<String, Object> dataNodeAsMap : dataNodesAsMaps) { + dataNodesAsFlatMap.putAll(dataNodeAsMap); + } + return dataNodesAsFlatMap; + } + + private static Map<String, List<DataNode>> groupDataNodesPerAnchor(final Collection<DataNode> dataNodes) { + return dataNodes.stream().collect(Collectors.groupingBy(DataNode::getAnchorName)); + } + +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java b/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java index bd348a25d1..e59029f916 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java +++ b/cps-service/src/main/java/org/onap/cps/utils/PrefixResolver.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2024 Nordix Foundation. + * Copyright (C) 2022-2025 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,25 +47,29 @@ public class PrefixResolver { * @return the prefix of the module the top level element of given xpath */ public String getPrefix(final Anchor anchor, final String xpath) { + return getPrefix(anchor.getDataspaceName(), anchor.getSchemaSetName(), xpath); + } + + private String getPrefix(final String dataspaceName, final String schemaSetName, final String xpath) { final CpsPathQuery cpsPathQuery = CpsPathUtil.getCpsPathQuery(xpath); if (cpsPathQuery.getCpsPathPrefixType() != CpsPathPrefixType.ABSOLUTE) { return ""; } - final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0); + final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0); final YangTextSchemaSourceSet yangTextSchemaSourceSet = - yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName()); + yangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName); final SchemaContext schemaContext = yangTextSchemaSourceSet.getSchemaContext(); return schemaContext.getChildNodes().stream() - .filter(DataNodeContainer.class::isInstance) - .map(SchemaNode::getQName) - .filter(qname -> qname.getLocalName().equals(topLevelContainerName)) - .findFirst() - .map(QName::getModule) - .flatMap(schemaContext::findModule) - .map(Module::getPrefix) - .orElse(""); + .filter(DataNodeContainer.class::isInstance) + .map(SchemaNode::getQName) + .filter(qname -> qname.getLocalName().equals(topLevelContainerName)) + .findFirst() + .map(QName::getModule) + .flatMap(schemaContext::findModule) + .map(Module::getPrefix) + .orElse(""); } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy index 126e5b197b..508178b419 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/parameters/FetchDescendantsOptionSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 Nordix Foundation + * Copyright (C) 2022-2025 Nordix Foundation * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,10 @@ package org.onap.cps.api.parameters import org.onap.cps.api.exceptions.DataValidationException import spock.lang.Specification +import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY +import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS +import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS + class FetchDescendantsOptionSpec extends Specification { def 'Has next descendant for fetch descendant option: #scenario'() { @@ -105,11 +109,22 @@ class FetchDescendantsOptionSpec extends Specification { expect: 'each fetch descendant option has the correct String value' assert fetchDescendantsOption.toString() == expectedStringValue where: 'the following option is used' - fetchDescendantsOption || expectedStringValue - FetchDescendantsOption.OMIT_DESCENDANTS || 'OmitDescendants' - FetchDescendantsOption.DIRECT_CHILDREN_ONLY || 'DirectChildrenOnly' - FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS || 'IncludeAllDescendants' - new FetchDescendantsOption(2) || 'Depth=2' + fetchDescendantsOption || expectedStringValue + OMIT_DESCENDANTS || 'OmitDescendants' + DIRECT_CHILDREN_ONLY || 'DirectChildrenOnly' + INCLUDE_ALL_DESCENDANTS || 'IncludeAllDescendants' + new FetchDescendantsOption(2) || 'Depth=2' + } + + def 'Convert include-descendants boolean to fetch descendants option with : #includeDescendants'() { + when: 'convert boolean #includeDescendants' + def result = FetchDescendantsOption.getFetchDescendantsOption(includeDescendants) + then: 'result is the expected option' + assert result == expectedFetchDescendantsOption + where: 'following parameters are used' + includeDescendants || expectedFetchDescendantsOption + true || INCLUDE_ALL_DESCENDANTS + false || OMIT_DESCENDANTS } } diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy index d78c8bb47f..a21a17fabd 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsAnchorServiceImplSpec.groovy @@ -20,7 +20,6 @@ package org.onap.cps.impl - import org.onap.cps.utils.CpsValidator import org.onap.cps.spi.CpsAdminPersistenceService import org.onap.cps.spi.CpsDataPersistenceService 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..967bcc0aa0 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 @@ -1,9 +1,9 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2024 Nordix Foundation + * Copyright (C) 2021-2025 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. @@ -30,17 +30,18 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDeltaService -import org.onap.cps.events.CpsDataUpdateEventsService -import org.onap.cps.utils.CpsValidator -import org.onap.cps.spi.CpsDataPersistenceService -import org.onap.cps.api.parameters.FetchDescendantsOption import org.onap.cps.api.exceptions.ConcurrencyException import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.exceptions.SessionManagerException import org.onap.cps.api.exceptions.SessionTimeoutException import org.onap.cps.api.model.Anchor +import org.onap.cps.api.parameters.FetchDescendantsOption +import org.onap.cps.events.CpsDataUpdateEventsService +import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.utils.ContentType +import org.onap.cps.utils.CpsValidator +import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.onap.cps.utils.YangParser @@ -68,9 +69,11 @@ class CpsDataServiceImplSpec extends Specification { def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def mockPrefixResolver = Mock(PrefixResolver) + def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver) + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, - mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) + dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper) def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) def loggingListAppender @@ -107,8 +110,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 +135,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 +143,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 +164,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 +174,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 +192,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 +206,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/CpsFacadeImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy new file mode 100644 index 0000000000..c754970518 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsFacadeImplSpec.groovy @@ -0,0 +1,114 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 Nordix Foundation + * ================================================================================ + * 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 org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsQueryService +import org.onap.cps.api.model.DataNode +import org.onap.cps.api.parameters.PaginationOption +import org.onap.cps.utils.DataMapper +import org.onap.cps.utils.PrefixResolver +import spock.lang.Specification + +import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS +import static org.onap.cps.api.parameters.PaginationOption.NO_PAGINATION + +class CpsFacadeImplSpec extends Specification { + + def mockCpsDataService = Mock(CpsDataService) + def mockCpsQueryService = Mock(CpsQueryService) + def mockCpsAnchorService = Mock(CpsAnchorService) + def mockPrefixResolver = Mock(PrefixResolver) + def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver) + + def myFetchDescendantsOption = OMIT_DESCENDANTS + def myPaginationOption = NO_PAGINATION + + def objectUnderTest = new CpsFacadeImpl(mockCpsDataService, mockCpsQueryService , dataMapper) + + def dataNode1 = new DataNode(xpath:'/path1', anchorName: 'my anchor') + def dataNode2 = new DataNode(xpath:'/path2', anchorName: 'my anchor') + def dataNode3 = new DataNode(xpath:'/path3', anchorName: 'other anchor') + + def setup() { + mockCpsDataService.getDataNodes('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) >> [ dataNode1, dataNode2] + mockPrefixResolver.getPrefix(_, '/path1') >> 'prefix1' + mockPrefixResolver.getPrefix(_, '/path2') >> 'prefix2' + mockPrefixResolver.getPrefix(_, '/path3') >> 'prefix3' + } + + def 'Get one data node.'() { + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.getFirstDataNodeByAnchor('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) + then: 'only the first node (from the data service result) is returned' + assert result.size() == 1 + assert result.keySet()[0] == 'prefix1:path1' + } + + def 'Get multiple data nodes.'() { + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.getDataNodesByAnchor('my dataspace', 'my anchor', 'my path', myFetchDescendantsOption) + then: 'all nodes (from the data service result) are returned' + assert result.size() == 2 + assert result[0].keySet()[0] == 'prefix1:path1' + assert result[1].keySet()[0] == 'prefix2:path2' + } + + def 'Execute anchor query.'() { + given: 'the cps query service returns two data nodes' + mockCpsQueryService.queryDataNodes('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption) >> [ dataNode1, dataNode2] + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.executeAnchorQuery('my dataspace', 'my anchor', 'my cps path', myFetchDescendantsOption) + then: 'all nodes (from the query service result) are returned' + assert result.size() == 2 + assert result[0].keySet()[0] == 'prefix1:path1' + assert result[1].keySet()[0] == 'prefix2:path2' + } + + def 'Execute dataspace query.'() { + given: 'the cps query service returns two data nodes (on two different anchors)' + mockCpsQueryService.queryDataNodesAcrossAnchors('my dataspace', 'my cps path', myFetchDescendantsOption, myPaginationOption) >> [ dataNode1, dataNode2, dataNode3 ] + when: 'get data node by dataspace and anchor' + def result = objectUnderTest.executeDataspaceQuery('my dataspace', 'my cps path', myFetchDescendantsOption, myPaginationOption) + then: 'all nodes (from the query service result) are returned, grouped by anchor' + assert result.size() == 2 + assert result[0].toString() == '{anchorName=my anchor, dataNodes=[{prefix1:path1={}}, {prefix1:path2={}}]}' + assert result[1].toString() == '{anchorName=other anchor, dataNodes=[{prefix3:path3={}}]}' + } + + def 'How many pages (anchors) could be in the output with #scenario.'() { + given: 'the query service says there are 10 anchors for the given query' + mockCpsQueryService.countAnchorsForDataspaceAndCpsPath('my dataspace', 'my cps path') >> 10 + expect: 'the correct number of pages is returned' + assert objectUnderTest.countAnchorsInDataspaceQuery('my dataspace', 'my cps path', paginationOption) == expectedNumberOfPages + where: 'the following pagination options are used' + scenario | paginationOption || expectedNumberOfPages + 'no pagination' | NO_PAGINATION || 1 + '1 anchor per page' | new PaginationOption(1,1) || 10 + '1 anchor per page, start at 2' | new PaginationOption(2,1) || 10 + '2 anchors per page' | new PaginationOption(1,2) || 5 + '3 anchors per page' | new PaginationOption(1,3) || 4 + '10 anchors per page' | new PaginationOption(1,10) || 1 + '100 anchors per page' | new PaginationOption(1,100) || 1 + } + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy index b7f06456c9..ab7853c8e6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2025 TechMahindra Ltd. + * Modifications Copyright (C) 2025 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,22 +22,22 @@ package org.onap.cps.impl import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.exceptions.DataNodeNotFoundException import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.model.Anchor -import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.api.parameters.FetchDescendantsOption import org.onap.cps.spi.CpsDataPersistenceService +import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.onap.cps.utils.YangParser -import org.onap.cps.TestUtils 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.springframework.test.context.ContextConfiguration - import spock.lang.Specification @ContextConfiguration(classes = [ObjectMapper, JsonObjectMapper]) @@ -53,9 +54,9 @@ class CpsNotificationServiceImplSpec extends Specification { def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) - def mockPrefixResolver = Mock(PrefixResolver) + def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver)) - def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, mockPrefixResolver) + def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, dataMapper) def 'add notification subscription for list of dataspaces'() { given: 'details for notification subscription and subscription root node xpath' diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsQueryServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsQueryServiceImplSpec.groovy index 9db4aa4c3e..d581727e40 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsQueryServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsQueryServiceImplSpec.groovy @@ -77,8 +77,8 @@ class CpsQueryServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName, schemaSetName and anchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName) where: 'all fetch descendants options are supported' - fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS, - FetchDescendantsOption.DIRECT_CHILDREN_ONLY, new FetchDescendantsOption(10)] + fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS, + FetchDescendantsOption.DIRECT_CHILDREN_ONLY, new FetchDescendantsOption(10)] } def 'Query total anchors for dataspace and cps path.'() { @@ -91,7 +91,7 @@ class CpsQueryServiceImplSpec extends Specification { def 'Query data leaf.'() { when: 'a query for a specific leaf is executed' objectUnderTest.queryDataLeaf('some-dataspace', 'some-anchor', '/cps-path/@id', Object.class) - then: 'solution is not implemented yet' - 1 * mockCpsDataPersistenceService.queryDataLeaf('some-dataspace', 'some-anchor', '/cps-path/@id', Object.class) + then: 'the persistence service is called once with the correct parameters' + 1 * mockCpsDataPersistenceService.queryDataLeaf('some-dataspace', 'some-anchor', '/cps-path/@id', 0, Object.class) } } 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..893cce6687 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. @@ -27,12 +27,13 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDeltaService +import org.onap.cps.api.model.Anchor import org.onap.cps.events.CpsDataUpdateEventsService -import org.onap.cps.utils.CpsValidator import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.CpsModulePersistenceService -import org.onap.cps.api.model.Anchor import org.onap.cps.utils.ContentType +import org.onap.cps.utils.CpsValidator +import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver import org.onap.cps.utils.YangParser @@ -42,23 +43,22 @@ import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Specification class E2ENetworkSliceSpec extends Specification { - def mockModuleStoreService = Mock(CpsModulePersistenceService) - def mockDataStoreService = Mock(CpsDataPersistenceService) + def mockCpsModulePersistenceService = Mock(CpsModulePersistenceService) + def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) def mockCpsAnchorService = Mock(CpsAnchorService) def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockCpsValidator = Mock(CpsValidator) def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder() def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder) def mockCpsDeltaService = Mock(CpsDeltaService) + def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver)) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - def mockPrefixResolver = Mock(PrefixResolver) - def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService, - mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) + def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder) def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService) - def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, - yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver) + def dataNodeFactory = new DataNodeFactoryImpl(yangParser) + def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper) def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' def schemaSetName = 'someSchemaSet' @@ -74,7 +74,7 @@ class E2ENetworkSliceSpec extends Specification { when: 'Create schema set method is invoked' cpsModuleServiceImpl.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockModuleStoreService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) + 1 * mockCpsModulePersistenceService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) } def 'E2E Coverage Area-Tracking Area & TA-Cell mapping model can be parsed by CPS.'() { @@ -84,7 +84,7 @@ class E2ENetworkSliceSpec extends Specification { when: 'Create schema set method is invoked' cpsModuleServiceImpl.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockModuleStoreService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) + 1 * mockCpsModulePersistenceService.createSchemaSet(dataspaceName, schemaSetName, yangResourceContentPerName) } def 'E2E Coverage Area-Tracking Area & TA-Cell mapping data can be parsed by CPS.'() { @@ -100,31 +100,28 @@ class E2ENetworkSliceSpec extends Specification { new Anchor().builder().name(anchorName).schemaSetName(schemaSetName).dataspaceName(dataspaceName).build() mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName) - mockModuleStoreService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext + mockCpsModulePersistenceService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext when: 'saveData method is invoked' cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >> + 1 * mockCpsDataPersistenceService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} def child = dataNodeStored[0].childDataNodes[0] assert child.childDataNodes.size() == 1 and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes ' def listOfTAForCoverageArea = child.childDataNodes[0] - listOfTAForCoverageArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/' + - 'coverage-area[@coverageArea=\'Washington\']' - listOfTAForCoverageArea.childDataNodes[0].leaves.get('nRTAC') == 234 + listOfTAForCoverageArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/coverage-area[@coverageArea=\'Washington\']' + assert listOfTAForCoverageArea.childDataNodes[0].leaves.get('nRTAC') == 234 and: 'list of cells in a tracking area are stored with correct xpath and child nodes ' def listOfCellsInTrackingArea = listOfTAForCoverageArea.childDataNodes[0] - listOfCellsInTrackingArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/' + - 'coverage-area[@coverageArea=\'Washington\']/coverageAreaTAList[@nRTAC=\'234\']' + listOfCellsInTrackingArea.xpath == '/ran-coverage-area/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']/coverage-area[@coverageArea=\'Washington\']/coverageAreaTAList[@nRTAC=\'234\']' listOfCellsInTrackingArea.childDataNodes[0].leaves.get('cellLocalId') == 15709 } def 'E2E Coverage Area-Tracking Area & TA-Cell mapping data can be parsed for RAN inventory.'() { def dataNodeStored given: 'valid yang resource as name-to-content map' - def yangResourceContentPerName = TestUtils.getYangResourcesAsMap( - 'e2e/basic/cps-ran-inventory@2021-01-28.yang') + def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('e2e/basic/cps-ran-inventory@2021-01-28.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName).getSchemaContext() and : 'a valid json is provided for the model' def jsonData = TestUtils.getResourceFileContent('e2e/basic/cps-ran-inventory-data.json') @@ -132,12 +129,11 @@ class E2ENetworkSliceSpec extends Specification { mockCpsAnchorService.getAnchor('someDataspace', 'someAnchor') >> new Anchor().builder().name('someAnchor').schemaSetName('someSchemaSet').dataspaceName(dataspaceName).build() mockYangTextSchemaSourceSetCache.get('someDataspace', 'someSchemaSet') >> YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName) - mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext + mockCpsModulePersistenceService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext when: 'saveData method is invoked' cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp) then: 'parameters are validated and processing is delegated to persistence service' - 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >> - { args -> dataNodeStored = args[2]} + 1 * mockCpsDataPersistenceService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} and: 'the size of the tree is correct' def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0]) assert cpsRanInventory.size() == 4 @@ -146,17 +142,16 @@ class E2ENetworkSliceSpec extends Specification { def ranSlices = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']') def sliceProfilesList = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']/sliceProfilesList[@sliceProfileId=\'f33a9dd8-ae51-4acf-8073-c9390c25f6f1\']') def pLMNIdList = cpsRanInventory.get('/ran-inventory/ran-slices[@rannfnssiid=\'14559ead-f4fe-4c1c-a94c-8015fad3ea35\']/sliceProfilesList[@sliceProfileId=\'f33a9dd8-ae51-4acf-8073-c9390c25f6f1\']/pLMNIdList[@mcc=\'310\' and @mnc=\'410\']') - ranInventory.getChildDataNodes().size() == 1 - ranInventory.getChildDataNodes().find( {it.xpath == ranSlices.xpath}) + assert ranInventory.getChildDataNodes().size() == 1 + assert ranInventory.getChildDataNodes().find( {it.xpath == ranSlices.xpath}) and: 'ranSlices contains the correct child node' - ranSlices.getChildDataNodes().size() == 1 - ranSlices.getChildDataNodes().find( {it.xpath == sliceProfilesList.xpath}) + assert ranSlices.getChildDataNodes().size() == 1 + assert ranSlices.getChildDataNodes().find( {it.xpath == sliceProfilesList.xpath}) and: 'sliceProfilesList contains the correct child node' - sliceProfilesList.getChildDataNodes().size() == 1 - sliceProfilesList.getChildDataNodes().find( {it.xpath == pLMNIdList.xpath}) + assert sliceProfilesList.getChildDataNodes().size() == 1 + assert sliceProfilesList.getChildDataNodes().find( {it.xpath == pLMNIdList.xpath}) and: 'pLMNIdList contains no children' - pLMNIdList.getChildDataNodes().size() == 0 - + assert pLMNIdList.getChildDataNodes().size() == 0 } def 'E2E RAN Schema Model.'(){ |