diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 750ab23b01..5aceb960a9 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -75,6 +75,7 @@ image:doc/screenshots/inside_outside_labels.png[Isinde outside label example, 70 - https://github.com/eclipse-sirius/sirius-web/issues/3237[#3237] [diagram] Add a `DiagramServices` class containing services to expand and collapse nodes from AQL expressions (via the new `diagramServices` variable) and Java services. - https://github.com/eclipse-sirius/sirius-web/issues/3282[#3282] [diagram] Add diagram assertions and navigators to ease the definition of diagram tests. - https://github.com/eclipse-sirius/sirius-web/issues/3277[#3277] [gantt] Add support for dependency creation +- https://github.com/eclipse-sirius/sirius-web/issues/3333[#3333] [sirius-web] Added IMigrationParticipant interface, contribute implemention to migrate models === Improvements @@ -89,6 +90,8 @@ They still support returning an `java.time.Instant` object directly. - https://github.com/eclipse-sirius/sirius-web/issues/3312[#3312] [form] Make the details view more compact - https://github.com/eclipse-sirius/sirius-web/issues/3349[#3349] [diagram] Add a default label when creating a NodeDescription - https://github.com/eclipse-sirius/sirius-web/issues/3342[#3342] [deck] Update deck and gantt view metamodel and deck view examples +- https://github.com/eclipse-sirius/sirius-web/issues/3285[#3285] Makes the icons in the diagram palette use the default color of Sirius Web theme +- https://github.com/eclipse-sirius/sirius-web/issues/3333[#3333] [sirius-web] Store the last migration performed in a document == v2024.3.0 diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/ResourceMetadataAdapter.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/ResourceMetadataAdapter.java index 37f7cc21a4..54e9f913f9 100644 --- a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/ResourceMetadataAdapter.java +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/ResourceMetadataAdapter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2023 Obeo. + * Copyright (c) 2019, 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 @@ -17,6 +17,7 @@ import org.eclipse.emf.common.notify.Adapter; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.notify.Notifier; +import org.eclipse.sirius.components.emf.utils.MigrationData; /** * An EMF adapter used to store some metadata related to EMF Resources. @@ -33,12 +34,16 @@ public class ResourceMetadataAdapter implements Adapter { private String name; + private MigrationData migrationData; + + private String migrationVersion = ""; + private Notifier notifier; public ResourceMetadataAdapter(String name) { this.name = Objects.requireNonNull(name); } - + public String getName() { return this.name; } @@ -47,6 +52,14 @@ public void setName(String name) { this.name = name; } + public MigrationData getMigrationData() { + return migrationData; + } + + public void setMigrationData(MigrationData migrationData) { + this.migrationData = migrationData; + } + @Override public void notifyChanged(Notification notification) { // do nothing diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/utils/MigrationData.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/utils/MigrationData.java new file mode 100644 index 0000000000..73d0f39455 --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/utils/MigrationData.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * 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.components.emf.utils; + +/** + * POJO for MigrationData. + * + * @author mcharfadi + */ +public class MigrationData { + + public static final String JSON_OBJECT_ROOT = "migration"; + private String lastMigrationPerformed; + + private String migrationVersion; + + public MigrationData() { + this.lastMigrationPerformed = "none"; + this.migrationVersion = "0"; + } + + public MigrationData(String lastMigrationPerformed, String migrationVersion) { + this.lastMigrationPerformed = lastMigrationPerformed; + this.migrationVersion = migrationVersion; + } + + public String getLastMigrationPerformed() { + return lastMigrationPerformed; + } + + public void setLastMigrationPerformed(String lastMigrationPerformed) { + this.lastMigrationPerformed = lastMigrationPerformed; + } + + public String getMigrationVersion() { + return migrationVersion; + } + + public void setMigrationVersion(String migrationVersion) { + this.migrationVersion = migrationVersion; + } + + @Override + public String toString() { + return "MigrationData{" + + "lastMigrationPerformed='" + lastMigrationPerformed + '\'' + + ", migrationVersion='" + migrationVersion + '\'' + + '}'; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/IMigrationParticipant.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/IMigrationParticipant.java new file mode 100644 index 0000000000..3c528ffb0a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/IMigrationParticipant.java @@ -0,0 +1,146 @@ +/******************************************************************************* + * 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.editingcontext.migration; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EClassifier; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.sirius.emfjson.resource.JsonResource; + +import com.google.gson.JsonObject; + +/** + * Interface of MigrationParticipant. + * + * @author mcharfadi + */ +public interface IMigrationParticipant { + String getVersion(); + + /** + * Called during the parsing of JSonResources (at loading time). It allows to + * retrieve a renamed EStructuralFeature from its old name. For example, if a + * EStructuralFeature 'aaa' has been renamed to 'bbb', then your MigrationParticipant + * should return the 'bbb' EStructuralFeature when given the 'aaa' name. + * + * @param eClass + * the given eClass. + * @param oldFeatureName + * the feature name before migration. + * @return the new structural feature or null if not found. The attribute + * corresponding to given old name into given eClass. + */ + default EStructuralFeature getStructuralFeature(EClass eClass, String oldFeatureName) { + return eClass.getEStructuralFeature(oldFeatureName); + } + + /** + * Called before a JsonResource is serialized. + * + * @param resource + * The JsonResource that is serialized + * @param jsonObject + * The root jsonObject + */ + default void preDeserialization(JsonResource resource, JsonObject jsonObject) { + + } + + /** + * Called after a JsonResource is serialized. + * + * @param resource + * The JsonResource that is serialized + * @param jsonObject + * The root jsonObject + */ + default void postSerialization(JsonResource resource, JsonObject jsonObject) { + + } + + /** + * Called during the parsing of JSonResources (at loading time). It allows to + * retrieve and modify an eobject just before it's loaded into the resource, + * after every feature were set. + * + * @param eobject + * the given eobject. + * @param jsonObject + * the jsonObject used to set the features of the eobject. + */ + default void getJSonObjectBeforeLoading(EObject eobject, JsonObject jsonObject) { + + } + + /** + * Called during the parsing of XMIResources (at loading time). If a feature value has changed since a previous + * version, use this method to return the correct expected value. The feature value do not have to be set here, + * that will be done by JsonHelper.setValue(). + * + * @param object + * the object containing the feature. + * @param feature + * the feature to set value. + * @param value + * the initial serialized value. + * @return the new value, or null otherwise. + */ + default Object getValue(EObject object, EStructuralFeature feature, Object value) { + return null; + } + + /** + * Called during the parsing of JSonResources (at loading time). It allows to + * retrieve a renamed EStructuralFeature from its old name. For example, if a + * feature 'aaa' has been renamed to 'bbb', then your MigrationParticipant + * should return the 'bbb' feature when given the 'aaa' name. + * + * @param eClass + * the given eClass. + * @param oldAttributeName + * the feature name before migration. + * @return the new structural feature or null if not found. The attribute + * corresponding to given old name into given eClass. + */ + default EStructuralFeature getElement(EClass eClass, String oldAttributeName) { + return eClass.getEStructuralFeature(oldAttributeName); + } + + /** + * Called during the parsing of JSonResources (at loading time). If an + * EClassifier name has changed, then you should return the correct one. + * + * @param ePackage + * the package where looking for classifier. + * @param typeName + * the old classifier name before migration. + * @return the new classifier corresponding to the old given name into given + * ePackage or null if not found. + */ + default EClassifier getType(EPackage ePackage, String typeName) { + return ePackage.getEClassifier(typeName); + } + + /** + * Return the EPackage to use for the given namespace. + * + * @param namespace + * the nsURI of the package we are looking for. + * @return an EPackage if some new mapping exists, null otherwise. + */ + default EPackage getPackage(String namespace) { + return null; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/MigrationExtendedMetaData.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/MigrationExtendedMetaData.java new file mode 100644 index 0000000000..e8980d472e --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/MigrationExtendedMetaData.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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.editingcontext.migration; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EClassifier; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.emf.ecore.util.BasicExtendedMetaData; +import org.eclipse.sirius.components.emf.utils.MigrationData; + +/** + * Specialized BasicExtendedMetaData. + * + * @author mcharfadi + */ +public class MigrationExtendedMetaData extends BasicExtendedMetaData { + private final MigrationData documentMigrationData; + private final List migrationParticipants; + public MigrationExtendedMetaData(List migrationParticipants, MigrationData documentMigrationData) { + this.migrationParticipants = Objects.requireNonNull(migrationParticipants); + this.documentMigrationData = Objects.requireNonNull(documentMigrationData); + } + private boolean isCandidateVersion(IMigrationParticipant migrationParticipant) { + return migrationParticipant.getVersion().compareTo(this.documentMigrationData.getMigrationVersion()) > 0; + } + + @Override + public EStructuralFeature getElement(EClass eClass, String namespace, String name) { + EStructuralFeature structuralFeature = eClass.getEStructuralFeature(name); + var migrationParticipantsCandidate = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidate) { + EStructuralFeature newStructuralFeature = migrationParticipant.getElement(eClass, name); + if (newStructuralFeature != null) { + structuralFeature = newStructuralFeature; + } + } + return structuralFeature; + } + @Override + public EClassifier getType(EPackage ePackage, String typeName) { + EClassifier eClassifier = ePackage.getEClassifier(typeName); + var migrationParticipantsCandidate = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidate) { + EClassifier newEClassifier = migrationParticipant.getType(ePackage, typeName); + if (newEClassifier != null) { + eClassifier = newEClassifier; + } + } + return eClassifier; + } + + @Override + public EPackage getPackage(String namespace) { + EPackage ePackage = super.getPackage(namespace); + var migrationParticipantsCandidate = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidate) { + EPackage newEPackage = migrationParticipant.getPackage(namespace); + if (newEPackage != null) { + ePackage = newEPackage; + } + } + return ePackage; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/MigrationJsonProcessor.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/MigrationJsonProcessor.java new file mode 100644 index 0000000000..763d68d10b --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/MigrationJsonProcessor.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * 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.editingcontext.migration; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; +import org.eclipse.sirius.components.emf.utils.MigrationData; +import org.eclipse.sirius.emfjson.resource.JsonResource; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +/** + * Implementation of JsonResource.IJsonResourceProcessor. + * + * @author mcharfadi + */ +public class MigrationJsonProcessor implements JsonResource.IJsonResourceProcessor { + private final MigrationData documentMigrationData; + private final List migrationParticipants; + + public MigrationJsonProcessor(List migrationParticipants, MigrationData documentMigrationData) { + this.migrationParticipants = Objects.requireNonNull(migrationParticipants); + this.documentMigrationData = Objects.requireNonNull(documentMigrationData); + } + + private boolean isCandidateVersion(IMigrationParticipant migrationParticipant) { + return migrationParticipant.getVersion().compareTo(this.documentMigrationData.getMigrationVersion()) > 0; + } + @Override + public void preDeserialization(JsonResource resource, JsonObject jsonObject) { + var optionalResourceMetadataAdapter = getOptionalResourceMetadataAdapter(resource); + var migrationParticipantsCandidates = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidates) { + migrationParticipant.preDeserialization(resource, jsonObject); + + optionalResourceMetadataAdapter.ifPresent(resourceMetadataAdapter -> { + var migrationData = new MigrationData(migrationParticipant.getClass().getSimpleName(), migrationParticipant.getVersion()); + resourceMetadataAdapter.setMigrationData(migrationData); + }); + } + } + @Override + public void postSerialization(JsonResource resource, JsonObject jsonObject) { + getOptionalResourceMetadataAdapter(resource).ifPresent(resourceMetadataAdapter -> { + var rootMigration = new Gson().toJsonTree(resourceMetadataAdapter.getMigrationData(), MigrationData.class); + jsonObject.add(MigrationData.JSON_OBJECT_ROOT, rootMigration); + }); + var migrationParticipantsCandidates = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidates) { + migrationParticipant.postSerialization(resource, jsonObject); + } + } + + @Override + public void getJSonObjectBeforeLoading(EObject eobject, JsonObject object, boolean isTopObject) { + var migrationParticipantsCandidates = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidates) { + migrationParticipant.getJSonObjectBeforeLoading(eobject, object); + } + } + + @Override + public Object getValue(EObject object, EStructuralFeature feature, Object value) { + Object returnValue = value; + var migrationParticipantsCandidates = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidates) { + Object newValue = migrationParticipant.getValue(object, feature, value); + if (newValue != null) { + returnValue = newValue; + } + } + return returnValue; + } + + private Optional getOptionalResourceMetadataAdapter(JsonResource resource) { + return resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .findFirst(); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceLoader.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceLoader.java index b6fca1918e..55287d4629 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceLoader.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceLoader.java @@ -15,15 +15,26 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; import java.util.Optional; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; import org.eclipse.sirius.components.emf.services.JSONResourceFactory; +import org.eclipse.sirius.components.emf.utils.MigrationData; +import org.eclipse.sirius.emfjson.resource.JsonResource; +import org.eclipse.sirius.web.application.editingcontext.migration.IMigrationParticipant; +import org.eclipse.sirius.web.application.editingcontext.migration.MigrationExtendedMetaData; +import org.eclipse.sirius.web.application.editingcontext.migration.MigrationJsonProcessor; import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.springframework.stereotype.Service; /** @@ -36,16 +47,29 @@ public class ResourceLoader implements IResourceLoader { private final Logger logger = LoggerFactory.getLogger(ResourceLoader.class); + private final List migrationParticipants; + public ResourceLoader(List migrationParticipants) { + this.migrationParticipants = migrationParticipants; + } @Override public Optional toResource(ResourceSet resourceSet, String id, String name, String content) { Optional optionalResource = Optional.empty(); + var migrationData = getMigrationDataFromDocument(content).orElse(new MigrationData()); + var migrationExtendedMetaData = new MigrationExtendedMetaData(this.migrationParticipants, migrationData); + var migrationJsonProcessor = new MigrationJsonProcessor(this.migrationParticipants, migrationData); + + HashMap options = new HashMap<>(); + options.put(JsonResource.OPTION_EXTENDED_META_DATA, migrationExtendedMetaData); + options.put(JsonResource.OPTION_JSON_RESSOURCE_PROCESSOR, migrationJsonProcessor); + var resource = new JSONResourceFactory().createResourceFromPath(id); + + try (var inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { resourceSet.getResources().add(resource); - resource.load(inputStream, null); - resource.eAdapters().add(new ResourceMetadataAdapter(name)); + resource.load(inputStream, options); optionalResource = Optional.of(resource); } catch (IOException | IllegalArgumentException exception) { @@ -55,4 +79,10 @@ public Optional toResource(ResourceSet resourceSet, String id, String return optionalResource; } + + public Optional getMigrationDataFromDocument(String content) { + JsonObject jsonObject = JsonParser.parseString(content).getAsJsonObject(); + return Optional.ofNullable(jsonObject.getAsJsonObject(MigrationData.JSON_OBJECT_ROOT)) + .map(migrationRootElement -> new Gson().fromJson(migrationRootElement, MigrationData.class)).stream().findFirst(); + } } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceToDocumentService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceToDocumentService.java index 6cb3f0546c..8dd398adec 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceToDocumentService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/services/ResourceToDocumentService.java @@ -15,18 +15,24 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Optional; import java.util.UUID; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; import org.eclipse.sirius.components.emf.services.EObjectIDManager; +import org.eclipse.sirius.components.emf.utils.MigrationData; import org.eclipse.sirius.emfjson.resource.JsonResource; import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.editingcontext.migration.IMigrationParticipant; +import org.eclipse.sirius.web.application.editingcontext.migration.MigrationExtendedMetaData; +import org.eclipse.sirius.web.application.editingcontext.migration.MigrationJsonProcessor; import org.eclipse.sirius.web.application.editingcontext.services.api.IResourceToDocumentService; import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.stereotype.Service; /** @@ -39,14 +45,23 @@ public class ResourceToDocumentService implements IResourceToDocumentService { private final Logger logger = LoggerFactory.getLogger(ResourceToDocumentService.class); + private final List migrationParticipants; + public ResourceToDocumentService(List migrationParticipants) { + this.migrationParticipants = migrationParticipants; + } @Override public Optional toDocument(Resource resource) { + var migrationData = getOptionalResourceMetadataAdapter(resource).map(ResourceMetadataAdapter::getMigrationData).orElse(new MigrationData()); var serializationListener = new JsonResourceSerializationListener(); + var migrationJsonProcessor = new MigrationJsonProcessor(this.migrationParticipants, migrationData); + var migrationExtendedMetaData = new MigrationExtendedMetaData(this.migrationParticipants, migrationData); HashMap options = new HashMap<>(); options.put(JsonResource.OPTION_ID_MANAGER, new EObjectIDManager()); options.put(JsonResource.OPTION_SCHEMA_LOCATION, true); options.put(JsonResource.OPTION_SERIALIZATION_LISTENER, serializationListener); + options.put(JsonResource.OPTION_JSON_RESSOURCE_PROCESSOR, migrationJsonProcessor); + options.put(JsonResource.OPTION_EXTENDED_META_DATA, migrationExtendedMetaData); Optional optionalDocumentData = Optional.empty(); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { @@ -81,4 +96,11 @@ public Optional toDocument(Resource resource) { } return optionalDocumentData; } + + public Optional getOptionalResourceMetadataAdapter(Resource resource) { + return resource.eAdapters().stream() + .filter(ResourceMetadataAdapter.class::isInstance) + .map(ResourceMetadataAdapter.class::cast) + .findFirst(); + } } diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/migration/MigrationParticipantTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/migration/MigrationParticipantTests.java new file mode 100644 index 0000000000..3e9daa458a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/migration/MigrationParticipantTests.java @@ -0,0 +1,200 @@ +/******************************************************************************* + * 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.services.migration; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.sirius.components.domain.DomainPackage; +import org.eclipse.sirius.components.view.View; +import org.eclipse.sirius.components.view.ViewPackage; +import org.eclipse.sirius.components.view.diagram.DiagramDescription; +import org.eclipse.sirius.components.view.diagram.DiagramPackage; +import org.eclipse.sirius.components.view.diagram.ImageNodeStyleDescription; +import org.eclipse.sirius.components.view.diagram.InsideLabelDescription; +import org.eclipse.sirius.components.view.diagram.NodeDescription; +import org.eclipse.sirius.components.view.diagram.OutsideLabelDescription; +import org.eclipse.sirius.components.view.diagram.RectangularNodeStyleDescription; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.editingcontext.migration.IMigrationParticipant; +import org.eclipse.sirius.web.application.editingcontext.services.DocumentData; +import org.eclipse.sirius.web.application.editingcontext.services.ResourceLoader; +import org.eclipse.sirius.web.application.editingcontext.services.ResourceToDocumentService; +import org.eclipse.sirius.web.domain.boundedcontexts.semanticdata.services.api.ISemanticDataSearchService; + +import com.google.gson.JsonObject; +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests of a MigrationParticipant. + * + * @author mcharfadi + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MigrationParticipantTests extends AbstractIntegrationTests { + @Autowired + private ISemanticDataSearchService semanticDataSearchService; + + ResourceLoader resourceLoader; + + ResourceToDocumentService resourceToDocumentService; + @BeforeEach + public void beforeEach() { + List migrationParticipants = List.of( + new UpdateLabelExpressionMigrationParticipant(), new UpdateNodeDescriptionDomainTypeFeatureName(), new UpdateDiagramDescriptionNameValue()); + resourceLoader = new ResourceLoader(migrationParticipants); + resourceToDocumentService = new ResourceToDocumentService(migrationParticipants); + } + + @Test + @DisplayName("Given a project with an old model, a migrationParticipant can be contributed to migrate") + @Sql(scripts = {"/scripts/initializeMigrationParticipant.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + void givenAnOldModelMigrationParticipantCanBeContributedToUpdateTheModel() { + var allSemanticData = this.semanticDataSearchService.findAllByDomains(List.of(DomainPackage.eNS_URI, ViewPackage.eNS_URI)); + ResourceSet resourceSet = new ResourceSetImpl(); + for (var semanticData: allSemanticData) { + resourceSet.getPackageRegistry().put(ViewPackage.eNS_URI, ViewPackage.eINSTANCE); + resourceSet.getPackageRegistry().put(DiagramPackage.eNS_URI, DiagramPackage.eINSTANCE); + semanticData.getDocuments().forEach(document -> this.resourceLoader.toResource(resourceSet, UUID.randomUUID().toString(), document.getName(), document.getContent())); + Resource jsonResource = resourceSet.getResources().get(0); + View view = (View) jsonResource.getContents().get(0); + DiagramDescription diagramDescription = (DiagramDescription) view.getDescriptions().get(0); + + //Test that UpdateDiagramDescriptionNameValue updated the diagramDescription name correctly + assertThat(diagramDescription.getName()).isEqualTo("Diagram Description Updated"); + + assertThat(diagramDescription.getNodeDescriptions()).hasSize(2); + diagramDescription.getNodeDescriptions().forEach(nodeDescription -> { + //Test that UpdateLabelExpressionMigrationParticipant updated the label correctly + if (nodeDescription.getStyle() instanceof RectangularNodeStyleDescription) { + assertThat(nodeDescription.getInsideLabel().getLabelExpression()).isEqualTo("aql:'NodeWithoutImage'"); + } + if (nodeDescription.getStyle() instanceof ImageNodeStyleDescription) { + assertThat(nodeDescription.getOutsideLabels().get(0).getLabelExpression()).isEqualTo("aql:'NodeWithImage'"); + } + //Test that UpdateNodeDescriptionDomainTypeFeatureName updated the feature domainType name correctly + assertThat(nodeDescription.getDomainType()).isEqualTo("flow::CompositeProcessor"); + }); + + //Test that when serialized, the document is updated with the last MigrationParticipant metadata + Optional document = resourceToDocumentService.toDocument(jsonResource); + assertThat(document).isPresent(); + document.ifPresent(documentData -> { + var optionalMigrationData = resourceLoader.getMigrationDataFromDocument(documentData.document().getContent()); + assertThat(optionalMigrationData).isPresent(); + optionalMigrationData.ifPresent(migrationData -> { + assertThat(migrationData.getMigrationVersion()).isEqualTo(UpdateDiagramDescriptionNameValue.PARTICIPANT_VERSION); + assertThat(migrationData.getLastMigrationPerformed()).isEqualTo(UpdateDiagramDescriptionNameValue.class.getSimpleName()); + }); + + }); + } + } + + private static class UpdateLabelExpressionMigrationParticipant implements IMigrationParticipant { + + private static final String PARTICIPANT_VERSION = "2024.4.0"; + + @Override + public String getVersion() { + return PARTICIPANT_VERSION; + } + + @Override + public void getJSonObjectBeforeLoading(EObject eobject, JsonObject object) { + if (eobject instanceof NodeDescription nodeDescription) { + var nodeDescriptionData = object.getAsJsonObject("data"); + var labelExpression = nodeDescriptionData.get("labelExpression").getAsString(); + if (nodeDescription.getStyle() instanceof RectangularNodeStyleDescription) { + InsideLabelDescription insideLabelDescription = DiagramPackage.eINSTANCE.getDiagramFactory().createInsideLabelDescription(); + insideLabelDescription.setLabelExpression(labelExpression); + nodeDescription.setInsideLabel(insideLabelDescription); + } + if (nodeDescription.getStyle() instanceof ImageNodeStyleDescription) { + OutsideLabelDescription outsideLabelDescription = DiagramPackage.eINSTANCE.getDiagramFactory().createOutsideLabelDescription(); + outsideLabelDescription.setLabelExpression(labelExpression); + nodeDescription.getOutsideLabels().add(outsideLabelDescription); + } + } + } + + @Override + public EStructuralFeature getElement(EClass eClass, String oldFeatureName) { + if (eClass == DiagramPackage.eINSTANCE.getDiagramDescription()) { + if (oldFeatureName.equals("nodeDescriptionsWrong")) { + return DiagramPackage.eINSTANCE.getDiagramDescription_NodeDescriptions(); + } + } + + return IMigrationParticipant.super.getElement(eClass, oldFeatureName); + } + } + + private static class UpdateNodeDescriptionDomainTypeFeatureName implements IMigrationParticipant { + private static final String PARTICIPANT_VERSION = "2024.3.0"; + + @Override + public String getVersion() { + return PARTICIPANT_VERSION; + } + + @Override + public EStructuralFeature getElement(EClass eClass, String oldFeatureName) { + if (eClass == DiagramPackage.eINSTANCE.getNodeDescription()) { + if (oldFeatureName.equals("domainTypeWrong")) { + return DiagramPackage.eINSTANCE.getDiagramElementDescription_DomainType(); + } + } + return IMigrationParticipant.super.getElement(eClass, oldFeatureName); + } + } + + private static class UpdateDiagramDescriptionNameValue implements IMigrationParticipant { + private static final String PARTICIPANT_VERSION = "2024.5.0"; + + @Override + public String getVersion() { + return PARTICIPANT_VERSION; + } + + @Override + public Object getValue(EObject object, EStructuralFeature feature, Object value) { + if (object instanceof DiagramDescription) { + if(ViewPackage.eINSTANCE.getRepresentationDescription_Name() == feature) { + if (value.equals("Diagram Description")) { + return value = "Diagram Description Updated"; + } + } + } + return null; + } + } + +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/initializeMigrationParticipant.sql b/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/initializeMigrationParticipant.sql new file mode 100644 index 0000000000..b2aeadf241 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/initializeMigrationParticipant.sql @@ -0,0 +1,52 @@ +-- Sample studio project +INSERT INTO project ( + id, + name, + created_on, + last_modified_on +) VALUES ( + '01234836-0902-418a-900a-4c0afd20323e', + 'Studio', + '2024-01-01 9:42:0.000', + '2024-01-02 9:42:0.000' +); +INSERT INTO nature ( + project_id, + name +) VALUES ( + '01234836-0902-418a-900a-4c0afd20323e', + 'siriusComponents://nature?kind=studio' +); +INSERT INTO semantic_data ( + id, + project_id, + created_on, + last_modified_on +) VALUES ( + 'e344d967-a639-4f6c-9c00-a466d51063c6', + '01234836-0902-418a-900a-4c0afd20323e', + '2024-01-01 9:42:0.000', + '2024-01-02 9:42:0.000' +); +INSERT INTO semantic_data_domain ( + semantic_data_id, + uri +) VALUES ( + 'e344d967-a639-4f6c-9c00-a466d51063c6', + 'http://www.eclipse.org/sirius-web/view' +); +INSERT INTO document ( + id, + semantic_data_id, + name, + content, + created_on, + last_modified_on +) VALUES ( + 'ed2a5355-991d-458f-87f1-ea3a18b1f104', + 'e344d967-a639-4f6c-9c00-a466d51063c6', + 'Flow View', + '{"json":{"version":"1.0","encoding":"utf-8"},"ns":{"diagram":"http://www.eclipse.org/sirius-web/diagram","view":"http://www.eclipse.org/sirius-web/view"},"content":[{"id":"9674f8f7-ff1a-4061-bb32-a4a235a9c2ca","eClass":"view:View","data":{"descriptions":[{"id":"22fb1f4d-109d-4e73-bff0-f7cd96fb5fbb","eClass":"diagram:DiagramDescription","data":{"name":"Diagram Description","domainType":"flow::System","nodeDescriptionsWrong":[{"id":"6949ddfe-f480-473b-bc8a-6f2bdde07e4d","eClass":"diagram:NodeDescription","data":{"name":"NodeWithoutImage","domainTypeWrong":"flow::CompositeProcessor","labelExpression":"aql:''NodeWithoutImage''","childrenLayoutStrategy":{"id":"20651d93-2ee5-41cb-b2bd-1e75958c73cf","eClass":"diagram:FreeFormLayoutStrategyDescription"},"style":{"id":"92fe9d3f-2c5b-41ab-81ea-8482c8cd57b9","eClass":"diagram:RectangularNodeStyleDescription","data":{"color":"view:FixedColor 1952d117-7d88-32c4-a839-3858e5e779ae#//@colorPalettes.1/@colors.1","borderColor":"view:FixedColor 1952d117-7d88-32c4-a839-3858e5e779ae#//@colorPalettes.1/@colors.0","labelColor":"view:FixedColor 1952d117-7d88-32c4-a839-3858e5e779ae#//@colorPalettes.1/@colors.0"}}}},{"id":"88f95390-ac15-4381-ae82-7b23a2017bd4","eClass":"diagram:NodeDescription","data":{"name":"NodeWithImage","domainTypeWrong":"flow::CompositeProcessor","labelExpression":"aql:''NodeWithImage''","childrenLayoutStrategy":{"id":"db92e810-5d51-4d0c-a6cf-6a1e00cea6d9","eClass":"diagram:FreeFormLayoutStrategyDescription"},"style":{"id":"fa35806a-bdd0-4ba7-81bf-8cfba8f8fef5","eClass":"diagram:ImageNodeStyleDescription","data":{"color":"view:FixedColor 1952d117-7d88-32c4-a839-3858e5e779ae#//@colorPalettes.1/@colors.1","borderColor":"view:FixedColor 1952d117-7d88-32c4-a839-3858e5e779ae#//@colorPalettes.1/@colors.0","labelColor":"view:FixedColor 1952d117-7d88-32c4-a839-3858e5e779ae#//@colorPalettes.1/@colors.0","shape":"ae1a67e3-d455-3198-9f35-372ef2a3006d"}}}}]}}]}}]}', + '2024-01-01 9:42:0.000', + '2024-01-02 9:42:0.000' +); \ No newline at end of file