diff options
Diffstat (limited to 'cps-service/src')
30 files changed, 484 insertions, 603 deletions
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index 68e1880d77..b3eff8eb26 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -322,4 +322,18 @@ public interface CpsDataService { Map<String, String> yangResourcesNameToContentMap, String targetData, FetchDescendantsOption fetchDescendantsOption); + + + /** + * Validates JSON or XML data by parsing it using the schema associated to an anchor within the given dataspace. + * Validation is performed without persisting the data. + * + * @param dataspaceName the name of the dataspace where the anchor is located. + * @param anchorName the name of the anchor used to validate the data. + * @param parentNodeXpath the xpath of the parent node where the data is to be validated. + * @param nodeData the JSON or XML data to be validated. + * @param contentType the content type of the data (e.g., JSON or XML). + */ + void validateData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData, + ContentType contentType); } diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java index 931209c998..bbfe496d85 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java @@ -178,8 +178,8 @@ public interface CpsModuleService { * an attribute key-value pair used in the WHERE clause for parent fragments. * @param childAttributes a map of attributes to filter child fragments. Each entry in this map represents * an attribute key-value pair used in the WHERE clause for child fragments. - * @return a collection of {@link ModuleReference} objects that match the given criteria. Each - * {@code ModuleReference} contains information about a module's name and revision. + * @return a collection of {@link ModuleReference} objects that match the given criteria. + * Each {@code ModuleReference} contains information about a module's name and revision. * @implNote The method assumes that both `parentAttributes` and `childAttributes` maps contain at least * one entry. The first entry from `parentAttributes` is used to filter parent fragments, * and the first entry from `childAttributes` is used to filter child fragments. diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 951770b053..b1b545be68 100644 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -64,6 +64,7 @@ import org.springframework.stereotype.Service; 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."; @@ -358,6 +359,14 @@ public class CpsDataServiceImpl implements CpsDataService { sendDataUpdatedEvent(anchor, listNodeXpath, Operation.DELETE, observedTimestamp); } + @Override + 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 : + CpsPathUtil.getNormalizedXpath(parentNodeXpath); + yangParser.validateData(contentType, nodeData, anchor, xpath); + } private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor, final Collection<DataNode> sourceDataNodes) { @@ -400,8 +409,7 @@ public class CpsDataServiceImpl implements CpsDataService { 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.getDataspaceName(), anchor.getName(), dataNode.getXpath()); + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); final Map<String, Object> prefixToDataNode = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); prefixToDataNodes.add(prefixToDataNode); } @@ -423,7 +431,8 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final ContentType contentType) { if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, ""); + final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, + anchor, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withContainerNode(containerNode) .buildCollection(); @@ -451,7 +460,7 @@ public class CpsDataServiceImpl implements CpsDataService { if (isRootNodeXpath(xpath)) { final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, - yangResourcesNameToContentMap, ""); + yangResourcesNameToContentMap, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withContainerNode(containerNode) .buildCollection(); diff --git a/cps-service/src/main/java/org/onap/cps/cache/AnchorDataCacheConfig.java b/cps-service/src/main/java/org/onap/cps/cache/AnchorDataCacheConfig.java deleted file mode 100644 index efe19c6cb7..0000000000 --- a/cps-service/src/main/java/org/onap/cps/cache/AnchorDataCacheConfig.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * ============LICENSE_START======================================================== - * Copyright (C) 2022-2023 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.cache; - -import com.hazelcast.config.MapConfig; -import com.hazelcast.map.IMap; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Core infrastructure of the hazelcast distributed cache for anchor data config use cases. - */ -@Configuration -public class AnchorDataCacheConfig extends HazelcastCacheConfig { - - private static final MapConfig anchorDataCacheMapConfig = createMapConfig("anchorDataCacheMapConfig"); - - /** - * Distributed instance of anchor data cache that contains module prefix by anchor name as properties. - * - * @return configured map of anchor data cache - */ - @Bean - public IMap<String, AnchorDataCacheEntry> anchorDataCache() { - return createHazelcastInstance("hazelCastInstanceCpsCore", anchorDataCacheMapConfig).getMap("anchorDataCache"); - } -} diff --git a/cps-service/src/main/java/org/onap/cps/cache/AnchorDataCacheEntry.java b/cps-service/src/main/java/org/onap/cps/cache/AnchorDataCacheEntry.java deleted file mode 100644 index 41adbdd5dc..0000000000 --- a/cps-service/src/main/java/org/onap/cps/cache/AnchorDataCacheEntry.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * ============LICENSE_START======================================================== - * Copyright (C) 2022 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.cache; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - -public class AnchorDataCacheEntry implements Serializable { - - private static final long serialVersionUID = 2111243947810370698L; - - private Map<String, Serializable> properties = new HashMap<>(); - - public Object getProperty(final String propertyName) { - return properties.get(propertyName); - } - - public void setProperty(final String propertyName, final Serializable value) { - properties.put(propertyName, value); - } - - public boolean hasProperty(final String propertyName) { - return properties.containsKey(propertyName); - } -} diff --git a/cps-service/src/main/java/org/onap/cps/cache/HazelcastCacheConfig.java b/cps-service/src/main/java/org/onap/cps/cache/HazelcastCacheConfig.java deleted file mode 100644 index 418de9b17a..0000000000 --- a/cps-service/src/main/java/org/onap/cps/cache/HazelcastCacheConfig.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * ============LICENSE_START======================================================== - * Copyright (C) 2023-2024 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.cache; - -import com.hazelcast.config.Config; -import com.hazelcast.config.MapConfig; -import com.hazelcast.config.NamedConfig; -import com.hazelcast.config.QueueConfig; -import com.hazelcast.config.RestEndpointGroup; -import com.hazelcast.config.SetConfig; -import com.hazelcast.core.Hazelcast; -import com.hazelcast.core.HazelcastInstance; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; - -/** - * Core infrastructure of the hazelcast distributed cache. - */ -@Slf4j -public class HazelcastCacheConfig { - - @Value("${hazelcast.cluster-name}") - protected String clusterName; - - @Value("${hazelcast.mode.kubernetes.enabled}") - protected boolean cacheKubernetesEnabled; - - @Value("${hazelcast.mode.kubernetes.service-name}") - protected String cacheKubernetesServiceName; - - protected HazelcastInstance createHazelcastInstance(final String hazelcastInstanceName, - final NamedConfig namedConfig) { - return Hazelcast.newHazelcastInstance(initializeConfig(hazelcastInstanceName, namedConfig)); - } - - private Config initializeConfig(final String instanceName, final NamedConfig namedConfig) { - final Config config = new Config(instanceName); - if (namedConfig instanceof MapConfig) { - config.addMapConfig((MapConfig) namedConfig); - } - if (namedConfig instanceof QueueConfig) { - config.addQueueConfig((QueueConfig) namedConfig); - } - if (namedConfig instanceof SetConfig) { - config.addSetConfig((SetConfig) namedConfig); - } - - config.setClusterName(clusterName); - config.setClassLoader(org.onap.cps.spi.model.Dataspace.class.getClassLoader()); - exposeClusterInformation(config); - updateDiscoveryMode(config); - return config; - } - - protected static MapConfig createMapConfig(final String configName) { - final MapConfig mapConfig = new MapConfig(configName); - mapConfig.setBackupCount(1); - return mapConfig; - } - - protected static QueueConfig createQueueConfig(final String configName) { - final QueueConfig commonQueueConfig = new QueueConfig(configName); - commonQueueConfig.setBackupCount(1); - return commonQueueConfig; - } - - protected static SetConfig createSetConfig(final String configName) { - final SetConfig commonSetConfig = new SetConfig(configName); - commonSetConfig.setBackupCount(1); - return commonSetConfig; - } - - protected void updateDiscoveryMode(final Config config) { - if (cacheKubernetesEnabled) { - log.info("Enabling kubernetes mode with service-name : {}", cacheKubernetesServiceName); - config.getNetworkConfig().getJoin().getKubernetesConfig().setEnabled(true) - .setProperty("service-name", cacheKubernetesServiceName); - } - } - - protected void exposeClusterInformation(final Config config) { - config.getNetworkConfig().getRestApiConfig().setEnabled(true) - .enableGroups(RestEndpointGroup.HEALTH_CHECK, RestEndpointGroup.CLUSTER_READ); - } - -} diff --git a/cps-service/src/main/java/org/onap/cps/events/EventsPublisher.java b/cps-service/src/main/java/org/onap/cps/events/EventsPublisher.java index 8023fbfb25..46384b5933 100644 --- a/cps-service/src/main/java/org/onap/cps/events/EventsPublisher.java +++ b/cps-service/src/main/java/org/onap/cps/events/EventsPublisher.java @@ -44,7 +44,7 @@ public class EventsPublisher<T> { /** * KafkaTemplate for legacy (non-cloud) events. - * Note: Cloud events should be used. This will be addressed as part of https://jira.onap.org/browse/CPS-1717. + * Note: Cloud events should be used. This will be addressed as part of https://lf-onap.atlassian.net/browse/CPS-1717. */ private final KafkaTemplate<String, T> legacyKafkaEventTemplate; @@ -73,7 +73,7 @@ public class EventsPublisher<T> { /** * Generic Event publisher. - * Note: Cloud events should be used. This will be addressed as part of https://jira.onap.org/browse/CPS-1717. + * Note: Cloud events should be used. This will be addressed as part of https://lf-onap.atlassian.net/browse/CPS-1717. * * @param topicName valid topic name * @param eventKey message key diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java index 9859acdf0e..de57914527 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java @@ -204,18 +204,17 @@ public class DataNodeBuilder { private static void addDataNodeFromNormalizedNode(final DataNode currentDataNode, final NormalizedNode normalizedNode) { - if (normalizedNode instanceof ChoiceNode) { - addChoiceNode(currentDataNode, (ChoiceNode) normalizedNode); - } else if (normalizedNode instanceof DataContainerNode) { - addYangContainer(currentDataNode, (DataContainerNode) normalizedNode); - } else if (normalizedNode instanceof MapNode) { - addDataNodeForEachListElement(currentDataNode, (MapNode) normalizedNode); - } else if (normalizedNode instanceof ValueNode) { - final ValueNode<NormalizedNode> valuesNode = (ValueNode) normalizedNode; - addYangLeaf(currentDataNode, valuesNode.getIdentifier().getNodeType().getLocalName(), - (Serializable) valuesNode.body()); - } else if (normalizedNode instanceof LeafSetNode) { - addYangLeafList(currentDataNode, (LeafSetNode<?>) normalizedNode); + if (normalizedNode instanceof ChoiceNode choiceNode) { + addChoiceNode(currentDataNode, choiceNode); + } else if (normalizedNode instanceof DataContainerNode dataContainerNode) { + addYangContainer(currentDataNode, dataContainerNode); + } else if (normalizedNode instanceof MapNode mapNode) { + addDataNodeForEachListElement(currentDataNode, mapNode); + } else if (normalizedNode instanceof ValueNode<?> valueNode) { + addYangLeaf(currentDataNode, valueNode.getIdentifier().getNodeType().getLocalName(), + (Serializable) valueNode.body()); + } else if (normalizedNode instanceof LeafSetNode<?> leafSetNode) { + addYangLeafList(currentDataNode, leafSetNode); } else { log.warn("Unsupported NormalizedNode type detected: {}", normalizedNode.getClass()); } @@ -243,7 +242,7 @@ public class DataNodeBuilder { private static void addYangLeafList(final DataNode currentDataNode, final LeafSetNode<?> leafSetNode) { final String leafListName = leafSetNode.getIdentifier().getNodeType().getLocalName(); - List<?> leafListValues = ((Collection<? extends NormalizedNode>) leafSetNode.body()) + List<?> leafListValues = (leafSetNode.body()) .stream() .map(NormalizedNode::body) .collect(Collectors.toList()); diff --git a/cps-service/src/main/java/org/onap/cps/utils/ContentType.java b/cps-service/src/main/java/org/onap/cps/utils/ContentType.java index f888504843..eb8e592d9b 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/ContentType.java +++ b/cps-service/src/main/java/org/onap/cps/utils/ContentType.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +21,14 @@ package org.onap.cps.utils; +import org.springframework.http.MediaType; + public enum ContentType { JSON, - XML + XML; + + public static ContentType fromString(final String contentTypeAsString) { + return contentTypeAsString.contains(MediaType.APPLICATION_XML_VALUE) ? XML : JSON; + } + } 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 35dc7347b2..c3097cc552 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-2023 Nordix Foundation. + * Copyright (C) 2022-2024 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,53 +20,25 @@ package org.onap.cps.utils; -import com.hazelcast.map.IMap; -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; -import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.impl.YangTextSchemaSourceSetCache; -import org.onap.cps.cache.AnchorDataCacheEntry; import org.onap.cps.cpspath.parser.CpsPathPrefixType; import org.onap.cps.cpspath.parser.CpsPathQuery; import org.onap.cps.cpspath.parser.CpsPathUtil; import org.onap.cps.spi.model.Anchor; import org.onap.cps.yang.YangTextSchemaSourceSet; -import org.opendaylight.yangtools.yang.common.QNameModule; +import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; -import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class PrefixResolver { - private static final long ANCHOR_DATA_CACHE_TTL_SECS = TimeUnit.HOURS.toSeconds(1); - - private static final String CACHE_ENTRY_PROPERTY_NAME = "prefixPerContainerName"; - - private final CpsAnchorService cpsAnchorService; - private final YangTextSchemaSourceSetCache yangTextSchemaSourceSetCache; - private final IMap<String, AnchorDataCacheEntry> anchorDataCache; - - /** - * Get the module prefix for the given xpath for a dataspace and anchor name. - * - * @param dataspaceName the name of the dataspace - * @param anchorName the name of the anchor the xpath belongs to - * @param xpath the xpath to prefix a prefix for - * @return the prefix of the module the top level element of given xpath - */ - public String getPrefix(final String dataspaceName, final String anchorName, final String xpath) { - final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - return getPrefix(anchor, xpath); - } - /** * Get the module prefix for the given xpath under the given anchor. * @@ -75,57 +47,25 @@ 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) { - final Map<String, String> prefixPerContainerName = getPrefixPerContainerName(anchor); - return getPrefixForTopContainer(prefixPerContainerName, xpath); - } - - private Map<String, String> getPrefixPerContainerName(final Anchor anchor) { - if (anchorDataCache.containsKey(anchor.getName())) { - final AnchorDataCacheEntry anchorDataCacheEntry = anchorDataCache.get(anchor.getName()); - if (anchorDataCacheEntry.hasProperty(CACHE_ENTRY_PROPERTY_NAME)) { - return (Map) anchorDataCacheEntry.getProperty(CACHE_ENTRY_PROPERTY_NAME); - } - } - return createAndCachePrefixPerContainerNameMap(anchor); - } - - private String getPrefixForTopContainer(final Map<String, String> prefixPerContainerName, - final String xpath) { final CpsPathQuery cpsPathQuery = CpsPathUtil.getCpsPathQuery(xpath); - if (cpsPathQuery.getCpsPathPrefixType() == CpsPathPrefixType.ABSOLUTE) { - final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0); - if (prefixPerContainerName.containsKey(topLevelContainerName)) { - return prefixPerContainerName.get(topLevelContainerName); - } + if (cpsPathQuery.getCpsPathPrefixType() != CpsPathPrefixType.ABSOLUTE) { + return ""; } - return ""; - } + final String topLevelContainerName = cpsPathQuery.getContainerNames().get(0); - private Map<String, String> createAndCachePrefixPerContainerNameMap(final Anchor anchor) { final YangTextSchemaSourceSet yangTextSchemaSourceSet = - yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName()); + yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), anchor.getSchemaSetName()); final SchemaContext schemaContext = yangTextSchemaSourceSet.getSchemaContext(); - final Map<QNameModule, String> prefixPerQNameModule = new HashMap<>(schemaContext.getModules().size()); - for (final Module module : schemaContext.getModules()) { - prefixPerQNameModule.put(module.getQNameModule(), module.getPrefix()); - } - final HashMap<String, String> prefixPerContainerName = new HashMap<>(); - for (final DataSchemaNode dataSchemaNode : schemaContext.getChildNodes()) { - if (dataSchemaNode instanceof DataNodeContainer) { - final String containerName = dataSchemaNode.getQName().getLocalName(); - final String prefix = prefixPerQNameModule.get(dataSchemaNode.getQName().getModule()); - prefixPerContainerName.put(containerName, prefix); - } - } - cachePrefixPerContainerNameMap(anchor.getName(), prefixPerContainerName); - return prefixPerContainerName; - } - private void cachePrefixPerContainerNameMap(final String anchorName, - final Serializable prefixPerContainerName) { - final AnchorDataCacheEntry anchorDataCacheEntry = new AnchorDataCacheEntry(); - anchorDataCacheEntry.setProperty(CACHE_ENTRY_PROPERTY_NAME, prefixPerContainerName); - anchorDataCache.put(anchorName, anchorDataCacheEntry, ANCHOR_DATA_CACHE_TTL_SECS, TimeUnit.SECONDS); + 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(""); } } diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java index 7a6d0bb3d5..94b97bd88f 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG * Modifications Copyright (C) 2023-2024 Nordix Foundation. + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +22,13 @@ package org.onap.cps.utils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -33,6 +36,7 @@ import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; @@ -40,10 +44,14 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.onap.cps.spi.exceptions.DataValidationException; import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.w3c.dom.DOMException; import org.w3c.dom.Document; +import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.xml.sax.SAXException; @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -156,6 +164,85 @@ public class XmlFileUtils { return document; } + /** + * Convert a list of data maps to XML format. + * + * @param dataMaps List of data maps to convert + * @return XML string representation of the data maps + */ + @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION") + public static String convertDataMapsToXml(final List<Map<String, Object>> dataMaps) { + try { + final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); + final Document document = documentBuilder.newDocument(); + final DocumentFragment documentFragment = document.createDocumentFragment(); + for (final Map<String, Object> dataMap : dataMaps) { + createXmlElements(document, documentFragment, dataMap); + } + return transformFragmentToString(documentFragment); + } catch (final DOMException | NullPointerException | ParserConfigurationException | TransformerException + exception) { + throw new DataValidationException( + "Data Validation Failed", "Failed to parse xml data: " + exception.getMessage(), exception); + } + } + + private static void createXmlElements(final Document document, final Node parentNode, + final Map<String, Object> dataMap) { + for (final Map.Entry<String, Object> mapEntry : dataMap.entrySet()) { + if (mapEntry.getValue() instanceof List) { + appendList(document, parentNode, mapEntry); + } else if (mapEntry.getValue() instanceof Map) { + appendMap(document, parentNode, mapEntry); + } else { + appendObject(document, parentNode, mapEntry); + } + } + } + + private static void appendList(final Document document, final Node parentNode, + final Map.Entry<String, Object> mapEntry) { + final List<Object> list = (List<Object>) mapEntry.getValue(); + if (list.isEmpty()) { + final Element listElement = document.createElement(mapEntry.getKey()); + parentNode.appendChild(listElement); + } else { + for (final Object element : list) { + final Element listElement = document.createElement(mapEntry.getKey()); + if (element instanceof Map) { + createXmlElements(document, listElement, (Map<String, Object>) element); + } else { + listElement.appendChild(document.createTextNode(element.toString())); + } + parentNode.appendChild(listElement); + } + } + } + + private static void appendMap(final Document document, final Node parentNode, + final Map.Entry<String, Object> mapEntry) { + final Element childElement = document.createElement(mapEntry.getKey()); + createXmlElements(document, childElement, (Map<String, Object>) mapEntry.getValue()); + parentNode.appendChild(childElement); + } + + private static void appendObject(final Document document, final Node parentNode, + final Map.Entry<String, Object> mapEntry) { + final Element element = document.createElement(mapEntry.getKey()); + element.appendChild(document.createTextNode(mapEntry.getValue().toString())); + parentNode.appendChild(element); + } + + private static String transformFragmentToString(final DocumentFragment documentFragment) + throws TransformerException { + final Transformer transformer = getTransformerFactory().newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + final StringWriter writer = new StringWriter(); + final StreamResult result = new StreamResult(writer); + transformer.transform(new DOMSource(documentFragment), result); + return writer.toString(); + } + private static DocumentBuilderFactory getDocumentBuilderFactory() { if (isNewDocumentBuilderFactoryInstance) { diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java index dc23c6bc4a..168e0999d5 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java @@ -21,6 +21,9 @@ package org.onap.cps.utils; +import static org.onap.cps.utils.YangParserHelper.VALIDATE_AND_PARSE; +import static org.onap.cps.utils.YangParserHelper.VALIDATE_ONLY; + import io.micrometer.core.annotation.Timed; import java.util.Map; import lombok.RequiredArgsConstructor; @@ -57,11 +60,12 @@ public class YangParser { final String parentNodeXpath) { final SchemaContext schemaContext = getSchemaContext(anchor); try { - return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return yangParserHelper + .parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE); } catch (final DataValidationException e) { invalidateCache(anchor); } - return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE); } /** @@ -78,7 +82,31 @@ public class YangParser { final Map<String, String> yangResourcesNameToContentMap, final String parentNodeXpath) { final SchemaContext schemaContext = getSchemaContext(yangResourcesNameToContentMap); - return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE); + } + + /** + * Parses data to validate it, using the schema context for given anchor. + * + * @param anchor the anchor used for node data validation + * @param parentNodeXpath the xpath of the parent node + * @param nodeData JSON or XML data string to validate + * @param contentType the content type of the data (e.g., JSON or XML) + * @throws DataValidationException if validation fails + */ + public void validateData(final ContentType contentType, + final String nodeData, + final Anchor anchor, + final String parentNodeXpath) { + final SchemaContext schemaContext = getSchemaContext(anchor); + try { + yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_ONLY); + } catch (final DataValidationException e) { + invalidateCache(anchor); + log.error("Data validation failed for anchor: {}, xpath: {}, details: {}", anchor, parentNodeXpath, + e.getMessage()); + } + yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_ONLY); } private SchemaContext getSchemaContext(final Anchor anchor) { diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java index 597164598a..5612945ea9 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java @@ -22,6 +22,7 @@ package org.onap.cps.utils; import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.io.StringReader; import java.net.URISyntaxException; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.XMLInputFactory; @@ -70,6 +72,9 @@ public class YangParserHelper { static final String DATA_ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0"; static final String DATA_ROOT_NODE_TAG_NAME = "data"; + static final String DATA_VALIDATION_FAILURE_MESSAGE = "Data Validation Failed"; + static final boolean VALIDATE_ONLY = true; + static final boolean VALIDATE_AND_PARSE = false; /** * Parses data into NormalizedNode according to given schema context. @@ -83,11 +88,20 @@ public class YangParserHelper { public ContainerNode parseData(final ContentType contentType, final String nodeData, final SchemaContext schemaContext, - final String parentNodeXpath) { + final String parentNodeXpath, + final boolean validateOnly) { if (contentType == ContentType.JSON) { - return parseJsonData(nodeData, schemaContext, parentNodeXpath); + final ContainerNode validatedAndParsedJson = parseJsonData(nodeData, schemaContext, parentNodeXpath); + if (validateOnly) { + return null; + } + return validatedAndParsedJson; + } + final NormalizedNodeResult normalizedNodeResult = parseXmlData(nodeData, schemaContext, parentNodeXpath); + if (validateOnly) { + return null; } - return parseXmlData(nodeData, schemaContext, parentNodeXpath); + return buildContainerNodeFormNormalizedNodeResult(normalizedNodeResult); } private ContainerNode parseJsonData(final String jsonData, @@ -122,12 +136,13 @@ public class YangParserHelper { jsonParserStream.parse(jsonReader); } catch (final IOException | JsonSyntaxException | IllegalStateException | IllegalArgumentException exception) { throw new DataValidationException( - "Data Validation Failed", "Failed to parse json data. " + exception.getMessage(), exception); + DATA_VALIDATION_FAILURE_MESSAGE, "Failed to parse json data. " + exception.getMessage(), exception); } return dataContainerNodeBuilder.build(); } - private ContainerNode parseXmlData(final String xmlData, + @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION", justification = "Problem originates in 3PP code") + private NormalizedNodeResult parseXmlData(final String xmlData, final SchemaContext schemaContext, final String parentNodeXpath) { final XMLInputFactory factory = XMLInputFactory.newInstance(); @@ -164,12 +179,17 @@ public class YangParserHelper { } catch (final XMLStreamException | URISyntaxException | IOException | SAXException | NullPointerException | ParserConfigurationException | TransformerException exception) { throw new DataValidationException( - "Data Validation Failed", "Failed to parse xml data: " + exception.getMessage(), exception); + DATA_VALIDATION_FAILURE_MESSAGE, "Failed to parse xml data: " + exception.getMessage(), exception); } + return normalizedNodeResult; + } + + private ContainerNode buildContainerNodeFormNormalizedNodeResult(final NormalizedNodeResult normalizedNodeResult) { + final DataContainerChild dataContainerChild = - (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); + (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = - new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); + new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); return Builders.containerBuilder().withChild(dataContainerChild).withNodeIdentifier(nodeIdentifier).build(); } @@ -181,12 +201,12 @@ public class YangParserHelper { private static Map<String, Object> getDataSchemaNodeAndIdentifiersByXpath(final String parentNodeXpath, final SchemaContext schemaContext) { - final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath); + final List<String> xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath); return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), new ArrayList<>()); } - private static String[] xpathToNodeIdSequence(final String xpath) { + private static List<String> xpathToNodeIdSequence(final String xpath) { try { return CpsPathUtil.getXpathNodeIdSequence(xpath); } catch (final PathParsingException pathParsingException) { @@ -196,17 +216,16 @@ public class YangParserHelper { } private static Map<String, Object> findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( - final String[] xpathNodeIdSequence, + final List<String> xpathNodeIdSequence, final Collection<? extends DataSchemaNode> dataSchemaNodes, final Collection<QName> dataSchemaNodeIdentifiers) { - final String currentXpathNodeId = xpathNodeIdSequence[0]; + final String currentXpathNodeId = xpathNodeIdSequence.get(0); final DataSchemaNode currentDataSchemaNode = dataSchemaNodes.stream() .filter(dataSchemaNode -> currentXpathNodeId.equals(dataSchemaNode.getQName().getLocalName())) .findFirst().orElseThrow(() -> schemaNodeNotFoundException(currentXpathNodeId)); dataSchemaNodeIdentifiers.add(currentDataSchemaNode.getQName()); - if (xpathNodeIdSequence.length <= 1) { - final Map<String, Object> dataSchemaNodeAndIdentifiers = - new HashMap<>(); + if (xpathNodeIdSequence.size() <= 1) { + final Map<String, Object> dataSchemaNodeAndIdentifiers = new HashMap<>(); dataSchemaNodeAndIdentifiers.put("dataSchemaNode", currentDataSchemaNode); dataSchemaNodeAndIdentifiers.put("dataSchemaNodeIdentifiers", dataSchemaNodeIdentifiers); return dataSchemaNodeAndIdentifiers; @@ -217,13 +236,11 @@ public class YangParserHelper { ((DataNodeContainer) currentDataSchemaNode).getChildNodes(), dataSchemaNodeIdentifiers); } - throw schemaNodeNotFoundException(xpathNodeIdSequence[1]); + throw schemaNodeNotFoundException(xpathNodeIdSequence.get(1)); } - private static String[] getNextLevelXpathNodeIdSequence(final String[] xpathNodeIdSequence) { - final String[] nextXpathNodeIdSequence = new String[xpathNodeIdSequence.length - 1]; - System.arraycopy(xpathNodeIdSequence, 1, nextXpathNodeIdSequence, 0, nextXpathNodeIdSequence.length); - return nextXpathNodeIdSequence; + private static List<String> getNextLevelXpathNodeIdSequence(final List<String> xpathNodeIdSequence) { + return xpathNodeIdSequence.subList(1, xpathNodeIdSequence.size()); } private static DataValidationException schemaNodeNotFoundException(final String schemaNodeIdentifier) { diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 9846b30158..8c208a1cf8 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -546,6 +546,20 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockDataUpdateEventsService.publishCpsDataUpdateEvent(anchor2, '/', DELETE, observedTimestamp) } + def "Validating #scenario when dry run is enabled."() { + given: 'schema set for given anchors and dataspace references bookstore model' + setupSchemaSetMocks('bookstore.yang') + when: 'validating the data with the given parameters' + objectUnderTest.validateData(dataspaceName, anchorName, parentNodeXpath, data,contentType) + then: 'the appropriate yang parser method is invoked with correct parameters' + yangParser.validateData(contentType, data, anchor, xpath) + where: 'the following parameters were used' + scenario | parentNodeXpath | xpath | contentType | data + 'JSON data with root node xpath' | '/' | '' | ContentType.JSON | '{"bookstore":{"bookstore-name":"Easons"}}' + 'JSON data with specific xpath' | '/bookstore' | '/bookstore' | ContentType.JSON | '{"bookstore-name":"Easons"}' + 'XML data with specific xpath' | '/bookstore' | '/bookstore' | ContentType.XML | '<bookstore-name>Easons</bookstore-name>' + } + def 'Start session.'() { when: 'start session method is called' objectUnderTest.startSession() diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy index 05c8983fc2..9f3456280e 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy @@ -171,6 +171,6 @@ class E2ENetworkSliceSpec extends Specification { expect: 'schema context is built with no exception indicating the schema set being valid '
def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext()
and: 'data is parsed with no exception indicating the model match'
- new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '') != null
+ new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '', false) != null
}
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/cache/AnchorDataCacheConfigSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/cache/AnchorDataCacheConfigSpec.groovy deleted file mode 100644 index 010308c42f..0000000000 --- a/cps-service/src/test/groovy/org/onap/cps/cache/AnchorDataCacheConfigSpec.groovy +++ /dev/null @@ -1,80 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-2023 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.cache - -import com.hazelcast.config.Config -import com.hazelcast.core.Hazelcast -import com.hazelcast.map.IMap -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -@SpringBootTest -@ContextConfiguration(classes = [AnchorDataCacheConfig]) -class AnchorDataCacheConfigSpec extends Specification { - - @Autowired - private IMap<String, AnchorDataCacheEntry> anchorDataCache - - def 'Embedded (hazelcast) cache for Anchor Data.'() { - expect: 'system is able to create an instance of the Anchor data cache' - assert null != anchorDataCache - and: 'there is at least 1 instance' - assert Hazelcast.allHazelcastInstances.size() > 0 - and: 'anchorDataCache is present' - assert Hazelcast.allHazelcastInstances.name.contains('hazelCastInstanceCpsCore') - } - - def 'Verify configs for Distributed Caches'(){ - given: 'the Anchor Data Cache config' - def anchorDataCacheConfig = Hazelcast.getHazelcastInstanceByName('hazelCastInstanceCpsCore').config - def anchorDataCacheMapConfig = anchorDataCacheConfig.mapConfigs.get('anchorDataCacheMapConfig') - expect: 'system created instance with correct config' - assert anchorDataCacheConfig.clusterName == 'cps-and-ncmp-test-caches' - assert anchorDataCacheMapConfig.backupCount == 1 - assert anchorDataCacheMapConfig.asyncBackupCount == 0 - } - - def 'Verify deployment network configs for Distributed Caches'() { - given: 'the Anchor Data Cache config' - def anchorDataCacheNetworkConfig = Hazelcast.getHazelcastInstanceByName('hazelCastInstanceCpsCore').config.networkConfig - expect: 'system created instance with correct config' - assert anchorDataCacheNetworkConfig.join.autoDetectionConfig.enabled - assert !anchorDataCacheNetworkConfig.join.kubernetesConfig.enabled - } - - def 'Verify network config'() { - given: 'Synchronization config object and test configuration' - def objectUnderTest = new AnchorDataCacheConfig() - def testConfig = new Config() - when: 'kubernetes properties are enabled' - objectUnderTest.cacheKubernetesEnabled = true - objectUnderTest.cacheKubernetesServiceName = 'test-service-name' - and: 'method called to update the discovery mode' - objectUnderTest.updateDiscoveryMode(testConfig) - then: 'applied properties are reflected' - assert testConfig.networkConfig.join.kubernetesConfig.enabled - assert testConfig.networkConfig.join.kubernetesConfig.properties.get('service-name') == 'test-service-name' - - } - -} diff --git a/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy deleted file mode 100644 index 022cd74ea6..0000000000 --- a/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy +++ /dev/null @@ -1,76 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2023 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.cache - -import com.hazelcast.config.Config -import com.hazelcast.config.RestEndpointGroup -import spock.lang.Specification - -class HazelcastCacheConfigSpec extends Specification { - - def objectUnderTest = new HazelcastCacheConfig() - - def 'Create Hazelcast instance with a #scenario'() { - given: 'a cluster name' - objectUnderTest.clusterName = 'my cluster' - when: 'an hazelcast instance is created (name has to be unique)' - def result = objectUnderTest.createHazelcastInstance(scenario, config) - then: 'the instance is created and has the correct name' - assert result.name == scenario - and: 'if applicable it has a map config with the expected name' - if (expectMapConfig) { - assert result.config.mapConfigs.values()[0].name == 'my map config' - } else { - assert result.config.mapConfigs.isEmpty() - } - and: 'if applicable it has a queue config with the expected name' - if (expectQueueConfig) { - assert result.config.queueConfigs.values()[0].name == 'my queue config' - } else { - assert result.config.queueConfigs.isEmpty() - } - and: 'if applicable it has a set config with the expected name' - if (expectSetConfig) { - assert result.config.setConfigs.values()[0].name == 'my set config' - } else { - assert result.config.setConfigs.isEmpty() - } - where: 'the following configs are used' - scenario | config || expectMapConfig | expectQueueConfig | expectSetConfig - 'Map Config' | HazelcastCacheConfig.createMapConfig('my map config') || true | false | false - 'Queue Config' | HazelcastCacheConfig.createQueueConfig('my queue config') || false | true | false - 'Set Config' | HazelcastCacheConfig.createSetConfig('my set config') || false | false | true - } - - def 'Verify Hazelcast Cluster Information'() { - given: 'a test configuration' - def testConfig = new Config() - when: 'cluster information is exposed' - objectUnderTest.exposeClusterInformation(testConfig) - then: 'REST api configs are enabled' - assert testConfig.networkConfig.restApiConfig.enabled - and: 'only health check and cluster read are enabled' - def enabledGroups = testConfig.networkConfig.restApiConfig.enabledGroups - assert enabledGroups.size() == 2 - assert enabledGroups.containsAll([RestEndpointGroup.CLUSTER_READ, RestEndpointGroup.HEALTH_CHECK]) - } - -} diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy index e305abee86..f028d5d5d9 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy @@ -35,6 +35,7 @@ class DataNodeBuilderSpec extends Specification { def objectUnderTest = new DataNodeBuilder() def yangParserHelper = new YangParserHelper() + def validateAndParse = false def expectedLeavesByXpathMap = [ '/test-tree' : [], @@ -60,7 +61,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data parsed into container node object' def jsonData = TestUtils.getResourceFileContent('test-tree.json') - def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -80,7 +81,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data parsed into container node object' def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }' - def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree', validateAndParse) when: 'the container node is converted to a data node with parent node xpath defined' def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath('/test-tree').build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -96,7 +97,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data parsed into container node object' def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json') - def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) when: 'the container node is converted to a data node ' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -129,7 +130,7 @@ class DataNodeBuilderSpec extends Specification { def parentNodeXpath = "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']" and: 'the json data fragment parsed into container node object for given parent node xpath' def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}' - def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath, validateAndParse) when: 'the container node is converted to a data node with given parent node xpath' def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).build() then: 'the resulting data node represents a child of augmentation node' @@ -144,7 +145,7 @@ class DataNodeBuilderSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'the json data fragment parsed into container node object' def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json') - def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -162,7 +163,7 @@ class DataNodeBuilderSpec extends Specification { and: 'parent node xpath referencing parent of list element' def parentNodeXpath = '/test-tree' and: 'the json data fragment (list element) parsed into container node object' - def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath, validateAndParse) when: 'the container node is converted to a data node collection' def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).buildCollection() def resultXpaths = result.collect { it.getXpath() } diff --git a/cps-service/src/test/groovy/org/onap/cps/cache/AnchorDataCacheEntrySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/ContentTypeSpec.groovy index f38b45d17c..cada33ef06 100644 --- a/cps-service/src/test/groovy/org/onap/cps/cache/AnchorDataCacheEntrySpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/ContentTypeSpec.groovy @@ -1,40 +1,37 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022 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.cache - - -import spock.lang.Specification - -class AnchorDataCacheEntrySpec extends Specification { - - def objectUnderTest = new AnchorDataCacheEntry() - - def 'Anchor Data Cache Properties Management.'() { - when: 'a property named "sample" is added to the cache' - objectUnderTest.setProperty('sample', 123) - then: 'the cache has that property' - assert objectUnderTest.hasProperty('sample') - and: 'the value is correct' - assert objectUnderTest.getProperty('sample') == 123 - and: 'the cache does not have an an object called "something else"' - assert objectUnderTest.hasProperty('something else') == false - } -} +/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 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.utils;
+
+import spock.lang.Specification;
+import org.springframework.http.MediaType
+
+
+class ContentTypeSpec extends Specification {
+
+ def 'Should return correct ContentType based on given input.'() {
+ given: 'contentType fromString method converts the input string as expectedContentType'
+ ContentType.fromString(contentTypeString) == expectedContentType
+ where:
+ contentTypeString || expectedContentType
+ MediaType.APPLICATION_XML_VALUE || ContentType.XML
+ MediaType.APPLICATION_JSON_VALUE || ContentType.JSON
+ }
+
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/PrefixResolverSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/PrefixResolverSpec.groovy index b975de6555..13b042f1ae 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/PrefixResolverSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/PrefixResolverSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. * ================================================================================ @@ -22,11 +22,8 @@ package org.onap.cps.utils -import com.hazelcast.map.IMap import org.onap.cps.TestUtils -import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.impl.YangTextSchemaSourceSetCache -import org.onap.cps.cache.AnchorDataCacheEntry import org.onap.cps.spi.model.Anchor import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder @@ -34,13 +31,9 @@ import spock.lang.Specification class PrefixResolverSpec extends Specification { - def mockCpsAnchorService = Mock(CpsAnchorService) - def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) - def mockAnchorDataCache = Mock(IMap<String, AnchorDataCacheEntry>) - - def objectUnderTest = new PrefixResolver(mockCpsAnchorService, mockYangTextSchemaSourceSetCache, mockAnchorDataCache) + def objectUnderTest = new PrefixResolver(mockYangTextSchemaSourceSetCache) def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) @@ -48,29 +41,16 @@ class PrefixResolverSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - def setup() { - given: 'an anchor for the test-tree model' - def anchor = new Anchor(dataspaceName: 'testDataspace', name: 'testAnchor') - and: 'the system can get this anchor' - mockCpsAnchorService.getAnchor('testDataspace', 'testAnchor') >> anchor - and: 'the schema source cache contains the schema context for the test-tree module' - mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext - } + def anchor = new Anchor(dataspaceName: 'testDataspace', name: 'testAnchor') def 'get xpath prefix using node schema context'() { + given: 'the schema source cache contains the schema context for the test-tree module' + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + mockYangTextSchemaSourceSetCache.get(*_) >> mockYangTextSchemaSourceSet when: 'the prefix of the yang module is retrieved' - def result = objectUnderTest.getPrefix('testDataspace', 'testAnchor', xpath) + def result = objectUnderTest.getPrefix(anchor, xpath) then: 'the expected prefix is returned' result == expectedPrefix - and: 'the cache is updated for the given anchor with a map of prefixes per top level container (just one one this case)' - 1 * mockAnchorDataCache.put('testAnchor',_ , _ ,_) >> { args -> { - def prefixPerContainerName = args[1].getProperty("prefixPerContainerName") - assert prefixPerContainerName.size() == 1 - assert prefixPerContainerName.get('test-tree') == 'tree' - } - } - and: 'schema source cache is used (i.e. need to build schema context)' - 1 * mockYangTextSchemaSourceSetCache.get(*_) >> mockYangTextSchemaSourceSet where: 'the following scenarios are applied' xpath || expectedPrefix '/test-tree' || 'tree' @@ -82,37 +62,4 @@ class PrefixResolverSpec extends Specification { '/not-defined' || '' } - def 'get prefix with populated anchor data cache with #scenario cache entry'() { - given: 'anchor data cache is populated for the anchor with a prefix for top level container named #cachedTopLevelContainerName' - def anchorDataCacheEntry = new AnchorDataCacheEntry() - def prefixPerContainerName = [(cachedTopLevelContainerName): 'cachedPrefix'] - anchorDataCacheEntry.setProperty('prefixPerContainerName',prefixPerContainerName) - mockAnchorDataCache.containsKey('testAnchor') >> true - mockAnchorDataCache.get('testAnchor') >> anchorDataCacheEntry - when: 'the prefix of the yang module is retrieved' - def result = objectUnderTest.getPrefix('testDataspace', 'testAnchor', '/test-tree') - then: 'the expected prefix is returned' - result == expectedPrefix - and: 'schema source cache is not used (i.e. no need to build schema context)' - 0 * mockYangTextSchemaSourceSetCache.get(*_) - where: 'the following scenarios are applied' - scenario | cachedTopLevelContainerName || expectedPrefix - 'matching' | 'test-tree' || 'cachedPrefix' - 'non-matching' | 'other' || '' - } - - def 'get prefix with other (non relevant) data in anchor data cache'() { - given: 'anchor data cache is populated with non relevant other property' - def anchorDataCacheEntry = new AnchorDataCacheEntry() - anchorDataCacheEntry.setProperty('something else', 'does not matter') - mockAnchorDataCache.containsKey('testAnchor') >> true - mockAnchorDataCache.get('testAnchor') >> anchorDataCacheEntry - when: 'the prefix of the yang module is retrieved' - def result = objectUnderTest.getPrefix('testDataspace', 'testAnchor', '/test-tree') - then: 'the expected prefix is returned' - result == 'tree' - and: 'schema source cache is used (i.e. need to build schema context)' - 1 * mockYangTextSchemaSourceSetCache.get(*_) >> mockYangTextSchemaSourceSet - } - } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy index dc6027de25..3b21145293 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG * Modifications Copyright (c) 2023-2024 Nordix Foundation + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +22,14 @@ package org.onap.cps.utils import org.onap.cps.TestUtils +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.w3c.dom.DOMException import org.xml.sax.SAXParseException import spock.lang.Specification +import static org.onap.cps.utils.XmlFileUtils.convertDataMapsToXml + class XmlFileUtilsSpec extends Specification { def 'Parse a valid xml content #scenario'(){ @@ -68,4 +73,56 @@ class XmlFileUtilsSpec extends Specification { 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>' | '/test-tree/branch[@name=\'Branch\']' || '<?xml version="1.0" encoding="UTF-8"?><branch xmlns="org:onap:cps:test:test-tree"><name>Branch</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' } + def 'Convert data maps to XML #scenario'() { + when: 'data maps are converted to XML' + def result = convertDataMapsToXml(dataMaps) + then: 'the result contains the expected XML' + assert result == expectedXmlOutput + where: + scenario | dataMaps || expectedXmlOutput + 'single XML branch' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': ['Sparrow', 'Owl']]]]] || '<branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds><birds>Owl</birds></nest></branch>' + 'nested XML branch' | [['test-tree': [branch: [name: 'Left', nest: [name: 'Small', birds: 'Sparrow']]]]] || '<test-tree><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' + 'list of branch within a test tree' | [['test-tree': [branch: [[name: 'Left', nest: [name: 'Small', birds: 'Sparrow']], [name: 'Right', nest: [name: 'Big', birds: 'Owl']]]]]] || '<test-tree><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch><branch><name>Right</name><nest><name>Big</name><birds>Owl</birds></nest></branch></test-tree>' + 'list of birds under a nest' | [['nest': ['name': 'Small', 'birds': ['Sparrow']]]] || '<nest><name>Small</name><birds>Sparrow</birds></nest>' + 'XML Content map with null key/value' | [['test-tree': [branch: [name: 'Left', nest: []]]]] || '<test-tree><branch><name>Left</name><nest/></branch></test-tree>' + 'XML Content list is empty' | [['nest': ['name': 'Small', 'birds': []]]] || '<nest><name>Small</name><birds/></nest>' + 'XML with mixed content in list' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': ['', 'Sparrow']]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/><birds>Sparrow</birds></nest></branch>' + } + + def 'Convert data maps to XML with null or empty maps and lists'() { + when: 'data maps with empty content are converted to XML' + def result = convertDataMapsToXml(dataMaps) + then: 'the result contains the expected XML or handles nulls correctly' + assert result == expectedXmlOutput + where: + scenario | dataMaps || expectedXmlOutput + 'null entry in map' | [['branch': []]] || '<branch/>' + 'list with null object' | [['branch': [name: 'Left', nest: [name: 'Small', birds: []]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/></nest></branch>' + 'list containing null list' | [['test-tree': [branch: '']]] || '<test-tree><branch/></test-tree>' + 'nested map with null values' | [['test-tree': [branch: [name: 'Left', nest: '']]]] || '<test-tree><branch><name>Left</name><nest/></branch></test-tree>' + } + + def 'Converting data maps to xml with no data'() { + given: 'A list of maps where entry is null' + def dataMapWithNull = [null] + when: 'convert the dataMaps to XML' + convertDataMapsToXml(dataMapWithNull) + then: 'a validation exception is thrown' + def exception = thrown(DataValidationException) + and:'the cause is a null pointer exception' + assert exception.cause instanceof NullPointerException + } + + def 'Converting data maps to xml with document syntax error'() { + given: 'A list of maps with an invalid entry' + def dataMap = [['invalid<tag>': 'value']] + when: 'convert the dataMaps to XML' + convertDataMapsToXml(dataMap) + then: 'a validation exception is thrown' + def exception = thrown(DataValidationException) + and:'the cause is a document object model exception' + assert exception.cause instanceof DOMException + + } + } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy index 073383113d..e1490c28ab 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy @@ -30,6 +30,8 @@ import spock.lang.Specification class YangParserHelperSpec extends Specification { def objectUnderTest = new YangParserHelper() + def validateOnly = true + def validateAndParse = false def 'Parsing a valid multicontainer Json String.'() { given: 'a yang model (file)' @@ -38,7 +40,7 @@ class YangParserHelperSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'the json data is parsed' - def result = objectUnderTest.parseData(ContentType.JSON, jsonData, schemaContext, '') + def result = objectUnderTest.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse) then: 'a ContainerNode holding collection of normalized nodes is returned' result.body().getAt(index) instanceof NormalizedNode == true then: 'qualified name of children created is as expected' @@ -56,7 +58,7 @@ class YangParserHelperSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'the data is parsed' - NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '') + NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '', validateAndParse) then: 'the result is a normalized node of the correct type' if (revision) { result.identifier.nodeType == QName.create(namespace, revision, localName) @@ -74,7 +76,7 @@ class YangParserHelperSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'invalid data is parsed' - objectUnderTest.parseData(contentType, invalidData, schemaContext, '') + objectUnderTest.parseData(contentType, invalidData, schemaContext, '', validateAndParse) then: 'an exception is thrown' thrown(DataValidationException) where: 'the following invalid data is provided' @@ -92,7 +94,7 @@ class YangParserHelperSpec extends Specification { def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() when: 'json string is parsed' - def result = objectUnderTest.parseData(contentType, nodeData, schemaContext, parentNodeXpath) + def result = objectUnderTest.parseData(contentType, nodeData, schemaContext, parentNodeXpath, validateAndParse) then: 'a ContainerNode holding collection of normalized nodes is returned' result.body().getAt(0) instanceof NormalizedNode == true then: 'result represents a node of expected type' @@ -112,7 +114,7 @@ class YangParserHelperSpec extends Specification { def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() when: 'json string is parsed' - objectUnderTest.parseData(ContentType.JSON, '{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, parentNodeXpath) + objectUnderTest.parseData(ContentType.JSON, '{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, parentNodeXpath, validateAndParse) then: 'expected exception is thrown' thrown(DataValidationException) where: @@ -129,7 +131,7 @@ class YangParserHelperSpec extends Specification { def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() when: 'malformed json string is parsed' - objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '') + objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '', validateAndParse) then: 'an exception is thrown' thrown(DataValidationException) where: 'the following malformed json is provided' @@ -145,7 +147,7 @@ class YangParserHelperSpec extends Specification { and: 'some json data with space in the array elements' def jsonDataWithSpacesInArrayElement = TestUtils.getResourceFileContent('bookstore.json') when: 'that json data is parsed' - objectUnderTest.parseData(ContentType.JSON, jsonDataWithSpacesInArrayElement, schemaContext, '') + objectUnderTest.parseData(ContentType.JSON, jsonDataWithSpacesInArrayElement, schemaContext, '', validateAndParse) then: 'no exception thrown' noExceptionThrown() } @@ -162,5 +164,22 @@ class YangParserHelperSpec extends Specification { 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] } + def 'Validating #scenario xpath String.'() { + given: 'a data model (file) is provided' + def fileData = TestUtils.getResourceFileContent(contentFile) + and: 'the schema context is built for that data model' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the data is parsed to be validated' + objectUnderTest.parseData(contentType, fileData, schemaContext, parentNodeXpath, validateOnly) + then: 'no exception is thrown' + noExceptionThrown() + where: + scenario | parentNodeXpath | contentFile | contentType + 'JSON without parent node' | '' | 'bookstore.json' | ContentType.JSON + 'JSON with parent node' | '/bookstore' | 'bookstore-categories-data.json' | ContentType.JSON + 'XML without parent node' | '' | 'bookstore.xml' | ContentType.XML + 'XML with parent node' | '/bookstore' | 'bookstore-categories-data.xml' | ContentType.XML + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy index 18d0502e30..6c52becbe1 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy @@ -26,7 +26,6 @@ import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.Anchor import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSet -import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode import org.opendaylight.yangtools.yang.model.api.SchemaContext import spock.lang.Specification @@ -47,6 +46,8 @@ class YangParserSpec extends Specification { def containerNodeFromYangUtils = Mock(ContainerNode) def noParent = '' + def validateOnly = true + def validateAndParse = false def setup() { mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet @@ -55,7 +56,7 @@ class YangParserSpec extends Specification { def 'Parsing data.'() { given: 'the yang parser (utility) always returns a container node' - mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> containerNodeFromYangUtils when: 'parsing some json data' def result = objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent) then: 'the schema source set for the correct dataspace and schema set is retrieved form the cache' @@ -68,7 +69,7 @@ class YangParserSpec extends Specification { def 'Parsing data with exception on first attempt.'() { given: 'the yang parser throws an exception on the first attempt only' - mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> { throw new DataValidationException(noParent, noParent) } >> containerNodeFromYangUtils + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> { throw new DataValidationException(noParent, noParent) } >> containerNodeFromYangUtils when: 'attempt to parse some data' def result = objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent) then: 'the cache is cleared for the correct dataspace and schema' @@ -79,7 +80,7 @@ class YangParserSpec extends Specification { def 'Parsing data with exception on all attempts.'() { given: 'the yang parser always throws an exception' - mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> { throw new DataValidationException(noParent, noParent) } + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> { throw new DataValidationException(noParent, noParent) } when: 'attempt to parse some data' objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent) then: 'a data validation exception is thrown' @@ -94,9 +95,46 @@ class YangParserSpec extends Specification { when: 'parsing some json data' def result = objectUnderTest.parseData(ContentType.JSON, 'some json', yangResourcesNameToContentMap, noParent) then: 'the yang parser helper always returns a container node' - 1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils + 1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> containerNodeFromYangUtils and: 'the result is the same container node as return from yang utils' assert result == containerNodeFromYangUtils } + def 'Validating #scenario data using Yang parser with cache retrieval.'() { + given: 'the yang parser (utility) is set up and schema context is available' + mockYangParserHelper.parseData(contentType, 'some json', mockSchemaContext, noParent, validateOnly) + when: 'attempt to parse data with no parent node xpath' + objectUnderTest.validateData(contentType, 'some json or xml data', anchor, noParent) + then: 'the correct schema set is retrieved from the cache for the dataspace and schema' + 1 * mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet + and: 'no cache entries are removed during validation' + 0 * mockYangTextSchemaSourceSetCache.removeFromCache(*_) + where: + scenario | contentType + 'JSON' | ContentType.JSON + 'XML' | ContentType.XML + } + + def 'Validating data when parsing fails on first attempt and recovers.'() { + given: 'the Yang parser throws an exception on the first attempt but succeeds on the second' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateOnly) >> { throw new DataValidationException(noParent, noParent) } >> null + when: 'attempting to parse JSON data' + objectUnderTest.validateData(ContentType.JSON, 'some json', anchor, noParent) + then: 'the cache is cleared for the correct dataspace and schema after the first failure' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + and: 'no exceptions are thrown after the second attempt' + noExceptionThrown() + } + + def 'Validating data with repeated parsing failures leading to exception.'() { + given: 'the yang parser throws an exception on the first attempt only' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateOnly) >> { throw new DataValidationException(noParent, noParent) } + when: 'attempting to parse JSON data' + objectUnderTest.validateData(ContentType.JSON, 'some json', anchor, noParent) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + and: 'the cache is cleared for the correct dataspace and schema after the failure' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + } + } diff --git a/cps-service/src/test/resources/application.yml b/cps-service/src/test/resources/application.yml index b666885f22..be71d37d2a 100644 --- a/cps-service/src/test/resources/application.yml +++ b/cps-service/src/test/resources/application.yml @@ -1,6 +1,6 @@ # ============LICENSE_START======================================================= # Copyright (c) 2021 Bell Canada. -# Modification Copyright (C) 2022 Nordix Foundation. +# Modification Copyright (C) 2022-2024 Nordix Foundation. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -41,11 +41,3 @@ spring: logging: level: org.apache.kafka: ERROR - -# Custom Hazelcast Config. -hazelcast: - cluster-name: "cps-and-ncmp-test-caches" - mode: - kubernetes: - enabled: false - service-name: "cps-and-ncmp-service" diff --git a/cps-service/src/test/resources/bookstore-categories-data.json b/cps-service/src/test/resources/bookstore-categories-data.json new file mode 100644 index 0000000000..7dc22b17f7 --- /dev/null +++ b/cps-service/src/test/resources/bookstore-categories-data.json @@ -0,0 +1,49 @@ +{ + "categories": [ + { + "code": "01/1", + "name": "SciFi", + "books": [ + { + "authors": [ + "Iain M. Banks" + ], + "lang": "en/it", + "price": "895", + "pub_year": "1994", + "title": "Feersum Endjinn/Endjinn Feersum" + }, + { + "authors": [ + "Ursula K. Le Guin", + "Joe Haldeman", + "Orson Scott Card", + "david Brin", + "Rober Silverberg", + "Dan Simmons", + "Greg Bear" + ], + "lang": "en", + "price": "1099", + "pub_year": "1999", + "title": "Far Horizons" + } + ] + }, + { + "name": "kids", + "code": "02", + "books": [ + { + "authors": [ + "Philip Pullman" + ], + "lang": "en", + "price": "699", + "pub_year": "1995", + "title": "The Golden Compass" + } + ] + } + ] +}
\ No newline at end of file diff --git a/cps-service/src/test/resources/bookstore-categories-data.xml b/cps-service/src/test/resources/bookstore-categories-data.xml new file mode 100644 index 0000000000..c8592c1f90 --- /dev/null +++ b/cps-service/src/test/resources/bookstore-categories-data.xml @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='UTF-8'?> +<categories> + <code>1</code> + <name>SciFi</name> + <books> + <title>2001: A Space Odyssey</title> + <lang>en</lang> + <authors> + Iain M. Banks + </authors> + <pub_year>1994</pub_year> + <price>895</price> + </books> +</categories>
\ No newline at end of file diff --git a/cps-service/src/test/resources/e2e/basic/cps-cavsta-onap-internal2021-01-28.yang b/cps-service/src/test/resources/e2e/basic/cps-cavsta-onap-internal2021-01-28.yang index 32517398a3..2dabd79e8e 100644 --- a/cps-service/src/test/resources/e2e/basic/cps-cavsta-onap-internal2021-01-28.yang +++ b/cps-service/src/test/resources/e2e/basic/cps-cavsta-onap-internal2021-01-28.yang @@ -36,7 +36,7 @@ module cps-cavsta-onap-internal { description "RAN Network YANG Model for ONAP/O-RAN POC"; reference - "https://wiki.onap.org/display/DW/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; + "https://lf-onap.atlassian.net/wiki/spaces/DW/pages/16414819/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; } typedef Tac { diff --git a/cps-service/src/test/resources/e2e/basic/cps-ran-inventory@2021-01-28.yang b/cps-service/src/test/resources/e2e/basic/cps-ran-inventory@2021-01-28.yang index c16a682512..2401409443 100644 --- a/cps-service/src/test/resources/e2e/basic/cps-ran-inventory@2021-01-28.yang +++ b/cps-service/src/test/resources/e2e/basic/cps-ran-inventory@2021-01-28.yang @@ -34,7 +34,7 @@ module cps-ran-inventory { description "RAN Network YANG Model for ONAP/O-RAN POC"; reference - "https://wiki.onap.org/display/DW/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; + "https://lf-onap.atlassian.net/wiki/spaces/DW/pages/16414819/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; } typedef Mcc { diff --git a/cps-service/src/test/resources/e2e/basic/cps-ran-schema-model@2021-05-19.yang b/cps-service/src/test/resources/e2e/basic/cps-ran-schema-model@2021-05-19.yang index 5fd292a99d..3223b15e65 100644 --- a/cps-service/src/test/resources/e2e/basic/cps-ran-schema-model@2021-05-19.yang +++ b/cps-service/src/test/resources/e2e/basic/cps-ran-schema-model@2021-05-19.yang @@ -43,14 +43,14 @@ module cps-ran-schema-model { description "Added support for OOF PCI SON Use case"; reference - "https://wiki.onap.org/display/DW/CPS+APIs"; + "https://lf-onap.atlassian.net/wiki/spaces/DW/pages/16456851/CPS+APIs"; } revision 2021-01-28 { description "CPS RAN Network YANG Model for ONAP/O-RAN POC"; reference - "https://wiki.onap.org/display/DW/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; + "https://lf-onap.atlassian.net/wiki/spaces/DW/pages/16414819/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; } typedef usageState { diff --git a/cps-service/src/test/resources/e2e/basic/ran-network2020-08-06.yang b/cps-service/src/test/resources/e2e/basic/ran-network2020-08-06.yang index 5065659307..a4612e73fb 100755 --- a/cps-service/src/test/resources/e2e/basic/ran-network2020-08-06.yang +++ b/cps-service/src/test/resources/e2e/basic/ran-network2020-08-06.yang @@ -43,7 +43,7 @@ module ran-network { description "RAN Network YANG Model for ONAP/O-RAN POC"; reference - "https://wiki.onap.org/display/DW/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; + "https://lf-onap.atlassian.net/wiki/spaces/DW/pages/16414819/E2E+Network+Slicing+Use+Case+in+R7+Guilin"; } typedef usageState { |