diff options
author | Ruslan Kashapov <ruslan.kashapov@pantheon.tech> | 2020-12-10 10:49:59 +0200 |
---|---|---|
committer | Ruslan Kashapov <ruslan.kashapov@pantheon.tech> | 2020-12-24 09:57:48 +0200 |
commit | acfb2078d510f5cec7b6ce57c03ba42663b8f3ee (patch) | |
tree | 0ed0437b534e2d94208c51398afb3dff682e8d12 | |
parent | 1d9845679de45007db30eee42c105edcffd972fb (diff) |
Create schema set REST API and service level
Issue-ID: CPS-123
Change-Id: Ie6d5fd4755454331415af7b80eaf85925efab395
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
14 files changed, 378 insertions, 70 deletions
diff --git a/cps-rest/docs/api/swagger/openapi.yml b/cps-rest/docs/api/swagger/openapi.yml index 587a37606c..d76ec5ecd5 100755 --- a/cps-rest/docs/api/swagger/openapi.yml +++ b/cps-rest/docs/api/swagger/openapi.yml @@ -38,6 +38,47 @@ paths: 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 336762cb49..6dc2cee72a 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 @@ public class AdminRestController implements CpsAdminApi { 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 @@ public class AdminRestController implements CpsAdminApi { */ @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 0000000000..0c527a5565 --- /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 0000000000..f0d5b3fa22 --- /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 7a777bf380..99ffbfd05a 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 @@ package org.onap.cps.rest.exceptions 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 @@ class CpsRestExceptionHandlerSpec extends Specification { 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 0000000000..ba5aa4cac0 --- /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 8a14926255..ef327c2592 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.CpsModulePersistenceService; 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 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ @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 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ @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 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ 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 325893d6fd..e7b02fbad8 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 @@ import org.opendaylight.yangtools.yang.model.api.SchemaContext; 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 2c600b5571..8a437dbdee 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 @@ public class CpsModuleServiceImpl implements CpsModuleService { 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 04a8836aca..b05a3f60eb 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 @@ public class ModelValidationException extends CpsException { * * @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 89eea97f61..6d825445c3 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 com.google.common.collect.ImmutableMap; 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.rfc7950.repo.YangStatementStreamSo 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 @@ public final class YangTextSchemaSourceSetBuilder { 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 @@ public final class YangTextSchemaSourceSetBuilder { * 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/CpsModuleServiceImplSpec.groovy index 39d8ec3bca..a93411bfe9 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModulePersistenceServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy @@ -20,12 +20,15 @@ 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 CpsModulePersistenceServiceImplSpec extends Specification { +class CpsModuleServiceImplSpec extends Specification { def mockModuleStoreService = Mock(CpsModulePersistenceService) def objectUnderTest = new CpsModuleServiceImpl() @@ -33,14 +36,22 @@ class CpsModulePersistenceServiceImplSpec extends Specification { 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 '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 'Store a SchemaContext'() { - expect: 'No exception to be thrown when a valid model (schema) is stored' - objectUnderTest.storeSchemaContext(Stub(SchemaContext.class), "sampleDataspace") + 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 fd1b144309..9a19def89c 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 @@ class YangTextSchemaSourceSetSpec extends Specification { 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 0000000000..3a0cc87f71 --- /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"; + } +} |