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-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";
+    }
+}