diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c51b6b8267..3764ea6bf8 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -93,6 +93,8 @@ image:doc/screenshots/inside_outside_labels.png[Isinde outside label example, 70 - https://github.com/eclipse-sirius/sirius-web/issues/3371[#3371] [diagram] Add new predicates in `NodeDescription` to specify whether a node is hidden/faded/normal by default. - https://github.com/eclipse-sirius/sirius-web/issues/3372[#3372] [diagram] Add services in `DiagramServices` to hide, reveal, fade, and unfade nodes from AQL expressions and Java services. - https://github.com/eclipse-sirius/sirius-web/issues/2839[#2839] [gantt] Add support for persisting task expand/collapse state +- https://github.com/eclipse-sirius/sirius-web/issues/3333[#3333] [emf] Add support for the migration of semantic data + === Improvements @@ -114,6 +116,7 @@ They still support returning an `java.time.Instant` object directly. - https://github.com/eclipse-sirius/sirius-web/issues/3377[#3377] [diagram] Only synchronize selection with explorer at the end of the process - https://github.com/eclipse-sirius/sirius-web/issues/3359[#3359] Add report on document upload - https://github.com/eclipse-sirius/sirius-web/issues/3405[#3405] [diagram] Display layout tools vertically in group palette +- https://github.com/eclipse-sirius/sirius-web/issues/3333[#3333] [emf] Store the last migration performed in a document == v2024.3.0 diff --git a/packages/emf/backend/sirius-components-emf/pom.xml b/packages/emf/backend/sirius-components-emf/pom.xml index 86f99860eb..a2eb61ca88 100644 --- a/packages/emf/backend/sirius-components-emf/pom.xml +++ b/packages/emf/backend/sirius-components-emf/pom.xml @@ -102,7 +102,7 @@ org.eclipse.sirius.emfjson org.eclipse.sirius.emfjson - 2.3.7-SNAPSHOT + 2.3.8-SNAPSHOT org.springframework.boot 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..a08fb45f57 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.migration.api.MigrationData; /** * An EMF adapter used to store some metadata related to EMF Resources. @@ -33,6 +34,8 @@ public class ResourceMetadataAdapter implements Adapter { private String name; + private MigrationData migrationData; + private Notifier notifier; public ResourceMetadataAdapter(String name) { @@ -47,6 +50,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/migration/MigrationService.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/MigrationService.java new file mode 100644 index 0000000000..4f1bf95b9c --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/MigrationService.java @@ -0,0 +1,177 @@ +/******************************************************************************* + * 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.migration; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +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.emf.ecore.util.BasicExtendedMetaData; +import org.eclipse.sirius.components.emf.migration.api.IMigrationParticipant; +import org.eclipse.sirius.components.emf.migration.api.MigrationData; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; +import org.eclipse.sirius.emfjson.resource.JsonResource; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +/** + * Specialized BasicExtendedMetaData. + * + * @author mcharfadi + */ +public class MigrationService extends BasicExtendedMetaData implements JsonResource.IJsonResourceProcessor { + private MigrationData documentMigrationData; + + private final List migrationParticipants; + + public MigrationService(List migrationParticipants) { + this.migrationParticipants = Objects.requireNonNull(migrationParticipants); + this.documentMigrationData = new MigrationData("none", "0"); + } + + private boolean isCandidateVersion(IMigrationParticipant migrationParticipant) { + return migrationParticipant.getVersion().compareTo(this.documentMigrationData.migrationVersion()) > 0; + } + + @Override + public EStructuralFeature getElement(EClass eClass, String namespace, String eStructuralFeatureName) { + EStructuralFeature structuralFeature = eClass.getEStructuralFeature(eStructuralFeatureName); + var migrationParticipantsCandidate = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidate) { + EStructuralFeature newStructuralFeature = migrationParticipant.getEStructuralFeature(eClass, eStructuralFeatureName); + 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.getEClassifier(ePackage, typeName); + if (newEClassifier != null) { + eClassifier = newEClassifier; + } + } + return eClassifier; + } + + @Override + public EPackage getPackage(String nsURI) { + EPackage ePackage = super.getPackage(nsURI); + var migrationParticipantsCandidate = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidate) { + EPackage newEPackage = migrationParticipant.getPackage(nsURI); + if (newEPackage != null) { + ePackage = newEPackage; + } + } + return ePackage; + } + + @Override + public void preDeserialization(JsonResource resource, JsonObject jsonObject) { + this.setMigrationDataFromDocumentContent(jsonObject); + this.setResourceMetadataAdapterMigrationData(resource, this.documentMigrationData); + var migrationParticipantsCandidates = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidates) { + migrationParticipant.preDeserialization(resource, jsonObject); + setResourceMetadataAdapterMigrationData(resource, new MigrationData(migrationParticipant.getClass().getSimpleName(), migrationParticipant.getVersion())); + } + } + + @Override + public void postSerialization(JsonResource resource, JsonObject jsonObject) { + getOptionalResourceMetadataAdapter(resource).ifPresent(resourceMetadataAdapter -> { + if (resourceMetadataAdapter.getMigrationData() != null) { + var rootMigration = new Gson().toJsonTree(resourceMetadataAdapter.getMigrationData(), MigrationData.class); + if (rootMigration != null) { + 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 postObjectLoading(EObject eObject, JsonObject jsonObject, boolean isTopObject) { + var migrationParticipantsCandidates = migrationParticipants.stream() + .filter(this::isCandidateVersion) + .sorted(Comparator.comparing(IMigrationParticipant::getVersion)) + .toList(); + for (IMigrationParticipant migrationParticipant : migrationParticipantsCandidates) { + migrationParticipant.postObjectLoading(eObject, jsonObject); + } + } + + @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(); + } + + private void setResourceMetadataAdapterMigrationData(JsonResource resource, MigrationData migrationData) { + getOptionalResourceMetadataAdapter(resource).ifPresent(resourceMetadataAdapter -> resourceMetadataAdapter.setMigrationData(migrationData)); + } + + private void setMigrationDataFromDocumentContent(JsonObject jsonObject) { + var optionalMigrationData = Optional.ofNullable(jsonObject.getAsJsonObject(MigrationData.JSON_OBJECT_ROOT)) + .map(migrationRootElement -> new Gson().fromJson(migrationRootElement, MigrationData.class)).stream().findFirst(); + optionalMigrationData.ifPresent(migrationData -> this.documentMigrationData = migrationData); + } +} diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/api/IMigrationParticipant.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/api/IMigrationParticipant.java new file mode 100644 index 0000000000..686c9ed15d --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/api/IMigrationParticipant.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * 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.migration.api; + +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 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 after loading an eObject. As such the eObject will have all his + * features set. The jsonObject is the one that was used to set the features and can be used to retrieve values + * of unknown features. + * + * @param eObject + * the eObject that have been loaded. + * @param jsonObject + * the jsonObject used to load the eObject. + */ + default void postObjectLoading(EObject eObject, JsonObject jsonObject) { + + } + + /** + * Called during the parsing of JsonResources (at loading time). If a feature value has changed since a previous + * version, use this method to return the correct expected value or null if it did not change. + * + * @param eObject + * 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 eObject, 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 eStructuralFeatureName + * 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 getEStructuralFeature(EClass eClass, String eStructuralFeatureName) { + return eClass.getEStructuralFeature(eStructuralFeatureName); + } + + /** + * 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 getEClassifier(EPackage ePackage, String typeName) { + return ePackage.getEClassifier(typeName); + } + + /** + * Return the EPackage to use for the given namespace. + * + * @param nsURI + * the nsURI of the package we are looking for. + * @return an EPackage if some new mapping exists, null otherwise. + */ + default EPackage getPackage(String nsURI) { + return null; + } +} diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/api/MigrationData.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/api/MigrationData.java new file mode 100644 index 0000000000..364cb0a729 --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/migration/api/MigrationData.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.components.emf.migration.api; + +/** + * POJO for MigrationData. + * + * @author mcharfadi + */ +public record MigrationData(String lastMigrationPerformed, String migrationVersion) { + + public static final String JSON_OBJECT_ROOT = "migration"; + + @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/document/services/DocumentSanitizedJsonContentProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/DocumentSanitizedJsonContentProvider.java index 84b19496b5..af842f3934 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/DocumentSanitizedJsonContentProvider.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/DocumentSanitizedJsonContentProvider.java @@ -25,6 +25,8 @@ import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.sirius.components.emf.migration.MigrationService; +import org.eclipse.sirius.components.emf.migration.api.IMigrationParticipant; import org.eclipse.sirius.components.emf.services.JSONResourceFactory; import org.eclipse.sirius.emfjson.resource.JsonResource; import org.eclipse.sirius.web.application.document.services.api.IDocumentSanitizedJsonContentProvider; @@ -45,8 +47,11 @@ public class DocumentSanitizedJsonContentProvider implements IDocumentSanitizedJ private final List externalResourceLoaderServices; - public DocumentSanitizedJsonContentProvider(List externalResourceLoaderServices) { + private final List migrationParticipants; + + public DocumentSanitizedJsonContentProvider(List externalResourceLoaderServices, List migrationParticipants) { this.externalResourceLoaderServices = Objects.requireNonNull(externalResourceLoaderServices); + this.migrationParticipants = migrationParticipants; } @Override @@ -63,10 +68,13 @@ public Optional getContent(ResourceSet resourceSet, String name, InputSt ouputResource.getContents().addAll(inputResource.getContents()); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + var migrationExtendedMetaData = new MigrationService(this.migrationParticipants); Map saveOptions = new HashMap<>(); saveOptions.put(JsonResource.OPTION_ENCODING, JsonResource.ENCODING_UTF_8); saveOptions.put(JsonResource.OPTION_SCHEMA_LOCATION, Boolean.TRUE); saveOptions.put(JsonResource.OPTION_ID_MANAGER, new EObjectRandomIDManager()); + saveOptions.put(JsonResource.OPTION_EXTENDED_META_DATA, migrationExtendedMetaData); + saveOptions.put(JsonResource.OPTION_JSON_RESSOURCE_PROCESSOR, migrationExtendedMetaData); ouputResource.save(outputStream, saveOptions); diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JSONExternalResourceLoaderService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JSONExternalResourceLoaderService.java index 8075c2b5d8..d43b7edcb1 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JSONExternalResourceLoaderService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/document/services/JSONExternalResourceLoaderService.java @@ -19,12 +19,17 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.sirius.components.emf.migration.MigrationService; +import org.eclipse.sirius.components.emf.migration.api.IMigrationParticipant; import org.eclipse.sirius.components.emf.services.JSONResourceFactory; +import org.eclipse.sirius.emfjson.resource.JsonResource; import org.eclipse.sirius.web.application.document.services.api.IExternalResourceLoaderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +45,12 @@ public class JSONExternalResourceLoaderService implements IExternalResourceLoade private final Logger logger = LoggerFactory.getLogger(JSONExternalResourceLoaderService.class); + private final List migrationParticipants; + + public JSONExternalResourceLoaderService(List migrationParticipants) { + this.migrationParticipants = migrationParticipants; + } + @Override public boolean canHandle(InputStream inputStream, URI resourceURI, ResourceSet resourceSet) { boolean canHandle = false; @@ -61,9 +72,14 @@ public boolean canHandle(InputStream inputStream, URI resourceURI, ResourceSet r public Optional getResource(InputStream inputStream, URI resourceURI, ResourceSet resourceSet) { Resource resource = null; try { + var migrationService = new MigrationService(this.migrationParticipants); + Map options = new HashMap<>(); + options.put(JsonResource.OPTION_JSON_RESSOURCE_PROCESSOR, migrationService); + options.put(JsonResource.OPTION_EXTENDED_META_DATA, migrationService); + var jsonResource = new JSONResourceFactory().createResource(resourceURI); resourceSet.getResources().add(jsonResource); - jsonResource.load(inputStream, new HashMap<>()); + jsonResource.load(inputStream, options); resource = jsonResource; } catch (IOException exception) { diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/participants/NodeDescriptionLabelExpressionMigrationParticipant.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/participants/NodeDescriptionLabelExpressionMigrationParticipant.java new file mode 100644 index 0000000000..430acf7d7a --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/migration/participants/NodeDescriptionLabelExpressionMigrationParticipant.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * 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.participants; + +import java.util.Optional; + +import org.eclipse.emf.ecore.EObject; +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.components.emf.migration.api.IMigrationParticipant; + +import com.google.gson.JsonObject; +import org.springframework.stereotype.Service; + +/** + * MigrationParticipant that update the LabelExpression of NodeDescription with + * InsideLabelDescription or OutsideLabelDescription. + * + * @author mcharfadi + */ +@Service +public class NodeDescriptionLabelExpressionMigrationParticipant implements IMigrationParticipant { + + private static final String PARTICIPANT_VERSION = "2024.4.0"; + + @Override + public String getVersion() { + return PARTICIPANT_VERSION; + } + + @Override + public void postObjectLoading(EObject eObject, JsonObject jsonObject) { + if (eObject instanceof NodeDescription nodeDescription) { + var optionalNodeDescriptionData = Optional.ofNullable(jsonObject.getAsJsonObject("data")); + optionalNodeDescriptionData.ifPresent(nodeDescriptionData -> { + var optionalLabelExpression = Optional.ofNullable(nodeDescriptionData.get("labelExpression")); + optionalLabelExpression.ifPresent(labelExpression -> { + if (nodeDescription.getStyle() instanceof RectangularNodeStyleDescription) { + InsideLabelDescription insideLabelDescription = DiagramPackage.eINSTANCE.getDiagramFactory().createInsideLabelDescription(); + insideLabelDescription.setLabelExpression(labelExpression.getAsString()); + nodeDescription.setInsideLabel(insideLabelDescription); + } + if (nodeDescription.getStyle() instanceof ImageNodeStyleDescription) { + OutsideLabelDescription outsideLabelDescription = DiagramPackage.eINSTANCE.getDiagramFactory().createOutsideLabelDescription(); + outsideLabelDescription.setLabelExpression(labelExpression.getAsString()); + nodeDescription.getOutsideLabels().add(outsideLabelDescription); + } + }); + }); + } + } +} \ No newline at end of file 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..4c6127064d 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,25 @@ 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.migration.api.MigrationData; import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; import org.eclipse.sirius.components.emf.services.JSONResourceFactory; +import org.eclipse.sirius.emfjson.resource.JsonResource; +import org.eclipse.sirius.components.emf.migration.api.IMigrationParticipant; +import org.eclipse.sirius.components.emf.migration.MigrationService; 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 +46,28 @@ 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 migrationExtendedMetaData = new MigrationService(this.migrationParticipants); + + HashMap options = new HashMap<>(); + options.put(JsonResource.OPTION_EXTENDED_META_DATA, migrationExtendedMetaData); + options.put(JsonResource.OPTION_JSON_RESSOURCE_PROCESSOR, migrationExtendedMetaData); + 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 +77,10 @@ public Optional toResource(ResourceSet resourceSet, String id, String return optionalResource; } + + public Optional getMigrationDataFromDocumentContent(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..17518131e1 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,6 +15,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -23,10 +24,13 @@ import org.eclipse.sirius.components.emf.services.EObjectIDManager; import org.eclipse.sirius.emfjson.resource.JsonResource; import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.components.emf.migration.api.IMigrationParticipant; +import org.eclipse.sirius.components.emf.migration.MigrationService; 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 +43,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 serializationListener = new JsonResourceSerializationListener(); + var migrationService = new MigrationService(this.migrationParticipants); 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, migrationService); + options.put(JsonResource.OPTION_EXTENDED_META_DATA, migrationService); Optional optionalDocumentData = Optional.empty(); try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { @@ -81,4 +94,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/data/MigrationIdentifiers.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/MigrationIdentifiers.java new file mode 100644 index 0000000000..fa57c7deea --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/MigrationIdentifiers.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.data; + +import java.util.UUID; + +/** + * Used to store some test identifiers related to the migration projects. + * + * @author mcharfadi + */ +public final class MigrationIdentifiers { + + public static final UUID MIGRATION_STUDIO = UUID.fromString("a3b86086-23f5-41cb-97b9-5ac7234a98af"); + + public static final String MIGRATION_STUDIO_DIAGRAM = "NodeDescription#labelExpression migration"; + + private MigrationIdentifiers() { + // Prevent instantiation + } +} 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..434214fe22 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/migration/MigrationParticipantTests.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * 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.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IEditingContextSearchService; +import org.eclipse.sirius.components.emf.ResourceMetadataAdapter; +import org.eclipse.sirius.components.graphql.api.UploadFile; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionInput; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionRunner; +import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionSuccessPayload; +import org.eclipse.sirius.components.view.diagram.DiagramDescription; +import org.eclipse.sirius.components.view.diagram.ImageNodeStyleDescription; +import org.eclipse.sirius.components.view.diagram.RectangularNodeStyleDescription; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.document.dto.UploadDocumentInput; +import org.eclipse.sirius.web.application.document.dto.UploadDocumentSuccessPayload; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.data.MigrationIdentifiers; +import org.eclipse.sirius.web.services.api.IGivenCommittedTransaction; +import org.eclipse.sirius.web.tests.graphql.UploadDocumentMutationRunner; + +import reactor.test.StepVerifier; + +import com.jayway.jsonpath.JsonPath; +import static org.assertj.core.api.Assertions.assertThat; +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.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests of NodeDescriptionLabelExpressionMigrationParticipant. + * + * @author mcharfadi + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class MigrationParticipantTests extends AbstractIntegrationTests { + + @Autowired + private IEditingContextSearchService editingContextSearchService; + + @Autowired + private IGivenCommittedTransaction givenCommittedTransaction; + + @Autowired + private ExecuteEditingContextFunctionRunner executeEditingContextFunctionRunner; + + @Autowired + private UploadDocumentMutationRunner uploadDocumentMutationRunner; + + @Test + @DisplayName("Given a project with an old model, NodeDescriptionLabelExpressionMigrationParticipant migrates the model correctly") + @Sql(scripts = {"/scripts/migration.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 givenAnOldModelMigrationParticipantCanBeContributedToUpdateTheModel() { + var optionalEditingContext = this.editingContextSearchService.findById(MigrationIdentifiers.MIGRATION_STUDIO.toString()); + assertThat(optionalEditingContext).isPresent(); + testIsMigrationSuccessful(optionalEditingContext.get()); + } + + @Test + @DisplayName("Given an uploaded project with an old model, NodeDescriptionLabelExpressionMigrationParticipant migrates the model correctly") + @Sql(scripts = {"/scripts/migration.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 givenAnOldViewDiagramMigrationServiceIsExecutedProperly() { + var content = """ + { + "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": "NodeDescription#labelExpression migration_upload", + "domainType": "flow::System", + "nodeDescriptions": [ + { + "id": "6949ddfe-f480-473b-bc8a-6f2bdde07e4d", + "eClass": "diagram:NodeDescription", + "data": { + "name": "NodeWithoutImage migration", + "domainType": "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" + } + } + }, + { + "id": "88f95390-ac15-4381-ae82-7b23a2017bd4", + "eClass": "diagram:NodeDescription", + "data": { + "name": "NodeWithImage migration", + "domainType": "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" + } + } + } + ] + } + } + ] + } + } + ] + } + """; + this.uploadDocument(MigrationIdentifiers.MIGRATION_STUDIO.toString(), "test_upload", content); + } + + private void uploadDocument(String editingContextId, String name, String content) { + this.givenCommittedTransaction.commit(); + + var file = new UploadFile(name, new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))); + var input = new UploadDocumentInput(UUID.randomUUID(), editingContextId, file); + var result = this.uploadDocumentMutationRunner.run(input); + + TestTransaction.flagForCommit(); + TestTransaction.end(); + + String typename = JsonPath.read(result, "$.data.uploadDocument.__typename"); + assertThat(typename).isEqualTo(UploadDocumentSuccessPayload.class.getSimpleName()); + + Predicate predicate = payload -> Optional.of(payload) + .map(ExecuteEditingContextFunctionSuccessPayload::result) + .filter(Boolean.class::isInstance) + .map(Boolean.class::cast) + .orElse(false); + + Function function = editingContext -> Optional.of(editingContext) + .filter(EditingContext.class::isInstance) + .map(EditingContext.class::cast) + .map(siriusWebEditingContext -> siriusWebEditingContext.getViews().stream() + .anyMatch(view -> view.eResource().eAdapters().stream() + .filter(adapter -> adapter instanceof ResourceMetadataAdapter) + .map(ResourceMetadataAdapter.class::cast) + .filter(resourceMetadataAdapter -> resourceMetadataAdapter.getMigrationData() != null) + .anyMatch(resourceMetadataAdapter -> resourceMetadataAdapter.getMigrationData().migrationVersion().equals("2024.4.0") + && resourceMetadataAdapter.getMigrationData().lastMigrationPerformed().equals("NodeDescriptionLabelExpressionMigrationParticipant")) + )) + .orElse(false); + + var mono = this.executeEditingContextFunctionRunner.execute(new ExecuteEditingContextFunctionInput(UUID.randomUUID(), editingContextId, function)); + StepVerifier.create(mono) + .expectNextMatches(predicate) + .thenCancel() + .verify(); + } + + private void testIsMigrationSuccessful(IEditingContext editingContext) { + if (editingContext instanceof EditingContext siriusWebEditingContext) { + var optionalDiagramDescription = siriusWebEditingContext.getViews().stream().flatMap(view -> view.getDescriptions().stream()).filter(representationDescription -> representationDescription.getName().equals(MigrationIdentifiers.MIGRATION_STUDIO_DIAGRAM)).findFirst(); + assertThat(optionalDiagramDescription).isPresent(); + assertThat(optionalDiagramDescription.get()).isInstanceOf(DiagramDescription.class); + optionalDiagramDescription.ifPresent(representationDescription -> { + if (representationDescription instanceof DiagramDescription diagramDescription) { + assertThat(diagramDescription.getNodeDescriptions()).hasSize(2); + diagramDescription.getNodeDescriptions().forEach(nodeDescription -> { + 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'"); + } + }); + } + }); + } + } + +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/migration.sql b/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/migration.sql new file mode 100644 index 0000000000..cc462e8342 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/migration.sql @@ -0,0 +1,119 @@ +-- Sample migration project +INSERT INTO project ( + id, + name, + created_on, + last_modified_on +) VALUES ( + 'a3b86086-23f5-41cb-97b9-5ac7234a98af', + 'Migration Studio', + '2024-01-01 9:42:0.000', + '2024-01-02 9:42:0.000' +); +INSERT INTO nature ( + project_id, + name +) VALUES ( + 'a3b86086-23f5-41cb-97b9-5ac7234a98af', + 'siriusComponents://nature?kind=studio' +); +INSERT INTO semantic_data ( + id, + project_id, + created_on, + last_modified_on +) VALUES ( + '89d67892-0cc9-4ca4-b30e-28688470c0d4', + 'a3b86086-23f5-41cb-97b9-5ac7234a98af', + '2024-01-01 9:42:0.000', + '2024-01-02 9:42:0.000' +); +INSERT INTO semantic_data_domain ( + semantic_data_id, + uri +) VALUES ( + '89d67892-0cc9-4ca4-b30e-28688470c0d4', + 'http://www.eclipse.org/sirius-web/view' +); +INSERT INTO semantic_data_domain ( + semantic_data_id, + uri +) VALUES ( + '89d67892-0cc9-4ca4-b30e-28688470c0d4', + 'http://www.eclipse.org/sirius-web/diagram' +); +INSERT INTO document ( + id, + semantic_data_id, + name, + content, + created_on, + last_modified_on +) VALUES ( + '1d8aac3e-5fe7-4787-b0fc-1f8eb491cd5e', + '89d67892-0cc9-4ca4-b30e-28688470c0d4', + 'NodeDescription#labelExpression migration', + '{ + "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": "NodeDescription#labelExpression migration", + "domainType": "flow::System", + "nodeDescriptions": [ + { + "id": "6949ddfe-f480-473b-bc8a-6f2bdde07e4d", + "eClass": "diagram:NodeDescription", + "data": { + "name": "NodeWithoutImage migration", + "domainType": "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" + } + } + }, + { + "id": "88f95390-ac15-4381-ae82-7b23a2017bd4", + "eClass": "diagram:NodeDescription", + "data": { + "name": "NodeWithImage migration", + "domainType": "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" + } + } + } + ] + } + } + ] + } + } + ] + }', + '2024-01-01 9:42:0.000', + '2024-01-02 9:42:0.000' +); \ No newline at end of file diff --git a/scripts/check-coverage.jsh b/scripts/check-coverage.jsh index 07241ac476..92193395c6 100644 --- a/scripts/check-coverage.jsh +++ b/scripts/check-coverage.jsh @@ -69,7 +69,7 @@ var moduleCoverageData = List.of( new ModuleCoverage("sirius-components-collaborative-portals", 60.0), new ModuleCoverage("sirius-components-portals-graphql", 59.0), new ModuleCoverage("sirius-components-interpreter", 80.0), - new ModuleCoverage("sirius-components-emf", 82.0), + new ModuleCoverage("sirius-components-emf", 83.0), new ModuleCoverage("sirius-components-compatibility", 55.0), new ModuleCoverage("sirius-components-compatibility-emf", 78.0), new ModuleCoverage("sirius-components-graphql", 54.0),