Implementation of Data validation feature in Create a Node API 76/138676/28
authorArpit Singh <as00745003@techmahindra.com>
Fri, 9 Aug 2024 06:53:49 +0000 (12:23 +0530)
committerArpit Singh <as00745003@techmahindra.com>
Wed, 9 Oct 2024 03:58:30 +0000 (09:28 +0530)
Added support to validate JSON/XML data without the need of persisting
it in the databse.
 - added "dryRunInQuery" flag as a new query parameter
 - added new method as part of CpsDataService layer to perform data
   validation
 - added new method in yang parser "validateData" to validate
   data without persisting it

Issue-ID: CPS-2361
Change-Id: I43dd33cc6120576d0fac606d5c4b0168d107311d
Signed-off-by: Arpit Singh <as00745003@techmahindra.com>
15 files changed:
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsData.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/utils/YangParser.java
cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangParserHelperSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy
cps-service/src/test/resources/bookstore-categories-data.json [new file with mode: 0644]
cps-service/src/test/resources/bookstore-categories-data.xml [new file with mode: 0644]

index 25ef6a4..40f0e17 100644 (file)
@@ -320,6 +320,15 @@ components:
       schema:
         type: integer
         example: 10
+    dryRunInQuery:
+      name: dry-run
+      in: query
+      description: Boolean flag to validate data, without persisting it. Default value is set to false.
+      required: false
+      schema:
+        type: boolean
+        default: false
+        example: false
 
   responses:
     NotFound:
index 4418a3b..daf59bb 100644 (file)
@@ -102,6 +102,7 @@ nodesByDataspaceAndAnchor:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
+      - $ref: 'components.yml#/components/parameters/dryRunInQuery'
       - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
       - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
     requestBody:
index f86073f..7390afc 100755 (executable)
@@ -74,16 +74,21 @@ public class DataRestController implements CpsDataApi {
                                              final String dataspaceName, final String anchorName,
                                              final String contentTypeInHeader,
                                              final String nodeData, final String parentNodeXpath,
-                                             final String observedTimestamp) {
+                                             final Boolean dryRunEnabled, final String observedTimestamp) {
         final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
-        if (isRootXpath(parentNodeXpath)) {
-            cpsDataService.saveData(dataspaceName, anchorName, nodeData,
-                    toOffsetDateTime(observedTimestamp), contentType);
+        if (Boolean.TRUE.equals(dryRunEnabled)) {
+            cpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType);
+            return ResponseEntity.ok().build();
         } else {
-            cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath,
-                    nodeData, toOffsetDateTime(observedTimestamp), contentType);
+            if (isRootXpath(parentNodeXpath)) {
+                cpsDataService.saveData(dataspaceName, anchorName, nodeData,
+                        toOffsetDateTime(observedTimestamp), contentType);
+            } else {
+                cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath,
+                        nodeData, toOffsetDateTime(observedTimestamp), contentType);
+            }
+            return ResponseEntity.status(HttpStatus.CREATED).build();
         }
-        return new ResponseEntity<>(HttpStatus.CREATED);
     }
 
     @Override
index e101ea6..705c2fe 100755 (executable)
@@ -164,6 +164,26 @@ class DataRestControllerSpec extends Specification {
             'with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
     }
 
+    def 'Validate data using create a node API'() {
+        given: 'an endpoint to create a node'
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
+            def parentNodeXpath = '/'
+            def dryRunEnabled = 'true'
+        when: 'post is invoked with json data and dry-run flag enabled'
+            def response =
+                    mvc.perform(
+                            post(endpoint)
+                                    .contentType(MediaType.APPLICATION_JSON)
+                                    .param('xpath', parentNodeXpath)
+                                    .param('dry-run', dryRunEnabled)
+                                    .content(requestBodyJson)
+                    ).andReturn().response
+        then: 'a 200 OK response is returned'
+            response.status == HttpStatus.OK.value()
+        then: 'the service was called with correct parameters'
+            1 * mockCpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, requestBodyJson, ContentType.JSON)
+    }
+
     def 'Create a child node #scenario'() {
         given: 'endpoint to create a node'
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
index 68e1880..b3eff8e 100644 (file)
@@ -322,4 +322,18 @@ public interface CpsDataService {
                                                           Map<String, String> yangResourcesNameToContentMap,
                                                           String targetData,
                                                           FetchDescendantsOption fetchDescendantsOption);
+
+
+    /**
+     * Validates JSON or XML data by parsing it using the schema associated to an anchor within the given dataspace.
+     * Validation is performed without persisting the data.
+     *
+     * @param dataspaceName     the name of the dataspace where the anchor is located.
+     * @param anchorName        the name of the anchor used to validate the data.
+     * @param parentNodeXpath   the xpath of the parent node where the data is to be validated.
+     * @param nodeData          the JSON or XML data to be validated.
+     * @param contentType       the content type of the data (e.g., JSON or XML).
+     */
+    void validateData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
+                                 ContentType contentType);
 }
index eed4f09..b1b545b 100644 (file)
@@ -64,6 +64,7 @@ import org.springframework.stereotype.Service;
 public class CpsDataServiceImpl implements CpsDataService {
 
     private static final String ROOT_NODE_XPATH = "/";
+    private static final String PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH = "";
     private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L;
     private static final String NO_DATA_NODES = "No data nodes.";
 
@@ -358,6 +359,14 @@ public class CpsDataServiceImpl implements CpsDataService {
         sendDataUpdatedEvent(anchor, listNodeXpath, Operation.DELETE, observedTimestamp);
     }
 
+    @Override
+    public void validateData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+                             final String nodeData, final ContentType contentType) {
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        final String xpath = ROOT_NODE_XPATH.equals(parentNodeXpath) ? PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH :
+                CpsPathUtil.getNormalizedXpath(parentNodeXpath);
+        yangParser.validateData(contentType, nodeData, anchor, xpath);
+    }
 
     private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor,
                                                         final Collection<DataNode> sourceDataNodes) {
@@ -422,7 +431,8 @@ public class CpsDataServiceImpl implements CpsDataService {
                                                                  final String nodeData, final ContentType contentType) {
 
         if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
-            final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, "");
+            final ContainerNode containerNode = yangParser.parseData(contentType, nodeData,
+                    anchor, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH);
             final Collection<DataNode> dataNodes = new DataNodeBuilder()
                     .withContainerNode(containerNode)
                     .buildCollection();
@@ -450,7 +460,7 @@ public class CpsDataServiceImpl implements CpsDataService {
 
         if (isRootNodeXpath(xpath)) {
             final ContainerNode containerNode = yangParser.parseData(contentType, nodeData,
-                    yangResourcesNameToContentMap, "");
+                    yangResourcesNameToContentMap, PARENT_NODE_XPATH_FOR_ROOT_NODE_XPATH);
             final Collection<DataNode> dataNodes = new DataNodeBuilder()
                     .withContainerNode(containerNode)
                     .buildCollection();
index dc23c6b..168e099 100644 (file)
@@ -21,6 +21,9 @@
 
 package org.onap.cps.utils;
 
+import static org.onap.cps.utils.YangParserHelper.VALIDATE_AND_PARSE;
+import static org.onap.cps.utils.YangParserHelper.VALIDATE_ONLY;
+
 import io.micrometer.core.annotation.Timed;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
@@ -57,11 +60,12 @@ public class YangParser {
                                    final String parentNodeXpath) {
         final SchemaContext schemaContext = getSchemaContext(anchor);
         try {
-            return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath);
+            return yangParserHelper
+                    .parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE);
         } catch (final DataValidationException e) {
             invalidateCache(anchor);
         }
-        return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath);
+        return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE);
     }
 
     /**
@@ -78,7 +82,31 @@ public class YangParser {
                                    final Map<String, String> yangResourcesNameToContentMap,
                                    final String parentNodeXpath) {
         final SchemaContext schemaContext = getSchemaContext(yangResourcesNameToContentMap);
-        return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath);
+        return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_AND_PARSE);
+    }
+
+    /**
+     * Parses data to validate it, using the schema context for given anchor.
+     *
+     * @param anchor                    the anchor used for node data validation
+     * @param parentNodeXpath           the xpath of the parent node
+     * @param nodeData                  JSON or XML data string to validate
+     * @param contentType               the content type of the data (e.g., JSON or XML)
+     * @throws DataValidationException  if validation fails
+     */
+    public void validateData(final ContentType contentType,
+                             final String nodeData,
+                             final Anchor anchor,
+                             final String parentNodeXpath) {
+        final SchemaContext schemaContext = getSchemaContext(anchor);
+        try {
+            yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_ONLY);
+        } catch (final DataValidationException e) {
+            invalidateCache(anchor);
+            log.error("Data validation failed for anchor: {}, xpath: {}, details: {}", anchor, parentNodeXpath,
+                    e.getMessage());
+        }
+        yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath, VALIDATE_ONLY);
     }
 
     private SchemaContext getSchemaContext(final Anchor anchor) {
index d95acea..5612945 100644 (file)
@@ -72,6 +72,9 @@ public class YangParserHelper {
 
     static final String DATA_ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0";
     static final String DATA_ROOT_NODE_TAG_NAME = "data";
+    static final String DATA_VALIDATION_FAILURE_MESSAGE = "Data Validation Failed";
+    static final boolean VALIDATE_ONLY = true;
+    static final boolean VALIDATE_AND_PARSE = false;
 
     /**
      * Parses data into NormalizedNode according to given schema context.
@@ -85,11 +88,20 @@ public class YangParserHelper {
     public ContainerNode parseData(final ContentType contentType,
                                    final String nodeData,
                                    final SchemaContext schemaContext,
-                                   final String parentNodeXpath) {
+                                   final String parentNodeXpath,
+                                   final boolean validateOnly) {
         if (contentType == ContentType.JSON) {
-            return parseJsonData(nodeData, schemaContext, parentNodeXpath);
+            final ContainerNode validatedAndParsedJson = parseJsonData(nodeData, schemaContext, parentNodeXpath);
+            if (validateOnly) {
+                return null;
+            }
+            return validatedAndParsedJson;
+        }
+        final NormalizedNodeResult normalizedNodeResult = parseXmlData(nodeData, schemaContext, parentNodeXpath);
+        if (validateOnly) {
+            return null;
         }
-        return parseXmlData(nodeData, schemaContext, parentNodeXpath);
+        return buildContainerNodeFormNormalizedNodeResult(normalizedNodeResult);
     }
 
     private ContainerNode parseJsonData(final String jsonData,
@@ -124,13 +136,13 @@ public class YangParserHelper {
             jsonParserStream.parse(jsonReader);
         } catch (final IOException | JsonSyntaxException | IllegalStateException | IllegalArgumentException exception) {
             throw new DataValidationException(
-                    "Data Validation Failed", "Failed to parse json data. " + exception.getMessage(), exception);
+                    DATA_VALIDATION_FAILURE_MESSAGE, "Failed to parse json data. " + exception.getMessage(), exception);
         }
         return dataContainerNodeBuilder.build();
     }
 
     @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION", justification = "Problem originates in 3PP code")
-    private ContainerNode parseXmlData(final String xmlData,
+    private NormalizedNodeResult parseXmlData(final String xmlData,
                                        final SchemaContext schemaContext,
                                        final String parentNodeXpath) {
         final XMLInputFactory factory = XMLInputFactory.newInstance();
@@ -167,12 +179,17 @@ public class YangParserHelper {
         } catch (final XMLStreamException | URISyntaxException | IOException | SAXException | NullPointerException
                        | ParserConfigurationException | TransformerException exception) {
             throw new DataValidationException(
-                "Data Validation Failed", "Failed to parse xml data: " + exception.getMessage(), exception);
+                    DATA_VALIDATION_FAILURE_MESSAGE, "Failed to parse xml data: " + exception.getMessage(), exception);
         }
+        return normalizedNodeResult;
+    }
+
+    private ContainerNode buildContainerNodeFormNormalizedNodeResult(final NormalizedNodeResult normalizedNodeResult) {
+
         final DataContainerChild dataContainerChild =
-            (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult());
+                (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult());
         final YangInstanceIdentifier.NodeIdentifier nodeIdentifier =
-            new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType());
+                new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType());
         return Builders.containerBuilder().withChild(dataContainerChild).withNodeIdentifier(nodeIdentifier).build();
     }
 
index 9846b30..8c208a1 100644 (file)
@@ -546,6 +546,20 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockDataUpdateEventsService.publishCpsDataUpdateEvent(anchor2, '/', DELETE, observedTimestamp)
     }
 
+    def "Validating #scenario when dry run is enabled."() {
+        given: 'schema set for given anchors and dataspace references bookstore model'
+            setupSchemaSetMocks('bookstore.yang')
+        when: 'validating the data with the given parameters'
+            objectUnderTest.validateData(dataspaceName, anchorName, parentNodeXpath, data,contentType)
+        then: 'the appropriate yang parser method is invoked with correct parameters'
+            yangParser.validateData(contentType, data, anchor, xpath)
+        where: 'the following parameters were used'
+            scenario                     | parentNodeXpath | xpath        | contentType      | data
+        'JSON data with root node xpath' | '/'             | ''           | ContentType.JSON | '{"bookstore":{"bookstore-name":"Easons"}}'
+        'JSON data with specific xpath'  | '/bookstore'    | '/bookstore' | ContentType.JSON | '{"bookstore-name":"Easons"}'
+        'XML data with specific xpath'   | '/bookstore'    | '/bookstore' | ContentType.XML  | '<bookstore-name>Easons</bookstore-name>'
+    }
+
     def 'Start session.'() {
         when: 'start session method is called'
             objectUnderTest.startSession()
index 05c8983..9f34562 100755 (executable)
@@ -171,6 +171,6 @@ class E2ENetworkSliceSpec extends Specification {
         expect: 'schema context is built with no exception indicating the schema set being valid '\r
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext()\r
         and: 'data is parsed with no exception indicating the model match'\r
-            new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '') != null\r
+            new YangParserHelper().parseData(ContentType.JSON, jsonData, schemaContext, '', false) != null\r
     }\r
 }\r
index e305abe..f028d5d 100644 (file)
@@ -35,6 +35,7 @@ class DataNodeBuilderSpec extends Specification {
 
     def objectUnderTest = new DataNodeBuilder()
     def yangParserHelper = new YangParserHelper()
+    def validateAndParse = false
 
     def expectedLeavesByXpathMap = [
             '/test-tree'                                            : [],
@@ -60,7 +61,7 @@ class DataNodeBuilderSpec extends Specification {
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'the json data parsed into container node object'
             def jsonData = TestUtils.getResourceFileContent('test-tree.json')
-            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '')
+            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse)
         when: 'the container node is converted to a data node'
             def result = objectUnderTest.withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
@@ -80,7 +81,7 @@ class DataNodeBuilderSpec extends Specification {
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'the json data parsed into container node object'
             def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }'
-            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree')
+            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '/test-tree', validateAndParse)
         when: 'the container node is converted to a data node with parent node xpath defined'
             def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath('/test-tree').build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
@@ -96,7 +97,7 @@ class DataNodeBuilderSpec extends Specification {
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'the json data parsed into container node object'
             def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json')
-            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '')
+            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse)
         when: 'the container node is converted to a data node '
             def result = objectUnderTest.withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
@@ -129,7 +130,7 @@ class DataNodeBuilderSpec extends Specification {
             def parentNodeXpath = "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']"
         and: 'the json data fragment parsed into container node object for given parent node xpath'
             def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}'
-            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath)
+            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext,parentNodeXpath, validateAndParse)
         when: 'the container node is converted to a data node with given parent node xpath'
             def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).build()
         then: 'the resulting data node represents a child of augmentation node'
@@ -144,7 +145,7 @@ class DataNodeBuilderSpec extends Specification {
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'the json data fragment parsed into container node object'
             def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json')
-            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '')
+            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse)
         when: 'the container node is converted to a data node'
             def result = objectUnderTest.withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
@@ -162,7 +163,7 @@ class DataNodeBuilderSpec extends Specification {
         and: 'parent node xpath referencing parent of list element'
             def parentNodeXpath = '/test-tree'
         and: 'the json data fragment (list element) parsed into container node object'
-            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath)
+            def containerNode = yangParserHelper.parseData(ContentType.JSON, jsonData, schemaContext, parentNodeXpath, validateAndParse)
         when: 'the container node is converted to a data node collection'
             def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).buildCollection()
             def resultXpaths = result.collect { it.getXpath() }
index 0733831..e1490c2 100644 (file)
@@ -30,6 +30,8 @@ import spock.lang.Specification
 class YangParserHelperSpec extends Specification {
 
     def objectUnderTest = new YangParserHelper()
+    def validateOnly = true
+    def validateAndParse = false
 
     def 'Parsing a valid multicontainer Json String.'() {
         given: 'a yang model (file)'
@@ -38,7 +40,7 @@ class YangParserHelperSpec extends Specification {
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         when: 'the json data is parsed'
-            def result = objectUnderTest.parseData(ContentType.JSON, jsonData, schemaContext, '')
+            def result = objectUnderTest.parseData(ContentType.JSON, jsonData, schemaContext, '', validateAndParse)
         then: 'a ContainerNode holding collection of normalized nodes is returned'
             result.body().getAt(index) instanceof NormalizedNode == true
         then: 'qualified name of children created is as expected'
@@ -56,7 +58,7 @@ class YangParserHelperSpec extends Specification {
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         when: 'the data is parsed'
-            NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '')
+            NormalizedNode result = objectUnderTest.parseData(contentType, fileData, schemaContext, '', validateAndParse)
         then: 'the result is a normalized node of the correct type'
             if (revision) {
                 result.identifier.nodeType == QName.create(namespace, revision, localName)
@@ -74,7 +76,7 @@ class YangParserHelperSpec extends Specification {
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         when: 'invalid data is parsed'
-            objectUnderTest.parseData(contentType, invalidData, schemaContext, '')
+            objectUnderTest.parseData(contentType, invalidData, schemaContext, '', validateAndParse)
         then: 'an exception is thrown'
             thrown(DataValidationException)
         where: 'the following invalid data is provided'
@@ -92,7 +94,7 @@ class YangParserHelperSpec extends Specification {
             def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         when: 'json string is parsed'
-            def result = objectUnderTest.parseData(contentType, nodeData, schemaContext, parentNodeXpath)
+            def result = objectUnderTest.parseData(contentType, nodeData, schemaContext, parentNodeXpath, validateAndParse)
         then: 'a ContainerNode holding collection of normalized nodes is returned'
             result.body().getAt(0) instanceof NormalizedNode == true
         then: 'result represents a node of expected type'
@@ -112,7 +114,7 @@ class YangParserHelperSpec extends Specification {
             def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         when: 'json string is parsed'
-            objectUnderTest.parseData(ContentType.JSON, '{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, parentNodeXpath)
+            objectUnderTest.parseData(ContentType.JSON, '{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext, parentNodeXpath, validateAndParse)
         then: 'expected exception is thrown'
             thrown(DataValidationException)
         where:
@@ -129,7 +131,7 @@ class YangParserHelperSpec extends Specification {
             def yangResourcesMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         when: 'malformed json string is parsed'
-            objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '')
+            objectUnderTest.parseData(ContentType.JSON, invalidJson, schemaContext, '', validateAndParse)
         then: 'an exception is thrown'
             thrown(DataValidationException)
         where: 'the following malformed json is provided'
@@ -145,7 +147,7 @@ class YangParserHelperSpec extends Specification {
         and: 'some json data with space in the array elements'
             def jsonDataWithSpacesInArrayElement = TestUtils.getResourceFileContent('bookstore.json')
         when: 'that json data is parsed'
-            objectUnderTest.parseData(ContentType.JSON, jsonDataWithSpacesInArrayElement, schemaContext, '')
+            objectUnderTest.parseData(ContentType.JSON, jsonDataWithSpacesInArrayElement, schemaContext, '', validateAndParse)
         then: 'no exception thrown'
             noExceptionThrown()
     }
@@ -162,5 +164,22 @@ class YangParserHelperSpec extends Specification {
             'xpath contains list attributes with /'        | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']'  || ['test-tree','branch','categories']
     }
 
+    def 'Validating #scenario xpath String.'() {
+        given: 'a data model (file) is provided'
+            def fileData = TestUtils.getResourceFileContent(contentFile)
+        and: 'the schema context is built for that data model'
+            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
+            def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+        when: 'the data is parsed to be validated'
+            objectUnderTest.parseData(contentType, fileData, schemaContext,  parentNodeXpath, validateOnly)
+        then: 'no exception is thrown'
+            noExceptionThrown()
+        where:
+            scenario                   | parentNodeXpath | contentFile                      | contentType
+            'JSON without parent node' | ''              | 'bookstore.json'                 | ContentType.JSON
+            'JSON with parent node'    | '/bookstore'    | 'bookstore-categories-data.json' | ContentType.JSON
+            'XML without parent node'  | ''              | 'bookstore.xml'                  | ContentType.XML
+            'XML with parent node'     | '/bookstore'    | 'bookstore-categories-data.xml'  | ContentType.XML
+    }
 
 }
index 18d0502..6c52bec 100644 (file)
@@ -26,7 +26,6 @@ import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
 import org.onap.cps.yang.YangTextSchemaSourceSet
-import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode
 import org.opendaylight.yangtools.yang.model.api.SchemaContext
 import spock.lang.Specification
@@ -47,6 +46,8 @@ class YangParserSpec extends Specification {
     def containerNodeFromYangUtils = Mock(ContainerNode)
 
     def noParent = ''
+    def validateOnly = true
+    def validateAndParse = false
 
     def setup() {
         mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet
@@ -55,7 +56,7 @@ class YangParserSpec extends Specification {
 
     def 'Parsing data.'() {
         given: 'the yang parser (utility) always returns a container node'
-            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils
+            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> containerNodeFromYangUtils
         when: 'parsing some json data'
             def result = objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent)
         then: 'the schema source set for the correct dataspace and schema set is retrieved form the cache'
@@ -68,7 +69,7 @@ class YangParserSpec extends Specification {
 
     def 'Parsing data with exception on first attempt.'() {
         given: 'the yang parser throws an exception on the first attempt only'
-            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent)  >> { throw new DataValidationException(noParent, noParent) } >> containerNodeFromYangUtils
+            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse)  >> { throw new DataValidationException(noParent, noParent) } >> containerNodeFromYangUtils
         when: 'attempt to parse some data'
             def result = objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent)
         then: 'the cache is cleared for the correct dataspace and schema'
@@ -79,7 +80,7 @@ class YangParserSpec extends Specification {
 
     def 'Parsing data with exception on all attempts.'() {
         given: 'the yang parser always throws an exception'
-            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent)  >> { throw new DataValidationException(noParent, noParent) }
+            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse)  >> { throw new DataValidationException(noParent, noParent) }
         when: 'attempt to parse some data'
             objectUnderTest.parseData(ContentType.JSON, 'some json', anchor, noParent)
         then: 'a data validation exception is thrown'
@@ -94,9 +95,46 @@ class YangParserSpec extends Specification {
         when: 'parsing some json data'
             def result = objectUnderTest.parseData(ContentType.JSON, 'some json', yangResourcesNameToContentMap, noParent)
         then: 'the yang parser helper always returns a container node'
-            1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils
+            1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateAndParse) >> containerNodeFromYangUtils
         and: 'the result is the same container node as return from yang utils'
             assert result == containerNodeFromYangUtils
     }
 
+    def 'Validating #scenario data using Yang parser with cache retrieval.'() {
+        given: 'the yang parser (utility) is set up and schema context is available'
+            mockYangParserHelper.parseData(contentType, 'some json', mockSchemaContext, noParent, validateOnly)
+        when: 'attempt to parse data with no parent node xpath'
+            objectUnderTest.validateData(contentType, 'some json or xml data', anchor, noParent)
+        then: 'the correct schema set is retrieved from the cache for the dataspace and schema'
+            1 * mockYangTextSchemaSourceSetCache.get('my dataspace', 'my schema') >> mockYangTextSchemaSourceSet
+        and: 'no cache entries are removed during validation'
+            0 * mockYangTextSchemaSourceSetCache.removeFromCache(*_)
+        where:
+            scenario | contentType
+            'JSON'   | ContentType.JSON
+            'XML'    | ContentType.XML
+    }
+
+    def 'Validating data when parsing fails on first attempt and recovers.'() {
+        given: 'the Yang parser throws an exception on the first attempt but succeeds on the second'
+            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateOnly)  >> { throw new DataValidationException(noParent, noParent) } >> null
+        when: 'attempting to parse JSON data'
+            objectUnderTest.validateData(ContentType.JSON, 'some json', anchor, noParent)
+        then: 'the cache is cleared for the correct dataspace and schema after the first failure'
+            1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema')
+        and: 'no exceptions are thrown after the second attempt'
+            noExceptionThrown()
+    }
+
+    def 'Validating data with repeated parsing failures leading to exception.'() {
+        given: 'the yang parser throws an exception on the first attempt only'
+            mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent, validateOnly)  >> { throw new DataValidationException(noParent, noParent) }
+        when: 'attempting to parse JSON data'
+            objectUnderTest.validateData(ContentType.JSON, 'some json', anchor, noParent)
+        then: 'a data validation exception is thrown'
+            thrown(DataValidationException)
+        and: 'the cache is cleared for the correct dataspace and schema after the failure'
+            1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema')
+    }
+
 }
diff --git a/cps-service/src/test/resources/bookstore-categories-data.json b/cps-service/src/test/resources/bookstore-categories-data.json
new file mode 100644 (file)
index 0000000..7dc22b1
--- /dev/null
@@ -0,0 +1,49 @@
+{
+  "categories": [
+    {
+      "code": "01/1",
+      "name": "SciFi",
+      "books": [
+        {
+          "authors": [
+            "Iain M. Banks"
+          ],
+          "lang": "en/it",
+          "price": "895",
+          "pub_year": "1994",
+          "title": "Feersum Endjinn/Endjinn Feersum"
+        },
+        {
+          "authors": [
+            "Ursula K. Le Guin",
+            "Joe Haldeman",
+            "Orson Scott Card",
+            "david Brin",
+            "Rober Silverberg",
+            "Dan Simmons",
+            "Greg Bear"
+          ],
+          "lang": "en",
+          "price": "1099",
+          "pub_year": "1999",
+          "title": "Far Horizons"
+        }
+      ]
+    },
+    {
+      "name": "kids",
+      "code": "02",
+      "books": [
+        {
+          "authors": [
+            "Philip Pullman"
+          ],
+          "lang": "en",
+          "price": "699",
+          "pub_year": "1995",
+          "title": "The Golden Compass"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/cps-service/src/test/resources/bookstore-categories-data.xml b/cps-service/src/test/resources/bookstore-categories-data.xml
new file mode 100644 (file)
index 0000000..c8592c1
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<categories>
+    <code>1</code>
+    <name>SciFi</name>
+    <books>
+        <title>2001: A Space Odyssey</title>
+        <lang>en</lang>
+        <authors>
+            Iain M. Banks
+        </authors>
+        <pub_year>1994</pub_year>
+        <price>895</price>
+    </books>
+</categories>
\ No newline at end of file