diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/dto/UploadDocumentSuccessPayload.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/dto/UploadDocumentSuccessPayload.java index a1df356601b..3e1af5aeb96 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/dto/UploadDocumentSuccessPayload.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/dto/UploadDocumentSuccessPayload.java @@ -23,5 +23,5 @@ * * @author sbegaudeau */ -public record UploadDocumentSuccessPayload(@NotNull UUID id, String report) implements IPayload { +public record UploadDocumentSuccessPayload(@NotNull UUID id, @NotNull DocumentDTO document, String report) implements IPayload { } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JsonDocumentExporter.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JsonDocumentExporter.java new file mode 100644 index 00000000000..2ef23817b55 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JsonDocumentExporter.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.document.services; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Optional; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.emf.services.EObjectIDManager; +import org.eclipse.sirius.emfjson.resource.JsonResource; +import org.eclipse.sirius.web.application.document.services.api.IDocumentExporter; +import org.eclipse.sirius.web.application.editingcontext.services.JsonResourceSerializationListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; + +/** + * Used to export documents as Json resources. + * + * @author sbegaudeau + */ +@Service +public class JsonDocumentExporter implements IDocumentExporter { + + private final Logger logger = LoggerFactory.getLogger(JsonDocumentExporter.class); + + @Override + public boolean canHandle(Resource resource, String mediaType) { + return MediaType.APPLICATION_JSON.toString().equals(mediaType); + } + + @Override + public Optional getBytes(Resource resource, String mediaType) { + Optional optionalBytes = Optional.empty(); + + if (resource instanceof JsonResource jsonResource) { + var serializationListener = new JsonResourceSerializationListener(); + + HashMap options = new HashMap<>(); + options.put(JsonResource.OPTION_ID_MANAGER, new EObjectIDManager()); + options.put(JsonResource.OPTION_SCHEMA_LOCATION, true); + options.put(JsonResource.OPTION_SERIALIZATION_LISTENER, serializationListener); + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) { + jsonResource.save(outputStream, options); + optionalBytes = Optional.of(outputStream.toByteArray()); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + } + + return optionalBytes; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/UploadDocumentEventHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/UploadDocumentEventHandler.java index bc3037f815a..0778d958dae 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/UploadDocumentEventHandler.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/UploadDocumentEventHandler.java @@ -13,6 +13,7 @@ package org.eclipse.sirius.web.application.document.services; import java.io.IOException; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -32,14 +33,18 @@ import org.eclipse.sirius.components.core.api.IEditingContextSearchService; import org.eclipse.sirius.components.core.api.IInput; import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; import org.eclipse.sirius.components.emf.services.JSONResourceFactory; import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.document.dto.DocumentDTO; import org.eclipse.sirius.web.application.document.dto.UploadDocumentInput; import org.eclipse.sirius.web.application.document.dto.UploadDocumentSuccessPayload; import org.eclipse.sirius.web.application.document.services.api.IDocumentSanitizedJsonContentProvider; import org.eclipse.sirius.web.application.document.services.api.IProxyValidator; import org.eclipse.sirius.web.application.document.services.api.IUploadDocumentReportProvider; import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceLoader; +import org.eclipse.sirius.web.application.views.explorer.services.ExplorerDescriptionProvider; import org.eclipse.sirius.web.domain.services.api.IMessageService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,12 +119,26 @@ public void handle(Sinks.One payloadSink, Sinks.Many newResource = this.resourceLoader.toResource(emfEditingContext.getDomain().getResourceSet(), UUID.randomUUID().toString(), fileName, content); - String report = null; - if (newResource.isPresent()) { - report = this.getReport(newResource.get()); + + var optionalId = newResource.map(Resource::getURI) + .map(uri -> uri.path().substring(1)) + .flatMap(new UUIDParser()::parse); + + var optionalName = newResource.map(Resource::eAdapters).stream() + .flatMap(Collection::stream) + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .findFirst() + .map(ResourceMetadataAdapter::getName); + if (newResource.isPresent() && optionalId.isPresent() && optionalName.isPresent()) { + var uploadedResource = newResource.get(); + var id = optionalId.get(); + var name = optionalName.get(); + + String report = this.getReport(uploadedResource); + payload = new UploadDocumentSuccessPayload(input.id(), new DocumentDTO(id, name, ExplorerDescriptionProvider.DOCUMENT_KIND), report); + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input); } - payload = new UploadDocumentSuccessPayload(input.id(), report); - changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input); } } } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/controllers/MutationUploadProjectDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/controllers/MutationUploadProjectDataFetcher.java new file mode 100644 index 00000000000..3594f8764b3 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/controllers/MutationUploadProjectDataFetcher.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.controllers; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.sirius.components.annotations.spring.graphql.MutationDataFetcher; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.UploadFile; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.project.services.api.IProjectImportService; +import org.eclipse.sirius.web.domain.services.api.IMessageService; + +import graphql.schema.DataFetchingEnvironment; + +/** + * Data fetcher for the field Mutation#uploadProject. + * + * @author gcoutable + */ +@MutationDataFetcher(type = "Mutation", field = "uploadProject") +public class MutationUploadProjectDataFetcher implements IDataFetcherWithFieldCoordinates { + + private static final String INPUT_ARGUMENT = "input"; + + private static final String ID = "id"; + + private static final String FILE = "file"; + + private final IProjectImportService projectImportService; + + private final IMessageService messageService; + + public MutationUploadProjectDataFetcher(IProjectImportService projectImportService, IMessageService messageService) { + this.projectImportService = Objects.requireNonNull(projectImportService); + this.messageService = Objects.requireNonNull(messageService); + } + + @Override + public IPayload get(DataFetchingEnvironment environment) throws Exception { + Map input = environment.getArgument(INPUT_ARGUMENT); + + var optionalId = Optional.ofNullable(input.get(ID)) + .map(Object::toString) + .flatMap(new UUIDParser()::parse); + + var optionalFile = Optional.ofNullable(input.get(FILE)) + .filter(UploadFile.class::isInstance) + .map(UploadFile.class::cast); + + if (optionalId.isPresent() && optionalFile.isPresent()) { + var id = optionalId.get(); + var uploadFile = optionalFile.get(); + return this.projectImportService.importProject(id, uploadFile); + } + + return new ErrorPayload(optionalId.orElse(UUID.randomUUID()), this.messageService.unexpectedError()); + + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/controllers/ProjectDownloadController.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/controllers/ProjectDownloadController.java new file mode 100644 index 00000000000..52cc7562054 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/controllers/ProjectDownloadController.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.controllers; + +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.web.application.project.services.api.IProjectExportService; +import org.eclipse.sirius.web.domain.boundedcontexts.project.services.api.IProjectSearchService; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * The entry point of the HTTP API to download a project in zip. + *

+ * This endpoint will be available on the API base path prefix with download segment and followed by the project Id used + * as a suffix. As such, users will be able to send project download request to the following URL: + *

+ * + *
+ * PROTOCOL://DOMAIN.TLD(:PORT)/API_BASE_PATH/projects/PROJECT_ID
+ * 
+ * + * @author gcoutable + */ +@Controller +@RequestMapping("/api/projects") +public class ProjectDownloadController { + + private final IProjectSearchService projectSearchService; + + private final IProjectExportService projectExportService; + + public ProjectDownloadController(IProjectSearchService projectSearchService, IProjectExportService projectExportService) { + this.projectSearchService = Objects.requireNonNull(projectSearchService); + this.projectExportService = Objects.requireNonNull(projectExportService); + } + + @ResponseBody + @GetMapping(path = "/{projectId}") + public ResponseEntity downloadProject(@PathVariable UUID projectId) { + var optionalProject = this.projectSearchService.findById(projectId); + if (optionalProject.isPresent()) { + var project = optionalProject.get(); + byte[] content = this.projectExportService.export(project); + + ContentDisposition contentDisposition = ContentDisposition.builder("attachment") + .filename(project.getName() + ".zip") + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentDisposition(contentDisposition); + headers.setContentType(MediaType.parseMediaType("application/zip")); + headers.setContentLength(content.length); + var resource = new ByteArrayResource(content); + + return new ResponseEntity<>(resource, headers, HttpStatus.OK); + } + return new ResponseEntity<>(null, new HttpHeaders(), HttpStatus.NOT_FOUND); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/dto/UploadProjectInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/dto/UploadProjectInput.java new file mode 100644 index 00000000000..4c3524bf149 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/dto/UploadProjectInput.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.dto; + +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.graphql.api.UploadFile; + +import jakarta.validation.constraints.NotNull; + +/** + * Input used to upload a new project. + * + * @author sbegaudeau + */ +public record UploadProjectInput(@NotNull UUID id, UploadFile file) implements IInput { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectExportService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectExportService.java new file mode 100644 index 00000000000..1d5f2cf4728 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectExportService.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.eclipse.sirius.web.application.project.services.api.IProjectExportParticipant; +import org.eclipse.sirius.web.application.project.services.api.IProjectExportService; +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Used to download a project as a zip. + * + * @author sbegaudeau + */ +@Service +public class ProjectExportService implements IProjectExportService { + + private final Logger logger = LoggerFactory.getLogger(ProjectExportService.class); + + private final List projectExportParticipants; + + private final ObjectMapper objectMapper; + + public ProjectExportService(List projectExportParticipants, ObjectMapper objectMapper) { + this.projectExportParticipants = Objects.requireNonNull(projectExportParticipants); + this.objectMapper = Objects.requireNonNull(objectMapper); + } + + @Override + @Transactional(readOnly = true) + public byte[] export(Project project) { + byte[] zip = new byte[0]; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (var zipOutputStream = new ZipOutputStream(outputStream)) { + var manifestEntries = this.projectExportParticipants.stream() + .map(projectExportParticipant -> projectExportParticipant.exportData(project, zipOutputStream)) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + this.addManifest(project, manifestEntries, zipOutputStream); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + outputStream.reset(); + } + + if (outputStream.size() > 0) { + zip = outputStream.toByteArray(); + } + + return zip; + } + + private void addManifest(Project project, Map manifestEntries, ZipOutputStream outputStream) { + try { + byte[] manifestContent = this.objectMapper.writeValueAsBytes(manifestEntries); + + ZipEntry zipEntry = new ZipEntry(project.getName() + "/manifest.json"); + outputStream.putNextEntry(zipEntry); + outputStream.write(manifestContent); + outputStream.closeEntry(); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectImportService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectImportService.java new file mode 100644 index 00000000000..e3a5be9fd4b --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectImportService.java @@ -0,0 +1,275 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessor; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessorRegistry; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.UploadFile; +import org.eclipse.sirius.web.application.project.dto.CreateProjectInput; +import org.eclipse.sirius.web.application.project.dto.CreateProjectSuccessPayload; +import org.eclipse.sirius.web.application.project.dto.DeleteProjectInput; +import org.eclipse.sirius.web.application.project.services.api.IProjectApplicationService; +import org.eclipse.sirius.web.application.project.services.api.IProjectImportService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * Service used to import a project. + * + * @author gcoutable + */ +@Service +public class ProjectImportService implements IProjectImportService { + + private static final String ZIP_FOLDER_SEPARATOR = "/"; + + private static final String MANIFEST_JSON_FILE = "manifest.json"; + + private static final String REPRESENTATIONS_FOLDER = "representations"; + + private static final String DOCUMENTS_FOLDER = "documents"; + + private final Logger logger = LoggerFactory.getLogger(ProjectImportService.class); + + private final IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry; + + private final ObjectMapper objectMapper; + + private final IProjectApplicationService projectApplicationService; + + public ProjectImportService(IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry, ObjectMapper objectMapper, IProjectApplicationService projectApplicationService) { + this.editingContextEventProcessorRegistry = Objects.requireNonNull(editingContextEventProcessorRegistry); + this.objectMapper = Objects.requireNonNull(objectMapper); + this.projectApplicationService = Objects.requireNonNull(projectApplicationService); + } + + /** + * Returns {@link UploadProjectSuccessPayload} if the project import has been successful, {@link ErrorPayload} + * otherwise. + * + *

+ * Unzip the given {@link UploadFile}, then creates a project with the name of the root directory in the zip file, + * then use {@link ProjectImporter} to create documents and representations. If the project has not been imported, + * it disposes the {@link IEditingContextEventProcessor} used to create documents and representations then delete + * the created project in order to keep the server in the same state before the project upload attempt. + *

+ * + * @param inputId + * The identifier of the input which has triggered the upload + * @param file + * the file to upload + * @return {@link UploadProjectSuccessPayload} whether the project import has been successful, {@link ErrorPayload} + * otherwise + */ + @Override + public IPayload importProject(UUID inputId, UploadFile file) { + Map zipEntryNameToContent = this.readZipFile(file.getInputStream()); + + // Manifest import + String projectName = null; + Map projectManifest = new HashMap<>(); + Optional optionalProjectName = this.handleProjectName(zipEntryNameToContent); + if (optionalProjectName.isPresent()) { + projectName = optionalProjectName.get(); + String manifestPathInZip = projectName + ZIP_FOLDER_SEPARATOR + MANIFEST_JSON_FILE; + + byte[] manifestBytes = zipEntryNameToContent.entrySet().stream() + .filter(entry -> entry.getKey().equals(manifestPathInZip)) + .map(Entry::getValue) + .map(ByteArrayOutputStream::toByteArray) + .findFirst() + .orElse(new byte[0]); + + try { + projectManifest = this.objectMapper.readValue(manifestBytes, HashMap.class); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + } + + // Semantic data Import + String documentsFolderInZip = projectName + ZIP_FOLDER_SEPARATOR + DOCUMENTS_FOLDER + ZIP_FOLDER_SEPARATOR; + Map documentIdToDocumentContent = this.selectAndTransformIntoDocumentIdToDocumentContent(zipEntryNameToContent, documentsFolderInZip); + Map documents = new HashMap<>(); + for (Entry entry : documentIdToDocumentContent.entrySet()) { + String documentId = entry.getKey(); + ByteArrayOutputStream outputStream = entry.getValue(); + String documentName = null; + Object documentIdsToName = projectManifest.get("documentIdsToName"); + if (documentIdsToName instanceof Map) { + documentName = (String) ((Map) documentIdsToName).get(documentId); + } + UploadFile uploadFile = new UploadFile(documentName, new ByteArrayInputStream(outputStream.toByteArray())); + documents.put(documentId, uploadFile); + } + + // representation import + String representationsFolderInZip = projectName + ZIP_FOLDER_SEPARATOR + REPRESENTATIONS_FOLDER + ZIP_FOLDER_SEPARATOR; + List representationDescritorsContent = this.selectAndTransformIntoRepresentationDescriptorsContent(zipEntryNameToContent, representationsFolderInZip); + List representationImportDatas = null; + try { + representationImportDatas = this.getRepresentationImportDatas(representationDescritorsContent); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + + var createProjectInput = new CreateProjectInput(inputId, projectName, (List) projectManifest.get("natures")); + IPayload createProjectPayload = this.projectApplicationService.createProject(createProjectInput); + IPayload payload = new ErrorPayload(inputId, ""); + if (createProjectPayload instanceof CreateProjectSuccessPayload createProjectSuccessPayload) { + var project = createProjectSuccessPayload.project(); + Optional optionalEditingContextEventProcessor = this.editingContextEventProcessorRegistry + .getOrCreateEditingContextEventProcessor(project.id().toString()); + if (optionalEditingContextEventProcessor.isPresent()) { + IEditingContextEventProcessor editingContextEventProcessor = optionalEditingContextEventProcessor.get(); + + ProjectImporter projectImporter = new ProjectImporter(project.id().toString(), editingContextEventProcessor, documents, representationImportDatas, projectManifest); + boolean hasBeenImported = projectImporter.importProject(inputId); + + if (!hasBeenImported) { + this.editingContextEventProcessorRegistry.disposeEditingContextEventProcessor(project.id().toString()); + this.projectApplicationService.deleteProject(new DeleteProjectInput(inputId, project.id())); + } else { + payload = new UploadProjectSuccessPayload(inputId, project); + } + } + } + return payload; + } + + /** + * Returns the project name if all zip entries represented by couple (zipEntry name -> OutputStream) in the given + * {@link Map}, have their zip entry name starting by the project name, which should be the first segment of the + * path of each zip entries. + * + * @param zipEntryToOutputStreams + * The map of zip entry name to the zip entry content + * @return The name of the project + */ + private Optional handleProjectName(Map zipEntryToOutputStreams) { + Iterator iterator = zipEntryToOutputStreams.keySet().iterator(); + if (!iterator.hasNext()) { + // zip was empty + return Optional.empty(); + } + + Optional optionalProjectName = Optional.empty(); + String possibleProjectName = iterator.next().split(ZIP_FOLDER_SEPARATOR)[0]; + if (!possibleProjectName.isBlank() && zipEntryToOutputStreams.keySet().stream().allMatch(key -> key.split(ZIP_FOLDER_SEPARATOR)[0].equals(possibleProjectName))) { + optionalProjectName = Optional.of(possibleProjectName); + } + + return optionalProjectName; + } + + /** + * Reads the zip file and returns the a map of zip entry name to zip entry content. + * + * @return The zip entry names mapped to its zip entry contents + */ + private Map readZipFile(InputStream inputStream) { + Map entryToHandle = new HashMap<>(); + try (var zipperProjectInputStream = new ZipInputStream(inputStream)) { + ZipEntry zipEntry = zipperProjectInputStream.getNextEntry(); + while (zipEntry != null) { + if (!zipEntry.isDirectory()) { + String name = zipEntry.getName(); + ByteArrayOutputStream entryBaos = new ByteArrayOutputStream(); + zipperProjectInputStream.transferTo(entryBaos); + entryToHandle.put(name, entryBaos); + } + zipEntry = zipperProjectInputStream.getNextEntry(); + } + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + + return entryToHandle; + } + + /** + * Selects and transforms the given map of zip entry name to zip entry content into a map of document id to document + * content. + * + *

+ * Select entries in the given map where the key represented by the zip entry name are in the document folder (start + * with documentsFolderInZip). For each remaining entries extract the document id by removing documentsFolderInZip + * from the zip entry name. + *

+ * + * @param zipEntryNameToContent + * The map of all zip entries to their content + * @param documentsFolderInZip + * The path of documents folder in zip + * @return The map of document id to document content + */ + private Map selectAndTransformIntoDocumentIdToDocumentContent(Map zipEntryNameToContent, String documentsFolderInZip) { + Function, String> mapZipEntryNameToDocumentId = e -> { + String fullPath = e.getKey(); + String fileName = fullPath.substring(documentsFolderInZip.length()); + int extensionIndex = fileName.lastIndexOf('.'); + if (extensionIndex >= 0) { + return fileName.substring(0, extensionIndex); + } else { + return fileName; + } + }; + + return zipEntryNameToContent.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(documentsFolderInZip)) + .collect(Collectors.toMap(mapZipEntryNameToDocumentId::apply, Entry::getValue)); + } + private List selectAndTransformIntoRepresentationDescriptorsContent(Map zipEntryNameToContent, String representationsFolderInZip) { + return zipEntryNameToContent.entrySet().stream() + .filter(entry -> entry.getKey().startsWith(representationsFolderInZip)) + .map(Entry::getValue) + .toList(); + } + + private List getRepresentationImportDatas(List outputStreamToTransformToRepresentationDescriptor) throws IOException { + List representations = new ArrayList<>(); + for (ByteArrayOutputStream outputStream : outputStreamToTransformToRepresentationDescriptor) { + byte[] representationDescriptorBytes = outputStream.toByteArray(); + RepresentationSerializedImportData representationSerializedImportData = this.objectMapper.readValue(representationDescriptorBytes, RepresentationSerializedImportData.class); + var representationDescriptor = new RepresentationImportData(representationSerializedImportData.id(), representationSerializedImportData.projectId(), + representationSerializedImportData.descriptionId(), representationSerializedImportData.targetObjectId(), representationSerializedImportData.label(), + representationSerializedImportData.kind(), representationSerializedImportData.representation().toString()); + representations.add(representationDescriptor); + } + return representations; + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectImporter.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectImporter.java new file mode 100644 index 00000000000..6884d78e4b6 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectImporter.java @@ -0,0 +1,182 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessor; +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationInput; +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationSuccessPayload; +import org.eclipse.sirius.components.graphql.api.UploadFile; +import org.eclipse.sirius.web.application.document.dto.DocumentDTO; +import org.eclipse.sirius.web.application.document.dto.UploadDocumentInput; +import org.eclipse.sirius.web.application.document.dto.UploadDocumentSuccessPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class used to import a project. + * + * @author jmallet + */ +public class ProjectImporter { + + private final Logger logger = LoggerFactory.getLogger(ProjectImporter.class); + + private final String projectId; + + private final IEditingContextEventProcessor editingContextEventProcessor; + + private final Map documents; + + private final List representations; + + private final Map projectManifest; + + private final Map oldDocumentIdToNewDocumentId = new HashMap<>(); + + public ProjectImporter(String projectId, IEditingContextEventProcessor editingContextEventProcessor, Map documents, List representations, + Map projectManifest) { + this.projectId = Objects.requireNonNull(projectId); + this.editingContextEventProcessor = Objects.requireNonNull(editingContextEventProcessor); + this.documents = Objects.requireNonNull(documents); + this.representations = Objects.requireNonNull(representations); + this.projectManifest = Objects.requireNonNull(projectManifest); + } + + public boolean importProject(UUID inputId) { + boolean errorOccurred = !this.createDocuments(inputId); + + if (!errorOccurred) { + this.createRepresentations(inputId); + } + + return !errorOccurred; + } + + /** + * Creates all representations in the project thanks to the {@link IEditingContextEventProcessor} and the create + * representation input. If at least one representation has not been created it will return false. + * + * @param inputId + * The identifier of the input which has triggered this import + * @return true whether all representations has been created, false otherwise + */ + private boolean createRepresentations(UUID inputId) { + boolean allRepresentationCreated = true; + + for (RepresentationImportData representationImportData : this.representations) { + Map representationManifest = this.getRepresentationManifest(representationImportData); + + String targetObjectURI = (String) representationManifest.get("targetObjectURI"); + String oldDocumentId = URI.create(targetObjectURI).getPath().substring(1); + UUID newDocumentId = this.oldDocumentIdToNewDocumentId.get(oldDocumentId); + final String objectId; + if (newDocumentId != null) { + objectId = targetObjectURI.replace(oldDocumentId, newDocumentId.toString()); + } else { + objectId = targetObjectURI; + } + + String descriptionURI = (String) representationManifest.get("descriptionURI"); + + boolean representationCreated = false; + + CreateRepresentationInput createRepresentationInput = new CreateRepresentationInput(inputId, this.projectId.toString(), descriptionURI, objectId, representationImportData.label()); + + representationCreated = this.editingContextEventProcessor.handle(createRepresentationInput) + .filter(CreateRepresentationSuccessPayload.class::isInstance) + .map(CreateRepresentationSuccessPayload.class::cast) + .map(CreateRepresentationSuccessPayload::representation) + .blockOptional() + .isPresent(); + + if (!representationCreated) { + this.logger.warn("The representation {} has not been created", representationImportData.label()); + } + + allRepresentationCreated = allRepresentationCreated && representationCreated; + } + + return allRepresentationCreated; + } + + /** + * Get the representation (type, targetObjectUri, descriptionUri) described into the Manifest from a given + * representation identifier. + * + * @param representationImportData + * the representation to look for in Manifest + * @return the representation details from Manifest + */ + private Map getRepresentationManifest(RepresentationImportData representationImportData) { + Object representationsFromManifest = this.projectManifest.get("representations"); + UUID representationId = representationImportData.id(); + if (representationsFromManifest instanceof Map && representationId != null) { + Object representationFromManifest = ((Map) representationsFromManifest).get(representationImportData.id().toString()); + if (representationFromManifest instanceof Map) { + return (Map) representationFromManifest; + } + } + return new HashMap<>(); + } + + /** + * Creates all documents in the project thanks to the {@link IEditingContextEventProcessor}. If at least one + * document has not been created it will return false. + * + * @param inputId + * @return true whether all documents has been created, false otherwise + */ + private boolean createDocuments(UUID inputId) { + for (Entry entry : this.documents.entrySet()) { + String oldDocumentId = entry.getKey(); + UploadFile uploadFile = entry.getValue(); + UploadDocumentInput input = new UploadDocumentInput(inputId, this.editingContextEventProcessor.getEditingContextId(), uploadFile); + + UUID newDocumentId = this.editingContextEventProcessor.handle(input) + .filter(UploadDocumentSuccessPayload.class::isInstance) + .map(UploadDocumentSuccessPayload.class::cast) + .map(UploadDocumentSuccessPayload::document) + .map(DocumentDTO::id) + .blockOptional() + .orElse(null); + + if (newDocumentId == null) { + String documentIdNotCreated = null; + Object documentIdsToName = this.projectManifest.get("documentIdsToName"); + if (documentIdsToName instanceof Map) { + documentIdNotCreated = (String) ((Map) documentIdsToName).get(oldDocumentId); + } + this.logger.warn("The document {} has not been created", documentIdNotCreated); + } + this.oldDocumentIdToNewDocumentId.put(oldDocumentId, newDocumentId); + } + + Map documentIds = new HashMap<>(); + for (Map.Entry entry : this.oldDocumentIdToNewDocumentId.entrySet()) { + documentIds.put(entry.getKey(), entry.getValue().toString()); + } + RewriteProxiesInput rewriteInput = new RewriteProxiesInput(UUID.randomUUID(), this.editingContextEventProcessor.getEditingContextId(), documentIds); + this.editingContextEventProcessor.handle(rewriteInput).blockOptional(); + + return this.oldDocumentIdToNewDocumentId.values().stream().allMatch(Objects::nonNull); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectRepresentationDataExportParticipant.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectRepresentationDataExportParticipant.java new file mode 100644 index 00000000000..95fc0fb3a15 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectRepresentationDataExportParticipant.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.emfjson.resource.JsonResourceFactoryImpl; +import org.eclipse.sirius.web.application.project.services.api.IProjectExportParticipant; +import org.eclipse.sirius.web.application.representation.services.RepresentationSearchService; +import org.eclipse.sirius.web.application.representation.services.api.IRepresentationDataMigrationService; +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationDataSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.stereotype.Service; + +/** + * Used to add representation data to the export of a project. + * + * @author sbegaudeau + */ +@Service +public class ProjectRepresentationDataExportParticipant implements IProjectExportParticipant { + + private final IEditingContextSearchService editingContextSearchService; + + private final IRepresentationDataSearchService representationDataSearchService; + + private final ObjectMapper objectMapper; + + private final IRepresentationDataMigrationService representationDataMigrationService; + + private final RepresentationSearchService representationSearchService; + + private final Logger logger = LoggerFactory.getLogger(ProjectRepresentationDataExportParticipant.class); + + public ProjectRepresentationDataExportParticipant(IEditingContextSearchService editingContextSearchService, IRepresentationDataSearchService representationDataSearchService, ObjectMapper objectMapper, IRepresentationDataMigrationService representationDataMigrationService, RepresentationSearchService representationSearchService) { + this.editingContextSearchService = Objects.requireNonNull(editingContextSearchService); + this.representationDataSearchService = Objects.requireNonNull(representationDataSearchService); + this.objectMapper = Objects.requireNonNull(objectMapper); + this.representationDataMigrationService = Objects.requireNonNull(representationDataMigrationService); + this.representationSearchService = Objects.requireNonNull(representationSearchService); + } + + @Override + public Map exportData(Project project, ZipOutputStream outputStream) { + Map> representationManifests = new HashMap<>(); + + var allRepresentationMetadata = this.representationDataSearchService.findAllMetadataByProject(AggregateReference.to(project.getId())); + for (var representationMetadata: allRepresentationMetadata) { + var optionalRepresentationContentNode = this.representationDataSearchService.findContentById(representationMetadata.id()) + .flatMap(this.representationDataMigrationService::getMigratedContent); + if (optionalRepresentationContentNode.isPresent()) { + var representationContentNode = optionalRepresentationContentNode.get(); + + var exportData = new RepresentationSerializedExportData( + representationMetadata.id(), + representationMetadata.project().getId(), + representationMetadata.descriptionId(), + representationMetadata.targetObjectId(), + representationMetadata.label(), + representationMetadata.kind(), + representationContentNode + ); + + // Get TargetObjectURI + String uriFragment = ""; + var optionalEditingContext = this.editingContextSearchService.findById(project.getId().toString()) + .filter(IEMFEditingContext.class::isInstance) + .map(IEMFEditingContext.class::cast); + if (optionalEditingContext.isPresent()) { + var editingContext = optionalEditingContext.get(); + String targetObjectId = representationMetadata.targetObjectId(); + for (Resource resource : editingContext.getDomain().getResourceSet().getResources()) { + EObject eObject = resource.getEObject(targetObjectId); + if (eObject != null) { + uriFragment = EcoreUtil.getURI(eObject).toString(); + break; + } + } + } + if (uriFragment.isEmpty()) { + this.logger.warn("The serialization of the representationManifest won't be complete."); + } + + Map representationManifest = Map.of( + "type", representationMetadata.kind(), + "descriptionURI", representationMetadata.descriptionId(), + "targetObjectURI", uriFragment + ); + representationManifests.put(representationMetadata.id().toString(), representationManifest); + + try { + byte[] bytes = this.objectMapper.writeValueAsBytes(exportData); + + String name = project.getName() + "/representations/" + representationMetadata.id() + "." + JsonResourceFactoryImpl.EXTENSION; + + ZipEntry zipEntry = new ZipEntry(name); + zipEntry.setSize(bytes.length); + zipEntry.setTime(System.currentTimeMillis()); + + outputStream.putNextEntry(zipEntry); + outputStream.write(bytes); + outputStream.closeEntry(); + } catch (IOException exception) { + this.logger.warn(exception.getMessage()); + } + } + } + + return Map.of("representations", representationManifests); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectSemanticDataExportParticipant.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectSemanticDataExportParticipant.java new file mode 100644 index 00000000000..d59ccb9238d --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/ProjectSemanticDataExportParticipant.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.io.IOException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.emfjson.resource.JsonResourceFactoryImpl; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.document.services.api.IDocumentExporter; +import org.eclipse.sirius.web.application.editingcontext.services.api.IEditingContextPersistenceFilter; +import org.eclipse.sirius.web.application.project.services.api.IProjectExportParticipant; +import org.eclipse.sirius.web.domain.boundedcontexts.project.Nature; +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; + +/** + * Used to add semantic data to the export of a project. + * + * @author sbegaudeau + */ +@Service +public class ProjectSemanticDataExportParticipant implements IProjectExportParticipant { + + private final IEditingContextSearchService editingContextSearchService; + + private final List documentExporters; + + private final List persistenceFilters; + + private final Logger logger = LoggerFactory.getLogger(ProjectSemanticDataExportParticipant.class); + + public ProjectSemanticDataExportParticipant(IEditingContextSearchService editingContextSearchService, List documentExporters, List persistenceFilters) { + this.editingContextSearchService = Objects.requireNonNull(editingContextSearchService); + this.documentExporters = Objects.requireNonNull(documentExporters); + this.persistenceFilters = Objects.requireNonNull(persistenceFilters); + } + + @Override + public Map exportData(Project project, ZipOutputStream outputStream) { + Map manifestEntries = new HashMap<>(); + + var optionalEditingContext = this.editingContextSearchService.findById(project.getId().toString()) + .filter(IEMFEditingContext.class::isInstance) + .map(IEMFEditingContext.class::cast); + if (optionalEditingContext.isPresent()) { + var editingContext = optionalEditingContext.get(); + + List metamodels = this.getMetamodels(editingContext); + Map id2DocumentName = this.exportSemanticData(editingContext, project.getName(), outputStream); + List natures = project.getNatures().stream() + .map(Nature::name) + .toList(); + + manifestEntries.put("metamodels", metamodels); + manifestEntries.put("documentIdsToName", id2DocumentName); + manifestEntries.put("natures", natures); + } + + return manifestEntries; + } + + private List getMetamodels(IEMFEditingContext editingContext) { + return editingContext.getDomain().getResourceSet().getPackageRegistry().values().stream() + .filter(EPackage.class::isInstance) + .map(EPackage.class::cast) + .map(EPackage::getNsURI) + .sorted(Comparator.naturalOrder()) + .toList(); + } + + private Map exportSemanticData(IEMFEditingContext editingContext, String projectName, ZipOutputStream outputStream) { + Map id2DocumentName = new HashMap<>(); + + List resources = editingContext.getDomain().getResourceSet().getResources().stream() + .filter(resource -> this.persistenceFilters.stream().allMatch(filter -> filter.shouldPersist(resource))).toList(); + for (var resource: resources) { + var resourceId = resource.getURI().path().substring(1); + var optionalDocumentId = new UUIDParser().parse(resourceId); + + var optionalDocumentName = resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .map(ResourceMetadataAdapter::getName) + .findFirst(); + + var optionalContent = this.documentExporters.stream() + .filter(documentExporter -> documentExporter.canHandle(resource, MediaType.APPLICATION_JSON_VALUE)) + .findFirst() + .flatMap(documentExporter -> documentExporter.getBytes(resource, MediaType.APPLICATION_JSON_VALUE)); + if (optionalDocumentId.isPresent() && optionalDocumentName.isPresent() && optionalContent.isPresent()) { + var documentId = optionalDocumentId.get(); + var documentName = optionalDocumentName.get(); + var content = optionalContent.get(); + + id2DocumentName.put(documentId.toString(), documentName); + String name = projectName + "/documents/" + documentId + "." + JsonResourceFactoryImpl.EXTENSION; + + ZipEntry zipEntry = new ZipEntry(name); + zipEntry.setSize(content.length); + zipEntry.setTime(System.currentTimeMillis()); + + try { + outputStream.putNextEntry(zipEntry); + outputStream.write(content); + outputStream.closeEntry(); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + } + } + + return id2DocumentName; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationExportData.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationExportData.java new file mode 100644 index 00000000000..3c14f97d0a6 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationExportData.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.util.UUID; + +/** + * Used to persist representation data in the exported project. + * + * @author sbegaudeau + */ +public record RepresentationExportData( + UUID id, + UUID projectId, + String descriptionId, + String targetObjectId, + String label, + String kind, + String content +) { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationImportData.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationImportData.java new file mode 100644 index 00000000000..8df8980f442 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationImportData.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.util.UUID; + +/** + * Used to get representation data from the exported project. + * + * @author jmallet + */ +public record RepresentationImportData( + UUID id, + UUID projectId, + String descriptionId, + String targetObjectId, + String label, + String kind, + String representation +) { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationSerializedExportData.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationSerializedExportData.java new file mode 100644 index 00000000000..c74d5348f99 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationSerializedExportData.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.UUID; + +/** + * Used to persist representation data in the exported project. + * + * @author jmallet + */ +public record RepresentationSerializedExportData( + UUID id, + UUID projectId, + String descriptionId, + String targetObjectId, + String label, + String kind, + ObjectNode representation +) { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationSerializedImportData.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationSerializedImportData.java new file mode 100644 index 00000000000..980f720b7cc --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RepresentationSerializedImportData.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.UUID; + +/** + * Used to get representation data from the exported project. + * + * @author jmallet + */ +public record RepresentationSerializedImportData( + UUID id, + UUID projectId, + String descriptionId, + String targetObjectId, + String label, + String kind, + ObjectNode representation +) { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RewriteProxiesEventHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RewriteProxiesEventHandler.java new file mode 100644 index 00000000000..ddd0c4ed43a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RewriteProxiesEventHandler.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.InternalEObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain; +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.web.domain.services.api.IMessageService; +import org.springframework.stereotype.Service; + +import reactor.core.publisher.Sinks.Many; +import reactor.core.publisher.Sinks.One; + +/** + * The event handler to rewrite broken proxy URIs in documents (typically after an upload where the newly created + * documents have different ids). + * + * @author pcdavid + */ +@Service +public class RewriteProxiesEventHandler implements IEditingContextEventHandler { + private final IMessageService messageService; + + public RewriteProxiesEventHandler(IMessageService messageService) { + this.messageService = Objects.requireNonNull(messageService); + } + + @Override + public boolean canHandle(IEditingContext editingContext, IInput input) { + return input instanceof RewriteProxiesInput; + } + + @Override + public void handle(One payloadSink, Many changeDescriptionSink, IEditingContext editingContext, IInput input) { + IPayload payload = new ErrorPayload(input.id(), this.messageService.unexpectedError()); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, editingContext.getId(), input); + + if (input instanceof RewriteProxiesInput && editingContext instanceof IEMFEditingContext) { + RewriteProxiesInput rewriteInput = (RewriteProxiesInput) input; + AdapterFactoryEditingDomain adapterFactoryEditingDomain = ((IEMFEditingContext) editingContext).getDomain(); + int totalRewrittenCount = 0; + for (Resource resource : adapterFactoryEditingDomain.getResourceSet().getResources()) { + totalRewrittenCount += this.rewriteProxyURIs(resource, rewriteInput.oldDocumentIdToNewDocumentId()); + } + if (totalRewrittenCount > 0) { + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input); + } + payload = new SuccessPayload(input.id()); + } + + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } + + private int rewriteProxyURIs(Resource resource, Map oldDocumentIdToNewDocumentId) { + AtomicInteger rewrittenCount = new AtomicInteger(); + resource.getAllContents().forEachRemaining(eObject -> { + eObject.eCrossReferences().forEach(target -> { + InternalEObject internalEObject = (InternalEObject) target; + if (internalEObject.eIsProxy()) { + URI proxyURI = internalEObject.eProxyURI(); + String oldDocumentId = proxyURI.path().substring(1); + String newDocumentId = oldDocumentIdToNewDocumentId.get(oldDocumentId); + if (newDocumentId != null) { + String prefix = IEMFEditingContext.RESOURCE_SCHEME + ":///"; + URI newProxyURI = URI.createURI(proxyURI.toString().replace(prefix + oldDocumentId, prefix + newDocumentId)); + internalEObject.eSetProxyURI(newProxyURI); + rewrittenCount.incrementAndGet(); + } + } + }); + }); + return rewrittenCount.get(); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RewriteProxiesInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RewriteProxiesInput.java new file mode 100644 index 00000000000..b44d5c7010f --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/RewriteProxiesInput.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.util.Map; +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; + +/** + * The input object for the operation to rewrite broken proxy URIs in documents. + * + * @author pcdavid + */ +public record RewriteProxiesInput(UUID id, String editingContextId, Map oldDocumentIdToNewDocumentId) implements IInput { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/UploadProjectSuccessPayload.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/UploadProjectSuccessPayload.java new file mode 100644 index 00000000000..de0388f65f5 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/UploadProjectSuccessPayload.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services; + +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.web.application.project.dto.ProjectDTO; + +/** + * The payload of the project upload mutation. + * + * @author gcoutable + */ +public record UploadProjectSuccessPayload(UUID id, ProjectDTO project) implements IPayload { + public UploadProjectSuccessPayload { + Objects.requireNonNull(id); + Objects.requireNonNull(project); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectExportParticipant.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectExportParticipant.java new file mode 100644 index 00000000000..19e3765bc3d --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectExportParticipant.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services.api; + +import java.util.Map; +import java.util.zip.ZipOutputStream; + +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; + +/** + * Used to participate in the export of a project. + * + * @author sbegaudeau + */ +public interface IProjectExportParticipant { + Map exportData(Project project, ZipOutputStream outputStream); +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectExportService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectExportService.java new file mode 100644 index 00000000000..13bfa98b3d2 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectExportService.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services.api; + +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; + +/** + * Used to download a project as a zip. + * + * @author sbegaudeau + */ +public interface IProjectExportService { + byte[] export(Project project); +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectImportService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectImportService.java new file mode 100644 index 00000000000..d640ce413ef --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/api/IProjectImportService.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.project.services.api; + +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.UploadFile; + +/** + * Service used to import a project. + * + * @author jmallet + */ +public interface IProjectImportService { + + IPayload importProject(UUID inputId, UploadFile file); + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationDataMigrationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationDataMigrationService.java new file mode 100644 index 00000000000..abacca8791a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationDataMigrationService.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.representation.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.components.collaborative.representations.migration.IRepresentationMigrationParticipant; +import org.eclipse.sirius.components.collaborative.representations.migration.RepresentationMigrationService; +import org.eclipse.sirius.web.application.representation.services.api.IRepresentationDataMigrationService; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.projections.RepresentationDataContentOnly; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** + * Used to retrieve the migrated content of the representation data. + * + * @author sbegaudeau + */ +@Service +public class RepresentationDataMigrationService implements IRepresentationDataMigrationService { + + private final ObjectMapper objectMapper; + + private final List migrationParticipants; + + private final Logger logger = LoggerFactory.getLogger(RepresentationDataMigrationService.class); + + public RepresentationDataMigrationService(ObjectMapper objectMapper, List migrationParticipants) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.migrationParticipants = Objects.requireNonNull(migrationParticipants); + } + + @Override + public Optional getMigratedContent(RepresentationDataContentOnly representationData) { + Optional optionalObjectNode = Optional.empty(); + try { + JsonNode rootJsonNode = this.objectMapper.readTree(representationData.content()); + if (rootJsonNode instanceof ObjectNode objectNode) { + + List applicableParticipants = this.getApplicableMigrationParticipants(representationData); + if (!applicableParticipants.isEmpty()) { + var migrationService = new RepresentationMigrationService(applicableParticipants, objectNode); + migrationService.parseProperties(objectNode, this.objectMapper); + } + + optionalObjectNode = Optional.of(objectNode); + } + } catch (JsonProcessingException | IllegalArgumentException exception) { + this.logger.warn(exception.getMessage()); + } + + return optionalObjectNode; + } + + private List getApplicableMigrationParticipants(RepresentationDataContentOnly representationData) { + var migrationVersion = representationData.migrationVersion(); + var kind = representationData.kind(); + + return this.migrationParticipants.stream() + .filter(migrationParticipant -> Objects.equals(migrationParticipant.getKind(), kind)) + .filter(migrationParticipant -> migrationParticipant.getVersion().compareTo(migrationVersion) > 0) + .sorted(Comparator.comparing(IRepresentationMigrationParticipant::getVersion)) + .toList(); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java index b6358f3f61f..0a343d2b09a 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java @@ -13,24 +13,18 @@ package org.eclipse.sirius.web.application.representation.services; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import java.util.Comparator; -import java.util.List; import java.util.Objects; import java.util.Optional; import org.eclipse.sirius.components.collaborative.api.IRepresentationSearchService; -import org.eclipse.sirius.components.collaborative.representations.migration.IRepresentationMigrationParticipant; -import org.eclipse.sirius.components.collaborative.representations.migration.RepresentationMigrationService; import org.eclipse.sirius.components.core.api.IEditingContext; import org.eclipse.sirius.components.representations.IRepresentation; import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.representation.services.api.IRepresentationDataMigrationService; import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.projections.RepresentationDataContentOnly; import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationDataSearchService; -import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationDataUpdateService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -45,16 +39,16 @@ public class RepresentationSearchService implements IRepresentationSearchService private final IRepresentationDataSearchService representationDataSearchService; + private final IRepresentationDataMigrationService representationDataMigrationService; + private final ObjectMapper objectMapper; private final Logger logger = LoggerFactory.getLogger(RepresentationSearchService.class); - private final List migrationParticipants; - - public RepresentationSearchService(IRepresentationDataSearchService representationDataSearchService, ObjectMapper objectMapper, List migrationParticipants, IRepresentationDataUpdateService representationDataUpdateService) { - this.objectMapper = Objects.requireNonNull(objectMapper); - this.migrationParticipants = Objects.requireNonNull(migrationParticipants); + public RepresentationSearchService(IRepresentationDataSearchService representationDataSearchService, IRepresentationDataMigrationService representationDataMigrationService, ObjectMapper objectMapper) { this.representationDataSearchService = Objects.requireNonNull(representationDataSearchService); + this.representationDataMigrationService = Objects.requireNonNull(representationDataMigrationService); + this.objectMapper = Objects.requireNonNull(objectMapper); } @Override @@ -81,31 +75,9 @@ private Optional toRepresentation(String content) { } private String migratedContent(RepresentationDataContentOnly representationData) { - List applicableParticipants = this.getApplicableMigrationParticipants(representationData); - if (!applicableParticipants.isEmpty()) { - try { - JsonNode rootJsonNode = this.objectMapper.readTree(representationData.content()); - ObjectNode rootObjectNode = (ObjectNode) rootJsonNode; - var migrationService = new RepresentationMigrationService(applicableParticipants, rootObjectNode); - migrationService.parseProperties(rootObjectNode, this.objectMapper); - return rootObjectNode.toString(); - } catch (JsonProcessingException | IllegalArgumentException exception) { - this.logger.warn(exception.getMessage()); - } - } - return representationData.content(); - } - - - private List getApplicableMigrationParticipants(RepresentationDataContentOnly representationData) { - var migrationVersion = representationData.migrationVersion(); - var kind = representationData.kind(); - - return this.migrationParticipants.stream() - .filter(migrationParticipant -> Objects.equals(migrationParticipant.getKind(), kind)) - .filter(migrationParticipant -> migrationParticipant.getVersion().compareTo(migrationVersion) > 0) - .sorted(Comparator.comparing(IRepresentationMigrationParticipant::getVersion)) - .toList(); + return this.representationDataMigrationService.getMigratedContent(representationData) + .map(Object::toString) + .orElse(""); } -} +} \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/api/IRepresentationDataMigrationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/api/IRepresentationDataMigrationService.java new file mode 100644 index 00000000000..712b4230cf2 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/api/IRepresentationDataMigrationService.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.representation.services.api; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Optional; + +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.projections.RepresentationDataContentOnly; + +/** + * Used to retrieved the migrated content of the representation data. + * + * @author sbegaudeau + */ +public interface IRepresentationDataMigrationService { + + Optional getMigratedContent(RepresentationDataContentOnly representationData); +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/siriusweb.graphqls b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/siriusweb.graphqls index d9e6f3d3d7d..59b36a8bed0 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/siriusweb.graphqls +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/siriusweb.graphqls @@ -41,6 +41,7 @@ extend type Mutation { deleteProject(input: DeleteProjectInput!): DeleteProjectPayload! renameRepresentation(input: RenameRepresentationInput!): RenameRepresentationPayload! deleteRepresentation(input: DeleteRepresentationInput!): DeleteRepresentationPayload! + uploadProject(input: UploadProjectInput!): UploadProjectPayload! } input CreateProjectInput { @@ -130,3 +131,15 @@ type DeleteRepresentationSuccessPayload { id: ID! representationId: ID! } + +input UploadProjectInput { + id: ID! + file: Upload! +} + +union UploadProjectPayload = ErrorPayload | UploadProjectSuccessPayload + +type UploadProjectSuccessPayload { + id: ID! + project: Project! +} diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/projections/RepresentationDataMetadataOnly.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/projections/RepresentationDataMetadataOnly.java index 32f1fbab5b0..5ea91d6f016 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/projections/RepresentationDataMetadataOnly.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/projections/RepresentationDataMetadataOnly.java @@ -14,6 +14,9 @@ import java.util.UUID; +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; +import org.springframework.data.jdbc.core.mapping.AggregateReference; + /** * Projection used to retrieve only the representation metadata of the representation data. * @@ -24,5 +27,6 @@ public record RepresentationDataMetadataOnly( String label, String kind, String targetObjectId, - String descriptionId) { + String descriptionId, + AggregateReference project) { } diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/repositories/IRepresentationDataRepository.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/repositories/IRepresentationDataRepository.java index 9d54b9b9f80..060ac212a6f 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/repositories/IRepresentationDataRepository.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/boundedcontexts/representationdata/repositories/IRepresentationDataRepository.java @@ -32,21 +32,21 @@ @Repository public interface IRepresentationDataRepository extends ListPagingAndSortingRepository, ListCrudRepository { @Query(""" - SELECT id, label, kind, target_object_id, description_id + SELECT id, label, kind, target_object_id, description_id, project_id AS project FROM representation_data representationData WHERE representationData.id = :id """) Optional findMetadataById(UUID id); @Query(""" - SELECT id, label, kind, target_object_id, description_id + SELECT id, label, kind, target_object_id, description_id, project_id AS project FROM representation_data representationData WHERE representationData.project_id = :projectId """) List findAllMetadataByProjectId(UUID projectId); @Query(""" - SELECT id, label, kind, target_object_id, description_id + SELECT id, label, kind, target_object_id, description_id, project_id AS project FROM representation_data representationData WHERE representationData.target_object_id = :targetObjectId """) diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java new file mode 100644 index 00000000000..d8dd8bf8a5c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java @@ -0,0 +1,522 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.controllers.projects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipInputStream; + +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.data.TestIdentifiers; +import org.eclipse.sirius.web.tests.services.api.IGivenCommittedTransaction; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Integration tests of the project download controllers. + * + * @author sbegaudeau + */ +@Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ProjectDownloadControllerIntegrationTests extends AbstractIntegrationTests { + + @LocalServerPort + private int port; + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private IGivenCommittedTransaction givenCommittedTransaction; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + @Test + @DisplayName("Given a studio, when the download of the project is requested, then the manifest is exported") + @Sql(scripts = { "/scripts/studio.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenStudioWhenTheDownloadOfProjectIsRequestedThenTheManifestIsExported() { + this.givenCommittedTransaction.commit(); + + var response = this.download(StudioIdentifiers.SAMPLE_STUDIO_PROJECT); + + try (ZipInputStream inputStream = new ZipInputStream(response.getBody().getInputStream())) { + HashMap zipEntries = this.toZipEntries(inputStream); + assertThat(zipEntries).isNotEmpty().containsKey("Studio/manifest.json"); + + String manifestContentExpected = """ + { + "natures":[ + "siriusComponents://nature?kind=studio" + ], + "documentIdsToName":{ + "356e45e8-7d70-439e-b2dd-d0313cd65174":"Ellipse Diagram View", + "f0e490c1-79f1-49a0-b1f2-3637f2958148":"Domain", + "ed2a5355-991d-458f-87f1-ea3a18b1f104":"Form View", + "fc1d7b23-2818-4874-bb30-8831ea287a44":"Diagram View" + }, + "metamodels":[ + "domain://buck", + "http://www.eclipse.org/emf/2002/Ecore", + "http://www.eclipse.org/sirius-web/customnodes", + "http://www.eclipse.org/sirius-web/deck", + "http://www.eclipse.org/sirius-web/diagram", + "http://www.eclipse.org/sirius-web/domain", + "http://www.eclipse.org/sirius-web/form", + "http://www.eclipse.org/sirius-web/gantt", + "http://www.eclipse.org/sirius-web/view", + "https://www.eclipse.org/sirius/widgets/reference" + ], + "representations":{} + } + """; + + String manifestContent = new String(zipEntries.get("Studio/manifest.json").toByteArray(), StandardCharsets.UTF_8); + var objectMapper = new ObjectMapper(); + assertThat(objectMapper.readTree(manifestContentExpected)).isEqualTo(objectMapper.readTree(manifestContent)); + } catch (IOException exception) { + fail(exception.getMessage()); + } + } + + @Test + @DisplayName("Given a studio, when the download of the project is requested, then the semantic data are retrieved") + @Sql(scripts = { "/scripts/studio.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenStudioWhenTheDownloadOfProjectIsRequestedThenTheSemanticDataAreRetrieved() { + this.givenCommittedTransaction.commit(); + + var response = this.download(StudioIdentifiers.SAMPLE_STUDIO_PROJECT); + + try (var inputStream = new ZipInputStream(response.getBody().getInputStream())) { + HashMap zipEntries = this.toZipEntries(inputStream); + String domainDocumentPath = "Studio/documents/" + StudioIdentifiers.DOMAIN_DOCUMENT.toString() + ".json"; + String viewDocumentPath = "Studio/documents/" + StudioIdentifiers.VIEW_DOCUMENT.toString() + ".json"; + assertThat(zipEntries).isNotEmpty().containsKey(domainDocumentPath).containsKey(viewDocumentPath); + + var objectMapper = new ObjectMapper(); + + String semanticDataContentExpected = this.getExpectedStudioDomainDocumentData(); + String semanticDataContent = new String(zipEntries.get(domainDocumentPath).toByteArray(), StandardCharsets.UTF_8); + assertThat(objectMapper.readTree(semanticDataContentExpected)).isEqualTo(objectMapper.readTree(semanticDataContent)); + + semanticDataContentExpected = this.getExpectedStudioViewDocumentData(); + semanticDataContent = new String(zipEntries.get(viewDocumentPath).toByteArray(), StandardCharsets.UTF_8); + assertThat(objectMapper.readTree(semanticDataContentExpected)).isEqualTo(objectMapper.readTree(semanticDataContent)); + } catch (IOException exception) { + fail(exception.getMessage()); + } + } + + @Test + @DisplayName("Given a project, when the download of the project is requested, then the representation data are retrieved") + @Sql(scripts = { "/scripts/initialize.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenTheDownloadOfProjectIsRequestedThenTheRepresentationDataAreRetrieved() { + this.givenCommittedTransaction.commit(); + + var response = this.download(TestIdentifiers.ECORE_SAMPLE_PROJECT); + + try (var inputStream = new ZipInputStream(response.getBody().getInputStream())) { + var zipEntries = this.toZipEntries(inputStream); + String representationPath = "Ecore Sample/representations/" + TestIdentifiers.EPACKAGE_PORTAL_REPRESENTATION.toString() + ".json"; + assertThat(zipEntries).isNotEmpty().containsKey(representationPath); + + String representationContentExpected = this.getExpectedRepresentation(); + String representationContent = new String(zipEntries.get(representationPath).toByteArray(), StandardCharsets.UTF_8); + + var objectMapper = new ObjectMapper(); + assertThat(objectMapper.readTree(representationContentExpected)).isEqualTo(objectMapper.readTree(representationContent)); + } catch (IOException exception) { + fail(exception.getMessage()); + } + } + + private ResponseEntity download(UUID projectId) { + var uri = "http://localhost:" + this.port + "/api/projects/" + projectId.toString(); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(List.of(MediaType.parseMediaType("application/zip"))); + HttpEntity entity = new HttpEntity<>(null, headers); + + var response = new TestRestTemplate().exchange(uri, HttpMethod.GET, entity, Resource.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + return response; + } + + private HashMap toZipEntries(ZipInputStream inputStream) { + HashMap zipEntries = new HashMap<>(); + + try { + var zipEntry = inputStream.getNextEntry(); + while (zipEntry != null) { + if (!zipEntry.isDirectory()) { + String name = zipEntry.getName(); + ByteArrayOutputStream entryBaos = new ByteArrayOutputStream(); + inputStream.transferTo(entryBaos); + zipEntries.put(name, entryBaos); + } + zipEntry = inputStream.getNextEntry(); + } + } catch (IOException exception) { + fail(exception.getMessage()); + } + + return zipEntries; + } + + private String getExpectedStudioDomainDocumentData() { + return """ + { + "json":{ + "version":"1.0", + "encoding":"utf-8" + }, + "ns":{ + "domain":"http://www.eclipse.org/sirius-web/domain" + }, + "content":[ + $CONTENT$ + ] + } + """.replace("$CONTENT$", this.getExpectedStudioDomainDocumentDataContent()); + } + + private String getExpectedStudioDomainDocumentDataContent() { + return """ + { + "id":"f8204cb6-3705-48a5-bee3-ad7e7d6cbdaf", + "eClass":"domain:Domain", + "data":{ + "name":"buck", + "types":[ + { + "id":"c341bf91-d315-4264-9787-c51b121a6375", + "eClass":"domain:Entity", + "data":{ + "name":"Root", + "attributes":[ + { + "id":"7ac92c9d-3cb6-4374-9774-11bb62962fe2", + "eClass":"domain:Attribute", + "data":{ + "name":"label" + } + } + ], + "relations":[ + { + "id":"f8fefc5d-4fee-4666-815e-94b24a95183f", + "eClass":"domain:Relation", + "data":{ + "name":"humans", + "many":true, + "containment":true, + "targetType":"//@types.2" + } + } + ] + } + }, + { + "id":"c6fdba07-dea5-4a53-99c7-7eefc1bfdfcc", + "eClass":"domain:Entity", + "data":{ + "name":"NamedElement", + "attributes":[ + { + "id":"520bb7c9-5f28-40f7-bda0-b35dd593876d", + "eClass":"domain:Attribute", + "data":{ + "name":"name" + } + } + ], + "abstract":true + } + }, + { + "id":"1731ffb5-bfb0-46f3-a23d-0c0650300005", + "eClass":"domain:Entity", + "data":{ + "name":"Human", + "attributes":[ + { + "id":"e86d3f45-d043-441e-b8ab-12e4b3f8915a", + "eClass":"domain:Attribute", + "data":{ + "name":"description" + } + }, + { + "id":"703e6db4-a193-4da7-a872-6efba2b3a2c5", + "eClass":"domain:Attribute", + "data":{ + "name":"promoted", + "type":"BOOLEAN" + } + }, + { + "id":"a480dbc0-14b7-4f06-a4f7-4c86139a803a", + "eClass":"domain:Attribute", + "data":{ + "name":"birthDate" + } + } + ], + "superTypes":[ + "//@types.1" + ] + } + } + ] + } + } + """; + } + + private String getExpectedStudioViewDocumentData() { + return """ + { + "json":{ + "version":"1.0", + "encoding":"utf-8" + }, + "ns":{ + "form":"http://www.eclipse.org/sirius-web/form", + "view":"http://www.eclipse.org/sirius-web/view" + }, + "content":[ + $CONTENT$ + ] + } + """.replace("$CONTENT$", this.getExpectedStudioViewDocumentDataContent()); + } + + private String getExpectedStudioViewDocumentDataContent() { + return """ + { + "id":"c4591605-8ea8-4e92-bb17-05c4538248f8", + "eClass":"view:View", + "data":{ + "descriptions":[ + { + "id":"ed20cb85-a58a-47ad-bc0d-749ec8b2ea03", + "eClass":"form:FormDescription", + "data":{ + "name":"Human Form", + "domainType":"buck::Human", + "pages":[ + { + "id":"b0c73654-6f1b-4be5-832d-b97f053b5196", + "eClass":"form:PageDescription", + "data":{ + "name":"Human", + "labelExpression":"aql:self.name", + "domainType":"buck::Human", + "groups": $GROUP$ + } + } + ] + } + } + ] + } + } + """.replace("$GROUP$", this.getGroups()); + } + + private String getGroups() { + return """ + [ + { + "id":"28d8d6de-7d6f-4434-9293-0ac4ef2461ac", + "eClass":"form:GroupDescription", + "data":{ + "name":"Properties", + "labelExpression":"Properties", + "children":[ + $WIDGETS$ + ] + } + } + ] + """.replace("$WIDGETS$", this.getWidgets()); + } + + private String getWidgets() { + return """ + { + "id":"b02b89b7-6c06-40f8-9366-83d5f885ada1", + "eClass":"form:TextfieldDescription", + "data":{ + "name":"Name", + "labelExpression":"Name", + "helpExpression":"The name of the human", + "valueExpression":"aql:self.name", + "body":[ + { + "id":"ecdc23ff-fd4b-47a4-939d-1bc03e656d3d", + "eClass":"view:ChangeContext", + "data":{ + "children":[ + { + "id":"a8b95d5b-833a-4b19-b783-3025225613de", + "eClass":"view:SetValue", + "data":{ + "featureName":"name", + "valueExpression":"aql:newValue" + } + } + ] + } + } + ] + } + }, + { + "id":"98e756a2-305f-4767-b75c-4130996ae6da", + "eClass":"form:TextAreaDescription", + "data":{ + "name":"Description", + "labelExpression":"Description", + "helpExpression":"The description of the human", + "valueExpression":"aql:self.description", + "body":[ + { + "id":"59ea57d5-c365-4421-9648-f38a74644768", + "eClass":"view:ChangeContext", + "data":{ + "children":[ + { + "id":"811bb719-ab53-49ea-9281-6558f7022ecc", + "eClass":"view:SetValue", + "data":{ + "featureName":"description", + "valueExpression":"aql:newValue" + } + } + ] + } + } + ] + } + }, + { + "id":"ba20babb-0e75-4f66-a382-a2f02bce904a", + "eClass":"form:CheckboxDescription", + "data":{ + "name":"Promoted", + "labelExpression":"Promoted", + "helpExpression":"Has this human been promoted?", + "valueExpression":"aql:self.promoted", + "body":[ + { + "id":"afac13bd-71ac-4287-baf6-3669f23ac806", + "eClass":"view:ChangeContext", + "data":{ + "children":[ + { + "id":"0eaeca64-ee2b-4f2c-9454-c528181d0d64", + "eClass":"view:SetValue", + "data":{ + "featureName":"promoted", + "valueExpression":"aql:newValue" + } + } + ] + } + } + ] + } + }, + { + "id":"91a4fcd9-a176-4df1-8f88-52a406fc3f73", + "eClass":"form:DateTimeDescription", + "data":{ + "name":"BirthDate", + "labelExpression":"Birth Date", + "helpExpression":"The birth date of the human", + "stringValueExpression":"aql:self.birthDate", + "type":"DATE" + } + } + """; + } + + private String getExpectedRepresentation() { + return """ + { + "id":"e81eec5c-42d6-491c-8bcc-9beb951356f8", + "projectId":"99d336a2-3049-439a-8853-b104ffb22653", + "descriptionId":"69030a1b-0b5f-3c1d-8399-8ca260e4a672", + "targetObjectId":"3237b215-ae23-48d7-861e-f542a4b9a4b8", + "label":"Portal", + "kind":"siriusComponents://representation?type=Portal", + "representation":{ + "id":"e81eec5c-42d6-491c-8bcc-9beb951356f8", + "kind":"siriusComponents://representation?type=Portal", + "descriptionId":"69030a1b-0b5f-3c1d-8399-8ca260e4a672", + "label":"Portal", + "targetObjectId":"3237b215-ae23-48d7-861e-f542a4b9a4b8", + "views":[ + { + "id":"9e277e97-7f71-4bdd-99af-9eeb8bd7f2df", + "representationId":"05e44ccc-9363-443f-a816-25fc73e3e7f7" + } + ], + "layoutData":[ + { + "portalViewId":"9e277e97-7f71-4bdd-99af-9eeb8bd7f2df", + "x":0, + "y":0, + "width":500, + "height":200 + } + ] + } + } + """; + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectUploadControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectUploadControllerIntegrationTests.java new file mode 100644 index 00000000000..4122f00ccbf --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectUploadControllerIntegrationTests.java @@ -0,0 +1,276 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.controllers.projects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.eclipse.sirius.components.graphql.controllers.GraphQLPayload; +import org.eclipse.sirius.emfjson.resource.JsonResourceFactoryImpl; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.project.dto.UploadProjectInput; +import org.eclipse.sirius.web.domain.boundedcontexts.project.services.api.IProjectSearchService; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Integration tests of the project upload controllers. + * + * @author jmallet + */ +@Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ProjectUploadControllerIntegrationTests extends AbstractIntegrationTests { + + private static final String ECORE_SAMPLE = "EcoreSample"; + + @LocalServerPort + private int port; + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + + @Autowired + private IProjectSearchService projectSearchService; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + @Test + @DisplayName("Given a project, when the upload of the project is requested, then the project with its representation and semantic data are available") + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenTheUploadOfProjectIsRequestedThenTheRepresentationAndSemanticDataAreAvailable() { + byte[] zipByte = this.getZipTestFile(); + this.checkImportedProject(this.upload(zipByte)); + } + + private String upload(byte[] zipByte) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + // Request arguments + MultiValueMap body = new LinkedMultiValueMap<>(); + + var query = """ + mutation uploadProject($input: UploadProjectInput!) { + uploadProject(input: $input) { + __typename + ... on UploadProjectSuccessPayload { + project { + id + } + } + ... on ErrorPayload { + message + } + } + } + """; + var payload = GraphQLPayload.newGraphQLPayload() + .query(query) + .variables(Map.of("input", new UploadProjectInput(UUID.randomUUID(), null))) + .build(); + String operations = ""; + try { + operations = new ObjectMapper().writeValueAsString(payload); + } catch (JsonProcessingException exception) { + fail(exception.getMessage()); + } + + ByteArrayResource contentsAsResource = new ByteArrayResource(zipByte) { + @Override + public String getFilename() { + return ECORE_SAMPLE + ".zip"; + } + }; + + body.add("operations", operations); + body.add("map", "{\"0\":\"variables.file\"}"); + body.add("0", contentsAsResource); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + String serverUrl = "http://localhost:" + this.port + "/api/graphql/upload"; + + // Send http request + var response = new TestRestTemplate().postForEntity(serverUrl, requestEntity, Map.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + try { + return new ObjectMapper().writeValueAsString(response.getBody()); + } catch (JsonProcessingException exception) { + fail(exception.getMessage()); + } + return ""; + } + + private void checkImportedProject(String response) { + String newProjectId = JsonPath.read(response, "$.data.uploadProject.project.id"); + assertTrue(this.projectSearchService.existsById(UUID.fromString(newProjectId))); + + var optionalProject = this.projectSearchService.findById(UUID.fromString(newProjectId)); + assertThat(optionalProject).isPresent(); + optionalProject.ifPresent(project -> assertThat(project.getName()).isEqualTo(ECORE_SAMPLE)); + } + + private byte[] getZipTestFile() { + byte[] zipByte = null; + String projectName = ECORE_SAMPLE; + String representationId = "e81eec5c-42d6-491c-8bcc-9beb951356f8"; + String documentId = "48dc942a-6b76-4133-bca5-5b29ebee133d"; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + // Add Manifest + ZipEntry zipEntry = new ZipEntry(projectName + "/manifest.json"); + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(this.manifest().getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + // Add Representation + zipEntry = new ZipEntry(projectName + "/representations/" + representationId + "." + JsonResourceFactoryImpl.EXTENSION); + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(this.representation().getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + // Add Document + zipEntry = new ZipEntry(projectName + "/documents/" + documentId + "." + JsonResourceFactoryImpl.EXTENSION); + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(this.document().getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } catch (IOException exception) { + fail(exception.getMessage()); + } + + if (outputStream.size() > 0) { + zipByte = outputStream.toByteArray(); + } + + return zipByte; + } + + private String manifest() { + return """ + { + "natures":[ + "ecore" + ], + "documentIdsToName":{ + "48dc942a-6b76-4133-bca5-5b29ebee133d":"Ecore" + }, + "metamodels":[ + "https://www.eclipse.org/sirius/widgets/reference", + "http://www.eclipse.org/emf/2002/Ecore" + ], + "representations":{ + "e81eec5c-42d6-491c-8bcc-9beb951356f8":{ + "targetObjectURI":"sirius:///48dc942a-6b76-4133-bca5-5b29ebee133d#/", + "type":"siriusComponents://representation?type=Portal", + "descriptionURI":"69030a1b-0b5f-3c1d-8399-8ca260e4a672" + } + } + } + """; + } + + private String representation() { + return """ + { + "id":"e81eec5c-42d6-491c-8bcc-9beb951356f8", + "projectId":"99d336a2-3049-439a-8853-b104ffb22653", + "descriptionId":"69030a1b-0b5f-3c1d-8399-8ca260e4a672", + "targetObjectId":"3237b215-ae23-48d7-861e-f542a4b9a4b8", + "label":"Portal", + "kind":"siriusComponents://representation?type=Portal", + "representation":{ + "id":"e81eec5c-42d6-491c-8bcc-9beb951356f8", + "kind":"siriusComponents://representation?type=Portal", + "descriptionId":"69030a1b-0b5f-3c1d-8399-8ca260e4a672", + "label":"Portal", + "targetObjectId":"3237b215-ae23-48d7-861e-f542a4b9a4b8", + "views":[ + { + "id":"9e277e97-7f71-4bdd-99af-9eeb8bd7f2df", + "representationId":"05e44ccc-9363-443f-a816-25fc73e3e7f7" + } + ], + "layoutData":[ + { + "portalViewId":"9e277e97-7f71-4bdd-99af-9eeb8bd7f2df", + "x":0, + "y":0, + "width":500, + "height":200 + } + ] + } + } + """; + } + + private String document() { + return """ + { + "json":{ + "version":"1.0", + "encoding":"utf-8" + }, + "ns":{ + "ecore":"http://www.eclipse.org/emf/2002/Ecore" + }, + "content":[ + { + "id":"3237b215-ae23-48d7-861e-f542a4b9a4b8", + "eClass":"ecore:EPackage", + "data":{ + "name":"Sample" + } + } + ] + } + """; + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java index f000b5fdf09..3e0bb52b7eb 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java @@ -31,6 +31,8 @@ public final class StudioIdentifiers { public static final UUID HUMAN_ENTITY_OBJECT = UUID.fromString("1731ffb5-bfb0-46f3-a23d-0c0650300005"); + public static final UUID VIEW_DOCUMENT = UUID.fromString("ed2a5355-991d-458f-87f1-ea3a18b1f104"); + public static final UUID FORM_DESCRIPTION_OBJECT = UUID.fromString("ed20cb85-a58a-47ad-bc0d-749ec8b2ea03"); public static final UUID GROUP_OBJECT = UUID.fromString("28d8d6de-7d6f-4434-9293-0ac4ef2461ac");