diff options
author | ToineSiebelink <toine.siebelink@est.tech> | 2024-01-16 08:40:51 +0000 |
---|---|---|
committer | ToineSiebelink <toine.siebelink@est.tech> | 2024-01-17 14:56:53 +0000 |
commit | 6229cfeafade160ed281fc410454c7498b8a21dc (patch) | |
tree | 714247ebb871c00fce2b60fd5f0dcbdcebf1a233 | |
parent | 2dce7ab2fd627450550666e3993e28e10f6a8548 (diff) |
Clear instance based Schema Context Cache upon validation errors
- retry yang parser exceptions one time by clearing cache
- split yangUtils into YangParserHelper (instance based) and YangUtils for non-parsering static methods
- removed public methods only used from test. Refactored to use variation with (empty) parent path
- removed use of optional for parentPath, easier to handle with just an empty string!
- make methods no longer used in production code in YangUtils are private
- udpate testware to use proper public methods instead of private methods of yang utils / parser (helper)
Issue-ID:CPS-2000
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
Change-Id: I0c7590a5e1495d047006e7136f1bd873be37f7b0
16 files changed, 650 insertions, 511 deletions
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 e49714b66d..b3f42279df 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 @@ -48,9 +48,8 @@ import org.onap.cps.spi.model.DataNodeBuilder; import org.onap.cps.spi.model.DeltaReport; import org.onap.cps.spi.utils.CpsValidator; import org.onap.cps.utils.ContentType; -import org.onap.cps.utils.TimedYangParser; +import org.onap.cps.utils.YangParser; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; -import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.springframework.stereotype.Service; @Service @@ -63,9 +62,8 @@ public class CpsDataServiceImpl implements CpsDataService { private final CpsDataPersistenceService cpsDataPersistenceService; private final CpsAnchorService cpsAnchorService; - private final YangTextSchemaSourceSetCache yangTextSchemaSourceSetCache; private final CpsValidator cpsValidator; - private final TimedYangParser timedYangParser; + private final YangParser yangParser; private final CpsDeltaService cpsDeltaService; @Override @@ -309,10 +307,9 @@ public class CpsDataServiceImpl implements CpsDataService { private Collection<DataNode> buildDataNodes(final Anchor anchor, final String parentNodeXpath, final String nodeData, final ContentType contentType) { - final SchemaContext schemaContext = getSchemaContext(anchor); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = timedYangParser.parseData(contentType, nodeData, schemaContext); + final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, ""); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withContainerNode(containerNode) .buildCollection(); @@ -323,7 +320,7 @@ public class CpsDataServiceImpl implements CpsDataService { } final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); final ContainerNode containerNode = - timedYangParser.parseData(contentType, nodeData, schemaContext, normalizedParentNodeXpath); + yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withParentNodeXpath(normalizedParentNodeXpath) .withContainerNode(containerNode) @@ -334,10 +331,6 @@ public class CpsDataServiceImpl implements CpsDataService { return dataNodes; } - private SchemaContext getSchemaContext(final Anchor anchor) { - return yangTextSchemaSourceSetCache - .get(anchor.getDataspaceName(), anchor.getSchemaSetName()).getSchemaContext(); - } private static boolean isRootNodeXpath(final String xpath) { return ROOT_NODE_XPATH.equals(xpath); diff --git a/cps-service/src/main/java/org/onap/cps/utils/TimedYangParser.java b/cps-service/src/main/java/org/onap/cps/utils/TimedYangParser.java deleted file mode 100644 index 4640593dc5..0000000000 --- a/cps-service/src/main/java/org/onap/cps/utils/TimedYangParser.java +++ /dev/null @@ -1,61 +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.utils; - -import io.micrometer.core.annotation.Timed; -import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; -import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import org.springframework.stereotype.Service; - -@Service -public class TimedYangParser { - - /** - * Parses data into Collection of NormalizedNode according to given schema context. - * - * @param nodeData data string - * @param schemaContext schema context describing associated data model - * @return the NormalizedNode object - */ - @Timed(value = "cps.utils.yangparser.nodedata.parse", - description = "Time taken to parse node data without a parent") - public ContainerNode parseData(final ContentType contentType, - final String nodeData, - final SchemaContext schemaContext) { - return YangUtils.parseData(contentType, nodeData, schemaContext); - } - - /** - * Parses data into NormalizedNode according to given schema context. - * - * @param nodeData data string - * @param schemaContext schema context describing associated data model - * @return the NormalizedNode object - */ - @Timed(value = "cps.utils.yangparser.nodedata.with.parent.parse", - description = "Time taken to parse node data with a parent") - public ContainerNode parseData(final ContentType contentType, - final String nodeData, - final SchemaContext schemaContext, - final String parentNodeXpath) { - return YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath); - } -} 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 98c7947e1c..7a6d0bb3d5 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 @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG - * Modifications Copyright (C) 2023 Nordix Foundation. + * Modifications 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. @@ -67,7 +67,7 @@ public class XmlFileUtils { public static String prepareXmlContent(final String xmlContent, final SchemaContext schemaContext) throws IOException, ParserConfigurationException, TransformerException, SAXException { return addRootNodeToXmlContent(xmlContent, schemaContext.getModules().iterator().next().getName(), - YangUtils.DATA_ROOT_NODE_NAMESPACE); + YangParserHelper.DATA_ROOT_NODE_NAMESPACE); } /** @@ -106,7 +106,7 @@ public class XmlFileUtils { documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8))); final Element root = document.getDocumentElement(); if (!root.getTagName().equals(rootNodeTagName) - && !root.getTagName().equals(YangUtils.DATA_ROOT_NODE_TAG_NAME)) { + && !root.getTagName().equals(YangParserHelper.DATA_ROOT_NODE_TAG_NAME)) { final Document documentWithRootNode = addDataRootNode(root, rootNodeTagName, namespace, rootNodeProperty); documentWithRootNode.setXmlStandalone(true); final Transformer transformer = getTransformerFactory().newTransformer(); 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 new file mode 100644 index 0000000000..6299ef39f4 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java @@ -0,0 +1,70 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.utils; + +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; +import org.onap.cps.api.impl.YangTextSchemaSourceSetCache; +import org.onap.cps.spi.exceptions.DataValidationException; +import org.onap.cps.spi.model.Anchor; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class YangParser { + + private final YangParserHelper yangParserHelper; + private final YangTextSchemaSourceSetCache yangTextSchemaSourceSetCache; + + /** + * Parses data into (normalized) ContainerNode according to schema context for the given anchor. + * + * @param nodeData data string + * @param anchor the anchor for the node data + * @return the NormalizedNode object + */ + @Timed(value = "cps.utils.yangparser.nodedata.with.parent.parse", + description = "Time taken to parse node data with a parent") + public ContainerNode parseData(final ContentType contentType, + final String nodeData, + final Anchor anchor, + final String parentNodeXpath) { + final SchemaContext schemaContext = getSchemaContext(anchor); + try { + return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + } catch (final DataValidationException e) { + invalidateCache(anchor); + } + return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + } + + private SchemaContext getSchemaContext(final Anchor anchor) { + return yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(), + anchor.getSchemaSetName()).getSchemaContext(); + } + + private void invalidateCache(final Anchor anchor) { + yangTextSchemaSourceSetCache.removeFromCache(anchor.getDataspaceName(), anchor.getSchemaSetName()); + } + +} 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 new file mode 100644 index 0000000000..5cadd29368 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java @@ -0,0 +1,252 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.utils; + +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; +import java.io.IOException; +import java.io.StringReader; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.TransformerException; +import lombok.RequiredArgsConstructor; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.cpspath.parser.PathParsingException; +import org.onap.cps.spi.exceptions.DataValidationException; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.springframework.stereotype.Service; +import org.xml.sax.SAXException; + +@Service +@RequiredArgsConstructor +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"; + + /** + * Parses data into NormalizedNode according to given schema context. + * + * @param contentType the type of the node data (json or xml) + * @param nodeData data string + * @param schemaContext schema context describing associated data model + * @param parentNodeXpath the xpath referencing the parent node current data fragment belong to + * @return the NormalizedNode object + */ + public ContainerNode parseData(final ContentType contentType, + final String nodeData, + final SchemaContext schemaContext, + final String parentNodeXpath) { + if (contentType == ContentType.JSON) { + return parseJsonData(nodeData, schemaContext, parentNodeXpath); + } + return parseXmlData(nodeData, schemaContext, parentNodeXpath); + } + + private ContainerNode parseJsonData(final String jsonData, + final SchemaContext schemaContext, + final String parentNodeXpath) { + final JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02 + .getShared((EffectiveModelContext) schemaContext); + final DataContainerNodeBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> dataContainerNodeBuilder = + Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier( + QName.create(DATA_ROOT_NODE_NAMESPACE, DATA_ROOT_NODE_TAG_NAME) + )); + final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter + .from(dataContainerNodeBuilder); + final JsonReader jsonReader = new JsonReader(new StringReader(jsonData)); + final JsonParserStream jsonParserStream; + + if (parentNodeXpath.isEmpty()) { + jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory); + } else { + final Collection<QName> dataSchemaNodeIdentifiers + = getDataSchemaNodeIdentifiers(schemaContext, parentNodeXpath); + final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext); + final EffectiveStatementInference effectiveStatementInference = + SchemaInferenceStack.of(effectiveModelContext, + SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); + jsonParserStream = + JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory, effectiveStatementInference); + } + + try (jsonParserStream) { + jsonParserStream.parse(jsonReader); + } catch (final IOException | JsonSyntaxException exception) { + throw new DataValidationException( + "Failed to parse json data: " + jsonData, exception.getMessage(), exception); + } catch (final IllegalStateException | IllegalArgumentException exception) { + throw new DataValidationException( + "Failed to parse json data. Unsupported xpath or json data:" + jsonData, exception + .getMessage(), exception); + } + return dataContainerNodeBuilder.build(); + } + + private ContainerNode parseXmlData(final String xmlData, + final SchemaContext schemaContext, + final String parentNodeXpath) { + final XMLInputFactory factory = XMLInputFactory.newInstance(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter + .from(normalizedNodeResult); + + final EffectiveModelContext effectiveModelContext = (EffectiveModelContext) schemaContext; + final XmlParserStream xmlParserStream; + final String preparedXmlContent; + try { + if (parentNodeXpath.isEmpty()) { + preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext); + xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveModelContext); + } else { + final DataSchemaNode parentSchemaNode = + (DataSchemaNode) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) + .get("dataSchemaNode"); + final Collection<QName> dataSchemaNodeIdentifiers = + getDataSchemaNodeIdentifiers(schemaContext, parentNodeXpath); + final EffectiveStatementInference effectiveStatementInference = + SchemaInferenceStack.of(effectiveModelContext, + SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); + preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, parentNodeXpath); + xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveStatementInference); + } + + try (xmlParserStream; + StringReader stringReader = new StringReader(preparedXmlContent)) { + final XMLStreamReader xmlStreamReader = factory.createXMLStreamReader(stringReader); + xmlParserStream.parse(xmlStreamReader); + } + } catch (final XMLStreamException | URISyntaxException | IOException | SAXException | NullPointerException + | ParserConfigurationException | TransformerException exception) { + throw new DataValidationException( + "Failed to parse xml data: " + xmlData, exception.getMessage(), exception); + } + final DataContainerChild dataContainerChild = + (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); + final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = + new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); + return Builders.containerBuilder().withChild(dataContainerChild).withNodeIdentifier(nodeIdentifier).build(); + } + + private static Collection<QName> getDataSchemaNodeIdentifiers(final SchemaContext schemaContext, + final String parentNodeXpath) { + return (Collection<QName>) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) + .get("dataSchemaNodeIdentifiers"); + } + + private static Map<String, Object> getDataSchemaNodeAndIdentifiersByXpath(final String parentNodeXpath, + final SchemaContext schemaContext) { + final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath); + return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), + new ArrayList<>()); + } + + private static String[] xpathToNodeIdSequence(final String xpath) { + try { + return CpsPathUtil.getXpathNodeIdSequence(xpath); + } catch (final PathParsingException pathParsingException) { + throw new DataValidationException(pathParsingException.getMessage(), pathParsingException.getDetails(), + pathParsingException); + } + } + + private static Map<String, Object> findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( + final String[] xpathNodeIdSequence, + final Collection<? extends DataSchemaNode> dataSchemaNodes, + final Collection<QName> dataSchemaNodeIdentifiers) { + final String currentXpathNodeId = xpathNodeIdSequence[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<>(); + dataSchemaNodeAndIdentifiers.put("dataSchemaNode", currentDataSchemaNode); + dataSchemaNodeAndIdentifiers.put("dataSchemaNodeIdentifiers", dataSchemaNodeIdentifiers); + return dataSchemaNodeAndIdentifiers; + } + if (currentDataSchemaNode instanceof DataNodeContainer) { + return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( + getNextLevelXpathNodeIdSequence(xpathNodeIdSequence), + ((DataNodeContainer) currentDataSchemaNode).getChildNodes(), + dataSchemaNodeIdentifiers); + } + throw schemaNodeNotFoundException(xpathNodeIdSequence[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 DataValidationException schemaNodeNotFoundException(final String schemaNodeIdentifier) { + return new DataValidationException("Invalid xpath.", + String.format("No schema node was found for xpath identifier '%s'.", schemaNodeIdentifier)); + } + + private static NormalizedNode getFirstChildXmlRoot(final NormalizedNode parent) { + final String rootNodeType = parent.getIdentifier().getNodeType().getLocalName(); + final Collection<DataContainerChild> children = (Collection<DataContainerChild>) parent.body(); + final Iterator<DataContainerChild> iterator = children.iterator(); + NormalizedNode child = null; + while (iterator.hasNext()) { + child = iterator.next(); + if (!child.getIdentifier().getNodeType().getLocalName().equals(rootNodeType) + && !(child instanceof LeafNode)) { + return child; + } + } + return getFirstChildXmlRoot(child); + } +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java b/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java index f00f9442ce..96841bf5c8 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2023 Nordix Foundation + * Copyright (C) 2020-2024 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. @@ -24,120 +24,16 @@ package org.onap.cps.utils; -import com.google.gson.JsonSyntaxException; -import com.google.gson.stream.JsonReader; -import java.io.IOException; -import java.io.StringReader; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamReader; -import javax.xml.transform.TransformerException; import lombok.AccessLevel; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.onap.cps.cpspath.parser.CpsPathUtil; -import org.onap.cps.cpspath.parser.PathParsingException; -import org.onap.cps.spi.exceptions.DataValidationException; -import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; -import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; -import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; -import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; -import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; -import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; -import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; -import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; -import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; -import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream; -import org.opendaylight.yangtools.yang.data.impl.schema.Builders; -import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; -import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; -import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; -import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; -import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; -import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference; -import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier; -import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; -import org.xml.sax.SAXException; -@Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public class YangUtils { - public static final String DATA_ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0"; - public static final String DATA_ROOT_NODE_TAG_NAME = "data"; - - /** - * Parses data into Collection of NormalizedNode according to given schema context. - * - * @param nodeData data string - * @param schemaContext schema context describing associated data model - * @return the NormalizedNode object - */ - static ContainerNode parseData(final ContentType contentType, - final String nodeData, - final SchemaContext schemaContext) { - if (contentType == ContentType.JSON) { - return parseJsonDataWithOptionalParent(nodeData, schemaContext, Optional.empty()); - } - return parseXmlDataWithOptionalParent(nodeData, schemaContext, Optional.empty()); - } - - /** - * Parses data into NormalizedNode according to given schema context. - * - * @param nodeData data string - * @param schemaContext schema context describing associated data model - * @return the NormalizedNode object - */ - static ContainerNode parseData(final ContentType contentType, - final String nodeData, - final SchemaContext schemaContext, - final String parentNodeXpath) { - if (contentType == ContentType.JSON) { - return parseJsonDataWithOptionalParent(nodeData, schemaContext, Optional.of(parentNodeXpath)); - } - return parseXmlDataWithOptionalParent(nodeData, schemaContext, Optional.of(parentNodeXpath)); - } - - /** - * Parses data into Collection of NormalizedNode according to given schema context. - * - * @param jsonData json data as string - * @param schemaContext schema context describing associated data model - * @return the Collection of NormalizedNode object - */ - public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext) { - return parseJsonDataWithOptionalParent(jsonData, schemaContext, Optional.empty()); - } - - /** - * Parses jsonData into Collection of NormalizedNode according to given schema context. - * - * @param jsonData json data fragment as string - * @param schemaContext schema context describing associated data model - * @param parentNodeXpath the xpath referencing the parent node current data fragment belong to - * @return the NormalizedNode object - */ - public static ContainerNode parseJsonData(final String jsonData, - final SchemaContext schemaContext, - final String parentNodeXpath) { - return parseJsonDataWithOptionalParent(jsonData, schemaContext, Optional.of(parentNodeXpath)); - } - /** * Create an xpath form a Yang Tools NodeIdentifier (i.e. PathArgument). * @@ -155,99 +51,6 @@ public class YangUtils { return xpathBuilder.toString(); } - private static ContainerNode parseJsonDataWithOptionalParent(final String jsonData, - final SchemaContext schemaContext, - final Optional<String> parentNodeXpath) { - final JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02 - .getShared((EffectiveModelContext) schemaContext); - final DataContainerNodeBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> dataContainerNodeBuilder = - Builders.containerBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier( - QName.create(DATA_ROOT_NODE_NAMESPACE, DATA_ROOT_NODE_TAG_NAME) - )); - final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter - .from(dataContainerNodeBuilder); - final JsonReader jsonReader = new JsonReader(new StringReader(jsonData)); - final JsonParserStream jsonParserStream; - - if (parentNodeXpath.isPresent()) { - final Collection<QName> dataSchemaNodeIdentifiers - = getDataSchemaNodeIdentifiers(schemaContext, parentNodeXpath.get()); - final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext); - final EffectiveStatementInference effectiveStatementInference = - SchemaInferenceStack.of(effectiveModelContext, - SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); - jsonParserStream = - JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory, effectiveStatementInference); - } else { - jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory); - } - - try (jsonParserStream) { - jsonParserStream.parse(jsonReader); - } catch (final IOException | JsonSyntaxException exception) { - throw new DataValidationException( - "Failed to parse json data: " + jsonData, exception.getMessage(), exception); - } catch (final IllegalStateException | IllegalArgumentException exception) { - throw new DataValidationException( - "Failed to parse json data. Unsupported xpath or json data:" + jsonData, exception - .getMessage(), exception); - } - return dataContainerNodeBuilder.build(); - } - - private static ContainerNode parseXmlDataWithOptionalParent(final String xmlData, - final SchemaContext schemaContext, - final Optional<String> parentNodeXpath) { - final XMLInputFactory factory = XMLInputFactory.newInstance(); - factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); - final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); - final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter - .from(normalizedNodeResult); - - final EffectiveModelContext effectiveModelContext = (EffectiveModelContext) schemaContext; - final XmlParserStream xmlParserStream; - final String preparedXmlContent; - try { - if (parentNodeXpath.isPresent()) { - final DataSchemaNode parentSchemaNode = - (DataSchemaNode) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath.get(), schemaContext) - .get("dataSchemaNode"); - final Collection<QName> dataSchemaNodeIdentifiers = - getDataSchemaNodeIdentifiers(schemaContext, parentNodeXpath.get()); - final EffectiveStatementInference effectiveStatementInference = - SchemaInferenceStack.of(effectiveModelContext, - SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); - preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, parentNodeXpath.get()); - xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveStatementInference); - } else { - preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext); - xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveModelContext); - } - - try (xmlParserStream; - StringReader stringReader = new StringReader(preparedXmlContent)) { - final XMLStreamReader xmlStreamReader = factory.createXMLStreamReader(stringReader); - xmlParserStream.parse(xmlStreamReader); - } - } catch (final XMLStreamException | URISyntaxException | IOException | SAXException | NullPointerException - | ParserConfigurationException | TransformerException exception) { - throw new DataValidationException( - "Failed to parse xml data: " + xmlData, exception.getMessage(), exception); - } - final DataContainerChild dataContainerChild = - (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); - final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = - new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); - return Builders.containerBuilder().withChild(dataContainerChild).withNodeIdentifier(nodeIdentifier).build(); - } - - private static Collection<QName> getDataSchemaNodeIdentifiers(final SchemaContext schemaContext, - final String parentNodeXpath) { - return (Collection<QName>) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) - .get("dataSchemaNodeIdentifiers"); - } - private static String getKeyAttributesStatement( final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) { final List<String> keyAttributes = nodeIdentifier.entrySet().stream().map( @@ -266,70 +69,4 @@ public class YangUtils { } } - private static Map<String, Object> getDataSchemaNodeAndIdentifiersByXpath(final String parentNodeXpath, - final SchemaContext schemaContext) { - final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath); - return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), - new ArrayList<>()); - } - - private static String[] xpathToNodeIdSequence(final String xpath) { - try { - return CpsPathUtil.getXpathNodeIdSequence(xpath); - } catch (final PathParsingException pathParsingException) { - throw new DataValidationException(pathParsingException.getMessage(), pathParsingException.getDetails(), - pathParsingException); - } - } - - private static Map<String, Object> findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( - final String[] xpathNodeIdSequence, - final Collection<? extends DataSchemaNode> dataSchemaNodes, - final Collection<QName> dataSchemaNodeIdentifiers) { - final String currentXpathNodeId = xpathNodeIdSequence[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<>(); - dataSchemaNodeAndIdentifiers.put("dataSchemaNode", currentDataSchemaNode); - dataSchemaNodeAndIdentifiers.put("dataSchemaNodeIdentifiers", dataSchemaNodeIdentifiers); - return dataSchemaNodeAndIdentifiers; - } - if (currentDataSchemaNode instanceof DataNodeContainer) { - return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( - getNextLevelXpathNodeIdSequence(xpathNodeIdSequence), - ((DataNodeContainer) currentDataSchemaNode).getChildNodes(), - dataSchemaNodeIdentifiers); - } - throw schemaNodeNotFoundException(xpathNodeIdSequence[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 DataValidationException schemaNodeNotFoundException(final String schemaNodeIdentifier) { - return new DataValidationException("Invalid xpath.", - String.format("No schema node was found for xpath identifier '%s'.", schemaNodeIdentifier)); - } - - private static NormalizedNode getFirstChildXmlRoot(final NormalizedNode parent) { - final String rootNodeType = parent.getIdentifier().getNodeType().getLocalName(); - final Collection<DataContainerChild> children = (Collection<DataContainerChild>) parent.body(); - final Iterator<DataContainerChild> iterator = children.iterator(); - NormalizedNode child = null; - while (iterator.hasNext()) { - child = iterator.next(); - if (!child.getIdentifier().getNodeType().getLocalName().equals(rootNodeType) - && !(child instanceof LeafNode)) { - return child; - } - } - return getFirstChildXmlRoot(child); - } } 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 322d2c9152..b2b2d7d44c 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 @@ -34,28 +34,26 @@ import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.exceptions.SessionManagerException import org.onap.cps.spi.exceptions.SessionTimeoutException import org.onap.cps.spi.model.Anchor -import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.spi.utils.CpsValidator import org.onap.cps.utils.ContentType -import org.onap.cps.utils.TimedYangParser +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Shared import spock.lang.Specification import java.time.OffsetDateTime -import java.util.stream.Collectors class CpsDataServiceImplSpec extends Specification { def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) def mockCpsAnchorService = Mock(CpsAnchorService) def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockCpsValidator = Mock(CpsValidator) - def timedYangParser = new TimedYangParser() + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache) def mockCpsDeltaService = Mock(CpsDeltaService); - def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService, - mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService) + def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService) def setup() { mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor 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 4782468f19..140dfaac96 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 @@ -1,6 +1,6 @@ /*
* ============LICENSE_START=======================================================
- * Copyright (C) 2021-2023 Nordix Foundation.
+ * Copyright (C) 2021-2024 Nordix Foundation.
* Modifications Copyright (C) 2021-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
@@ -27,12 +27,12 @@ import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService
import org.onap.cps.api.CpsDeltaService
import org.onap.cps.spi.CpsDataPersistenceService
-import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.CpsModulePersistenceService
import org.onap.cps.spi.model.Anchor
import org.onap.cps.spi.utils.CpsValidator
-import org.onap.cps.utils.TimedYangParser
-import org.onap.cps.utils.YangUtils
+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.YangTextSchemaSourceSetBuilder
import spock.lang.Specification
@@ -44,14 +44,13 @@ class E2ENetworkSliceSpec extends Specification { def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
def mockCpsValidator = Mock(CpsValidator)
def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
- def timedYangParser = new TimedYangParser()
+ def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)
def mockCpsDeltaService = Mock(CpsDeltaService)
def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,
mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
- def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAnchorService,
- mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService)
+ def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
def dataspaceName = 'someDataspace'
def anchorName = 'someAnchor'
@@ -165,6 +164,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'
- YangUtils.parseJsonData(jsonData, schemaContext) != null
+ new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '') != null
}
}
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 fcbae628e6..e305abee86 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 @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2021-2023 Nordix Foundation. + * Modifications Copyright (C) 2021-2024 Nordix Foundation. * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,8 +23,9 @@ package org.onap.cps.spi.model import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.utils.ContentType import org.onap.cps.utils.DataMapUtils -import org.onap.cps.utils.YangUtils +import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode import org.opendaylight.yangtools.yang.data.api.schema.ForeignDataNode @@ -33,6 +34,7 @@ import spock.lang.Specification class DataNodeBuilderSpec extends Specification { def objectUnderTest = new DataNodeBuilder() + def yangParserHelper = new YangParserHelper() def expectedLeavesByXpathMap = [ '/test-tree' : [], @@ -58,7 +60,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 = YangUtils.parseJsonData(jsonData, schemaContext) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -78,7 +80,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 = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree") + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree') 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) @@ -94,7 +96,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 = YangUtils.parseJsonData(jsonData, schemaContext) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') when: 'the container node is converted to a data node ' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -127,7 +129,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 = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath) 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' @@ -142,7 +144,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 = YangUtils.parseJsonData(jsonData, schemaContext) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '') when: 'the container node is converted to a data node' def result = objectUnderTest.withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -160,7 +162,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 = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath) 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/utils/GsonSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy index c100ea31d5..7e211deb78 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/GsonSpec.groovy @@ -1,13 +1,32 @@ +/* + * ============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.utils import com.google.gson.stream.JsonReader import org.onap.cps.TestUtils import spock.lang.Specification +class GsonSpec extends Specification { -class GsonSpec extends Specification{ - - def 'Iterate over JSON data with gson JsonReader'(){ + def 'Iterate over JSON data with gson JsonReader'() { given: 'json data with two objects and JSON reader' def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json') def objectUnderTest = new JsonReader(new StringReader(jsonData)); @@ -17,7 +36,7 @@ class GsonSpec extends Specification{ noExceptionThrown() } - def iterateWithJsonReader(JsonReader jsonReader){ + def iterateWithJsonReader(JsonReader jsonReader) { switch(jsonReader.peek()) { case "STRING": print(jsonReader.nextString() + " ") @@ -36,5 +55,4 @@ class GsonSpec extends Specification{ } } - } 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 3864a5253a..dc6027de25 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 @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG - * Modifications Copyright (c) 2023 Nordix Foundation + * Modifications 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. @@ -57,7 +57,7 @@ class XmlFileUtilsSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() and: 'Parent schema node by xPath' - def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get("dataSchemaNode") + def parentSchemaNode = YangParserHelper.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get('dataSchemaNode') when: 'the XML data is parsed' def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath) then: 'the result XML is wrapped by xPath defined parent root node' 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 new file mode 100644 index 0000000000..073383113d --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy @@ -0,0 +1,166 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.utils + +import org.onap.cps.TestUtils +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.opendaylight.yangtools.yang.common.QName +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode +import spock.lang.Specification + +class YangParserHelperSpec extends Specification { + + def objectUnderTest = new YangParserHelper() + + def 'Parsing a valid multicontainer Json String.'() { + given: 'a yang model (file)' + def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json') + and: 'a model for that data' + 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, '') + 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' + result.body().getAt(index).getIdentifier().nodeType == QName.create('org:onap:ccsdk:multiDataTree', '2020-09-15', nodeName) + where: + index | nodeName + 0 | 'first-container' + 1 | 'last-container' + } + + def 'Parsing a valid #scenario String.'() { + given: 'a yang model (file)' + def fileData = TestUtils.getResourceFileContent(contentFile) + and: 'a model for that data' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the data is parsed' + NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '') + then: 'the result is a normalized node of the correct type' + if (revision) { + result.identifier.nodeType == QName.create(namespace, revision, localName) + } else { + result.identifier.nodeType == QName.create(namespace, localName) + } + where: + scenario | contentFile | contentType | namespace | revision | localName + 'JSON' | 'bookstore.json' | ContentType.JSON | 'org:onap:ccsdk:sample' | '2020-09-15' | 'bookstore' + 'XML' | 'bookstore.xml' | ContentType.XML | 'urn:ietf:params:xml:ns:netconf:base:1.0' | '' | 'bookstore' + } + + def 'Parsing invalid data: #description.'() { + given: 'a yang model (file)' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'invalid data is parsed' + objectUnderTest.parseData(contentType, invalidData, schemaContext, '') + then: 'an exception is thrown' + thrown(DataValidationException) + where: 'the following invalid data is provided' + invalidData | contentType | description + '{incomplete json' | ContentType.JSON | 'incomplete json' + '{"test:bookstore": {"address": "Parnell st." }}' | ContentType.JSON | 'json with un-modelled data' + '{" }' | ContentType.JSON | 'json with syntax exception' + '<data>' | ContentType.XML | 'incomplete xml' + '<data><bookstore><bookstore-anything>blabla</bookstore-anything></bookstore</data>' | ContentType.XML | 'xml with invalid model' + '' | ContentType.XML | 'empty xml' + } + + def 'Parsing data fragment by xpath for #scenario.'() { + given: 'schema context' + 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) + 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' + result.body().getAt(0).getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName) + where: + scenario | contentType | nodeData | parentNodeXpath || nodeName + 'JSON list element as container' | ContentType.JSON | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' + 'JSON list element within list' | ContentType.JSON | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' + 'JSON container element' | ContentType.JSON | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' + 'XML element test tree' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' | '/test-tree' || 'branch' + 'XML element branch xpath' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds><birds>Robin</birds></nest></branch>' | '/test-tree' || 'branch' + 'XML container element' | ContentType.XML | '<?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\']' || 'nest' + } + + def 'Parsing json data fragment by xpath error scenario: #scenario.'() { + given: 'schema context' + 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) + then: 'expected exception is thrown' + thrown(DataValidationException) + where: + scenario | parentNodeXpath + 'xpath has no identifiers' | '/' + 'xpath has no valid identifiers' | '/[@name=\'Name\']' + 'invalid parent path' | '/test-bush' + 'another invalid parent path' | '/test-tree/branch[@name=\'Branch\']/nest/name/last-name' + 'fragment does not belong to parent' | '/test-tree/' + } + + def 'Parsing json data with invalid json string: #description.'() { + given: 'schema context' + def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() + when: 'malformed json string is parsed' + objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '') + then: 'an exception is thrown' + thrown(DataValidationException) + where: 'the following malformed json is provided' + description | invalidJson + 'malformed json string with unterminated array data' | '{bookstore={categories=[{books=[{authors=[Iain M. Banks]}]}]}}' + 'incorrect json' | '{" }' + } + + def 'Parsing json data with space.'() { + given: 'schema context' + def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() + 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, '') + then: 'no exception thrown' + noExceptionThrown() + } + + def 'Converting xPath to nodeId for #scenario.'() { + when: 'xPath is parsed' + def result = objectUnderTest.xpathToNodeIdSequence(xPath) + then: 'result represents an array of expected identifiers' + assert result == expectedNodeIdentifier + where: 'the following parameters are used' + scenario | xPath || expectedNodeIdentifier + 'container xpath' | '/test-tree' || ['test-tree'] + 'xpath contains list attribute' | '/test-tree/branch[@name=\'Branch\']' || ['test-tree','branch'] + 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] + } + + +} 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 new file mode 100644 index 0000000000..99070fe729 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy @@ -0,0 +1,85 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 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.utils + +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.spi.model.Anchor +import org.onap.cps.yang.YangTextSchemaSourceSet +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode +import org.opendaylight.yangtools.yang.model.api.SchemaContext +import spock.lang.Specification +import org.onap.cps.api.impl.YangTextSchemaSourceSetCache + +class YangParserSpec extends Specification { + + def mockYangParserHelper = Mock(YangParserHelper) + def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) + + def objectUnderTest = new YangParser(mockYangParserHelper, mockYangTextSchemaSourceSetCache) + + def anchor = new Anchor(dataspaceName: 'my dataspace', schemaSetName: 'my schema') + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + def mockSchemaContext = Mock(SchemaContext) + def containerNodeFromYangUtils = Mock(ContainerNode) + + def noParent = '' + + def setup() { + mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet + mockYangTextSchemaSourceSet.getSchemaContext() >> mockSchemaContext + } + + def 'Parsing data.'() { + given: 'the yang parser (utility) always returns a container node' + mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> 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' + 1 * mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet + and: 'the result is the same container node as return from yang utils' + assert result == containerNodeFromYangUtils + and: 'nothing is removed from the cache' + 0 * mockYangTextSchemaSourceSetCache.removeFromCache(*_) + } + + 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 + 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' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + and: 'the result is the same container node as return from yang utils (no exception thrown!)' + assert result == containerNodeFromYangUtils + } + + 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) } + when: 'attempt to parse some data' + objectUnderTest.parseData(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 (but that did not help)' + 1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema') + } + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy index e6344d3035..3852bae570 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020-2023 Nordix Foundation + * Copyright (C) 2020-2024 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG @@ -23,146 +23,10 @@ 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.opendaylight.yangtools.yang.common.QName import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode import spock.lang.Specification class YangUtilsSpec extends Specification { - def 'Parsing a valid multicontainer Json String.'() { - given: 'a yang model (file)' - def jsonData = org.onap.cps.TestUtils.getResourceFileContent('multiple-object-data.json') - and: 'a model for that data' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'the json data is parsed' - def result = YangUtils.parseJsonData(jsonData, schemaContext) - 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' - result.body().getAt(index).getIdentifier().nodeType == QName.create('org:onap:ccsdk:multiDataTree', '2020-09-15', nodeName) - where: - index | nodeName - 0 | 'first-container' - 1 | 'last-container' - } - - def 'Parsing a valid #scenario String.'() { - given: 'a yang model (file)' - def fileData = org.onap.cps.TestUtils.getResourceFileContent(contentFile) - and: 'a model for that data' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'the data is parsed' - NormalizedNode result = YangUtils.parseData(contentType, fileData, schemaContext) - then: 'the result is a normalized node of the correct type' - if (revision) { - result.identifier.nodeType == QName.create(namespace, revision, localName) - } else { - result.identifier.nodeType == QName.create(namespace, localName) - } - where: - scenario | contentFile | contentType | namespace | revision | localName - 'JSON' | 'bookstore.json' | ContentType.JSON | 'org:onap:ccsdk:sample' | '2020-09-15' | 'bookstore' - 'XML' | 'bookstore.xml' | ContentType.XML | 'urn:ietf:params:xml:ns:netconf:base:1.0' | '' | 'bookstore' - } - - def 'Parsing invalid data: #description.'() { - given: 'a yang model (file)' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() - when: 'invalid data is parsed' - YangUtils.parseData(contentType, invalidData, schemaContext) - then: 'an exception is thrown' - thrown(DataValidationException) - where: 'the following invalid data is provided' - invalidData | contentType | description - '{incomplete json' | ContentType.JSON | 'incomplete json' - '{"test:bookstore": {"address": "Parnell st." }}' | ContentType.JSON | 'json with un-modelled data' - '{" }' | ContentType.JSON | 'json with syntax exception' - '<data>' | ContentType.XML | 'incomplete xml' - '<data><bookstore><bookstore-anything>blabla</bookstore-anything></bookstore</data>' | ContentType.XML | 'xml with invalid model' - '' | ContentType.XML | 'empty xml' - } - - def 'Parsing data fragment by xpath for #scenario.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - when: 'json string is parsed' - def result = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath) - 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' - result.body().getAt(0).getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName) - where: - scenario | contentType | nodeData | parentNodeXpath || nodeName - 'JSON list element as container' | ContentType.JSON | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' - 'JSON list element within list' | ContentType.JSON | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' - 'JSON container element' | ContentType.JSON | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' - 'XML element test tree' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' | '/test-tree' || 'branch' - 'XML element branch xpath' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds><birds>Robin</birds></nest></branch>' | '/test-tree' || 'branch' - 'XML container element' | ContentType.XML | '<?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\']' || 'nest' - } - - def 'Parsing json data fragment by xpath error scenario: #scenario.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - when: 'json string is parsed' - YangUtils.parseJsonData('{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, - parentNodeXpath) - then: 'expected exception is thrown' - thrown(DataValidationException) - where: - scenario | parentNodeXpath - 'xpath has no identifiers' | '/' - 'xpath has no valid identifiers' | '/[@name=\'Name\']' - 'invalid parent path' | '/test-bush' - 'another invalid parent path' | '/test-tree/branch[@name=\'Branch\']/nest/name/last-name' - 'fragment does not belong to parent' | '/test-tree/' - } - - def 'Parsing json data with invalid json string: #description.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - when: 'malformed json string is parsed' - YangUtils.parseJsonData(invalidJson, schemaContext) - then: 'an exception is thrown' - thrown(DataValidationException) - where: 'the following malformed json is provided' - description | invalidJson - 'malformed json string with unterminated array data' | '{bookstore={categories=[{books=[{authors=[Iain M. Banks]}]}]}}' - 'incorrect json' | '{" }' - } - - def 'Parsing json data with space.'() { - given: 'schema context' - def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang') - def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() - and: 'some json data with space in the array elements' - def jsonDataWithSpacesInArrayElement = TestUtils.getResourceFileContent('bookstore.json') - when: 'that json data is parsed' - YangUtils.parseJsonData(jsonDataWithSpacesInArrayElement, schemaContext) - then: 'no exception thrown' - noExceptionThrown() - } - - def 'Parsing xPath to nodeId for #scenario.'() { - when: 'xPath is parsed' - def result = YangUtils.xpathToNodeIdSequence(xPath) - then: 'result represents an array of expected identifiers' - assert result == expectedNodeIdentifier - where: 'the following parameters are used' - scenario | xPath || expectedNodeIdentifier - 'container xpath' | '/test-tree' || ['test-tree'] - 'xpath contains list attribute' | '/test-tree/branch[@name=\'Branch\']' || ['test-tree','branch'] - 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] - } def 'Get key attribute statement without key attributes'() { given: 'a path argument without key attributes' diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy index b10194560a..20687c3ea3 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation + * Copyright (C) 2023-2024 Nordix Foundation * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); @@ -75,7 +75,7 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase { def anchorName = 'bookstoreAnchor' + anchorNumber cpsAnchorService.deleteAnchor(FUNCTIONAL_TEST_DATASPACE_1, anchorName) cpsAnchorService.createAnchor(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_SCHEMA_SET, anchorName) - cpsDataService.saveData(FUNCTIONAL_TEST_DATASPACE_1, anchorName, bookstoreJsonData.replace("Easons", "Easons-"+anchorNumber.toString()), OffsetDateTime.now()) + cpsDataService.saveData(FUNCTIONAL_TEST_DATASPACE_1, anchorName, bookstoreJsonData.replace('Easons', 'Easons-'+anchorNumber.toString()), OffsetDateTime.now()) } } diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/TestConfig.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/TestConfig.groovy index e1ccdaa6ee..69c2d17445 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/TestConfig.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/TestConfig.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation + * 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. @@ -21,6 +21,7 @@ package org.onap.cps.integration.base import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.api.impl.YangTextSchemaSourceSetCache import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.CpsModulePersistenceService import org.onap.cps.spi.impl.CpsAdminPersistenceServiceImpl @@ -35,7 +36,8 @@ import org.onap.cps.spi.repository.YangResourceRepository import org.onap.cps.spi.utils.SessionManager import org.onap.cps.spi.utils.TimeLimiterProvider import org.onap.cps.utils.JsonObjectMapper -import org.onap.cps.utils.TimedYangParser +import org.onap.cps.utils.YangParser +import org.onap.cps.utils.YangParserHelper import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean @@ -77,6 +79,15 @@ class TestConfig extends Specification{ @Lazy SessionManager sessionManager + @Autowired + @Lazy + YangParserHelper yangParserHelper + + @Autowired + @Lazy + YangTextSchemaSourceSetCache YangTextSchemaSourceSetCache + + @Bean CpsAdminPersistenceServiceImpl cpsAdminPersistenceService() { new CpsAdminPersistenceServiceImpl(dataspaceRepository, anchorRepository, schemaSetRepository, yangResourceRepository) @@ -98,8 +109,13 @@ class TestConfig extends Specification{ } @Bean - TimedYangParser timedYangParser() { - return new TimedYangParser() + YangParserHelper yangParserHelper() { + return new YangParserHelper() + } + + @Bean + YangParser yangParser() { + return new YangParser(yangParserHelper, yangTextSchemaSourceSetCache) } @Bean |