From 6ce84d98f68b45f02f16dc99423670f4a53fd946 Mon Sep 17 00:00:00 2001 From: Michal Jagiello Date: Tue, 13 Dec 2022 07:40:19 +0000 Subject: XML content on create anchors node support Add XML content type support on anchor node creation. Issue-ID: CPS-1257 Change-Id: I7e7a9a1961b6e81de93a4e32e842b47f8a163a09 Signed-off-by: Michal Jagiello Signed-off-by: Lee Anjella Macabuhay --- cps-service/pom.xml | 5 + .../main/java/org/onap/cps/api/CpsDataService.java | 38 ++++- .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 58 +++++--- .../main/java/org/onap/cps/utils/ContentType.java | 26 ++++ .../main/java/org/onap/cps/utils/XmlFileUtils.java | 165 +++++++++++++++++++++ .../main/java/org/onap/cps/utils/YangUtils.java | 154 ++++++++++++++++--- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 38 ++++- .../org/onap/cps/utils/XmlFileUtilsSpec.groovy | 61 ++++++++ .../groovy/org/onap/cps/utils/YangUtilsSpec.groovy | 54 +++++-- cps-service/src/test/resources/bookstore.xml | 19 +++ cps-service/src/test/resources/bookstore_xpath.xml | 17 +++ cps-service/src/test/resources/test-tree.xml | 27 ++++ 12 files changed, 597 insertions(+), 65 deletions(-) create mode 100644 cps-service/src/main/java/org/onap/cps/utils/ContentType.java create mode 100644 cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java create mode 100644 cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy create mode 100644 cps-service/src/test/resources/bookstore.xml create mode 100644 cps-service/src/test/resources/bookstore_xpath.xml create mode 100644 cps-service/src/test/resources/test-tree.xml (limited to 'cps-service') diff --git a/cps-service/pom.xml b/cps-service/pom.xml index 77f262c32..7173d8674 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -4,6 +4,7 @@ Copyright (C) 2021-2022 Nordix Foundation Modifications Copyright (C) 2021 Bell Canada. Modifications Copyright (C) 2021 Pantheon.tech + Modifications Copyright (C) 2022 Deutsche Telekom AG ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -64,6 +65,10 @@ org.opendaylight.yangtools yang-data-codec-gson + + org.opendaylight.yangtools + yang-data-codec-xml + org.projectlombok lombok 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 b2e8c5ba4..012d7f825 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 @@ -3,6 +3,7 @@ * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +28,7 @@ import java.util.Collection; import java.util.Map; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.utils.ContentType; /* * Datastore interface for handling CPS data. @@ -38,10 +40,22 @@ public interface CpsDataService { * * @param dataspaceName dataspace name * @param anchorName anchor name - * @param jsonData json data + * @param nodeData node data * @param observedTimestamp observedTimestamp */ - void saveData(String dataspaceName, String anchorName, String jsonData, OffsetDateTime observedTimestamp); + void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp); + + /** + * Persists data for the given anchor and dataspace. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param nodeData node data + * @param observedTimestamp observedTimestamp + * @param contentType node data content type + */ + void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp, + ContentType contentType); /** * Persists child data fragment under existing data node for the given anchor and dataspace. @@ -49,11 +63,25 @@ public interface CpsDataService { * @param dataspaceName dataspace name * @param anchorName anchor name * @param parentNodeXpath parent node xpath - * @param jsonData json data + * @param nodeData node data * @param observedTimestamp observedTimestamp */ - void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData, - OffsetDateTime observedTimestamp); + void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData, + OffsetDateTime observedTimestamp); + + /** + * Persists child data fragment under existing data node for the given anchor, dataspace and content type. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentNodeXpath parent node xpath + * @param nodeData node data + * @param observedTimestamp observedTimestamp + * @param contentType node data content type + * + */ + void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData, + OffsetDateTime observedTimestamp, ContentType contentType); /** * Persists child data fragment representing one or more list elements under existing data node for the 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 732b49499..c776e5bb3 100755 --- 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 @@ -4,6 +4,7 @@ * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +47,7 @@ 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.YangUtils; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; @@ -66,21 +68,34 @@ public class CpsDataServiceImpl implements CpsDataService { private final CpsValidator cpsValidator; @Override - public void saveData(final String dataspaceName, final String anchorName, final String jsonData, + public void saveData(final String dataspaceName, final String anchorName, final String nodeData, final OffsetDateTime observedTimestamp) { + saveData(dataspaceName, anchorName, nodeData, observedTimestamp, ContentType.JSON); + } + + @Override + public void saveData(final String dataspaceName, final String anchorName, final String nodeData, + final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection dataNodes = - buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData); + buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, nodeData, contentType); cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, CREATE, observedTimestamp); } @Override public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData, final OffsetDateTime observedTimestamp) { + final String nodeData, final OffsetDateTime observedTimestamp) { + saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, ContentType.JSON); + } + + @Override + public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final String nodeData, final OffsetDateTime observedTimestamp, + final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection dataNodes = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, CREATE, observedTimestamp); } @@ -90,7 +105,7 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection listElementDataNodeCollection = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollection); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -101,7 +116,7 @@ public class CpsDataServiceImpl implements CpsDataService { final Collection jsonDataList, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection> listElementDataNodeCollections = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList, ContentType.JSON); cpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollections); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -118,7 +133,7 @@ public class CpsDataServiceImpl implements CpsDataService { public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); + final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); cpsDataPersistenceService .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves()); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -132,7 +147,7 @@ public class CpsDataServiceImpl implements CpsDataService { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection dataNodeUpdates = buildDataNodes(dataspaceName, anchorName, - parentNodeXpath, dataNodeUpdatesAsJson); + parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate); } @@ -166,7 +181,7 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection dataNodes = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); final ArrayList nodes = new ArrayList<>(dataNodes); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, nodes); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -189,7 +204,7 @@ public class CpsDataServiceImpl implements CpsDataService { final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection newListElements = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); } @@ -226,18 +241,20 @@ public class CpsDataServiceImpl implements CpsDataService { } private DataNode buildDataNode(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final String jsonData) { + final String parentNodeXpath, final String nodeData, + final ContentType contentType) { final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext); + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext); return new DataNodeBuilder().withContainerNode(containerNode).build(); } final ContainerNode containerNode = YangUtils - .parseJsonData(jsonData, schemaContext, parentNodeXpath); + .parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return new DataNodeBuilder() .withParentNodeXpath(parentNodeXpath) .withContainerNode(containerNode) @@ -248,18 +265,19 @@ public class CpsDataServiceImpl implements CpsDataService { final Map nodesJsonData) { return nodesJsonData.entrySet().stream().map(nodeJsonData -> buildDataNode(dataspaceName, anchorName, nodeJsonData.getKey(), - nodeJsonData.getValue())).collect(Collectors.toList()); + nodeJsonData.getValue(), ContentType.JSON)).collect(Collectors.toList()); } private Collection buildDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData) { + final String nodeData, + final ContentType contentType) { final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext); + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext); final Collection dataNodes = new DataNodeBuilder() .withContainerNode(containerNode) .buildCollection(); @@ -268,7 +286,7 @@ public class CpsDataServiceImpl implements CpsDataService { } return dataNodes; } - final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath); + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath); final Collection dataNodes = new DataNodeBuilder() .withParentNodeXpath(parentNodeXpath) .withContainerNode(containerNode) @@ -281,9 +299,9 @@ public class CpsDataServiceImpl implements CpsDataService { } private Collection> buildDataNodes(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final Collection jsonDataList) { - return jsonDataList.stream() - .map(jsonData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData)) + final String parentNodeXpath, final Collection nodeDataList, final ContentType contentType) { + return nodeDataList.stream() + .map(nodeData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType)) .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 new file mode 100644 index 000000000..f88850484 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/ContentType.java @@ -0,0 +1,26 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Deutsche Telekom AG + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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; + +public enum ContentType { + JSON, + XML +} 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 new file mode 100644 index 000000000..0946ae3f6 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java @@ -0,0 +1,165 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Deutsche Telekom AG + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +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.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +public class XmlFileUtils { + + private static DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + private static final String DATA_ROOT_NODE_TAG_NAME = "data"; + private static final String ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0"; + private static final Pattern XPATH_PROPERTY_REGEX = Pattern.compile( + "\\[@(\\S{1,100})=['\\\"](\\S{1,100})['\\\"]\\]"); + + /** + * Prepare XML content. + * + * @param xmlContent XML content sent to store + * @param schemaContext schema context + * @return XML content wrapped by root node (if needed) + */ + public static String prepareXmlContent(final String xmlContent, final SchemaContext schemaContext) { + + return addRootNodeToXmlContent(xmlContent, schemaContext.getModules().iterator().next().getName(), + ROOT_NODE_NAMESPACE); + + } + + /** + * Prepare XML content. + * + * @param xmlContent XML content sent to store + * @param parentSchemaNode Parent schema node + * @return XML content wrapped by root node (if needed) + */ + public static String prepareXmlContent(final String xmlContent, final DataSchemaNode parentSchemaNode, + final String xpath) { + final String namespace = parentSchemaNode.getQName().getNamespace().toString(); + final String parentXpathPart = xpath.substring(xpath.lastIndexOf('/') + 1); + final Matcher regexMatcher = XPATH_PROPERTY_REGEX.matcher(parentXpathPart); + if (regexMatcher.find()) { + final HashMap rootNodePropertyMap = new HashMap(); + rootNodePropertyMap.put(regexMatcher.group(1), regexMatcher.group(2)); + return addRootNodeToXmlContent(xmlContent, parentSchemaNode.getQName().getLocalName(), namespace, + rootNodePropertyMap); + } + + return addRootNodeToXmlContent(xmlContent, parentSchemaNode.getQName().getLocalName(), namespace); + } + + /** + * Add root node to XML content. + * + * @param xmlContent xml content to add root node + * @param rootNodeTagName root node tag name + * @param namespace root node namespace + * @param rootNodeProperty root node properites map + * @return An edited content with added root node (if needed) + */ + public static String addRootNodeToXmlContent(final String xmlContent, final String rootNodeTagName, + final String namespace, + final HashMap rootNodeProperty) { + try { + final DocumentBuilder documentBuilder = dbFactory.newDocumentBuilder(); + final StringBuilder xmlStringBuilder = new StringBuilder(); + xmlStringBuilder.append(xmlContent); + Document xmlDoc = documentBuilder.parse( + new ByteArrayInputStream(xmlStringBuilder.toString().getBytes("utf-8"))); + final Element root = xmlDoc.getDocumentElement(); + if (!root.getTagName().equals(rootNodeTagName) && !root.getTagName().equals(DATA_ROOT_NODE_TAG_NAME)) { + xmlDoc = addDataRootNode(root, rootNodeTagName, namespace, rootNodeProperty); + xmlDoc.setXmlStandalone(true); + final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + final Transformer transformer = transformerFactory.newTransformer(); + final StringWriter stringWriter = new StringWriter(); + transformer.transform(new DOMSource(xmlDoc), new StreamResult(stringWriter)); + return stringWriter.toString(); + } + return xmlContent; + } catch (SAXException | IOException | ParserConfigurationException | TransformerException exception) { + throw new DataValidationException("Failed to parse XML data", "Invalid xml input " + exception.getMessage(), + exception); + } + } + + /** + * Add root node to XML content. + * + * @param xmlContent XML content to add root node into + * @param rootNodeTagName Root node tag name + * @return XML content with root node tag added (if needed) + */ + public static String addRootNodeToXmlContent(final String xmlContent, final String rootNodeTagName, + final String namespace) { + return addRootNodeToXmlContent(xmlContent, rootNodeTagName, namespace, new HashMap()); + } + + /** + * Add root node into DOM element. + * + * @param node DOM element to add root node into + * @param tagName Root tag name to add + * @return DOM element with a root node + */ + static Document addDataRootNode(final Element node, final String tagName, final String namespace, + final HashMap rootNodeProperty) { + try { + final DocumentBuilder docBuilder = dbFactory.newDocumentBuilder(); + final Document document = docBuilder.newDocument(); + final Element rootElement = document.createElementNS(namespace, tagName); + for (final Map.Entry entry : rootNodeProperty.entrySet()) { + final Element propertyElement = document.createElement(entry.getKey()); + propertyElement.setTextContent(entry.getValue()); + rootElement.appendChild(propertyElement); + } + rootElement.appendChild(document.adoptNode(node)); + document.appendChild(rootElement); + return document; + } catch (final ParserConfigurationException exception) { + throw new DataValidationException("Can't parse XML", "XML can't be parsed", exception); + } + } +} 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 9a61579b1..49d3a70f9 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 @@ -4,6 +4,7 @@ * Modifications Copyright (C) 2021 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,13 +28,20 @@ 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.Arrays; 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.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,13 +49,18 @@ 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; @@ -55,6 +68,7 @@ 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) @@ -64,7 +78,45 @@ public class YangUtils { private static final String XPATH_NODE_KEY_ATTRIBUTES_REGEX = "\\[.*?\\]"; /** - * Parses jsonData into Collection of NormalizedNode according to given schema context. + * 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 + */ + public static ContainerNode parseData(final ContentType contentType, final String nodeData, + final SchemaContext schemaContext) { + if (contentType == ContentType.JSON) { + return parseJsonData(nodeData, schemaContext, Optional.empty()); + } + return parseXmlData(XmlFileUtils.prepareXmlContent(nodeData, schemaContext), 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 + */ + public static ContainerNode parseData(final ContentType contentType, final String nodeData, + final SchemaContext schemaContext, final String parentNodeXpath) { + final DataSchemaNode parentSchemaNode = + (DataSchemaNode) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) + .get("dataSchemaNode"); + final Collection dataSchemaNodeIdentifiers = + (Collection) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) + .get("dataSchemaNodeIdentifiers"); + if (contentType == ContentType.JSON) { + return parseJsonData(nodeData, schemaContext, Optional.of(dataSchemaNodeIdentifiers)); + } + return parseXmlData(XmlFileUtils.prepareXmlContent(nodeData, parentSchemaNode, parentNodeXpath), schemaContext, + Optional.of(dataSchemaNodeIdentifiers)); + } + + /** + * 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 @@ -85,7 +137,8 @@ public class YangUtils { public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext, final String parentNodeXpath) { final Collection dataSchemaNodeIdentifiers = - getDataSchemaNodeIdentifiersByXpath(parentNodeXpath, schemaContext); + (Collection) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) + .get("dataSchemaNodeIdentifiers"); return parseJsonData(jsonData, schemaContext, Optional.of(dataSchemaNodeIdentifiers)); } @@ -105,7 +158,7 @@ public class YangUtils { final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext); final EffectiveStatementInference effectiveStatementInference = SchemaInferenceStack.of(effectiveModelContext, - SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference(); + SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference(); jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory, effectiveStatementInference); } else { @@ -115,22 +168,56 @@ public class YangUtils { try { jsonParserStream.parse(jsonReader); jsonParserStream.close(); - } catch (final JsonSyntaxException exception) { + } catch (final IOException | JsonSyntaxException exception) { throw new DataValidationException( - "Failed to parse json data: " + jsonData, exception.getMessage(), exception); - } catch (final IOException | IllegalStateException illegalStateException) { + "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, illegalStateException - .getMessage(), illegalStateException); + "Failed to parse json data. Unsupported xpath or json data:" + jsonData, exception + .getMessage(), exception); } return dataContainerNodeBuilder.build(); } + private static ContainerNode parseXmlData(final String xmlData, final SchemaContext schemaContext, + final Optional> dataSchemaNodeIdentifiers) { + final XMLInputFactory factory = XMLInputFactory.newInstance(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter + .from(normalizedNodeResult); + + final XmlParserStream xmlParser; + final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext); + + if (dataSchemaNodeIdentifiers.isPresent()) { + final EffectiveStatementInference effectiveStatementInference = + SchemaInferenceStack.of(effectiveModelContext, + SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference(); + xmlParser = XmlParserStream.create(normalizedNodeStreamWriter, effectiveStatementInference); + } else { + xmlParser = XmlParserStream.create(normalizedNodeStreamWriter, effectiveModelContext); + } + + try { + final XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(xmlData)); + xmlParser.parse(reader); + xmlParser.close(); + } catch (final XMLStreamException | URISyntaxException | IOException + | SAXException | NullPointerException exception) { + throw new DataValidationException( + "Failed to parse xml data: " + xmlData, exception.getMessage(), exception); + } + final NormalizedNode normalizedNode = getFirstChildXmlRoot(normalizedNodeResult.getResult()); + return Builders.containerBuilder().withChild((DataContainerChild) normalizedNode) + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName())).build(); + } + /** * Create an xpath form a Yang Tools NodeIdentifier (i.e. PathArgument). * * @param nodeIdentifier the NodeIdentifier - * @return an xpath + * @return a xpath */ public static String buildXpath(final YangInstanceIdentifier.PathArgument nodeIdentifier) { final StringBuilder xpathBuilder = new StringBuilder(); @@ -138,20 +225,20 @@ public class YangUtils { if (nodeIdentifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) { xpathBuilder.append(getKeyAttributesStatement( - (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier)); + (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier)); } return xpathBuilder.toString(); } private static String getKeyAttributesStatement( - final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) { + final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) { final List keyAttributes = nodeIdentifier.entrySet().stream().map( - entry -> { - final String name = entry.getKey().getLocalName(); - final String value = String.valueOf(entry.getValue()).replace("'", "\\'"); - return String.format("@%s='%s'", name, value); - } + entry -> { + final String name = entry.getKey().getLocalName(); + final String value = String.valueOf(entry.getValue()).replace("'", "\\'"); + return String.format("@%s='%s'", name, value); + } ).collect(Collectors.toList()); if (keyAttributes.isEmpty()) { @@ -162,10 +249,10 @@ public class YangUtils { } } - private static Collection getDataSchemaNodeIdentifiersByXpath(final String parentNodeXpath, - final SchemaContext schemaContext) { + private static Map getDataSchemaNodeAndIdentifiersByXpath(final String parentNodeXpath, + final SchemaContext schemaContext) { final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath); - return findDataSchemaNodeIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), + return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), new ArrayList<>()); } @@ -181,7 +268,7 @@ public class YangUtils { return xpathNodeIdSequence; } - private static Collection findDataSchemaNodeIdentifiersByXpathNodeIdSequence( + private static Map findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( final String[] xpathNodeIdSequence, final Collection dataSchemaNodes, final Collection dataSchemaNodeIdentifiers) { @@ -191,11 +278,15 @@ public class YangUtils { .findFirst().orElseThrow(() -> schemaNodeNotFoundException(currentXpathNodeId)); dataSchemaNodeIdentifiers.add(currentDataSchemaNode.getQName()); if (xpathNodeIdSequence.length <= 1) { - return dataSchemaNodeIdentifiers; + final Map dataSchemaNodeAndIdentifiers = + new HashMap<>(); + dataSchemaNodeAndIdentifiers.put("dataSchemaNode", currentDataSchemaNode); + dataSchemaNodeAndIdentifiers.put("dataSchemaNodeIdentifiers", dataSchemaNodeIdentifiers); + return dataSchemaNodeAndIdentifiers; } if (currentDataSchemaNode instanceof DataNodeContainer) { - return findDataSchemaNodeIdentifiersByXpathNodeIdSequence( - getNextLevelXpathNodeIdSequence(xpathNodeIdSequence), + return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( + getNextLevelXpathNodeIdSequence(xpathNodeIdSequence), ((DataNodeContainer) currentDataSchemaNode).getChildNodes(), dataSchemaNodeIdentifiers); } @@ -212,4 +303,19 @@ public class YangUtils { 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 children = (Collection) parent.body(); + final Iterator 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); + } +} \ No newline at end of file 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 b78ab8a45..c81a50ea7 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 @@ -4,7 +4,7 @@ * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. * Modifications Copyright (C) 2022 TechMahindra Ltd. - * ================================================================================ + * Modifications Copyright (C) 2022 Deutsche Telekom AG * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -33,6 +33,7 @@ import org.onap.cps.spi.exceptions.DataValidationException 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.utils.ContentType import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Specification @@ -61,7 +62,7 @@ class CpsDataServiceImplSpec extends Specification { def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build() def observedTimestamp = OffsetDateTime.now() - def 'Saving json data.'() { + def 'Saving multicontainer json data.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('multipleDataTree.yang') when: 'save data method is invoked with test-tree json data' @@ -81,6 +82,39 @@ class CpsDataServiceImplSpec extends Specification { } + def 'Saving #scenario data.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'save data method is invoked with test-tree #scenario data' + def data = TestUtils.getResourceFileContent(dataFile) + objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType) + then: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, + { dataNode -> dataNode.xpath[0] == '/test-tree' }) + and: 'the CpsValidator is called on the dataspaceName and AnchorName' + 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) + and: 'data updated event is sent to notification service' + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp) + where: 'given parameters' + scenario | dataFile | contentType + 'json' | 'test-tree.json' | ContentType.JSON + 'xml' | 'test-tree.xml' | ContentType.XML + } + + def 'Saving #scenarioDesired data with invalid data.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'save data method is invoked with test-tree json data' + objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + where: 'given parameters' + scenarioDesired | invalidData | contentType + 'json' | '{invalid json' | ContentType.XML + 'xml' | ' ' || ' ' + 'with root data node' | ' ' || ' ' + 'no xml header' | ' ' || ' ' + } + + def 'Parse a xml content with XPath container #scenario'() { + given: 'YANG model schema context' + 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") + 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' + assert parsedXmlContent == expectedOutput + where: + scenario | xmlData | xPath || expectedOutput + 'XML element test tree' | 'LeftSmallSparrow' | '/test-tree' || 'LeftSmallSparrow' + 'without root data node' | 'SmallSparrow' | '/test-tree/branch[@name=\'Branch\']' || 'BranchSmallSparrow' + + + } + +} 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 990b7186f..bf6e134a6 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 @@ -3,6 +3,7 @@ * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +31,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode import spock.lang.Specification class YangUtilsSpec extends Specification { - def 'Parsing a valid Json String.'() { + 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' @@ -48,36 +49,62 @@ class YangUtilsSpec extends Specification { 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.parseJsonData(invalidJson, schemaContext) + YangUtils.parseData(contentType, invalidData, schemaContext) then: 'an exception is thrown' thrown(DataValidationException) - where: 'the following invalid json is provided' - invalidJson | description - '{incomplete json' | 'incomplete json' - '{"test:bookstore": {"address": "Parnell st." }}' | 'json with un-modelled data' - '{" }' | 'json with syntax exception' + 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' + '' | ContentType.XML | 'incomplete xml' + 'blabla' | ContentType.XML | 'xml with invalid model' + '' | ContentType.XML | 'empty xml' } - def 'Parsing json data fragment by xpath for #scenario.'() { + 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.parseJsonData(jsonData, schemaContext, parentNodeXpath) + 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 | jsonData | parentNodeXpath || nodeName - 'list element as container' | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' - 'list element within list' | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' - 'container element' | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' + 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 | 'LeftSmallSparrow' | '/test-tree' || 'branch' + 'XML element branch xpath' | ContentType.XML | 'LeftSmallSparrowRobin' | '/test-tree' || 'branch' + 'XML container element' | ContentType.XML | 'SmallSparrow' | '/test-tree/branch[@name=\'Branch\']' || 'nest' } def 'Parsing json data fragment by xpath error scenario: #scenario.'() { @@ -135,5 +162,4 @@ class YangUtilsSpec extends Specification { '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/resources/bookstore.xml b/cps-service/src/test/resources/bookstore.xml new file mode 100644 index 000000000..dd45e1689 --- /dev/null +++ b/cps-service/src/test/resources/bookstore.xml @@ -0,0 +1,19 @@ + + + + Chapters + + 1 + SciFi + + 2001: A Space Odyssey + en + + Iain M. Banks + + 1994 + 895 + + + + \ No newline at end of file diff --git a/cps-service/src/test/resources/bookstore_xpath.xml b/cps-service/src/test/resources/bookstore_xpath.xml new file mode 100644 index 000000000..e206901d6 --- /dev/null +++ b/cps-service/src/test/resources/bookstore_xpath.xml @@ -0,0 +1,17 @@ + + + Chapters + + 1 + SciFi + + 2001: A Space Odyssey + en + + Iain M. Banks + + 1994 + 895 + + + \ No newline at end of file diff --git a/cps-service/src/test/resources/test-tree.xml b/cps-service/src/test/resources/test-tree.xml new file mode 100644 index 000000000..3daa814cf --- /dev/null +++ b/cps-service/src/test/resources/test-tree.xml @@ -0,0 +1,27 @@ + + + + + Left + + Small + Sparrow + Robin + Finch + + + + Right + + Big + Owl + Raven + Crow + + + + Apple + Green + + + -- cgit 1.2.3-korg