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)
+    }
+
+}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
index 8a14926..ef327c2 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
@@ -21,7 +21,6 @@
 package org.onap.cps.spi.impl;
 
 import com.google.common.collect.ImmutableSet;
-import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -32,17 +31,12 @@
 import org.onap.cps.spi.entities.Dataspace;
 import org.onap.cps.spi.entities.SchemaSet;
 import org.onap.cps.spi.entities.YangResource;
-import org.onap.cps.spi.exceptions.CpsException;
-import org.onap.cps.spi.exceptions.ModelValidationException;
 import org.onap.cps.spi.exceptions.SchemaSetAlreadyDefinedException;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.spi.repository.DataspaceRepository;
 import org.onap.cps.spi.repository.SchemaSetRepository;
 import org.onap.cps.spi.repository.YangResourceRepository;
-import org.onap.cps.yang.YangTextSchemaSourceSet;
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
-import org.opendaylight.yangtools.yang.model.parser.api.YangSyntaxErrorException;
-import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.stereotype.Component;
@@ -62,7 +56,7 @@
 
     @Override
     public void storeModule(final String namespace, final String moduleContent, final String revision,
-                            final String dataspaceName) {
+        final String dataspaceName) {
         // TODO this method should be removed as obsolete.
         // Modules to be processed within schema sets only.
     }
@@ -70,7 +64,7 @@
     @Override
     @Transactional
     public void storeSchemaSet(final String dataspaceName, final String schemaSetName,
-                               final Map<String, String> yangResourcesNameToContentMap) {
+        final Map<String, String> yangResourcesNameToContentMap) {
 
         final Dataspace dataspace = dataspaceRepository.getByName(dataspaceName);
         final Set<YangResource> yangResources = synchronizeYangResources(yangResourcesNameToContentMap);
@@ -119,13 +113,7 @@
         final Dataspace dataspace = dataspaceRepository.getByName(dataspaceName);
         final SchemaSet schemaSet = schemaSetRepository.getByDataspaceAndName(dataspace, schemaSetName);
         final Map<String, String> yangResourceNameToContent = schemaSet.getYangResources().stream().collect(
-                Collectors.toMap(YangResource::getName, YangResource::getContent));
-        try {
-            final YangTextSchemaSourceSet schemaSourceSet = YangTextSchemaSourceSetBuilder
-                .of(yangResourceNameToContent);
-            return schemaSourceSet.getModuleReferences();
-        } catch (final ReactorException | YangSyntaxErrorException e) {
-            throw new ModelValidationException("Yang file validation failed", e.getMessage(), e);
-        }
+            Collectors.toMap(YangResource::getName, YangResource::getContent));
+        return YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getModuleReferences();
     }
 }
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
index 325893d..e7b02fb 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java
@@ -19,6 +19,8 @@
 
 package org.onap.cps.api;
 
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.exceptions.CpsException;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
@@ -28,11 +30,15 @@
 public interface CpsModuleService {
 
     /**
-     * Store schema context for a yang model.
+     * Create schema set.
      *
-     * @param schemaContext the schema context
-     * @param dataspaceName the dataspace name
-     * @throws CpsException if input data already exists.
+     * @param dataspaceName                 dataspace name
+     * @param schemaSetName                 schema set name
+     * @param yangResourcesNameToContentMap yang resources (files) as a mep where key is resource name
+     *                                      and value is content
      */
-    void storeSchemaContext(SchemaContext schemaContext, String dataspaceName);
+    void createSchemaSet(@NonNull String dataspaceName, @NonNull String schemaSetName,
+                         @NonNull Map<String, String> yangResourcesNameToContentMap);
+
+    
 }
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
index 2c600b5..8a437db 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java
@@ -20,12 +20,10 @@
 package org.onap.cps.api.impl;
 
 
-import java.util.Optional;
+import java.util.Map;
 import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.spi.CpsModulePersistenceService;
-import org.opendaylight.yangtools.yang.common.Revision;
-import org.opendaylight.yangtools.yang.model.api.Module;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.onap.cps.yang.YangTextSchemaSourceSetBuilder;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -36,12 +34,12 @@
     private CpsModulePersistenceService cpsModulePersistenceService;
 
     @Override
-    public void storeSchemaContext(final SchemaContext schemaContext, final String dataspaceName) {
-        for (final Module module : schemaContext.getModules()) {
-            final Optional<Revision> optionalRevision = module.getRevision();
-            final String revisionValue = optionalRevision.map(Object::toString).orElse(null);
-            cpsModulePersistenceService.storeModule(module.getNamespace().toString(), module.toString(),
-                revisionValue, dataspaceName);
-        }
+    public void createSchemaSet(final String dataspaceName, final String schemaSetName,
+                                final Map<String, String> yangResourcesNameToContentMap) {
+
+        YangTextSchemaSourceSetBuilder.validate(yangResourcesNameToContentMap);
+        cpsModulePersistenceService
+            .storeSchemaSet(dataspaceName, schemaSetName, yangResourcesNameToContentMap);
     }
+
 }
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/ModelValidationException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/ModelValidationException.java
index 04a8836..b05a3f6 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/ModelValidationException.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/ModelValidationException.java
@@ -31,6 +31,16 @@
      *
      * @param message the error message
      * @param details the error details
+     */
+    public ModelValidationException(final String message, final String details) {
+        super(message, details);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
      * @param cause   the cause of the exception
      */
     public ModelValidationException(final String message, final String details, final Throwable cause) {
diff --git a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
index 89eea97..6d82544 100644
--- a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
+++ b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
@@ -24,10 +24,13 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
+import lombok.NoArgsConstructor;
 import org.onap.cps.spi.exceptions.CpsException;
+import org.onap.cps.spi.exceptions.ModelValidationException;
 import org.onap.cps.spi.model.ModuleReference;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.YangNames;
@@ -41,13 +44,11 @@
 import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
 import org.opendaylight.yangtools.yang.parser.stmt.reactor.CrossSourceStatementReactor;
 
+@NoArgsConstructor
 public final class YangTextSchemaSourceSetBuilder {
 
     private final ImmutableMap.Builder<String, String> yangModelMap = new ImmutableMap.Builder<>();
 
-    public YangTextSchemaSourceSetBuilder() {
-    }
-
     public YangTextSchemaSourceSetBuilder put(final String fileName, final String content) {
         this.yangModelMap.put(fileName, content);
         return this;
@@ -58,36 +59,45 @@
         return this;
     }
 
-    public YangTextSchemaSourceSet build() throws ReactorException, YangSyntaxErrorException {
+    public YangTextSchemaSourceSet build() {
         final SchemaContext schemaContext = generateSchemaContext(yangModelMap.build());
         return new YangTextSchemaSourceSetImpl(schemaContext);
     }
 
-    public static YangTextSchemaSourceSet of(final Map<String, String> yangResourceNameToContent)
-            throws ReactorException, YangSyntaxErrorException {
+    public static YangTextSchemaSourceSet of(final Map<String, String> yangResourceNameToContent) {
         return new YangTextSchemaSourceSetBuilder().putAll(yangResourceNameToContent).build();
     }
 
+    /**
+     * Validates if SchemaContext can be successfully built from given yang resources.
+     *
+     * @param yangResourceNameToContent the yang resources as map where key is name and value is content
+     * @throws ModelValidationException if validation fails
+     */
+    public static void validate(final Map<String, String> yangResourceNameToContent) {
+        generateSchemaContext(yangResourceNameToContent);
+    }
+
     private static class YangTextSchemaSourceSetImpl implements YangTextSchemaSourceSet {
 
         private final SchemaContext schemaContext;
 
-        public YangTextSchemaSourceSetImpl(final SchemaContext schemaContext) {
+        private YangTextSchemaSourceSetImpl(final SchemaContext schemaContext) {
             this.schemaContext = schemaContext;
         }
 
         @Override
         public List<ModuleReference> getModuleReferences() {
             return schemaContext.getModules().stream()
-                           .map(YangTextSchemaSourceSetImpl::toModuleReference)
-                           .collect(Collectors.toList());
+                .map(YangTextSchemaSourceSetImpl::toModuleReference)
+                .collect(Collectors.toList());
         }
 
         private static ModuleReference toModuleReference(final Module module) {
             return ModuleReference.builder()
-                           .namespace(module.getName())
-                           .revision(module.getRevision().map(Revision::toString).orElse(null))
-                           .build();
+                .namespace(module.getNamespace().toString())
+                .revision(module.getRevision().map(Revision::toString).orElse(null))
+                .build();
         }
 
         @Override
@@ -100,38 +110,49 @@
      * Parse and validate a string representing a yang model to generate a SchemaContext context.
      *
      * @param yangResourceNameToContent is a {@link Map} collection that contains the name of the model represented
-     *                     on yangModelContent as key and the yangModelContent as value.
+     *                                  on yangModelContent as key and the yangModelContent as value.
      * @return the schema context
      */
-    private SchemaContext generateSchemaContext(final Map<String, String> yangResourceNameToContent)
-            throws ReactorException, YangSyntaxErrorException {
+    private static SchemaContext generateSchemaContext(final Map<String, String> yangResourceNameToContent) {
         final CrossSourceStatementReactor.BuildAction reactor = RFC7950Reactors.defaultReactor().newBuild();
-        final List<YangTextSchemaSource> yangTextSchemaSources = forResources(yangResourceNameToContent);
-        for (final YangTextSchemaSource yangTextSchemaSource : yangTextSchemaSources) {
+        for (final YangTextSchemaSource yangTextSchemaSource : forResources(yangResourceNameToContent)) {
+            final String resourceName = yangTextSchemaSource.getIdentifier().getName();
             try {
                 reactor.addSource(YangStatementStreamSource.create(yangTextSchemaSource));
             } catch (final IOException e) {
-                throw new CpsException("Failed to read yangTextSchemaSource %s.",
-                        yangTextSchemaSource.getIdentifier().getName(), e);
+                throw new CpsException("Failed to read yang resource.",
+                    String.format("Exception occurred on reading resource %s.", resourceName), e);
+            } catch (final YangSyntaxErrorException e) {
+                throw new ModelValidationException("Yang resource is invalid.",
+                    String.format("Yang syntax validation failed for resource %s.", resourceName), e);
             }
         }
-        return reactor.buildEffective();
+        try {
+            return reactor.buildEffective();
+        } catch (final ReactorException e) {
+            final List<String> resourceNames = yangResourceNameToContent.keySet().stream().collect(Collectors.toList());
+            Collections.sort(resourceNames);
+            throw new ModelValidationException("Invalid schema set.",
+                String.format("Effective schema context build failed for resources %s.", resourceNames.toString()),
+                e);
+        }
     }
 
-    private List<YangTextSchemaSource> forResources(final Map<String, String> yangResourceNameToContent) {
+    private static List<YangTextSchemaSource> forResources(final Map<String, String> yangResourceNameToContent) {
         return yangResourceNameToContent.entrySet().stream()
-                       .map(entry -> toYangTextSchemaSource(entry.getKey(), entry.getValue()))
-                       .collect(Collectors.toList());
+            .map(entry -> toYangTextSchemaSource(entry.getKey(), entry.getValue()))
+            .collect(Collectors.toList());
     }
 
-    private YangTextSchemaSource toYangTextSchemaSource(final String sourceName, final String source) {
+    private static YangTextSchemaSource toYangTextSchemaSource(final String sourceName, final String source) {
         final Map.Entry<String, String> sourceNameParsed = YangNames.parseFilename(sourceName);
         final RevisionSourceIdentifier revisionSourceIdentifier = RevisionSourceIdentifier
             .create(sourceNameParsed.getKey(), Revision.ofNullable(sourceNameParsed.getValue()));
+
         return new YangTextSchemaSource(revisionSourceIdentifier) {
             @Override
             protected MoreObjects.ToStringHelper addToStringAttributes(
-                    final MoreObjects.ToStringHelper toStringHelper) {
+                final MoreObjects.ToStringHelper toStringHelper) {
                 return toStringHelper;
             }
 
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModulePersistenceServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModulePersistenceServiceImplSpec.groovy
deleted file mode 100644
index 39d8ec3..0000000
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModulePersistenceServiceImplSpec.groovy
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
- *  Modifications Copyright (C) 2020 Bell Canada. All rights reserved.
- *  ================================================================================
- *  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.api.impl
-
-import org.onap.cps.spi.CpsModulePersistenceService
-import org.opendaylight.yangtools.yang.common.Revision
-import org.opendaylight.yangtools.yang.model.api.SchemaContext
-import spock.lang.Specification
-
-class CpsModulePersistenceServiceImplSpec extends Specification {
-    def mockModuleStoreService = Mock(CpsModulePersistenceService)
-    def objectUnderTest = new CpsModuleServiceImpl()
-
-    def setup() {
-        objectUnderTest.cpsModulePersistenceService = mockModuleStoreService
-    }
-
-    def assertModule(SchemaContext schemaContext) {
-        def optionalModule = schemaContext.findModule('stores', Revision.of('2020-09-15'))
-        return schemaContext.modules.size() == 1 && optionalModule.isPresent()
-    }
-
-    def 'Store a SchemaContext'() {
-        expect: 'No exception to be thrown when a valid model (schema) is stored'
-            objectUnderTest.storeSchemaContext(Stub(SchemaContext.class), "sampleDataspace")
-    }
-
-}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
new file mode 100644
index 0000000..a93411b
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
@@ -0,0 +1,57 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2020 Nordix Foundation
+ *  Modifications Copyright (C) 2020 Bell Canada. All rights reserved.
+ *  ================================================================================
+ *  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.api.impl
+
+import org.onap.cps.TestUtils
+import org.onap.cps.spi.CpsModulePersistenceService
+import org.onap.cps.spi.exceptions.ModelValidationException
+import org.onap.cps.utils.YangUtils
+import org.opendaylight.yangtools.yang.common.Revision
+import org.opendaylight.yangtools.yang.model.api.SchemaContext
+import spock.lang.Specification
+
+class CpsModuleServiceImplSpec extends Specification {
+    def mockModuleStoreService = Mock(CpsModulePersistenceService)
+    def objectUnderTest = new CpsModuleServiceImpl()
+
+    def setup() {
+        objectUnderTest.cpsModulePersistenceService = mockModuleStoreService
+    }
+
+    def 'Create schema set'() {
+        given: 'Valid yang resource as name-to-content map'
+            def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
+        when: 'Create schema set method is invoked'
+            objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
+        then: 'Parameters are validated and processing is delegated to persistence service'
+            1 * mockModuleStoreService.storeSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
+    }
+
+    def 'Create schema set from invalid resources'() {
+        given: 'Invalid yang resource as name-to-content map'
+            def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('invalid.yang')
+        when: 'Create schema set method is invoked'
+            objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
+        then: 'Model validation exception is thrown'
+            thrown(ModelValidationException.class)
+    }
+
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy
index fd1b144..9a19def 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangTextSchemaSourceSetSpec.groovy
@@ -20,6 +20,7 @@
 package org.onap.cps.utils
 
 import org.onap.cps.TestUtils
+import org.onap.cps.spi.exceptions.ModelValidationException
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.opendaylight.yangtools.yang.common.Revision
 import org.opendaylight.yangtools.yang.model.parser.api.YangSyntaxErrorException
@@ -48,8 +49,9 @@
         then: 'an exception is thrown'
             thrown(expectedException)
         where: 'the following parameters are used'
-             filename           | description          || expectedException
-            'invalid.yang'      | 'invalid content'   || YangSyntaxErrorException
-            'invalid-empty.yang'| 'no valid content'   || YangSyntaxErrorException
+            filename                      | description            || expectedException
+            'invalid.yang'                | 'invalid content'      || ModelValidationException
+            'invalid-empty.yang'          | 'no valid content'     || ModelValidationException
+            'invalid-missing-import.yang' | 'no dependency module' || ModelValidationException
     }
 }
diff --git a/cps-service/src/test/resources/invalid-missing-import.yang b/cps-service/src/test/resources/invalid-missing-import.yang
new file mode 100644
index 0000000..3a0cc87
--- /dev/null
+++ b/cps-service/src/test/resources/invalid-missing-import.yang
@@ -0,0 +1,15 @@
+module test-module {
+    yang-version 1.1;
+
+    namespace "org:onap:cps:test:test-module";
+    revision "2020-02-02";
+    prefix "self";
+
+    import missing-module {
+        prefix "missing";
+    }
+
+    container self-container {
+        uses "missing:missing-group";
+    }
+}