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..5b0a8aa44cc 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, String report, UUID newDocumentId) implements IPayload { } 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..d722fc976d4 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 @@ -34,6 +34,7 @@ import org.eclipse.sirius.components.core.api.IPayload; 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.UploadDocumentInput; import org.eclipse.sirius.web.application.document.dto.UploadDocumentSuccessPayload; import org.eclipse.sirius.web.application.document.services.api.IDocumentSanitizedJsonContentProvider; @@ -118,8 +119,13 @@ public void handle(Sinks.One payloadSink, Sinks.Many optionalDocumentId = new UUIDParser().parse(newResourceId); + if (optionalDocumentId.isPresent()) { + var documentId = optionalDocumentId.get(); + payload = new UploadDocumentSuccessPayload(input.id(), report, documentId); + 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..9bb6d31c809 --- /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,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.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; + +/** + * The data fetcher used to create a new {@link Project} thanks to an {@link UploadProjectInput}. + *

+ * It will be used to handle the following GraphQL field: + *

+ * + *
+ * type Mutation {
+ *   uploadProject(input: UploadProjectInput!): UploadProjectPayload!
+ * }
+ * 
+ * + * @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/services/CreateProjectSuccessPayload.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/CreateProjectSuccessPayload.java new file mode 100644 index 00000000000..d29d44d298a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/project/services/CreateProjectSuccessPayload.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.domain.boundedcontexts.project.Project; + +/** + * The payload of the create project mutation. + * + * @author jmallet + */ +public record CreateProjectSuccessPayload(UUID id, Project project) implements IPayload { + public CreateProjectSuccessPayload { + 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/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..9dbae0917e9 --- /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,289 @@ +/******************************************************************************* + * 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.services.api.IProjectImportService; +import org.eclipse.sirius.web.domain.boundedcontexts.project.Project; +import org.eclipse.sirius.web.domain.boundedcontexts.project.services.api.IProjectCreationService; +import org.eclipse.sirius.web.domain.boundedcontexts.project.services.api.IProjectDeletionService; +import org.eclipse.sirius.web.domain.services.Failure; +import org.eclipse.sirius.web.domain.services.Success; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 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 IProjectCreationService projectCreationService; + + private final IProjectDeletionService projectDeletionService; + + public ProjectImportService(IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry, ObjectMapper objectMapper, IProjectCreationService projectCreationService, + IProjectDeletionService projectDeletionService) { + this.editingContextEventProcessorRegistry = Objects.requireNonNull(editingContextEventProcessorRegistry); + this.objectMapper = Objects.requireNonNull(objectMapper); + this.projectCreationService = Objects.requireNonNull(projectCreationService); + this.projectDeletionService = Objects.requireNonNull(projectDeletionService); + } + + /** + * 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(e -> e.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); + } + + IPayload createProjectPayload = this.createProject(inputId, projectName, (List) projectManifest.get("natures")); + IPayload payload = new ErrorPayload(inputId, ""); + if (createProjectPayload instanceof CreateProjectSuccessPayload createProjectSuccessPayload) { + Project project = createProjectSuccessPayload.project(); + Optional optionalEditingContextEventProcessor = this.editingContextEventProcessorRegistry + .getOrCreateEditingContextEventProcessor(project.getId().toString()); + if (optionalEditingContextEventProcessor.isPresent()) { + IEditingContextEventProcessor editingContextEventProcessor = optionalEditingContextEventProcessor.get(); + + ProjectImporter projectImporter = new ProjectImporter(project.getId().toString(), editingContextEventProcessor, documents, representationImportDatas, projectManifest); + boolean hasBeenImported = projectImporter.importProject(inputId); + + if (!hasBeenImported) { + this.editingContextEventProcessorRegistry.disposeEditingContextEventProcessor(project.getId().toString()); + this.projectDeletionService.deleteProject(project.getId()); + } else { + payload = new UploadProjectSuccessPayload(inputId, project); + } + } + } + return payload; + } + + @Transactional + public IPayload createProject(UUID inputId, String projectName, List natures) { + var result = this.projectCreationService.createProject(projectName, natures); + + IPayload payload = null; + if (result instanceof Failure failure) { + payload = new ErrorPayload(inputId, failure.message()); + } else if (result instanceof Success success) { + payload = new CreateProjectSuccessPayload(inputId, success.data()); + } + 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..1707a69c093 --- /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,180 @@ +/******************************************************************************* + * 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.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::newDocumentId) + .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 index b148fffaebe..95fc0fb3a15 100644 --- 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 @@ -21,8 +21,15 @@ 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; @@ -38,58 +45,93 @@ @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(IRepresentationDataSearchService representationDataSearchService, ObjectMapper objectMapper) { + 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 allRepresentationData = this.representationDataSearchService.findAllByProject(AggregateReference.to(project.getId())); - for (var representationData: allRepresentationData) { - var exportData = new RepresentationExportData( - representationData.getId(), - representationData.getProject().getId(), - representationData.getDescriptionId(), - representationData.getTargetObjectId(), - representationData.getLabel(), - representationData.getKind(), - representationData.getContent() - ); - - Map representationManifest = Map.of( - "type", representationData.getKind(), - "descriptionURI", representationData.getDescriptionId(), - "targetObjectURI", representationData.getTargetObjectId() - ); - representationManifests.put(representationData.getId().toString(), representationManifest); - - try { - byte[] bytes = this.objectMapper.writeValueAsBytes(exportData); - - String name = project.getName() + "/representations/" + representationData.getId() + "." + 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()); + 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 index d0545c0d5c6..0190e6dd8a4 100644 --- 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 @@ -21,13 +21,16 @@ 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; @@ -46,11 +49,14 @@ public class ProjectSemanticDataExportParticipant implements IProjectExportParti private final List documentExporters; + private final List persistenceFilters; + private final Logger logger = LoggerFactory.getLogger(ProjectSemanticDataExportParticipant.class); - public ProjectSemanticDataExportParticipant(IEditingContextSearchService editingContextSearchService, List documentExporters) { + public ProjectSemanticDataExportParticipant(IEditingContextSearchService editingContextSearchService, List documentExporters, List persistenceFilters) { this.editingContextSearchService = Objects.requireNonNull(editingContextSearchService); this.documentExporters = Objects.requireNonNull(documentExporters); + this.persistenceFilters = Objects.requireNonNull(persistenceFilters); } @Override @@ -65,9 +71,13 @@ public Map exportData(Project project, ZipOutputStream outputStr 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; @@ -84,7 +94,9 @@ private List getMetamodels(IEMFEditingContext editingContext) { private Map exportSemanticData(IEMFEditingContext editingContext, String projectName, ZipOutputStream outputStream) { Map id2DocumentName = new HashMap<>(); - for (var resource: editingContext.getDomain().getResourceSet().getResources()) { + 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); 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..44727124d19 --- /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) 2022, 2023 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..0d04dda1a73 --- /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.domain.boundedcontexts.project.Project; + +/** + * The payload of the project upload mutation. + * + * @author gcoutable + */ +public record UploadProjectSuccessPayload(UUID id, Project 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/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 index 6cfa1153c7a..45fc327b32d 100644 --- 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 @@ -14,19 +14,21 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.junit.Assert.assertEquals; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; import java.util.UUID; -import java.util.zip.ZipEntry; 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.services.api.IGivenCommittedTransaction; -import org.eclipse.sirius.web.services.api.IGivenInitialServerState; +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; @@ -71,17 +73,18 @@ public void beforeEach() { @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)) + @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() { var response = this.download(StudioIdentifiers.SAMPLE_STUDIO_PROJECT); - try (var inputStream = new ZipInputStream(response.getBody().getInputStream())) { - var zipEntries = this.toZipEntries(inputStream); - assertThat(zipEntries) - .isNotEmpty() - .extracting(ZipEntry::getName) - .contains("Studio/manifest.json"); + try (ZipInputStream inputStream = new ZipInputStream(response.getBody().getInputStream())) { + HashMap zipEntries = this.toZipEntries(inputStream); + assertThat(zipEntries).isNotEmpty().containsKey("Studio/manifest.json"); + + String manifestContentExpected = this.getExpectedStudioManifest(); + String manifestContent = new String(zipEntries.get("Studio/manifest.json").toByteArray(), StandardCharsets.UTF_8); + assertEquals(manifestContentExpected, manifestContent); } catch (IOException exception) { fail(exception.getMessage()); } @@ -89,19 +92,24 @@ public void givenStudioWhenTheDownloadOfProjectIsRequestedThenTheManifestIsExpor @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)) + @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() { var response = this.download(StudioIdentifiers.SAMPLE_STUDIO_PROJECT); try (var inputStream = new ZipInputStream(response.getBody().getInputStream())) { - var zipEntries = this.toZipEntries(inputStream); - assertThat(zipEntries) - .isNotEmpty() - .extracting(ZipEntry::getName) - .contains( - "Studio/documents/" + StudioIdentifiers.DOMAIN_DOCUMENT.toString() + ".json", - "Studio/documents/" + StudioIdentifiers.VIEW_DOCUMENT.toString() + ".json"); + 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); + + String semanticDataContentExpected = this.getExpectedStudioDomainDocumentData(); + String semanticDataContent = new String(zipEntries.get(domainDocumentPath).toByteArray(), StandardCharsets.UTF_8); + assertEquals(semanticDataContentExpected, semanticDataContent); + + semanticDataContentExpected = this.getExpectedStudioViewDocumentData(); + semanticDataContent = new String(zipEntries.get(viewDocumentPath).toByteArray(), StandardCharsets.UTF_8); + assertEquals(semanticDataContentExpected, semanticDataContent); } catch (IOException exception) { fail(exception.getMessage()); } @@ -109,18 +117,19 @@ public void givenStudioWhenTheDownloadOfProjectIsRequestedThenTheSemanticDataAre @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)) + @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() { var response = this.download(TestIdentifiers.ECORE_SAMPLE_PROJECT); try (var inputStream = new ZipInputStream(response.getBody().getInputStream())) { var zipEntries = this.toZipEntries(inputStream); - assertThat(zipEntries) - .isNotEmpty() - .extracting(ZipEntry::getName) - .contains( - "Ecore Sample/representations/" + TestIdentifiers.EPACKAGE_PORTAL_REPRESENTATION.toString() + ".json"); + 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); + assertEquals(representationContentExpected, representationContent); } catch (IOException exception) { fail(exception.getMessage()); } @@ -129,7 +138,7 @@ public void givenProjectWhenTheDownloadOfProjectIsRequestedThenTheRepresentation private ResponseEntity download(UUID projectId) { this.givenCommittedTransaction.commit(); - var uri = "http://localhost:" + port + "/api/projects/" + projectId.toString(); + var uri = "http://localhost:" + this.port + "/api/projects/" + projectId.toString(); HttpHeaders headers = new HttpHeaders(); headers.setAccept(List.of(MediaType.parseMediaType("application/zip"))); @@ -141,13 +150,18 @@ private ResponseEntity download(UUID projectId) { return response; } - private List toZipEntries(ZipInputStream inputStream) { - List zipEntries = new ArrayList<>(); + private HashMap toZipEntries(ZipInputStream inputStream) { + HashMap zipEntries = new HashMap<>(); try { var zipEntry = inputStream.getNextEntry(); while (zipEntry != null) { - zipEntries.add(zipEntry); + if (!zipEntry.isDirectory()) { + String name = zipEntry.getName(); + ByteArrayOutputStream entryBaos = new ByteArrayOutputStream(); + inputStream.transferTo(entryBaos); + zipEntries.put(name, entryBaos); + } zipEntry = inputStream.getNextEntry(); } } catch (IOException exception) { @@ -156,4 +170,20 @@ private List toZipEntries(ZipInputStream inputStream) { return zipEntries; } + + private String getExpectedStudioManifest() { + return "{\"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\":[\"http://www.eclipse.org/sirius-web/domain\",\"http://www.eclipse.org/sirius-web/deck\",\"http://www.eclipse.org/sirius-web/diagram\",\"http://www.eclipse.org/sirius-web/view\",\"http://www.eclipse.org/sirius-web/gantt\",\"https://www.eclipse.org/sirius/widgets/reference\",\"domain://buck\",\"http://www.eclipse.org/sirius-web/form\",\"http://www.eclipse.org/sirius-web/customnodes\",\"http://www.eclipse.org/emf/2002/Ecore\"],\"representations\":{}}"; + } + + private String getExpectedStudioDomainDocumentData() { + return "{\"json\":{\"version\":\"1.0\",\"encoding\":\"utf-8\"},\"ns\":{\"domain\":\"http://www.eclipse.org/sirius-web/domain\"},\"content\":[{\"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\":[{\"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\":[{\"id\":\"28d8d6de-7d6f-4434-9293-0ac4ef2461ac\",\"eClass\":\"form:GroupDescription\",\"data\":{\"name\":\"Properties\",\"labelExpression\":\"Properties\",\"children\":[{\"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..ac0de39bfef --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectUploadControllerIntegrationTests.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.controllers.projects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +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.emfjson.resource.JsonResourceFactoryImpl; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.data.TestIdentifiers; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.http.ResponseEntity; +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"; + + private final Logger logger = LoggerFactory.getLogger(ProjectUploadControllerIntegrationTests.class); + + @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() { + UUID projectId = TestIdentifiers.ECORE_SAMPLE_PROJECT; + byte[] zipByte = this.getZipTestFile(); + ResponseEntity response = this.upload(projectId, zipByte); + this.checkImportedProject(response); + } + + private ResponseEntity upload(UUID projectId, byte[] zipByte) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + // Request arguments + MultiValueMap body = new LinkedMultiValueMap<>(); + + String operations = "{\"query\":\"\\n mutation uploadProject($input: UploadProjectInput!) {\\n uploadProject(input: $input) {\\n __typename\\n ... on UploadProjectSuccessPayload {\\n project {\\n id\\n }\\n }\\n ... on ErrorPayload {\\n message\\n }\\n }\\n }\\n\",\"variables\":{\"input\":{\"id\":\"99d336a2-3049-439a-8853-b104ffb22653\",\"file\":null}}}"; + + 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 + ResponseEntity response = new TestRestTemplate().postForEntity(serverUrl, requestEntity, Map.class); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + return response; + } + + private void checkImportedProject(ResponseEntity response) { + String jsonNodeAsString = null; + try { + jsonNodeAsString = new ObjectMapper().writeValueAsString(response.getBody()); + } catch (JsonProcessingException e) { + this.logger.warn(e.getMessage(), e); + } + String newProjectId = JsonPath.read(jsonNodeAsString, "$.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"; + + String manifestAsString = "{\"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\"}}}"; + + String representationAsString = "{\"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}]}}"; + + String documentAsString = "{\"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\"}}]}"; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + // Add Manifest + ZipEntry zipEntry = new ZipEntry(projectName + "/manifest.json"); + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(manifestAsString.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + // Add Representation + zipEntry = new ZipEntry(projectName + "/representations/" + representationId + "." + JsonResourceFactoryImpl.EXTENSION); + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(representationAsString.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + + // Add Document + zipEntry = new ZipEntry(projectName + "/documents/" + documentId + "." + JsonResourceFactoryImpl.EXTENSION); + zipOutputStream.putNextEntry(zipEntry); + zipOutputStream.write(documentAsString.getBytes(StandardCharsets.UTF_8)); + zipOutputStream.closeEntry(); + } catch (IOException exception) { + this.logger.warn(exception.getMessage()); + } + + if (outputStream.size() > 0) { + zipByte = outputStream.toByteArray(); + } + + return zipByte; + } +}