Create schema set REST API and service level

Issue-ID: CPS-123
Change-Id: Ie6d5fd4755454331415af7b80eaf85925efab395
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
diff --git a/cps-rest/docs/api/swagger/openapi.yml b/cps-rest/docs/api/swagger/openapi.yml
index 587a376..d76ec5e 100755
--- a/cps-rest/docs/api/swagger/openapi.yml
+++ b/cps-rest/docs/api/swagger/openapi.yml
@@ -38,6 +38,47 @@
         403:
           description: Forbidden
           content: {}
+  /v1/dataspaces/{dataspace-name}/schema-sets:
+    post:
+      tags:
+        - cps-admin
+      summary: Create a new schema set in the given dataspace
+      operationId: createSchemaSet
+      parameters:
+        - name: dataspace-name
+          in: path
+          description: dataspace-name
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          multipart/form-data:
+            schema:
+              required:
+                - schemaSetName
+                - multipartFile
+              properties:
+                schemaSetName:
+                  type: string
+                multipartFile:
+                  type: string
+                  description: multipartFile
+                  format: binary
+      responses:
+        201:
+          description: Created
+          content:
+            application/json:
+              schema:
+                type: string
+        401:
+          description: Unauthorized
+          content: { }
+        403:
+          description: Forbidden
+          content: { }
   /v1/dataspaces/{dataspace-name}/anchors:
     get:
       tags:
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java
index 336762c..6dc2cee 100644
--- a/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java
@@ -20,16 +20,19 @@
 
 package org.onap.cps.rest.controller;
 
+import static org.onap.cps.rest.utils.MultipartFileUtil.extractYangResourcesMap;
+
 import java.util.Collection;
-import javax.validation.Valid;
 import org.modelmapper.ModelMapper;
 import org.onap.cps.api.CpsAdminService;
+import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.rest.api.CpsAdminApi;
 import org.onap.cps.spi.model.Anchor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
 
 @RestController
 public class AdminRestController implements CpsAdminApi {
@@ -38,8 +41,18 @@
     private CpsAdminService cpsAdminService;
 
     @Autowired
+    private CpsModuleService cpsModuleService;
+
+    @Autowired
     private ModelMapper modelMapper;
 
+    @Override
+    public ResponseEntity<String> createSchemaSet(final String schemaSetName, final MultipartFile multipartFile,
+        final String dataspaceName) {
+        cpsModuleService.createSchemaSet(dataspaceName, schemaSetName, extractYangResourcesMap(multipartFile));
+        return new ResponseEntity<>(schemaSetName, HttpStatus.CREATED);
+    }
+
     /**
      * Create a new anchor.
      *
@@ -50,7 +63,7 @@
      */
     @Override
     public ResponseEntity<String> createAnchor(final String dataspaceName, final String schemaSetName,
-                                               final String anchorName) {
+        final String anchorName) {
         cpsAdminService.createAnchor(dataspaceName, schemaSetName, anchorName);
         return new ResponseEntity<>(anchorName, HttpStatus.CREATED);
     }
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java b/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
new file mode 100644
index 0000000..0c527a5
--- /dev/null
+++ b/cps-rest/src/main/java/org/onap/cps/rest/utils/MultipartFileUtil.java
@@ -0,0 +1,66 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2020 Pantheon.tech
+ *  ================================================================================
+ *  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.rest.utils;
+
+import static org.opendaylight.yangtools.yang.common.YangConstants.RFC6020_YANG_FILE_EXTENSION;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.onap.cps.spi.exceptions.CpsException;
+import org.onap.cps.spi.exceptions.ModelValidationException;
+import org.springframework.web.multipart.MultipartFile;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MultipartFileUtil {
+
+    /**
+     * Extracts yang resources from multipart file instance.
+     *
+     * @param multipartFile the yang file uploaded
+     * @return yang resources as {map} where the key is original file name, and the value is file content
+     * @throws ModelValidationException if the file name extension is not '.yang'
+     * @throws CpsException             if the file content cannot be read
+     */
+
+    public static Map<String, String> extractYangResourcesMap(final MultipartFile multipartFile) {
+        return ImmutableMap.of(extractYangResourceName(multipartFile), extractYangResourceContent(multipartFile));
+    }
+
+    private static String extractYangResourceName(final MultipartFile multipartFile) {
+        final String fileName = multipartFile.getOriginalFilename();
+        if (!fileName.endsWith(RFC6020_YANG_FILE_EXTENSION)) {
+            throw new ModelValidationException("Unsupported file type.",
+                String.format("Filename %s does not end with '%s'", fileName, RFC6020_YANG_FILE_EXTENSION));
+        }
+        return fileName;
+    }
+
+    private static String extractYangResourceContent(final MultipartFile multipartFile) {
+        try {
+            return new String(multipartFile.getBytes());
+        } catch (final IOException e) {
+            throw new CpsException("Cannot read the resource file.", e.getMessage(), e);
+        }
+    }
+
+}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
new file mode 100644
index 0000000..f0d5b3f
--- /dev/null
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
@@ -0,0 +1,85 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2020 Pantheon.tech
+ *  ================================================================================
+ *  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.rest.controller
+
+import org.modelmapper.ModelMapper
+import org.onap.cps.api.CpsAdminService
+import org.onap.cps.api.CpsModuleService
+import org.spockframework.spring.SpringBean
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.http.HttpStatus
+import org.springframework.mock.web.MockMultipartFile
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
+import spock.lang.Specification
+
+@WebMvcTest
+class AdminRestControllerSpec extends Specification {
+
+    @SpringBean
+    CpsModuleService mockCpsModuleService = Mock()
+
+    @SpringBean
+    CpsAdminService mockCpsAdminService = Mock()
+
+    @SpringBean
+    ModelMapper modelMapper = Mock();
+
+    @Autowired
+    MockMvc mvc
+
+    def 'Create schema set from yang file'() {
+        def yangResourceMapCapture
+        given:
+            def multipartFile = createMultipartFile("filename.yang", "content")
+        when:
+            def response = performCreateSchemaSetRequest(multipartFile)
+        then: 'Service method is invoked with expected parameters'
+            1 * mockCpsModuleService.createSchemaSet('test-dataspace', 'test-schema-set', _) >>
+                    { args -> yangResourceMapCapture = args[2] }
+            yangResourceMapCapture['filename.yang'] == 'content'
+        and: 'Response code indicates success'
+            response.status == HttpStatus.CREATED.value()
+    }
+
+    def 'Create schema set from file with invalid filename extension'() {
+        given:
+            def multipartFile = createMultipartFile("filename.doc", "content")
+        when:
+            def response = performCreateSchemaSetRequest(multipartFile)
+        then:
+            response.status == HttpStatus.BAD_REQUEST.value()
+    }
+
+    def createMultipartFile(filename, content) {
+        return new MockMultipartFile("file", filename, "text/plain", content.getBytes())
+    }
+
+    def performCreateSchemaSetRequest(multipartFile) {
+        return mvc.perform(
+                MockMvcRequestBuilders
+                        .multipart('/v1/dataspaces/test-dataspace/schema-sets')
+                        .file(multipartFile)
+                        .param('schemaSetName', 'test-schema-set')
+        ).andReturn().response
+    }
+
+}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
index 7a777bf..99ffbfd 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
@@ -22,11 +22,12 @@
 import groovy.json.JsonSlurper
 import org.modelmapper.ModelMapper
 import org.onap.cps.api.CpsAdminService
+import org.onap.cps.api.CpsModuleService
 import org.onap.cps.spi.exceptions.AnchorAlreadyDefinedException
 import org.onap.cps.spi.exceptions.CpsException
 import org.onap.cps.spi.exceptions.DataValidationException
-import org.onap.cps.spi.exceptions.NotFoundInDataspaceException
 import org.onap.cps.spi.exceptions.ModelValidationException
+import org.onap.cps.spi.exceptions.NotFoundInDataspaceException
 import org.onap.cps.spi.exceptions.SchemaSetAlreadyDefinedException
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
@@ -48,6 +49,9 @@
     CpsAdminService mockCpsAdminService = Mock()
 
     @SpringBean
+    CpsModuleService mockCpsModuleService = Mock()
+
+    @SpringBean
     ModelMapper modelMapper = Mock()
 
     @Autowired
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy
new file mode 100644
index 0000000..ba5aa4c
--- /dev/null
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/utils/MultipartFileUtilSpec.groovy
@@ -0,0 +1,48 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2020 Pantheon.tech
+ *  ================================================================================
+ *  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.rest.utils
+
+import org.onap.cps.spi.exceptions.ModelValidationException
+import org.springframework.mock.web.MockMultipartFile
+import spock.lang.Specification
+
+class MultipartFileUtilSpec extends Specification {
+
+    def 'Extract yang resource from multipart file'() {
+        given:
+            def multipartFile = new MockMultipartFile("file", "filename.yang", "text/plain", "content".getBytes())
+        when:
+            def result = MultipartFileUtil.extractYangResourcesMap(multipartFile)
+        then:
+            assert result != null
+            assert result.size() == 1
+            assert result.get("filename.yang") == "content"
+    }
+
+    def 'Extract yang resource from  file with invalid filename extension'() {
+        given:
+            def multipartFile = new MockMultipartFile("file", "filename.doc", "text/plain", "content".getBytes())
+        when:
+            MultipartFileUtil.extractYangResourcesMap(multipartFile)
+        then:
+            thrown(ModelValidationException)
+    }
+
+}