Create child data node (part 1): CPS service + REST 25/120625/2
authorRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Mon, 19 Apr 2021 09:40:01 +0000 (12:40 +0300)
committerRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Tue, 20 Apr 2021 09:58:55 +0000 (12:58 +0300)
Issue-ID: CPS-337
Change-Id: I9c5c62d144b5301ac80e2b82a5cc66a980dad011
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
cps-rest/docs/api/swagger/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/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy

index 2464489..54c8966 100755 (executable)
@@ -33,6 +33,7 @@ nodesByDataspaceAndAnchor:
     parameters:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
+      - $ref: 'components.yml#/components/parameters/xpathInQuery'
     requestBody:
       required: true
       content:
index f466ebc..3385f35 100755 (executable)
@@ -21,8 +21,6 @@
 
 package org.onap.cps.rest.controller;
 
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.rest.api.CpsDataApi;
 import org.onap.cps.spi.FetchDescendantsOption;
@@ -38,13 +36,19 @@ import org.springframework.web.bind.annotation.RestController;
 @RequestMapping("${rest.api.cps-base-path}")
 public class DataRestController implements CpsDataApi {
 
+    private static final String ROOT_XPATH = "/";
+
     @Autowired
     private CpsDataService cpsDataService;
 
     @Override
-    public ResponseEntity<String> createNode(@Valid final String jsonData, @NotNull final String dataspaceName,
-        @NotNull @Valid final String anchorName) {
-        cpsDataService.saveData(dataspaceName, anchorName, jsonData);
+    public ResponseEntity<String> createNode(final String jsonData, final String dataspaceName, final String anchorName,
+        final String parentNodeXpath) {
+        if (isRootXpath(parentNodeXpath)) {
+            cpsDataService.saveData(dataspaceName, anchorName, jsonData);
+        } else {
+            cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        }
         return new ResponseEntity<>(HttpStatus.CREATED);
     }
 
@@ -56,7 +60,7 @@ public class DataRestController implements CpsDataApi {
     @Override
     public ResponseEntity<Object> getNodeByDataspaceAndAnchor(final String dataspaceName, final String anchorName,
         final String xpath, final Boolean includeDescendants) {
-        if ("/".equals(xpath)) {
+        if (isRootXpath(xpath)) {
             return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
         }
         final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
@@ -79,4 +83,8 @@ public class DataRestController implements CpsDataApi {
         cpsDataService.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData);
         return new ResponseEntity<>(HttpStatus.OK);
     }
+
+    private static boolean isRootXpath(final String xpath) {
+        return ROOT_XPATH.equals(xpath);
+    }
 }
index ef43641..713dda1 100755 (executable)
@@ -90,7 +90,8 @@ class DataRestControllerSpec extends Specification {
         dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
     }
 
-    def 'Create a node.'() {
+    @Unroll
+    def 'Create a node: #scenario.'() {
         given: 'some json to create a data node'
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
             def json = 'some json (this is not validated)'
@@ -98,12 +99,38 @@ class DataRestControllerSpec extends Specification {
             def response =
                     mvc.perform(
                             post(endpoint)
-                                    .contentType(MediaType.APPLICATION_JSON).content(json))
-                            .andReturn().response
+                                    .contentType(MediaType.APPLICATION_JSON)
+                                    .param('xpath', parentNodeXpath)
+                                    .content(json)
+                    ).andReturn().response
         then: 'a created response is returned'
             response.status == HttpStatus.CREATED.value()
         then: 'the java API was called with the correct parameters'
             1 * mockCpsDataService.saveData(dataspaceName, anchorName, json)
+        where: 'following xpath parameters are are used'
+            scenario                     | parentNodeXpath
+            'no xpath parameter'         | ''
+            'xpath parameter point root' | '/'
+    }
+
+    def 'Create a child node'() {
+        given: 'some json to create a data node'
+            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def json = 'some json (this is not validated)'
+        and: 'parent node xpath'
+            def parentNodeXpath = 'some xpath'
+        when: 'post is invoked with datanode endpoint and json'
+            def response =
+                    mvc.perform(
+                            post(endpoint)
+                                    .contentType(MediaType.APPLICATION_JSON)
+                                    .param('xpath', parentNodeXpath)
+                                    .content(json)
+                    ).andReturn().response
+        then: 'a created response is returned'
+            response.status == HttpStatus.CREATED.value()
+        then: 'the java API was called with the correct parameters'
+            1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, json)
     }
 
     @Unroll
index 54d9258..8552c6c 100644 (file)
@@ -22,6 +22,8 @@ package org.onap.cps.api;
 
 import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.exceptions.AlreadyDefinedException;
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.DataNode;
 
@@ -40,6 +42,20 @@ public interface CpsDataService {
      */
     void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String jsonData);
 
+    /**
+     * Persists child data fragment under existing data node for the given anchor and dataspace.
+     *
+     * @param dataspaceName   dataspace name
+     * @param anchorName      anchor name
+     * @param parentNodeXpath parent node xpath
+     * @param jsonData        json data
+     * @throws DataValidationException   when json data is invalid
+     * @throws DataNodeNotFoundException when parent node cannot be found by parent node xpath
+     * @throws AlreadyDefinedException   when child data node with same xpath already exists
+     */
+    void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
+        @NonNull String jsonData);
+
     /**
      * Retrieves datanode by XPath for given dataspace and anchor.
      *
index 6f7d643..cc290bf 100755 (executable)
@@ -58,6 +58,13 @@ public class CpsDataServiceImpl implements CpsDataService {
         cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode);
     }
 
+    @Override
+    public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+        final String jsonData) {
+        final DataNode dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
+    }
+
     @Override
     public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
         final FetchDescendantsOption fetchDescendantsOption) {
index d561475..29a2314 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Nordix Foundation
+ *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -69,6 +70,25 @@ class CpsDataServiceImplSpec extends Specification {
                     { dataNode -> dataNode.xpath == '/test-tree' })
     }
 
+    def 'Saving child data fragment under existing node.'() {
+        given: 'that the admin service will return an anchor'
+            def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
+            mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
+        and: 'the schema source set cache returns a schema source set'
+            def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
+            mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
+        and: 'the schema source sets returns the test-tree schema context'
+            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
+            def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+            mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
+        when: 'save data method is invoked with test-tree json data'
+            def jsonData = '{"branch": [{"name": "New"}]}'
+            objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree',jsonData)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName,'/test-tree',
+                    { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' })
+    }
+
     @Unroll
     def 'Get data node with option #fetchDescendantsOption.'() {
         def xpath = '/xpath'