From c82aebcde26e34c4151531b4d7a8f6e7689734ba Mon Sep 17 00:00:00 2001 From: "andre.schmid" Date: Fri, 14 May 2021 20:38:45 +0100 Subject: [PATCH] Add models imports endpoint and persistence structure MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Create the structure to persist the model imports. Changed create model API, allowing to create a model along its TOSCA descriptor import structure. Introduced an endpoint to update the imports of a model. Change-Id: Ic775ef544051c29c721cacc20b37c2fb20338be9 Issue-ID: SDC-3614 Signed-off-by: André Schmid --- catalog-be/pom.xml | 3 +- .../files/default/error-configuration.yaml | 22 +++ .../sdc/be/components/impl/ModelBusinessLogic.java | 63 +++++++- .../openecomp/sdc/be/servlets/ModelServlet.java | 65 +++++++-- .../exception/OperationExceptionMapper.java | 11 +- .../be/components/impl/ModelBusinessLogicTest.java | 137 ++++++++++++++++-- .../beans/ToscaModelImportCassandraDaoMock.java | 39 +++++ .../sdc/be/servlets/ModelServletTest.java | 138 +++++++++++++++--- .../resources/modelImports/emptyModelImports.zip | Bin 0 -> 186 bytes .../modelWithSubFolderAndEmptyFolder.zip | Bin 0 -> 1961 bytes .../src/test/resources/paths/path-context.xml | 1 + .../org/openecomp/sdc/be/dao/api/ActionStatus.java | 2 +- .../api/exception/CassandraDaoInitException.java | 27 ++++ .../CassandraDaoInitExceptionProvider.java | 36 +++++ .../sdc/be/dao/cassandra/CassandraClient.java | 3 +- .../dao/cassandra/ToscaImportByModelAccessor.java | 33 +++++ .../cassandra/ToscaModelImportCassandraDao.java | 106 ++++++++++++++ .../be/dao/cassandra/schema/SdcSchemaBuilder.java | 24 ++-- .../sdc/be/dao/cassandra/schema/Table.java | 4 +- .../tables/ToscaImportByModelTableDescription.java | 78 ++++++++++ .../ToscaModelImportCassandraDaoTest.java | 152 ++++++++++++++++++++ .../exception/ModelOperationExceptionSupplier.java | 50 +++++++ .../be/model/operations/impl/ModelOperation.java | 93 ++++++++++-- .../model/operations/impl/ModelOperationTest.java | 160 ++++++++++++++++++++- .../sdc/be/data/model/ToscaImportByModel.java | 40 ++++++ .../datatypes/model/CapabilityDefinition.java | 57 +------- .../sdc/tosca/datatypes/model/EntrySchema.java | 28 +--- 27 files changed, 1220 insertions(+), 152 deletions(-) create mode 100644 catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java create mode 100644 catalog-be/src/test/resources/modelImports/emptyModelImports.zip create mode 100644 catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip create mode 100644 catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitException.java create mode 100644 catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitExceptionProvider.java create mode 100644 catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaImportByModelAccessor.java create mode 100644 catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDao.java create mode 100644 catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/tables/ToscaImportByModelTableDescription.java create mode 100644 catalog-dao/src/test/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDaoTest.java create mode 100644 catalog-model/src/main/java/org/openecomp/sdc/be/model/jsonjanusgraph/operations/exception/ModelOperationExceptionSupplier.java create mode 100644 common-be/src/main/java/org/openecomp/sdc/be/data/model/ToscaImportByModel.java diff --git a/catalog-be/pom.xml b/catalog-be/pom.xml index 3f56c02afa..53af2c7b27 100644 --- a/catalog-be/pom.xml +++ b/catalog-be/pom.xml @@ -1221,8 +1221,7 @@ - ${project.parent.basedir}/catalog-be/target - + ${project.build.directory} normatives.tar.gz diff --git a/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml b/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml index 2c6a0c852b..b277aeef2f 100644 --- a/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml +++ b/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml @@ -2473,3 +2473,25 @@ errors: message: "Error: Model name '%1' already exists.", messageId: "SVC4144" } + + #---------SVC4145------------------------------ + # %1 - "Model name" + INVALID_MODEL: { + code: 400, + message: "Invalid model '%1'.", + messageId: "SVC4145" + } + + #---------SVC4146------------------------------ + MODEL_IMPORTS_IS_EMPTY: { + code: 400, + message: "Given model imports zip is empty.", + messageId: "SVC4146" + } + + #---------SVC4147------------------------------ + COULD_NOT_READ_MODEL_IMPORTS: { + code: 400, + message: "Could not read imports zip.", + messageId: "SVC4147" + } diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java index 1ef4cef701..7f68a00a8b 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java @@ -18,17 +18,25 @@ */ package org.openecomp.sdc.be.components.impl; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.openecomp.sdc.be.model.Model; +import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.ModelOperationExceptionSupplier; import org.openecomp.sdc.be.model.operations.impl.ModelOperation; +import org.openecomp.sdc.common.zip.ZipUtils; +import org.openecomp.sdc.common.zip.exception.ZipException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; - @Component("modelBusinessLogic") public class ModelBusinessLogic { - private static final Logger log = LoggerFactory.getLogger(ModelBusinessLogic.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ModelBusinessLogic.class); private final ModelOperation modelOperation; @Autowired @@ -37,7 +45,56 @@ public class ModelBusinessLogic { } public Model createModel(final Model model) { - log.debug("createModel: creating model {}", model); + LOGGER.debug("createModel: creating model {}", model); return modelOperation.createModel(model, false); } + + public Optional findModel(final String modelName) { + if (StringUtils.isEmpty(modelName)) { + return Optional.empty(); + } + return modelOperation.findModelByName(modelName); + } + + public void createModelImports(final String modelName, final InputStream modelImportsZip) { + if (StringUtils.isEmpty(modelName)) { + throw ModelOperationExceptionSupplier.invalidModel(modelName).get(); + } + if (modelImportsZip == null) { + throw ModelOperationExceptionSupplier.emptyModelImports().get(); + } + if (findModel(modelName).isEmpty()) { + throw ModelOperationExceptionSupplier.invalidModel(modelName).get(); + } + + final var fileBytes = readBytes(modelImportsZip); + final Map zipFilesPathContentMap = unzipInMemory(fileBytes); + if (zipFilesPathContentMap.isEmpty()) { + throw ModelOperationExceptionSupplier.emptyModelImports().get(); + } + + modelOperation.createModelImports(modelName, zipFilesPathContentMap); + } + + private Map unzipInMemory(final byte[] fileBytes) { + try { + return ZipUtils.readZip(fileBytes, false); + } catch (final ZipException e) { + throw ModelOperationExceptionSupplier.couldNotReadImports().get(); + } + } + + private byte[] readBytes(final InputStream modelImportsZip) { + try (final InputStream in = modelImportsZip; final ByteArrayOutputStream os = new ByteArrayOutputStream()) { + final var buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) != -1) { + os.write(buffer, 0, len); + } + return os.toByteArray(); + } catch (final IOException e) { + LOGGER.debug("Could not read the model imports zip", e); + throw ModelOperationExceptionSupplier.couldNotReadImports().get(); + } + } } \ No newline at end of file diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java index f4fc883b4c..0c5e4aebd6 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java @@ -28,6 +28,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.InputStream; import java.util.Arrays; import javax.inject.Inject; import javax.validation.Valid; @@ -35,10 +36,13 @@ import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.glassfish.jersey.media.multipart.FormDataParam; import org.openecomp.sdc.be.components.impl.ComponentInstanceBusinessLogic; import org.openecomp.sdc.be.components.impl.ModelBusinessLogic; import org.openecomp.sdc.be.components.impl.ResourceImportManager; @@ -55,10 +59,10 @@ import org.openecomp.sdc.be.ui.model.ModelCreateRequest; import org.openecomp.sdc.be.user.Role; import org.openecomp.sdc.be.user.UserBusinessLogic; import org.openecomp.sdc.common.api.Constants; +import org.openecomp.sdc.common.util.ValidationUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestBody; /** * Root resource (exposed at "/" path) @@ -85,34 +89,67 @@ public class ModelServlet extends AbstractValidationsServlet { @POST @Path("/model") - @Consumes(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Create model", method = "POST", summary = "Returns created model", responses = { + @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) + @Operation(description = "Create a TOSCA model, along with its imports files", method = "POST", summary = "Create a TOSCA model", responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Response.class)))), @ApiResponse(responseCode = "201", description = "Model created"), - @ApiResponse(responseCode = "403", description = "Restricted operation"), @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), - @ApiResponse(responseCode = "409", description = "Resource already exists")}) - @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) + @ApiResponse(responseCode = "403", description = "Restricted operation"), + @ApiResponse(responseCode = "409", description = "Model already exists")}) public Response createModel(@Parameter(description = "model to be created", required = true) - @Valid @RequestBody @NotNull final ModelCreateRequest modelCreateRequest, - @HeaderParam(value = Constants.USER_ID_HEADER) String userId) { - - validateUser(userId); + @NotNull @Valid @FormDataParam("model") final ModelCreateRequest modelCreateRequest, + @Parameter(description = "the model TOSCA imports zipped", required = true) + @NotNull @FormDataParam("modelImportsZip") final InputStream modelImportsZip, + @HeaderParam(value = Constants.USER_ID_HEADER) final String userId) { + validateUser(ValidationUtils.sanitizeInputString(userId)); + final var modelName = ValidationUtils.sanitizeInputString(modelCreateRequest.getName().trim()); try { - final Model modelCreateResponse = modelBusinessLogic + final Model createdModel = modelBusinessLogic .createModel(new JMapper<>(Model.class, ModelCreateRequest.class).getDestination(modelCreateRequest)); + modelBusinessLogic.createModelImports(modelName, modelImportsZip); return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), - RepresentationUtils.toRepresentation(modelCreateResponse)); + RepresentationUtils.toRepresentation(createdModel)); + } catch (final BusinessException e) { + throw e; + } catch (final Exception e) { + var errorMsg = String.format("Unexpected error while creating model '%s' imports", modelName); + BeEcompErrorManager.getInstance().logBeRestApiGeneralError(errorMsg); + log.error(errorMsg, e); + return buildErrorResponse(getComponentsUtils().getResponseFormat(ActionStatus.GENERAL_ERROR)); + } + } + + @PUT + @Path("/model/imports") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) + @Operation(description = "Update a model TOSCA imports", method = "PUT", summary = "Update a model TOSCA imports", responses = { + @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Response.class)))), + @ApiResponse(responseCode = "204", description = "Model imports updated"), + @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), + @ApiResponse(responseCode = "403", description = "Restricted operation"), + @ApiResponse(responseCode = "404", description = "Model not found")}) + public Response updateModelImports(@Parameter(description = "model to be created", required = true) + @NotNull @FormDataParam("modelName") String modelName, + @Parameter(description = "the model TOSCA imports zipped", required = true) + @NotNull @FormDataParam("modelImportsZip") final InputStream modelImportsZip, + @HeaderParam(value = Constants.USER_ID_HEADER) final String userId) { + validateUser(ValidationUtils.sanitizeInputString(userId)); + modelName = ValidationUtils.sanitizeInputString(modelName); + try { + modelBusinessLogic.createModelImports(modelName, modelImportsZip); } catch (final BusinessException e) { throw e; } catch (final Exception e) { - var errorMsg = String - .format("Unexpected error while creating model '%s'", modelCreateRequest.getName()); + var errorMsg = String.format("Unexpected error while creating model '%s' imports", modelName); BeEcompErrorManager.getInstance().logBeRestApiGeneralError(errorMsg); log.error(errorMsg, e); return buildErrorResponse(getComponentsUtils().getResponseFormat(ActionStatus.GENERAL_ERROR)); } + return Response.status(Status.NO_CONTENT).build(); } private void validateUser(final String userId) { diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java index 7c25f8aef8..062e03b0da 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java @@ -26,6 +26,7 @@ import org.openecomp.sdc.be.components.impl.ResponseFormatManager; import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.OperationException; import org.openecomp.sdc.be.servlets.builder.ServletResponseBuilder; import org.openecomp.sdc.common.log.wrappers.Logger; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component @@ -35,16 +36,22 @@ public class OperationExceptionMapper implements ExceptionMapper modelBusinessLogic.createModel(model)); assertThat(((OperationException) exception).getActionStatus().name()).isEqualTo(ActionStatus.MODEL_ALREADY_EXISTS.name()); assertThat(((OperationException) exception).getParams()).contains(model.getName()); } + @Test + void createModelImportsSuccessTest() throws IOException, ZipException { + final var modelId = "modelId"; + final var resolve = modelImportsResourcePath.resolve("modelWithSubFolderAndEmptyFolder.zip"); + final var zipBytes = Files.readAllBytes(resolve); + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(zipBytes); + final Map expectedZipMap = ZipUtils.readZip(zipBytes, false); + + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.of(new Model(modelId))); + doNothing().when(modelOperation).createModelImports(eq(modelId), anyMap()); + + modelBusinessLogic.createModelImports(modelId, byteArrayInputStream); + + final ArgumentCaptor> zipMapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + verify(modelOperation).createModelImports(eq(modelId), zipMapArgumentCaptor.capture()); + expectedZipMap.keySet().forEach(key -> assertTrue(zipMapArgumentCaptor.getValue().containsKey(key), "Expecting import " + key)); + } + + @Test + void createModelImportsTest_invalidModel() { + //given an empty model + final var modelId = ""; + + final var emptyByteArrayInputStream = new ByteArrayInputStream(new byte[0]); + var actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, emptyByteArrayInputStream)); + + var expectedOperationException = ModelOperationExceptionSupplier.invalidModel(modelId).get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + assertEquals(actualOperationException.getParams()[0], expectedOperationException.getParams()[0]); + + //given a null model + actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(null, emptyByteArrayInputStream)); + + expectedOperationException = ModelOperationExceptionSupplier.invalidModel(null).get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + assertEquals(actualOperationException.getParams()[0], expectedOperationException.getParams()[0]); + } + + @Test + void createModelImportsTest_nullInputStream() { + final var modelId = "modelId"; + + final OperationException actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, null)); + + final OperationException expectedOperationException = ModelOperationExceptionSupplier.emptyModelImports().get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + } + + @Test + void createModelImportsTest_emptyModelImports() throws IOException { + final var modelId = "modelId"; + + final var resolve = modelImportsResourcePath.resolve("emptyModelImports.zip"); + final var zipBytes = Files.readAllBytes(resolve); + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(zipBytes); + + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.of(new Model(modelId))); + + final OperationException actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, byteArrayInputStream)); + + final OperationException expectedOperationException = ModelOperationExceptionSupplier.emptyModelImports().get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + } + + @Test + void createModelImportsTest_modelNotFound() { + final var modelId = "modelId"; + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[0]); + + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.empty()); + + final OperationException actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, byteArrayInputStream)); + + final OperationException expectedOperationException = ModelOperationExceptionSupplier.invalidModel(modelId).get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + } + + @Test + void findModelSuccessTest() { + final var modelId = "modelId"; + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.of(new Model(modelId))); + final Optional actualModel = modelBusinessLogic.findModel(modelId); + assertTrue(actualModel.isPresent()); + assertEquals(new Model(modelId), actualModel.get()); + } + + @Test + void findModelTest_emptyOrNullModelName() { + when(modelOperation.findModelByName(anyString())).thenReturn(Optional.of(new Model())); + var actualModel = modelBusinessLogic.findModel(""); + assertTrue(actualModel.isEmpty()); + actualModel = modelBusinessLogic.findModel(null); + assertTrue(actualModel.isEmpty()); + } } \ No newline at end of file diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java b/catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java new file mode 100644 index 0000000000..9f293b1a1c --- /dev/null +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.components.path.beans; + +import javax.annotation.PostConstruct; +import org.openecomp.sdc.be.dao.cassandra.CassandraClient; +import org.openecomp.sdc.be.dao.cassandra.ToscaModelImportCassandraDao; +import org.springframework.stereotype.Component; + +@Component("tosca-model-import-cassandra-dao") +public class ToscaModelImportCassandraDaoMock extends ToscaModelImportCassandraDao { + + public ToscaModelImportCassandraDaoMock(final CassandraClient cassandraClient) { + super(cassandraClient); + } + + @PostConstruct + @Override + public void init() { + + } +} diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java b/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java index 34201d3c1f..5992be4e9d 100644 --- a/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java @@ -18,19 +18,28 @@ */ package org.openecomp.sdc.be.servlets; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import java.nio.file.Path; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import javax.validation.ConstraintViolationException; -import javax.ws.rs.core.Response; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; import org.eclipse.jetty.http.HttpStatus; import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; @@ -46,6 +55,7 @@ import org.mockito.MockitoAnnotations; import org.openecomp.sdc.be.components.impl.ComponentInstanceBusinessLogic; import org.openecomp.sdc.be.components.impl.ModelBusinessLogic; import org.openecomp.sdc.be.components.impl.ResourceImportManager; +import org.openecomp.sdc.be.components.impl.ResponseFormatManager; import org.openecomp.sdc.be.components.validation.UserValidations; import org.openecomp.sdc.be.config.ConfigurationManager; import org.openecomp.sdc.be.config.SpringConfig; @@ -55,6 +65,9 @@ import org.openecomp.sdc.be.impl.ComponentsUtils; import org.openecomp.sdc.be.impl.ServletUtils; import org.openecomp.sdc.be.impl.WebAppContextWrapper; import org.openecomp.sdc.be.model.Model; +import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.OperationException; +import org.openecomp.sdc.be.servlets.builder.ServletResponseBuilder; +import org.openecomp.sdc.be.servlets.exception.OperationExceptionMapper; import org.openecomp.sdc.be.ui.model.ModelCreateRequest; import org.openecomp.sdc.be.user.UserBusinessLogic; import org.openecomp.sdc.common.api.ConfigurationSource; @@ -100,13 +113,16 @@ class ModelServletTest extends JerseyTest { @Mock private UserValidations userValidations; + @Mock + private ResponseFormatManager responseFormatManager; + private Model model; - private Response response; private ModelCreateRequest modelCreateRequest; + private final Path rootPath = Path.of("/v1/catalog/model"); + private final Path importsPath = rootPath.resolve("imports"); @BeforeAll public void initClass() { - MockitoAnnotations.openMocks(this); when(request.getSession()).thenReturn(session); when(session.getServletContext()).thenReturn(servletContext); when(servletContext.getAttribute(Constants.WEB_APPLICATION_CONTEXT_WRAPPER_ATTR)) @@ -145,6 +161,7 @@ class ModelServletTest extends JerseyTest { @Override protected ResourceConfig configure() { + MockitoAnnotations.openMocks(this); forceSet(TestProperties.CONTAINER_PORT, "0"); final ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); return new ResourceConfig(ModelServlet.class) @@ -158,51 +175,132 @@ class ModelServletTest extends JerseyTest { bind(servletUtils).to(ServletUtils.class); bind(resourceImportManager).to(ResourceImportManager.class); bind(modelBusinessLogic).to(ModelBusinessLogic.class); + bind(userValidations).to(UserValidations.class); } }) + .register(new OperationExceptionMapper(new ServletResponseBuilder(), responseFormatManager)) + .register(MultiPartFeature.class) .property("contextConfig", context); } + @Override + protected void configureClient(final ClientConfig config) { + config.register(MultiPartFeature.class); + } + @Test - void createModelSuccessTest() { + void createModelSuccessTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.OK_200); when(componentsUtils.getResponseFormat(ActionStatus.CREATED)).thenReturn(responseFormat); when(modelBusinessLogic.createModel(any(Model.class))).thenReturn(model); - response = modelServlet.createModel(modelCreateRequest, USER_ID); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(new byte[0], parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.OK.getStatusCode(), response.getStatus()); } @Test - void createModelFailTest() { + void createModelFailTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); when(modelBusinessLogic.createModel(any(Model.class))).thenReturn(model); - response = modelServlet.createModel(modelCreateRequest, USER_ID); - assertThat(response.getStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR_500); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(new byte[0], parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); } @Test - void createModelFailWithModelNameEmptyTest() { + void createModelFailWithModelNameEmptyTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); modelCreateRequest.setName(StringUtils.EMPTY); - final Exception exception = assertThrows(ConstraintViolationException.class, () -> modelServlet.createModel(modelCreateRequest, USER_ID)); - assertThat(exception.getMessage()).isEqualTo("Model name cannot be empty"); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(new byte[0], parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); } @Test - void createModelFailWithModelNameNullTest() { + void createModelFailWithModelNameNullTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); modelCreateRequest.setName(null); - final Exception exception = assertThrows(ConstraintViolationException.class, () -> modelServlet.createModel(modelCreateRequest, USER_ID)); - assertThat(exception.getMessage()).isEqualTo("Model name cannot be null"); + final var modelFile = new byte[0]; + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(modelFile, parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); } @Test - void createModelThrowsBusinessExceptionTest() { + void createModelThrowsBusinessExceptionTest() throws JsonProcessingException { + final var modelFile = new byte[0]; + final String modelCreateAsJson = parseToJsonString(modelCreateRequest); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(modelFile, modelCreateAsJson); when(modelBusinessLogic.createModel(model)).thenThrow(new BusinessException() {}); - assertThrows(BusinessException.class, () -> modelServlet.createModel(modelCreateRequest, USER_ID)); + + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + @Test + void updateModelImportsSuccessTest() { + final FormDataMultiPart formDataMultiPart = buildUpdateFormDataMultiPart("model1", new byte[0]); + + final var response = target(importsPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .put(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + + @Test + void updateModelImports_businessException() { + final var modelId = "model1"; + final FormDataMultiPart formDataMultiPart = buildUpdateFormDataMultiPart(modelId, new byte[0]); + final OperationException operationException = new OperationException(ActionStatus.INVALID_MODEL, modelId); + doThrow(operationException).when(modelBusinessLogic).createModelImports(eq(modelId), any(InputStream.class)); + when(responseFormatManager.getResponseFormat(ActionStatus.INVALID_MODEL, modelId)) + .thenReturn(new ResponseFormat(Status.BAD_REQUEST.getStatusCode())); + final var response = target(importsPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .put(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + void updateModelImports_unknownException() { + final var modelName = "model1"; + final FormDataMultiPart formDataMultiPart = buildUpdateFormDataMultiPart(modelName, new byte[0]); + doThrow(new RuntimeException()).when(modelBusinessLogic).createModelImports(eq(modelName), any(InputStream.class)); + when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); + when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); + final var response = target(importsPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .put(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + private FormDataMultiPart buildUpdateFormDataMultiPart(final String modelName, final byte[] importFilesZip) { + return new FormDataMultiPart() + .field("modelName", modelName) + .field("modelImportsZip", importFilesZip, MediaType.MULTIPART_FORM_DATA_TYPE); + } + + private FormDataMultiPart buildCreateFormDataMultiPart(final byte[] modelFile, final String modelCreateAsJson) { + return new FormDataMultiPart() + .field("model", modelCreateAsJson, MediaType.APPLICATION_JSON_TYPE) + .field("modelImportsZip", modelFile, MediaType.MULTIPART_FORM_DATA_TYPE); + } + + private String parseToJsonString(final Object object) throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(object); } } \ No newline at end of file diff --git a/catalog-be/src/test/resources/modelImports/emptyModelImports.zip b/catalog-be/src/test/resources/modelImports/emptyModelImports.zip new file mode 100644 index 0000000000000000000000000000000000000000..323472535d1b5da083b2c1baa6410f06f7542a30 GIT binary patch literal 186 zcmWIWW@Zs#0D<+{yMw?CD8a*^z>uGpQ|aiIpOcbWq#qi>%fP;R!ukXlF0J5ZU}Sm0 m%)kI90=yZSM3`}#1vC4vBZx(s$*gQ3J&ZsY1*E+|90mY|XC|-! literal 0 HcmV?d00001 diff --git a/catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip b/catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip new file mode 100644 index 0000000000000000000000000000000000000000..281e3fc3c18975588338cdb2f1dd8d1bac2bb0d0 GIT binary patch literal 1961 zcmWIWW@Zs#0DAzkYUkg4fj52_U+(f}4Sn z8X3^>>1q?`dSxFSr`Jm**TPdFgz0lS_86*d`B3O>j);WBWAATtVeYOC=k&@ zR2)4dC=Fh5pe4q*4F!cY!SGJU9p1WZkGA}oGv$iX-7|NZ{wxxTbq0HOSDxLr(u@l;k6e+g+)-1tpb`6!q45DL6%eXsjs;WOO>3(E=z&BboLXY8r^fY8ra_ z1f_flplL<<`6Zail897!>b(9%tjY41w4|pP&~%Uu*pnq5yMd{ZJi8&O(ZNgL0>W-a zCJ|=bIUMLbF!<{TqDV=Q5OZN6ikuBWIUWYKGKk@?+IcI_LH4JQNT#nlkP>2!?RUEk-*_YTdFw6me z9qpNL`VuJv;?2&8gorIeBdnT*Vil6<{9 literal 0 HcmV?d00001 diff --git a/catalog-be/src/test/resources/paths/path-context.xml b/catalog-be/src/test/resources/paths/path-context.xml index 3b472dd16f..7994b0c44d 100644 --- a/catalog-be/src/test/resources/paths/path-context.xml +++ b/catalog-be/src/test/resources/paths/path-context.xml @@ -80,6 +80,7 @@ Modifications copyright (c) 2018 Nokia + diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/ActionStatus.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/ActionStatus.java index 1ebaeab54f..ed448c18d4 100644 --- a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/ActionStatus.java +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/ActionStatus.java @@ -52,7 +52,7 @@ public enum ActionStatus { // Category related COMPONENT_MISSING_CATEGORY, COMPONENT_INVALID_CATEGORY, COMPONENT_ELEMENT_INVALID_NAME_FORMAT, COMPONENT_ELEMENT_INVALID_NAME_LENGTH, COMPONENT_CATEGORY_ALREADY_EXISTS, COMPONENT_CATEGORY_NOT_FOUND, COMPONENT_SUB_CATEGORY_NOT_FOUND_FOR_CATEGORY, COMPONENT_SUB_CATEGORY_EXISTS_FOR_CATEGORY, COMPONENT_GROUPING_EXISTS_FOR_SUB_CATEGORY, // Model related - MODEL_ALREADY_EXISTS, + MODEL_ALREADY_EXISTS, INVALID_MODEL, MODEL_IMPORTS_IS_EMPTY, COULD_NOT_READ_MODEL_IMPORTS, // Service API URL INVALID_SERVICE_API_URL, // Property related diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitException.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitException.java new file mode 100644 index 0000000000..b1ee5f0ae1 --- /dev/null +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitException.java @@ -0,0 +1,27 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.dao.api.exception; + +public class CassandraDaoInitException extends RuntimeException { + + public CassandraDaoInitException(final String message) { + super(message); + } +} diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitExceptionProvider.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitExceptionProvider.java new file mode 100644 index 0000000000..4fec8fbdd9 --- /dev/null +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/api/exception/CassandraDaoInitExceptionProvider.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.dao.api.exception; + +import java.util.function.Supplier; +import org.openecomp.sdc.be.dao.cassandra.CassandraOperationStatus; + +public class CassandraDaoInitExceptionProvider { + + private CassandraDaoInitExceptionProvider() { + + } + + public static Supplier keySpaceConnectError(final String keyspace, final CassandraOperationStatus cassandraOperationStatus) { + var errorMsg = String.format("Could not connect to keyspace '%s'. Operation status was '%s'", keyspace, cassandraOperationStatus); + return () -> new CassandraDaoInitException(errorMsg); + } + +} diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/CassandraClient.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/CassandraClient.java index 624f9b44f2..25a6b49bba 100644 --- a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/CassandraClient.java +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/CassandraClient.java @@ -34,6 +34,7 @@ import java.util.List; import javax.annotation.PreDestroy; import org.apache.commons.lang3.tuple.ImmutablePair; import org.openecomp.sdc.be.config.ConfigurationManager; +import org.openecomp.sdc.common.log.enums.EcompLoggerErrorCode; import org.openecomp.sdc.common.log.wrappers.Logger; import org.springframework.stereotype.Component; @@ -155,7 +156,7 @@ public class CassandraClient { Mapper mapper = manager.mapper(clazz); mapper.save(entity); } catch (Exception e) { - logger.debug("Failed to save entity [{}], error :", entity, e); + logger.error(EcompLoggerErrorCode.DATA_ERROR, CassandraClient.class.getName(), "Failed to save entity [{}], error :", entity, e); return CassandraOperationStatus.GENERAL_ERROR; } return CassandraOperationStatus.OK; diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaImportByModelAccessor.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaImportByModelAccessor.java new file mode 100644 index 0000000000..eb54bd7fca --- /dev/null +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaImportByModelAccessor.java @@ -0,0 +1,33 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.dao.cassandra; + +import com.datastax.driver.mapping.Result; +import com.datastax.driver.mapping.annotations.Accessor; +import com.datastax.driver.mapping.annotations.Param; +import com.datastax.driver.mapping.annotations.Query; +import org.openecomp.sdc.be.data.model.ToscaImportByModel; + +@Accessor +public interface ToscaImportByModelAccessor { + + @Query("SELECT * FROM sdcartifact.tosca_import_by_model WHERE model_id = :modelId") + Result findAllByModel(@Param("modelId") String modelId); +} diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDao.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDao.java new file mode 100644 index 0000000000..a8b1ec635d --- /dev/null +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDao.java @@ -0,0 +1,106 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.dao.cassandra; + +import static java.util.function.Predicate.not; + +import com.datastax.driver.core.Session; +import com.datastax.driver.mapping.Mapper; +import com.datastax.driver.mapping.MappingManager; +import fj.data.Either; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openecomp.sdc.be.dao.api.exception.CassandraDaoInitException; +import org.openecomp.sdc.be.dao.api.exception.CassandraDaoInitExceptionProvider; +import org.openecomp.sdc.be.data.model.ToscaImportByModel; +import org.openecomp.sdc.be.resources.data.auditing.AuditingTypesConstants; +import org.openecomp.sdc.common.log.enums.EcompLoggerErrorCode; +import org.openecomp.sdc.common.log.wrappers.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component("tosca-model-import-cassandra-dao") +public class ToscaModelImportCassandraDao extends CassandraDao { + + private static final Logger LOGGER = Logger.getLogger(ToscaModelImportCassandraDao.class.getName()); + + private ToscaImportByModelAccessor toscaImportByModelAccessor; + private Mapper toscaImportByModelMapper; + + @Autowired + public ToscaModelImportCassandraDao(final CassandraClient cassandraClient) { + super(cassandraClient); + } + + /** + * For test purposes. + * + * @param toscaImportByModelAccessor the sdcartifact.tosca_import_by_model accessor + */ + ToscaModelImportCassandraDao(final ToscaImportByModelAccessor toscaImportByModelAccessor, + final Mapper toscaImportByModelMapper) { + super(null); + this.toscaImportByModelAccessor = toscaImportByModelAccessor; + this.toscaImportByModelMapper = toscaImportByModelMapper; + } + + @PostConstruct + public void init() { + final var keyspace = AuditingTypesConstants.ARTIFACT_KEYSPACE; + if (!client.isConnected()) { + LOGGER.error(EcompLoggerErrorCode.SCHEMA_ERROR, ToscaModelImportCassandraDao.class.getName(), "Cassandra client isn't connected"); + return; + } + final Either, CassandraOperationStatus> connectionResult = client.connect(keyspace); + if (connectionResult.isRight()) { + final CassandraDaoInitException exception = + CassandraDaoInitExceptionProvider.keySpaceConnectError(keyspace, connectionResult.right().value()).get(); + LOGGER.error(EcompLoggerErrorCode.SCHEMA_ERROR, ToscaModelImportCassandraDao.class.getName(), exception.getMessage()); + throw exception; + } + session = connectionResult.left().value().getLeft(); + manager = connectionResult.left().value().getRight(); + toscaImportByModelMapper = manager.mapper(ToscaImportByModel.class); + toscaImportByModelAccessor = manager.createAccessor(ToscaImportByModelAccessor.class); + LOGGER.info("{} successfully initialized", ToscaModelImportCassandraDao.class.getName()); + } + + public void importAll(final String modelId, final List toscaImportByModelList) { + final List importOfModelList = toscaImportByModelList.stream() + .filter(toscaImportByModel -> modelId.equals(toscaImportByModel.getModelId())) + .collect(Collectors.toList()); + final List actualImportOfModelList = toscaImportByModelAccessor.findAllByModel(modelId).all(); + final List removedImportList = actualImportOfModelList.stream() + .filter(not(importOfModelList::contains)) + .collect(Collectors.toList()); + + importOfModelList.forEach(toscaImportByModelMapper::save); + removedImportList.forEach(toscaImportByModel -> + toscaImportByModelMapper.delete(toscaImportByModel.getModelId(), toscaImportByModel.getFullPath()) + ); + } + + public List findAllByModel(final String modelId) { + return toscaImportByModelAccessor.findAllByModel(modelId).all(); + } + +} diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/SdcSchemaBuilder.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/SdcSchemaBuilder.java index 0c6bb453ae..c0c12aa0b2 100644 --- a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/SdcSchemaBuilder.java +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/SdcSchemaBuilder.java @@ -31,10 +31,12 @@ import com.datastax.driver.core.schemabuilder.Alter; import com.datastax.driver.core.schemabuilder.Create; import com.datastax.driver.core.schemabuilder.SchemaBuilder; import com.datastax.driver.core.schemabuilder.SchemaStatement; +import com.datastax.oss.driver.shaded.guava.common.base.Function; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -112,17 +114,17 @@ public class SdcSchemaBuilder { * the method creats all the tables and indexes thet do not already exist * * @param iTableDescriptions: a list of table description we want to create - * @param keyspaceMetadate: the current tables that exist in the cassandra under this keyspace + * @param keyspaceMetadata: the current tables that exist in the cassandra under this keyspace * @param session: the session object used for the execution of the query. * @param existingTablesMetadata the current tables columns that exist in the cassandra under this keyspace */ - private static void createTables(List iTableDescriptions, Map> keyspaceMetadate, Session session, + private static void createTables(List iTableDescriptions, Map> keyspaceMetadata, Session session, Map> existingTablesMetadata) { for (ITableDescription tableDescription : iTableDescriptions) { String tableName = tableDescription.getTableName().toLowerCase(); Map> columnDescription = tableDescription.getColumnDescription(); log.info("creating tables:{}.", tableName); - if (keyspaceMetadate == null || !keyspaceMetadate.keySet().contains(tableName)) { + if (keyspaceMetadata == null || !keyspaceMetadata.containsKey(tableName)) { Create create = SchemaBuilder.createTable(tableDescription.getKeyspace(), tableDescription.getTableName()); for (ImmutablePair key : tableDescription.primaryKeys()) { create.addPartitionKey(key.getLeft(), key.getRight()); @@ -132,9 +134,15 @@ public class SdcSchemaBuilder { create.addClusteringColumn(key.getLeft(), key.getRight()); } } - for (Map.Entry> entry : columnDescription.entrySet()) { - create.addColumn(entry.getKey(), entry.getValue().getLeft()); - } + final Function, Boolean> notPrimaryKeyFilter = (Entry entry) -> { + if (entry == null) { + return true; + } + return tableDescription.primaryKeys().stream().noneMatch(primaryKeyPair -> primaryKeyPair.getLeft().equals(entry.getKey())); + }; + columnDescription.entrySet().stream() + .filter(notPrimaryKeyFilter::apply) + .forEach(entry -> create.addColumn(entry.getKey(), entry.getValue().getLeft())); log.trace("exacuting :{}", create); session.execute(create); log.info("table:{} created successfully.", tableName); @@ -142,8 +150,8 @@ public class SdcSchemaBuilder { log.info("table:{} already exists, skipping.", tableName); alterTable(session, existingTablesMetadata, tableDescription, tableName, columnDescription); } - log.info("keyspacemetadata:{}", keyspaceMetadate); - List indexNames = (keyspaceMetadate != null && keyspaceMetadate.get(tableName) != null ? keyspaceMetadate.get(tableName) + log.info("keyspacemetadata:{}", keyspaceMetadata); + List indexNames = (keyspaceMetadata != null && keyspaceMetadata.get(tableName) != null ? keyspaceMetadata.get(tableName) : new ArrayList<>()); log.info("table:{} creating indexes.", tableName); for (Map.Entry> description : columnDescription.entrySet()) { diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/Table.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/Table.java index 4c3c8fab2d..710a9a2921 100644 --- a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/Table.java +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/Table.java @@ -37,6 +37,7 @@ import org.openecomp.sdc.be.dao.cassandra.schema.tables.MigrationTasksTableDescr import org.openecomp.sdc.be.dao.cassandra.schema.tables.OperationalEnvironmentsTableDescription; import org.openecomp.sdc.be.dao.cassandra.schema.tables.ResAdminEventTableDescription; import org.openecomp.sdc.be.dao.cassandra.schema.tables.SdcSchemaFilesTableDescription; +import org.openecomp.sdc.be.dao.cassandra.schema.tables.ToscaImportByModelTableDescription; import org.openecomp.sdc.be.dao.cassandra.schema.tables.UserAccessEventTableDescription; import org.openecomp.sdc.be.dao.cassandra.schema.tables.UserAdminEventTableDescription; @@ -61,7 +62,8 @@ public enum Table { SDC_REPO(new MigrationTasksTableDescription()), SDC_OPERATIONAL_ENVIRONMENT(new OperationalEnvironmentsTableDescription()), AUDIT_ECOMP_OPERATIONAL_ENVIRONMENT(new EcompOperationalEnvironmentEventTableDesc()), - FEATURE_TOGGLE_STATE(new FeatureToggleEventTableDesc()); + FEATURE_TOGGLE_STATE(new FeatureToggleEventTableDesc()), + TOSCA_IMPORT_BY_MODEL(new ToscaImportByModelTableDescription()); // @formatter:on ITableDescription tableDescription; diff --git a/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/tables/ToscaImportByModelTableDescription.java b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/tables/ToscaImportByModelTableDescription.java new file mode 100644 index 0000000000..5b67136285 --- /dev/null +++ b/catalog-dao/src/main/java/org/openecomp/sdc/be/dao/cassandra/schema/tables/ToscaImportByModelTableDescription.java @@ -0,0 +1,78 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.dao.cassandra.schema.tables; + +import com.datastax.driver.core.DataType; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openecomp.sdc.be.dao.cassandra.schema.ITableDescription; +import org.openecomp.sdc.be.resources.data.auditing.AuditingTypesConstants; + +public class ToscaImportByModelTableDescription implements ITableDescription { + + private static final String MODEL_ID = "model_id"; + private static final String FULL_PATH = "full_path"; + + @Override + public List> primaryKeys() { + return List.of( + new ImmutablePair<>(MODEL_ID, DataType.varchar()), + new ImmutablePair<>(FULL_PATH, DataType.varchar()) + ); + } + + @Override + public List> clusteringKeys() { + return Collections.emptyList(); + } + + @Override + public Map> getColumnDescription() { + return Stream.of(SdcSchemaFilesFieldsDescription.values()) + .collect(Collectors.toMap(SdcSchemaFilesFieldsDescription::getName, field -> new ImmutablePair<>(field.type, field.indexed))); + } + + @Override + public String getKeyspace() { + return AuditingTypesConstants.ARTIFACT_KEYSPACE; + } + + @Override + public String getTableName() { + return "tosca_import_by_model"; + } + + @Getter + @AllArgsConstructor + enum SdcSchemaFilesFieldsDescription { + MODEL_ID("model_id", DataType.varchar(), true), + CONTENT("content", DataType.varchar(), false); + + private final String name; + private final DataType type; + private final boolean indexed; + } +} diff --git a/catalog-dao/src/test/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDaoTest.java b/catalog-dao/src/test/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDaoTest.java new file mode 100644 index 0000000000..cddf3a2708 --- /dev/null +++ b/catalog-dao/src/test/java/org/openecomp/sdc/be/dao/cassandra/ToscaModelImportCassandraDaoTest.java @@ -0,0 +1,152 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.dao.cassandra; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.driver.core.Session; +import com.datastax.driver.mapping.Mapper; +import com.datastax.driver.mapping.MappingManager; +import com.datastax.driver.mapping.Result; +import fj.data.Either; +import java.util.List; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.openecomp.sdc.be.dao.api.exception.CassandraDaoInitException; +import org.openecomp.sdc.be.dao.api.exception.CassandraDaoInitExceptionProvider; +import org.openecomp.sdc.be.data.model.ToscaImportByModel; +import org.openecomp.sdc.be.resources.data.auditing.AuditingTypesConstants; + +class ToscaModelImportCassandraDaoTest { + + @Mock + private CassandraClient cassandraClient; + + @InjectMocks + private ToscaModelImportCassandraDao toscaModelImportCassandraDao; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + } + + @Test + void findAllByModelTest() { + final var toscaImportByModelAccessorMock = mock(ToscaImportByModelAccessor.class); + toscaModelImportCassandraDao = new ToscaModelImportCassandraDao(toscaImportByModelAccessorMock, null); + final var modelId = "modelId"; + final ToscaImportByModel toscaImportByModel1 = new ToscaImportByModel(); + final ToscaImportByModel toscaImportByModel2 = new ToscaImportByModel(); + final List expectedImportModelList = List.of(toscaImportByModel1, toscaImportByModel2); + + final Result result = mock(Result.class); + when(result.all()).thenReturn(expectedImportModelList); + when(toscaImportByModelAccessorMock.findAllByModel(modelId)).thenReturn(result); + + final List actualImportModelList = toscaModelImportCassandraDao.findAllByModel(modelId); + assertEquals(expectedImportModelList.size(), actualImportModelList.size()); + assertTrue(actualImportModelList.contains(toscaImportByModel1)); + assertTrue(actualImportModelList.contains(toscaImportByModel2)); + } + + @Test + void importAllTest() { + final var toscaImportByModelAccessorMock = mock(ToscaImportByModelAccessor.class); + final Mapper toscaImportByModelMapperMock = mock(Mapper.class); + toscaModelImportCassandraDao = new ToscaModelImportCassandraDao(toscaImportByModelAccessorMock, toscaImportByModelMapperMock); + + final var modelId = "modelId"; + final var toscaImportByModel1 = createToscaModelByImport(modelId, "path/model/1"); + final var toscaImportByModel2 = createToscaModelByImport(modelId, "path/model/2"); + final var toscaImportByModel3 = createToscaModelByImport(modelId, "path/model/3"); + final var toscaImportByModelWrongModel = createToscaModelByImport("otherModel", "path/wrong-model/1"); + final var importModelList = List.of(toscaImportByModel1, toscaImportByModel2, toscaImportByModel3); + + final var toscaImportByModelDatabase1 = createToscaModelByImport(modelId, "toscaImportByModelDatabase1"); + final Result findAllByModelResult = mock(Result.class); + when(findAllByModelResult.all()).thenReturn(List.of(toscaImportByModel1, toscaImportByModelDatabase1)); + when(toscaImportByModelAccessorMock.findAllByModel(modelId)).thenReturn(findAllByModelResult); + + toscaModelImportCassandraDao.importAll(modelId, importModelList); + + verify(toscaImportByModelMapperMock).save(toscaImportByModel1); + verify(toscaImportByModelMapperMock).save(toscaImportByModel2); + verify(toscaImportByModelMapperMock).save(toscaImportByModel3); + verify(toscaImportByModelMapperMock, never()).save(toscaImportByModelWrongModel); + verify(toscaImportByModelMapperMock).delete(toscaImportByModelDatabase1.getModelId(), toscaImportByModelDatabase1.getFullPath()); + verify(toscaImportByModelMapperMock, never()).delete(toscaImportByModel1.getModelId(), toscaImportByModel1.getFullPath()); + verify(toscaImportByModelMapperMock, never()).delete(toscaImportByModel2.getModelId(), toscaImportByModel2.getFullPath()); + verify(toscaImportByModelMapperMock, never()).delete(toscaImportByModel3.getModelId(), toscaImportByModel3.getFullPath()); + verify(toscaImportByModelMapperMock, never()).delete(toscaImportByModelWrongModel.getModelId(), toscaImportByModelWrongModel.getFullPath()); + } + + @Test + void initSuccessTest() { + toscaModelImportCassandraDao = new ToscaModelImportCassandraDao(cassandraClient); + when(cassandraClient.isConnected()).thenReturn(true); + final Session sessionMock = mock(Session.class); + final MappingManager mappingManagerMock = mock(MappingManager.class); + when(cassandraClient.connect(AuditingTypesConstants.ARTIFACT_KEYSPACE)).thenReturn(Either.left(new ImmutablePair<>(sessionMock, mappingManagerMock))); + toscaModelImportCassandraDao.init(); + verify(cassandraClient).connect(AuditingTypesConstants.ARTIFACT_KEYSPACE); + verify(mappingManagerMock).mapper(ToscaImportByModel.class); + verify(mappingManagerMock).createAccessor(ToscaImportByModelAccessor.class); + } + + @Test + void initTest_clientNotConnected() { + toscaModelImportCassandraDao = new ToscaModelImportCassandraDao(cassandraClient); + when(cassandraClient.isConnected()).thenReturn(false); + toscaModelImportCassandraDao.init(); + verify(cassandraClient, never()).connect(anyString()); + } + + @Test + void initTest_keyspaceConnectionFailure() { + toscaModelImportCassandraDao = new ToscaModelImportCassandraDao(cassandraClient); + when(cassandraClient.isConnected()).thenReturn(true); + when(cassandraClient.connect(AuditingTypesConstants.ARTIFACT_KEYSPACE)) + .thenReturn(Either.right(CassandraOperationStatus.KEYSPACE_NOT_CONNECTED)); + + final CassandraDaoInitException actualException = assertThrows(CassandraDaoInitException.class, () -> toscaModelImportCassandraDao.init()); + + final CassandraDaoInitException expectedException = CassandraDaoInitExceptionProvider + .keySpaceConnectError(AuditingTypesConstants.ARTIFACT_KEYSPACE, CassandraOperationStatus.KEYSPACE_NOT_CONNECTED).get(); + assertEquals(expectedException.getMessage(), actualException.getMessage()); + } + + private ToscaImportByModel createToscaModelByImport(final String modelId, final String path) { + final var toscaImportByModel3 = new ToscaImportByModel(); + toscaImportByModel3.setModelId(modelId); + toscaImportByModel3.setFullPath(path); + return toscaImportByModel3; + } +} \ No newline at end of file diff --git a/catalog-model/src/main/java/org/openecomp/sdc/be/model/jsonjanusgraph/operations/exception/ModelOperationExceptionSupplier.java b/catalog-model/src/main/java/org/openecomp/sdc/be/model/jsonjanusgraph/operations/exception/ModelOperationExceptionSupplier.java new file mode 100644 index 0000000000..c2ad071d13 --- /dev/null +++ b/catalog-model/src/main/java/org/openecomp/sdc/be/model/jsonjanusgraph/operations/exception/ModelOperationExceptionSupplier.java @@ -0,0 +1,50 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.model.jsonjanusgraph.operations.exception; + +import java.util.function.Supplier; +import org.openecomp.sdc.be.dao.api.ActionStatus; + +/** + * Supplies operation exception needed by the the Model logic + */ +public class ModelOperationExceptionSupplier { + + private ModelOperationExceptionSupplier() { + + } + + public static Supplier invalidModel(final String modelName) { + return () -> new OperationException(ActionStatus.INVALID_MODEL, modelName); + } + + public static Supplier emptyModelImports() { + return () -> new OperationException(ActionStatus.MODEL_IMPORTS_IS_EMPTY); + } + + public static Supplier couldNotReadImports() { + return () -> new OperationException(ActionStatus.COULD_NOT_READ_MODEL_IMPORTS); + } + + public static Supplier modelAlreadyExists(final String modelName) { + return () -> new OperationException(ActionStatus.MODEL_ALREADY_EXISTS, modelName); + } + +} diff --git a/catalog-model/src/main/java/org/openecomp/sdc/be/model/operations/impl/ModelOperation.java b/catalog-model/src/main/java/org/openecomp/sdc/be/model/operations/impl/ModelOperation.java index c604df6dde..ccc18e57dd 100644 --- a/catalog-model/src/main/java/org/openecomp/sdc/be/model/operations/impl/ModelOperation.java +++ b/catalog-model/src/main/java/org/openecomp/sdc/be/model/operations/impl/ModelOperation.java @@ -19,12 +19,27 @@ package org.openecomp.sdc.be.model.operations.impl; import fj.data.Either; +import java.nio.charset.StandardCharsets; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.openecomp.sdc.be.dao.api.ActionStatus; +import org.openecomp.sdc.be.dao.cassandra.ToscaModelImportCassandraDao; import org.openecomp.sdc.be.dao.janusgraph.JanusGraphGenericDao; import org.openecomp.sdc.be.dao.janusgraph.JanusGraphOperationStatus; +import org.openecomp.sdc.be.dao.jsongraph.GraphVertex; +import org.openecomp.sdc.be.dao.jsongraph.JanusGraphDao; +import org.openecomp.sdc.be.dao.jsongraph.types.VertexTypeEnum; +import org.openecomp.sdc.be.data.model.ToscaImportByModel; +import org.openecomp.sdc.be.datatypes.enums.GraphPropertyEnum; import org.openecomp.sdc.be.model.Model; import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.OperationException; +import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.ModelOperationExceptionSupplier; import org.openecomp.sdc.be.resources.data.ModelData; import org.openecomp.sdc.common.log.enums.EcompLoggerErrorCode; import org.openecomp.sdc.common.log.wrappers.Logger; @@ -32,28 +47,34 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component("model-operation") -public class ModelOperation extends AbstractOperation { +public class ModelOperation { private static final Logger log = Logger.getLogger(ModelOperation.class); - private final JanusGraphGenericDao genericDao; + private final JanusGraphGenericDao janusGraphGenericDao; + private final JanusGraphDao janusGraphDao; + private final ToscaModelImportCassandraDao toscaModelImportCassandraDao; @Autowired - public ModelOperation(final JanusGraphGenericDao janusGraphGenericDao) { - this.genericDao = janusGraphGenericDao; + public ModelOperation(final JanusGraphGenericDao janusGraphGenericDao, + final JanusGraphDao janusGraphDao, + final ToscaModelImportCassandraDao toscaModelImportCassandraDao) { + this.janusGraphGenericDao = janusGraphGenericDao; + this.janusGraphDao = janusGraphDao; + this.toscaModelImportCassandraDao = toscaModelImportCassandraDao; } public Model createModel(final Model model, final boolean inTransaction) { Model result = null; - final ModelData modelData = new ModelData(model.getName(), UniqueIdBuilder.buildModelUid(model.getName())); + final var modelData = new ModelData(model.getName(), UniqueIdBuilder.buildModelUid(model.getName())); try { - final Either createNode = genericDao.createNode(modelData, ModelData.class); + final Either createNode = janusGraphGenericDao.createNode(modelData, ModelData.class); if (createNode.isRight()) { - final JanusGraphOperationStatus janusGraphOperationStatus = createNode.right().value(); + final var janusGraphOperationStatus = createNode.right().value(); log.error(EcompLoggerErrorCode.DATA_ERROR, ModelOperation.class.getName(), "Problem while creating model, reason {}", janusGraphOperationStatus); if (janusGraphOperationStatus == JanusGraphOperationStatus.JANUSGRAPH_SCHEMA_VIOLATION) { - throw new OperationException(ActionStatus.MODEL_ALREADY_EXISTS, model.getName()); + throw ModelOperationExceptionSupplier.modelAlreadyExists(model.getName()).get(); } throw new OperationException(ActionStatus.GENERAL_ERROR, String.format("Failed to create model %s on JanusGraph with %s error", model, janusGraphOperationStatus)); @@ -63,14 +84,66 @@ public class ModelOperation extends AbstractOperation { } finally { if (!inTransaction) { if (Objects.nonNull(result)) { - genericDao.commit(); + janusGraphGenericDao.commit(); } else { - genericDao.rollback(); + janusGraphGenericDao.rollback(); } } } } + public Optional findModelVertexByName(final String name) { + if (StringUtils.isEmpty(name)) { + return Optional.empty(); + } + final Map props = new EnumMap<>(GraphPropertyEnum.class); + props.put(GraphPropertyEnum.NAME, name); + props.put(GraphPropertyEnum.UNIQUE_ID, UniqueIdBuilder.buildModelUid(name)); + final Either, JanusGraphOperationStatus> result = janusGraphDao.getByCriteria(VertexTypeEnum.MODEL, props); + if (result.isRight()) { + final JanusGraphOperationStatus janusGraphOperationStatus = result.right().value(); + if (janusGraphOperationStatus == JanusGraphOperationStatus.NOT_FOUND) { + return Optional.empty(); + } + log.error(EcompLoggerErrorCode.DATA_ERROR, this.getClass().getName(), + String.format("Problem while getting model %s. reason %s", name, janusGraphOperationStatus)); + throw new OperationException(ActionStatus.GENERAL_ERROR, + String.format("Failed to get model %s on JanusGraph with %s error", name, janusGraphOperationStatus)); + } + return Optional.ofNullable(result.left().value().get(0)); + } + + public Optional findModelByName(final String name) { + if (StringUtils.isEmpty(name)) { + return Optional.empty(); + } + final Optional modelVertexOpt = findModelVertexByName(name); + if (modelVertexOpt.isEmpty()) { + return Optional.empty(); + } + + final GraphVertex graphVertex = modelVertexOpt.get(); + final var model = new Model((String) graphVertex.getMetadataProperty(GraphPropertyEnum.NAME)); + return Optional.of(model); + } + + public void createModelImports(final String modelId, final Map zipContent) { + if (MapUtils.isEmpty(zipContent)) { + return; + } + final List toscaImportByModelList = zipContent.entrySet().stream() + .map(entry -> { + final String path = entry.getKey(); + final byte[] bytes = entry.getValue(); + final String content = new String(bytes, StandardCharsets.UTF_8); + final var toscaImportByModel = new ToscaImportByModel(); + toscaImportByModel.setModelId(modelId); + toscaImportByModel.setFullPath(path); + toscaImportByModel.setContent(content); + return toscaImportByModel; + }).collect(Collectors.toList()); + toscaModelImportCassandraDao.importAll(modelId, toscaImportByModelList); + } } diff --git a/catalog-model/src/test/java/org/openecomp/sdc/be/model/operations/impl/ModelOperationTest.java b/catalog-model/src/test/java/org/openecomp/sdc/be/model/operations/impl/ModelOperationTest.java index c1c0132b2c..620c7f76a6 100644 --- a/catalog-model/src/test/java/org/openecomp/sdc/be/model/operations/impl/ModelOperationTest.java +++ b/catalog-model/src/test/java/org/openecomp/sdc/be/model/operations/impl/ModelOperationTest.java @@ -19,20 +19,40 @@ package org.openecomp.sdc.be.model.operations.impl; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import fj.data.Either; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.openecomp.sdc.be.dao.api.ActionStatus; +import org.openecomp.sdc.be.dao.cassandra.ToscaModelImportCassandraDao; import org.openecomp.sdc.be.dao.janusgraph.JanusGraphGenericDao; import org.openecomp.sdc.be.dao.janusgraph.JanusGraphOperationStatus; +import org.openecomp.sdc.be.dao.jsongraph.GraphVertex; +import org.openecomp.sdc.be.dao.jsongraph.JanusGraphDao; +import org.openecomp.sdc.be.dao.jsongraph.types.VertexTypeEnum; +import org.openecomp.sdc.be.data.model.ToscaImportByModel; +import org.openecomp.sdc.be.datatypes.enums.GraphPropertyEnum; import org.openecomp.sdc.be.model.Model; import org.openecomp.sdc.be.model.ModelTestBase; import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.OperationException; @@ -40,19 +60,26 @@ import org.openecomp.sdc.be.resources.data.ModelData; import org.springframework.test.context.ContextConfiguration; @ContextConfiguration("classpath:application-context-test.xml") -@TestInstance(Lifecycle.PER_CLASS) class ModelOperationTest extends ModelTestBase { @InjectMocks private ModelOperation modelOperation; @Mock private JanusGraphGenericDao janusGraphGenericDao; + @Mock + private JanusGraphDao janusGraphDao; + @Mock + private ToscaModelImportCassandraDao toscaModelImportCassandraDao; private final String modelName = "ETSI-SDC-MODEL-TEST"; @BeforeAll - void setup() { + static void beforeAllInit() { init(); + } + + @BeforeEach + void beforeEachInit() { MockitoAnnotations.openMocks(this); } @@ -68,13 +95,134 @@ class ModelOperationTest extends ModelTestBase { @Test void createModelFailWithModelAlreadyExistTest() { when(janusGraphGenericDao.createNode(any(),any())).thenReturn(Either.right(JanusGraphOperationStatus.JANUSGRAPH_SCHEMA_VIOLATION)); - assertThrows(OperationException.class, () -> modelOperation.createModel(new Model(modelName), false)); + final var model = new Model(modelName); + assertThrows(OperationException.class, () -> modelOperation.createModel(model, false)); } @Test void createModelFailTest() { when(janusGraphGenericDao.createNode(any(),any())).thenReturn(Either.right(JanusGraphOperationStatus.GRAPH_IS_NOT_AVAILABLE)); - assertThrows(OperationException.class, () -> modelOperation.createModel(new Model(modelName), false)); + final var model = new Model(modelName); + assertThrows(OperationException.class, () -> modelOperation.createModel(model, false)); + } + + @Test + void createModelImportsSuccessTest() { + var modelId = "modelId"; + var contentEntry1 = "contentEntry1"; + var pathEntry1 = "entry1"; + var contentEntry2 = "contentEntry2"; + var pathEntry2 = "entry2/path"; + final Map zipContent = new TreeMap<>(); + zipContent.put(pathEntry1, contentEntry1.getBytes(StandardCharsets.UTF_8)); + zipContent.put(pathEntry2, contentEntry2.getBytes(StandardCharsets.UTF_8)); + + modelOperation.createModelImports(modelId, zipContent); + + final var toscaImport1 = new ToscaImportByModel(); + toscaImport1.setModelId(modelId); + toscaImport1.setContent(contentEntry1); + toscaImport1.setFullPath(pathEntry1); + final var toscaImport2 = new ToscaImportByModel(); + toscaImport2.setModelId(modelId); + toscaImport2.setContent(contentEntry2); + toscaImport2.setFullPath(pathEntry2); + final List toscaImportByModelList = List.of(toscaImport1, toscaImport2); + + verify(toscaModelImportCassandraDao).importAll(modelId, toscaImportByModelList); + } + + @Test + void createModelImportsTest_emptyZipContent() { + var modelId = "modelId"; + modelOperation.createModelImports(modelId, Collections.emptyMap()); + verify(toscaModelImportCassandraDao, never()).importAll(eq(modelId), anyList()); + modelOperation.createModelImports(modelId, null); + verify(toscaModelImportCassandraDao, never()).importAll(eq(null), anyList()); + } + + @Test + void findModelVertexSuccessTest() { + final ArgumentCaptor> mapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + final GraphVertex expectedVertex = new GraphVertex(); + when(janusGraphDao.getByCriteria(eq(VertexTypeEnum.MODEL), mapArgumentCaptor.capture())).thenReturn(Either.left(List.of(expectedVertex))); + var modelName = "modelName"; + final Optional modelVertexByNameOpt = modelOperation.findModelVertexByName(modelName); + assertTrue(modelVertexByNameOpt.isPresent()); + assertEquals(expectedVertex, modelVertexByNameOpt.get()); + final Map value = mapArgumentCaptor.getValue(); + assertEquals(modelName, value.get(GraphPropertyEnum.NAME)); + assertEquals(UniqueIdBuilder.buildModelUid(modelName), value.get(GraphPropertyEnum.UNIQUE_ID)); + } + + @Test + void findModelVertexTest_modelNotFound() { + final ArgumentCaptor> mapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + when(janusGraphDao.getByCriteria(eq(VertexTypeEnum.MODEL), mapArgumentCaptor.capture())) + .thenReturn(Either.right(JanusGraphOperationStatus.NOT_FOUND)); + var modelName = "modelName"; + + final Optional modelVertexByNameOpt = modelOperation.findModelVertexByName(modelName); + + assertTrue(modelVertexByNameOpt.isEmpty()); + final Map value = mapArgumentCaptor.getValue(); + assertEquals(modelName, value.get(GraphPropertyEnum.NAME)); + assertEquals(UniqueIdBuilder.buildModelUid(modelName), value.get(GraphPropertyEnum.UNIQUE_ID)); + } + + @Test + void findModelVertexTest_janusGraphError() { + final ArgumentCaptor> mapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + when(janusGraphDao.getByCriteria(eq(VertexTypeEnum.MODEL), mapArgumentCaptor.capture())) + .thenReturn(Either.right(JanusGraphOperationStatus.GENERAL_ERROR)); + var modelName = "modelName"; + + final var actualException = assertThrows(OperationException.class, () -> modelOperation.findModelVertexByName(modelName)); + + assertEquals(ActionStatus.GENERAL_ERROR, actualException.getActionStatus()); + final Map value = mapArgumentCaptor.getValue(); + assertEquals(modelName, value.get(GraphPropertyEnum.NAME)); + assertEquals(UniqueIdBuilder.buildModelUid(modelName), value.get(GraphPropertyEnum.UNIQUE_ID)); + } + + @Test + void findModelVertexTest_emptyOrNullModelName() { + assertTrue(modelOperation.findModelVertexByName("").isEmpty()); + assertTrue(modelOperation.findModelVertexByName(null).isEmpty()); + } + + @Test + void findModelByNameSuccessTest() { + final ArgumentCaptor> mapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + var modelName = "modelName"; + final GraphVertex expectedVertex = mock(GraphVertex.class); + when(expectedVertex.getMetadataProperty(GraphPropertyEnum.NAME)).thenReturn(modelName); + when(janusGraphDao.getByCriteria(eq(VertexTypeEnum.MODEL), mapArgumentCaptor.capture())).thenReturn(Either.left(List.of(expectedVertex))); + final Optional modelByNameOpt = modelOperation.findModelByName(modelName); + + final Map value = mapArgumentCaptor.getValue(); + assertEquals(modelName, value.get(GraphPropertyEnum.NAME)); + assertEquals(UniqueIdBuilder.buildModelUid(modelName), value.get(GraphPropertyEnum.UNIQUE_ID)); + + final Model expectedModel = new Model(modelName); + assertTrue(modelByNameOpt.isPresent()); + assertEquals(expectedModel, modelByNameOpt.get()); + } + + @Test + void findModelByNameTest_modelNameNotFound() { + final ArgumentCaptor> mapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + var modelName = "modelName"; + when(janusGraphDao.getByCriteria(eq(VertexTypeEnum.MODEL), mapArgumentCaptor.capture())) + .thenReturn(Either.right(JanusGraphOperationStatus.NOT_FOUND)); + final Optional modelByNameOpt = modelOperation.findModelByName(modelName); + assertTrue(modelByNameOpt.isEmpty()); + } + + @Test + void findModelByNameTest_emptyOrNullModelName() { + assertTrue(modelOperation.findModelByName("").isEmpty()); + assertTrue(modelOperation.findModelByName(null).isEmpty()); } } diff --git a/common-be/src/main/java/org/openecomp/sdc/be/data/model/ToscaImportByModel.java b/common-be/src/main/java/org/openecomp/sdc/be/data/model/ToscaImportByModel.java new file mode 100644 index 0000000000..5e8818b0b6 --- /dev/null +++ b/common-be/src/main/java/org/openecomp/sdc/be/data/model/ToscaImportByModel.java @@ -0,0 +1,40 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * 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.openecomp.sdc.be.data.model; + +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.PartitionKey; +import com.datastax.driver.mapping.annotations.Table; +import lombok.Data; + +@Data +@Table(keyspace = "sdcartifact", name = "tosca_import_by_model") +public class ToscaImportByModel { + + @PartitionKey + @Column(name = "model_id") + private String modelId; + @PartitionKey(1) + @Column(name = "full_path") + private String fullPath; + @Column(name = "content") + private String content; + +} diff --git a/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/CapabilityDefinition.java b/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/CapabilityDefinition.java index 144822d07d..ad38d6f1a1 100644 --- a/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/CapabilityDefinition.java +++ b/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/CapabilityDefinition.java @@ -21,8 +21,12 @@ package org.onap.sdc.tosca.datatypes.model; import java.util.List; import java.util.Map; +import lombok.Getter; +import lombok.Setter; import org.onap.sdc.tosca.services.DataModelCloneUtil; +@Getter +@Setter public class CapabilityDefinition implements Cloneable { private String type; @@ -32,63 +36,14 @@ public class CapabilityDefinition implements Cloneable { private List valid_source_types; private Object[] occurrences; - /** - * Constructor. - */ public CapabilityDefinition() { - occurrences = new Object[2]; - occurrences[0] = 1; - occurrences[1] = "UNBOUNDED"; + occurrences = new Object[] {1, "UNBOUNDED"}; } - public String getType() { - return type; - } - - public void setType(String type) { + public CapabilityDefinition(final String type) { this.type = type; } - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public Map getProperties() { - return properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - - public Map getAttributes() { - return attributes; - } - - public void setAttributes(Map attributes) { - this.attributes = attributes; - } - - public List getValid_source_types() { - return valid_source_types; - } - - public void setValid_source_types(List valid_source_types) { - this.valid_source_types = valid_source_types; - } - - public Object[] getOccurrences() { - return occurrences; - } - - public void setOccurrences(Object[] occurrences) { - this.occurrences = occurrences; - } - @Override public CapabilityDefinition clone() { CapabilityDefinition capabilityDefinition = new CapabilityDefinition(); diff --git a/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/EntrySchema.java b/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/EntrySchema.java index 932e3c7836..7e08dd85bc 100644 --- a/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/EntrySchema.java +++ b/common/onap-tosca-datatype/src/main/java/org/onap/sdc/tosca/datatypes/model/EntrySchema.java @@ -20,38 +20,24 @@ package org.onap.sdc.tosca.datatypes.model; import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import org.onap.sdc.tosca.services.DataModelCloneUtil; +@NoArgsConstructor +@Getter +@Setter public class EntrySchema implements Cloneable { private String description; private String type; private List constraints; - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getType() { - return type; - } - - public void setType(String type) { + public EntrySchema(final String type) { this.type = type; } - public List getConstraints() { - return constraints; - } - - public void setConstraints(List constraints) { - this.constraints = constraints; - } - @Override public EntrySchema clone() { EntrySchema entrySchema = new EntrySchema(); -- 2.16.6