diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 4853a7d32..c37ff254a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -96,6 +96,7 @@ Both tools optionally allow selecting an existing `StateUsage` to exhibit. - https://github.com/eclipse-syson/syson/issues/1979[#1979] [diagrams] Add tools to create timeslices and snapshots, available on `OccurrenceUsage`, `ItemUsage`, and `PartUsage` graphical nodes. These tools leverage a selection dialog to either create a specific timeslice/snapshot graphical node or, if no selection is made, default to a timeslice/snapshot `OccurrenceUsage` graphical node. - https://github.com/eclipse-syson/syson/issues/2176[#2176] [metamodel] Implements `ownedConcern` and `referencedConcern` of `FramedConcernMembership`. +- https://github.com/eclipse-syson/syson/issues/2174[#2174] [diagrams] Add a new edge tool creating a _frame_ graphical edge between a `RequirementUsage` or a `RequirementDefinition` graphical node, and a `ConcernUsage` graphical node. == v2026.3.0 diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVFramedConcernTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVFramedConcernTests.java new file mode 100644 index 000000000..5b4c7cf86 --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVFramedConcernTests.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (c) 2026 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.syson.application.controllers.diagrams.general.view; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat; +import static org.eclipse.sirius.components.diagrams.tests.assertions.DiagramInstanceOfAssertFactories.EDGE; +import static org.eclipse.sirius.components.diagrams.tests.assertions.DiagramInstanceOfAssertFactories.EDGE_STYLE; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramEventInput; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramRefreshedEventPayload; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.diagrams.ArrowStyle; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.Edge; +import org.eclipse.sirius.components.diagrams.Label; +import org.eclipse.sirius.components.diagrams.ViewModifier; +import org.eclipse.sirius.components.view.emf.diagram.IDiagramIdProvider; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.eclipse.syson.AbstractIntegrationTests; +import org.eclipse.syson.GivenSysONServer; +import org.eclipse.syson.application.controller.editingcontext.checkers.SemanticCheckerService; +import org.eclipse.syson.application.controllers.diagrams.testers.EdgeCreationTester; +import org.eclipse.syson.application.data.GeneralViewWithTopNodesTestProjectData; +import org.eclipse.syson.services.SemanticRunnableFactory; +import org.eclipse.syson.services.diagrams.DiagramComparator; +import org.eclipse.syson.services.diagrams.DiagramDescriptionIdProvider; +import org.eclipse.syson.services.diagrams.api.IGivenDiagramDescription; +import org.eclipse.syson.services.diagrams.api.IGivenDiagramSubscription; +import org.eclipse.syson.standard.diagrams.view.SDVDescriptionNameGenerator; +import org.eclipse.syson.sysml.FramedConcernMembership; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.helper.LabelConstants; +import org.eclipse.syson.util.IDescriptionNameGenerator; +import org.eclipse.syson.util.SysONRepresentationDescriptionIdentifiers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +/** + * {@link org.eclipse.syson.sysml.FramedConcernMembership} related test in General View. + * + * @author gcoutable + */ +@Transactional +@GivenSysONServer({ GeneralViewWithTopNodesTestProjectData.SCRIPT_PATH }) +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GVFramedConcernTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private IGivenDiagramSubscription givenDiagramSubscription; + + @Autowired + private IGivenDiagramDescription givenDiagramDescription; + + @Autowired + private IDiagramIdProvider diagramIdProvider; + + @Autowired + private DiagramComparator diagramComparator; + + @Autowired + private SemanticRunnableFactory semanticRunnableFactory; + + @Autowired + private IObjectSearchService objectSearchService; + + @Autowired + private EdgeCreationTester edgeCreationTester; + + private final IDescriptionNameGenerator descriptionNameGenerator = new SDVDescriptionNameGenerator(); + + private SemanticCheckerService semanticCheckerService; + + private Flux givenSubscriptionToDiagram() { + var diagramEventInput = new DiagramEventInput(UUID.randomUUID(), GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, GeneralViewWithTopNodesTestProjectData.GraphicalIds.DIAGRAM_ID); + return this.givenDiagramSubscription.subscribe(diagramEventInput); + } + + @BeforeEach + public void setUp() { + this.givenInitialServerState.initialize(); + this.semanticCheckerService = new SemanticCheckerService(this.semanticRunnableFactory, this.objectSearchService, GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, + GeneralViewWithTopNodesTestProjectData.SemanticIds.PACKAGE_1_ID); + } + + @Test + public void testCreateFramedConcernWithEdgeBetweenRequirementUsageAndConcernUsage() { + this.createFramedConcernWithEdge(SysmlPackage.eINSTANCE.getRequirementUsage(), GeneralViewWithTopNodesTestProjectData.GraphicalIds.REQUIREMENT_USAGE_ID, "requirement"); + } + + @Test + public void testCreateFramedConcernWithEdgeBetweenRequirementDefinitionAndConcernUsage() { + this.createFramedConcernWithEdge(SysmlPackage.eINSTANCE.getRequirementDefinition(), GeneralViewWithTopNodesTestProjectData.GraphicalIds.REQUIREMENT_DEFINITION_ID, "RequirementDefinition"); + } + + private void createFramedConcernWithEdge(EClass parentEClass, String graphicalSourceNodeId, String parentLabel) { + var flux = this.givenSubscriptionToDiagram(); + + var diagramDescription = this.givenDiagramDescription.getDiagramDescription(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, + SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID); + var diagramDescriptionIdProvider = new DiagramDescriptionIdProvider(diagramDescription, this.diagramIdProvider); + + var edgeCreationToolId = diagramDescriptionIdProvider.getEdgeCreationToolId( + this.descriptionNameGenerator.getNodeName(parentEClass), + "New framed Concern"); + + AtomicReference diagram = new AtomicReference<>(); + Consumer initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set); + + Runnable createEdgeRunnable = () -> this.edgeCreationTester.createEdgeUsingNodeId(GeneralViewWithTopNodesTestProjectData.EDITING_CONTEXT_ID, + diagram, + graphicalSourceNodeId, + GeneralViewWithTopNodesTestProjectData.GraphicalIds.CONCERN_USAGE_ID, + edgeCreationToolId); + + Consumer diagramContentConsumerAfterNewEdge = assertRefreshedDiagramThat(newDiagram -> { + var newEdges = this.diagramComparator.newEdges(diagram.get(), newDiagram); + var newVisibleEdges = newEdges.stream().filter(edge -> edge.getState() != ViewModifier.Hidden).toList(); + + assertThat(newVisibleEdges).hasSize(1).first(EDGE) + .hasSourceId(graphicalSourceNodeId) + .hasTargetId(GeneralViewWithTopNodesTestProjectData.GraphicalIds.CONCERN_USAGE_ID) + .extracting(Edge::getCenterLabel) + .extracting(Label::text) + .hasToString(LabelConstants.OPEN_QUOTE + LabelConstants.FRAME + LabelConstants.CLOSE_QUOTE); + + assertThat(newVisibleEdges).hasSize(1).first(EDGE) + .extracting(Edge::getStyle, EDGE_STYLE) + .hasSourceArrow(ArrowStyle.None) + .hasTargetArrow(ArrowStyle.InputArrow); + }); + + Consumer additionalCheck = object -> { + assertThat(object).isInstanceOf(List.class) + .asInstanceOf(type(List.class)) + .satisfies(framedConcerns -> { + assertThat((List) framedConcerns).size().isEqualTo(1); + assertThat(framedConcerns.getFirst()) + .isInstanceOf(FramedConcernMembership.class) + .asInstanceOf(type(FramedConcernMembership.class)) + .satisfies(framedConcernMembership -> { + var ownedConcern = framedConcernMembership.getOwnedConcern(); + var ownedSubsetting = ownedConcern.getOwnedSubsetting(); + assertThat(ownedSubsetting).isNotEmpty(); + assertThat(ownedSubsetting.getFirst().getSubsettedFeature()).isEqualTo(framedConcernMembership.getReferencedConcern()); + }); + }); + }; + + Runnable semanticCheck = this.semanticCheckerService.checkEditingContext(this.semanticCheckerService.getElementInParentSemanticChecker(parentLabel, SysmlPackage.eINSTANCE.getElement_OwnedRelationship(), SysmlPackage.eINSTANCE.getFramedConcernMembership(), additionalCheck)); + + StepVerifier.create(flux) + .consumeNextWith(initialDiagramContentConsumer) + .then(createEdgeRunnable) + .consumeNextWith(diagramContentConsumerAfterNewEdge) + .then(semanticCheck) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } +} diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/testers/EdgeCreationTester.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/testers/EdgeCreationTester.java index 073bf50e1..456318a2e 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/testers/EdgeCreationTester.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/testers/EdgeCreationTester.java @@ -47,6 +47,7 @@ public class EdgeCreationTester { @Autowired private InvokeSingleClickOnTwoDiagramElementsToolMutationRunner invokeSingleClickOnTwoDiagramElementsToolMutationRunner; + @Deprecated public void createEdge(String projectId, AtomicReference diagram, String sourceNodeTargetObjectLabel, String targetNodeTargetObjectLabel, String toolId) { DiagramNavigator diagramNavigator = new DiagramNavigator(diagram.get()); String sourceId = diagramNavigator.nodeWithTargetObjectLabel(sourceNodeTargetObjectLabel).getNode().getId(); diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java index f5f41aa8d..66abadd3a 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewWithTopNodesTestProjectData.java @@ -28,23 +28,27 @@ public class GeneralViewWithTopNodesTestProjectData { */ public static class GraphicalIds { + public static final String ACTION_USAGE_ID = "61aaf64a-4fbc-356e-ba73-4bd47b386989"; + + public static final String ATTRIBUTE_USAGE_ID = "7b8e6835-c563-35cd-8991-e2f894fc2139"; + + public static final String CONCERN_USAGE_ID = "0999b8c3-d37c-3644-a1d6-b9777a499d11"; + public static final String DIAGRAM_ID = "fa8c8a8d-2813-404c-876f-c04e8b297134"; + public static final String ITEM_DEFINITION_ID = "df3542d9-6314-3da5-993c-a296f4042674"; + public static final String PART_USAGE_ID = "4c4fe0d5-4974-377e-9113-9ab022c75f8c"; public static final String PART_DEFINITION_ID = "fa617798-658e-3812-92f2-52e2fc39f851"; public static final String PART_DEFINITION_TEXTUAL_REP_ID = "3a992e49-95fa-384a-bb54-47284825bf17"; - public static final String ACTION_USAGE_ID = "61aaf64a-4fbc-356e-ba73-4bd47b386989"; - - public static final String STATE_USAGE_ID = "1541c013-2cc7-3dd7-a39f-6e33d07b411e"; - - public static final String ATTRIBUTE_USAGE_ID = "7b8e6835-c563-35cd-8991-e2f894fc2139"; + public static final String REQUIREMENT_USAGE_ID = "3eea9d01-7033-3f31-b7ef-561b5bc86d10"; - public static final String ITEM_DEFINITION_ID = "df3542d9-6314-3da5-993c-a296f4042674"; + public static final String REQUIREMENT_DEFINITION_ID = "b83f2b54-1602-382d-beb1-c500e08a8684"; - public static final String REQUIREMENT_USAGE_ID = "3eea9d01-7033-3f31-b7ef-561b5bc86d10"; + public static final String STATE_USAGE_ID = "1541c013-2cc7-3dd7-a39f-6e33d07b411e"; } /** diff --git a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/helper/LabelConstants.java b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/helper/LabelConstants.java index 034db8269..ab7a2e91b 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/helper/LabelConstants.java +++ b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/helper/LabelConstants.java @@ -49,6 +49,8 @@ public class LabelConstants { public static final String EQUAL = SysMLv2Keywords.EQUALS; + public static final String FRAME = SysMLv2Keywords.FRAME; + public static final String GREATER_THAN = SysMLv2Keywords.GREATER_THAN; public static final String IN = SysMLv2Keywords.IN; diff --git a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/impl/FramedConcernMembershipImpl.java b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/impl/FramedConcernMembershipImpl.java index 7433487e5..a2f3fcec3 100644 --- a/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/impl/FramedConcernMembershipImpl.java +++ b/backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/impl/FramedConcernMembershipImpl.java @@ -91,7 +91,7 @@ public ConcernUsage getReferencedConcern() { /** * * - * @generated + * @generated NOT */ public ConcernUsage basicGetReferencedConcern() { var referencedConstraint = super.getReferencedConstraint(); diff --git a/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/ModelMutationElementService.java b/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/ModelMutationElementService.java index 09da1c24e..4a90a2a22 100644 --- a/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/ModelMutationElementService.java +++ b/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/ModelMutationElementService.java @@ -18,15 +18,19 @@ import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.sirius.components.emf.utils.SiriusEMFCopier; +import org.eclipse.syson.sysml.ConcernUsage; import org.eclipse.syson.sysml.Definition; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.FeatureTyping; +import org.eclipse.syson.sysml.FramedConcernMembership; import org.eclipse.syson.sysml.Namespace; import org.eclipse.syson.sysml.PartUsage; import org.eclipse.syson.sysml.Relationship; +import org.eclipse.syson.sysml.RequirementDefinition; import org.eclipse.syson.sysml.RequirementUsage; import org.eclipse.syson.sysml.SatisfyRequirementUsage; import org.eclipse.syson.sysml.SysmlFactory; +import org.eclipse.syson.sysml.Type; import org.eclipse.syson.sysml.ViewDefinition; import org.eclipse.syson.sysml.ViewUsage; import org.eclipse.syson.sysml.helper.EMFUtils; @@ -199,6 +203,33 @@ public SatisfyRequirementUsage createSatisfy(Element element, RequirementUsage e return newSatisfyRequirementUsage; } + /** + * In a {@link RequirementDefinition}, or a {@link RequirementUsage}, creates {@link FramedConcernMembership} containing a {@link ConcernUsage} subsetted by reference the given concernUsage. + * + * @param type The type that will hold the {@link FramedConcernMembership}, must be a {@link RequirementDefinition}, or a {@link RequirementUsage} + * @param concernUsage The {@link ConcernUsage} subsetted by reference + * @return The created {@link FramedConcernMembership} when type is a {@link RequirementDefinition}, or a {@link RequirementUsage}, {@code null} otherwise + */ + public FramedConcernMembership createFramedConcern(Type type, ConcernUsage concernUsage) { + if (type instanceof RequirementDefinition || type instanceof RequirementUsage) { + var newFramedConcernMembership = SysmlFactory.eINSTANCE.createFramedConcernMembership(); + type.getOwnedRelationship().add(newFramedConcernMembership); + + var newConcernUsage = SysmlFactory.eINSTANCE.createConcernUsage(); + newFramedConcernMembership.getOwnedRelatedElement().add(newConcernUsage); + + var newReferenceSubsetting = SysmlFactory.eINSTANCE.createReferenceSubsetting(); + newConcernUsage.getOwnedRelationship().add(newReferenceSubsetting); + newReferenceSubsetting.setReferencedFeature(concernUsage); + + this.metamodelMutationElementService.initialize(newFramedConcernMembership); + this.metamodelMutationElementService.initialize(newConcernUsage); + this.metamodelMutationElementService.initialize(newReferenceSubsetting); + return newFramedConcernMembership; + } + return null; + } + /** * Creates a {@link SatisfyRequirementUsage SatisfyRequirement} on {@code parentElement}. *

diff --git a/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelMutationAQLService.java b/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelMutationAQLService.java index 2e9e3714a..c6fc77dfe 100644 --- a/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelMutationAQLService.java +++ b/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelMutationAQLService.java @@ -17,12 +17,14 @@ import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.syson.model.services.ModelMutationElementService; +import org.eclipse.syson.sysml.ConcernUsage; import org.eclipse.syson.sysml.Documentation; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.Membership; import org.eclipse.syson.sysml.OccurrenceUsage; import org.eclipse.syson.sysml.PartUsage; import org.eclipse.syson.sysml.RequirementUsage; +import org.eclipse.syson.sysml.Type; import org.eclipse.syson.sysml.ViewUsage; import org.eclipse.syson.sysml.metamodel.services.MetamodelMutationElementService; @@ -79,7 +81,7 @@ public Element createPartUsageAndFlowConnection(PartUsage self) { } /** - * {@link ModelMutationElementService#createPartUsageAndFlowConnection(PartUsage)}. + * {@link ModelMutationElementService#createSatisfy(Element, RequirementUsage)}. */ public Element createSatisfy(Element element, RequirementUsage existingRequirement) { return this.modelMutationElementService.createSatisfy(element, existingRequirement); @@ -105,4 +107,11 @@ public EObject createOccurrenceInOccurrence(OccurrenceUsage container, EClass eC public Element setAsView(ViewUsage viewUsage, String newViewDefinition) { return this.modelMutationElementService.setAsView(viewUsage, newViewDefinition); } + + /** + * {@link ModelMutationElementService#createFramedConcern(Type, ConcernUsage)}. + */ + public Element createFramedConcern(Type type, ConcernUsage concernUsage) { + return this.modelMutationElementService.createFramedConcern(type, concernUsage); + } } diff --git a/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelQueryAQLService.java b/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelQueryAQLService.java index 6bff05564..348f200d4 100644 --- a/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelQueryAQLService.java +++ b/backend/services/syson-model-services/src/main/java/org/eclipse/syson/model/services/aql/ModelQueryAQLService.java @@ -14,10 +14,12 @@ import java.util.List; +import org.eclipse.syson.sysml.ConcernUsage; import org.eclipse.syson.sysml.Connector; import org.eclipse.syson.sysml.ConnectorAsUsage; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.Feature; +import org.eclipse.syson.sysml.FramedConcernMembership; import org.eclipse.syson.sysml.metamodel.services.MetamodelQueryElementService; /** @@ -68,4 +70,11 @@ public Feature getConnectorSource(Connector connector) { public List getConnectorTarget(ConnectorAsUsage connector) { return this.metamodelQueryElementService.getConnectorTarget(connector); } + + /** + * {@link MetamodelQueryElementService#getFramedConcernTarget(FramedConcernMembership)}. + */ + public ConcernUsage getFramedConcernTarget(FramedConcernMembership framedConcernMembership) { + return this.metamodelQueryElementService.getFramedConcernTarget(framedConcernMembership); + } } diff --git a/backend/services/syson-services/src/main/java/org/eclipse/syson/util/SysONEContentAdapter.java b/backend/services/syson-services/src/main/java/org/eclipse/syson/util/SysONEContentAdapter.java index 6daec31a2..6f3609d58 100644 --- a/backend/services/syson-services/src/main/java/org/eclipse/syson/util/SysONEContentAdapter.java +++ b/backend/services/syson-services/src/main/java/org/eclipse/syson/util/SysONEContentAdapter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024, 2025 Obeo. + * Copyright (c) 2024, 2026 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 @@ -23,6 +23,7 @@ import org.eclipse.emf.ecore.util.EContentAdapter; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.FeatureValue; +import org.eclipse.syson.sysml.FramedConcernMembership; import org.eclipse.syson.sysml.Membership; /** @@ -41,9 +42,8 @@ public Map> getCache() { @Override protected void addAdapter(Notifier notifier) { super.addAdapter(notifier); - // We need to keep FeatureValue since they may be displayed in representations // This adapter is used for semantic candidate expression - if (notifier instanceof Element element && (!(notifier instanceof Membership) || notifier instanceof FeatureValue)) { + if (notifier instanceof Element element && (!(notifier instanceof Membership) || this.shouldCacheMembership(notifier))) { EClass eClass = element.eClass(); List value; if (this.cache.containsKey(eClass)) { @@ -74,4 +74,16 @@ protected void removeAdapter(Notifier notifier) { public boolean isAdapterForType(Object type) { return SysONEContentAdapter.class.equals(type); } + + /** + * Whether a membership should be cached. + *

+ * We cache some memberships because they are displayed in a representation, either as a Node, or as an Edge. + *

+ * @param notifier The notifier + * @return whether the notifier is a membership that should be cached + */ + private boolean shouldCacheMembership(Notifier notifier) { + return notifier instanceof FramedConcernMembership || notifier instanceof FeatureValue; + } } diff --git a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java index 2f3bc0086..bb513edf4 100644 --- a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java +++ b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java @@ -19,11 +19,13 @@ import java.util.function.Predicate; import org.eclipse.syson.sysml.ActorMembership; +import org.eclipse.syson.sysml.ConcernUsage; import org.eclipse.syson.sysml.Connector; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.Expression; import org.eclipse.syson.sysml.Feature; import org.eclipse.syson.sysml.FeatureValue; +import org.eclipse.syson.sysml.FramedConcernMembership; import org.eclipse.syson.sysml.PartUsage; import org.eclipse.syson.sysml.ReferenceUsage; import org.eclipse.syson.sysml.StakeholderMembership; @@ -158,7 +160,7 @@ public Optional getCommonOwnerAncestor(Element e1, Elemen * Gets the value expression of a feature (Value of the FeatureValue owned by this feature). * * @param feature - * a non null feature + * a non-null feature * @return an optional expression */ public Optional getValueExpression(Feature feature) { @@ -168,4 +170,21 @@ public Optional getValueExpression(Feature feature) { .findFirst() .map(FeatureValue::getValue); } + + /** + * Returns the framed concern target of {@link FramedConcernMembership}. + *

+ * It returns a concern when the framed concern membership owned concern is subsetted by another concern. + *

+ * + * @param framedConcernMembership + * The framed concern membership + * @return the framed concern target of {@link FramedConcernMembership} + */ + public ConcernUsage getFramedConcernTarget(FramedConcernMembership framedConcernMembership) { + if (framedConcernMembership.getOwnedConcern() != framedConcernMembership.getReferencedConcern()) { + return framedConcernMembership.getReferencedConcern(); + } + return null; + } } diff --git a/backend/views/syson-diagram-common-view/DescriptionsId.txt b/backend/views/syson-diagram-common-view/DescriptionsId.txt index 2a3a69f70..0ad604e1d 100644 --- a/backend/views/syson-diagram-common-view/DescriptionsId.txt +++ b/backend/views/syson-diagram-common-view/DescriptionsId.txt @@ -534,3 +534,4 @@ GV Edge BindingConnectorAsUsage => siriusComponents://edgeDescription?sourceKind GV Edge FeatureValue => siriusComponents://edgeDescription?sourceKind=view&sourceId=8dcd14b0-6259-3193-ad2c-743f394c68e4&sourceElementId=6fde9074-7fd4-375f-9ba0-c70c4408ca66 GV Edge IncludeUseCaseUsage => siriusComponents://edgeDescription?sourceKind=view&sourceId=8dcd14b0-6259-3193-ad2c-743f394c68e4&sourceElementId=72e2d57a-8fd1-3b99-b9de-e6575a97ff10 GV Edge ConnectionUsage => siriusComponents://edgeDescription?sourceKind=view&sourceId=8dcd14b0-6259-3193-ad2c-743f394c68e4&sourceElementId=b51cf026-b4cc-3d4a-a4e3-ca4a1d50a2e0 +GV Edge FramedConcernMembership => siriusComponents://edgeDescription?sourceKind=view&sourceId=8dcd14b0-6259-3193-ad2c-743f394c68e4&sourceElementId=64234f9b-80ec-3cdd-97aa-15a42b7d3fc2 diff --git a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java index 333ec0f0a..1bc3c8d3e 100644 --- a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java +++ b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolService.java @@ -662,4 +662,24 @@ public EdgeTool createSatisfyRequirementEdgeTool() { .targetElementDescriptions(targetNodeDescriptions.toArray(NodeDescription[]::new)) .build(); } + + public EdgeTool createFramedConcernEdgeTool() { + var builder = this.diagramBuilderHelper.newEdgeTool(); + var body = this.viewBuilderHelper.newChangeContext() + .expression(ServiceMethod.of1(ModelMutationAQLService::createFramedConcern).aql(EdgeDescription.SEMANTIC_EDGE_SOURCE, EdgeDescription.SEMANTIC_EDGE_TARGET)); + + var targetNodeDescriptions = new ArrayList(); + var concernUsageNodeDescriptionName = this.nameGenerator.getNodeName(SysmlPackage.eINSTANCE.getConcernUsage()); + this.allNodeDescriptions.stream() + .filter(nd -> concernUsageNodeDescriptionName.equals(nd.getName())) + .findFirst() + .ifPresent(targetNodeDescriptions::add); + + return builder + .name(this.nameGenerator.getCreationToolName("New framed ", SysmlPackage.eINSTANCE.getConcernUsage())) + .iconURLsExpression(METAMODEL_ICONS_PATH + SysmlPackage.eINSTANCE.getFramedConcernMembership().getName() + SVG) + .body(body.build()) + .targetElementDescriptions(targetNodeDescriptions.toArray(NodeDescription[]::new)) + .build(); + } } diff --git a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java index 9f0967d15..edbca1802 100644 --- a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java +++ b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewEdgeToolSwitch.java @@ -43,6 +43,7 @@ import org.eclipse.syson.sysml.PartDefinition; import org.eclipse.syson.sysml.PartUsage; import org.eclipse.syson.sysml.PortUsage; +import org.eclipse.syson.sysml.RequirementDefinition; import org.eclipse.syson.sysml.RequirementUsage; import org.eclipse.syson.sysml.StateDefinition; import org.eclipse.syson.sysml.StateUsage; @@ -251,6 +252,14 @@ public List casePortUsage(PortUsage object) { return edgeTools; } + @Override + public List caseRequirementDefinition(RequirementDefinition object) { + var edgeTools = new ArrayList(); + edgeTools.add(this.edgeToolService.createFramedConcernEdgeTool()); + edgeTools.addAll(this.caseDefinition(object)); + return edgeTools; + } + @Override public List caseRequirementUsage(RequirementUsage object) { var edgeTools = new ArrayList(); @@ -265,6 +274,7 @@ public List caseRequirementUsage(RequirementUsage object) { || this.edgeToolService.isTheNodeDescriptionFor(n, SysmlPackage.eINSTANCE.getUseCaseDefinition())) .toList(); edgeTools.add(this.edgeToolService.createBecomeObjectiveRequirementEdgeTool(objectiveTargets)); + edgeTools.add(this.edgeToolService.createFramedConcernEdgeTool()); edgeTools.addAll(this.caseUsage(object)); return edgeTools; } diff --git a/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionProvider.java b/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionProvider.java index ec38cd0e8..8e1e952bf 100644 --- a/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionProvider.java +++ b/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionProvider.java @@ -77,6 +77,7 @@ import org.eclipse.syson.standard.diagrams.view.edges.FeatureTypingEdgeDescriptionProvider; import org.eclipse.syson.standard.diagrams.view.edges.FeatureValueEdgeDescriptionProvider; import org.eclipse.syson.standard.diagrams.view.edges.FlowUsageEdgeDescriptionProvider; +import org.eclipse.syson.standard.diagrams.view.edges.FrameConcernEdgeDescriptionProvider; import org.eclipse.syson.standard.diagrams.view.edges.IncludeUseCaseDescriptionProvider; import org.eclipse.syson.standard.diagrams.view.edges.InterfaceUsageEdgeDescriptionProvider; import org.eclipse.syson.standard.diagrams.view.edges.NestedActorEdgeDescriptionProvider; @@ -882,6 +883,7 @@ private List> createAllEdgeDescriptionProv edgeDescriptionProviders.add(new IncludeUseCaseDescriptionProvider(colorProvider, this.getDescriptionNameGenerator())); edgeDescriptionProviders.add(new ConnectionUsageEdgeDescriptionProvider(colorProvider, this.getDescriptionNameGenerator())); edgeDescriptionProviders.add(new SatisfyRequirementEdgeDescriptionProvider(colorProvider, this.getDescriptionNameGenerator())); + edgeDescriptionProviders.add(new FrameConcernEdgeDescriptionProvider(colorProvider, this.getDescriptionNameGenerator())); return edgeDescriptionProviders; } diff --git a/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/edges/FrameConcernEdgeDescriptionProvider.java b/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/edges/FrameConcernEdgeDescriptionProvider.java new file mode 100644 index 000000000..eeca4f992 --- /dev/null +++ b/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/edges/FrameConcernEdgeDescriptionProvider.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2026 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.syson.standard.diagrams.view.edges; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.eclipse.sirius.components.view.builder.IViewDiagramElementFinder; +import org.eclipse.sirius.components.view.builder.generated.view.ChangeContextBuilder; +import org.eclipse.sirius.components.view.builder.providers.IColorProvider; +import org.eclipse.sirius.components.view.diagram.ArrowStyle; +import org.eclipse.sirius.components.view.diagram.DeleteTool; +import org.eclipse.sirius.components.view.diagram.DiagramDescription; +import org.eclipse.sirius.components.view.diagram.EdgeDescription; +import org.eclipse.sirius.components.view.diagram.EdgeStyle; +import org.eclipse.sirius.components.view.diagram.LineStyle; +import org.eclipse.sirius.components.view.diagram.NodeDescription; +import org.eclipse.sirius.components.view.diagram.SynchronizationPolicy; +import org.eclipse.syson.diagram.common.view.DescriptionFinder; +import org.eclipse.syson.diagram.common.view.edges.AbstractEdgeDescriptionProvider; +import org.eclipse.syson.model.services.aql.ModelQueryAQLService; +import org.eclipse.syson.services.DeleteService; +import org.eclipse.syson.services.UtilService; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.helper.LabelConstants; +import org.eclipse.syson.util.AQLConstants; +import org.eclipse.syson.util.IDescriptionNameGenerator; +import org.eclipse.syson.util.ServiceMethod; +import org.eclipse.syson.util.SysMLMetamodelHelper; +import org.eclipse.syson.util.ViewConstants; + +/** + * Used to create the edge description representing the `frame` relationship between a {@link org.eclipse.syson.sysml.RequirementUsage} and a {@link org.eclipse.syson.sysml.ConcernUsage}. + * + * @author gcoutable + */ +public class FrameConcernEdgeDescriptionProvider extends AbstractEdgeDescriptionProvider { + + private final IDescriptionNameGenerator descriptionNameGenerator; + + public FrameConcernEdgeDescriptionProvider(IColorProvider colorProvider, IDescriptionNameGenerator descriptionNameGenerator) { + super(colorProvider); + this.descriptionNameGenerator = Objects.requireNonNull(descriptionNameGenerator); + } + + @Override + protected ChangeContextBuilder getSourceReconnectToolBody() { + return this.viewBuilderHelper.newChangeContext() + .expression(""); + } + + @Override + protected ChangeContextBuilder getTargetReconnectToolBody() { + return this.viewBuilderHelper.newChangeContext() + .expression(""); + } + + @Override + public EdgeDescription create() { + String domainType = SysMLMetamodelHelper.buildQualifiedName(SysmlPackage.eINSTANCE.getFramedConcernMembership()); + return this.diagramBuilderHelper.newEdgeDescription() + .domainType(domainType) + .isDomainBasedEdge(true) + .centerLabelExpression(LabelConstants.OPEN_QUOTE + LabelConstants.FRAME + LabelConstants.CLOSE_QUOTE) + .name(this.getName()) + .semanticCandidatesExpression(ServiceMethod.of1(UtilService::getAllReachable).aqlSelf(domainType)) + .sourceExpression(AQLConstants.AQL_SELF + "." + SysmlPackage.eINSTANCE.getRelationship_OwningRelatedElement().getName()) + .style(this.createEdgeStyle()) + .synchronizationPolicy(SynchronizationPolicy.SYNCHRONIZED) + .targetExpression(ServiceMethod.of0(ModelQueryAQLService::getFramedConcernTarget).aqlSelf()) + .build(); + } + + @Override + public void link(DiagramDescription diagramDescription, IViewDiagramElementFinder cache) { + var optEdgeDescription = cache.getEdgeDescription(this.getName()); + optEdgeDescription.ifPresent(edgeDescription -> { + diagramDescription.getEdgeDescriptions().add(edgeDescription); + + var sourceNodes = this.getSourceNodes(cache); + var targetNodes = this.getTargetNodes(cache); + edgeDescription.getSourceDescriptions().addAll(sourceNodes); + edgeDescription.getTargetDescriptions().addAll(targetNodes); + + edgeDescription.setPalette(this.createEdgePalette(cache)); + }); + + } + + @Override + protected DeleteTool getEdgeDeleteTool() { + // Delete the referenced subsetting only. + var referenceSubsetting = this.viewBuilderHelper.newChangeContext() + .expression(AQLConstants.AQL_SELF + ".ownedConcern.ownedRelationship->select(rel | rel.oclIsKindOf(sysml::ReferenceSubsetting))->first()") + .children(this.viewBuilderHelper.newChangeContext() + .expression(ServiceMethod.of0(DeleteService::deleteFromModel).aqlSelf()) + .build() + ); + + return this.diagramBuilderHelper.newDeleteTool() + .name("Delete from Model") + .body(referenceSubsetting.build()) + .build(); + } + + private String getName() { + return this.descriptionNameGenerator.getEdgeName(SysmlPackage.eINSTANCE.getFramedConcernMembership()); + } + + private EdgeStyle createEdgeStyle() { + return this.diagramBuilderHelper.newEdgeStyle() + .borderSize(0) + .color(this.colorProvider.getColor(ViewConstants.DEFAULT_EDGE_COLOR)) + .edgeWidth(1) + .lineStyle(LineStyle.SOLID) + .sourceArrowStyle(ArrowStyle.NONE) + .targetArrowStyle(ArrowStyle.INPUT_ARROW) + .build(); + } + + private List getSourceNodes(IViewDiagramElementFinder cache) { + List sources = new ArrayList<>(); + var descriptionFinder = new DescriptionFinder(this.descriptionNameGenerator); + sources.addAll(descriptionFinder.getConnectableNodeDescriptions(cache.getNodeDescriptions(), SysmlPackage.eINSTANCE.getRequirementUsage())); + sources.addAll(descriptionFinder.getConnectableNodeDescriptions(cache.getNodeDescriptions(), SysmlPackage.eINSTANCE.getRequirementDefinition())); + return sources; + } + + private List getTargetNodes(IViewDiagramElementFinder cache) { + return new DescriptionFinder(this.descriptionNameGenerator).getConnectableNodeDescriptions(cache.getNodeDescriptions(), SysmlPackage.eINSTANCE.getConcernUsage()); + } +} diff --git a/backend/views/syson-standard-diagrams-view/src/test/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionTests.java b/backend/views/syson-standard-diagrams-view/src/test/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionTests.java index 22d4b3c13..5c3a1248a 100644 --- a/backend/views/syson-standard-diagrams-view/src/test/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionTests.java +++ b/backend/views/syson-standard-diagrams-view/src/test/java/org/eclipse/syson/standard/diagrams/view/SDVDiagramDescriptionTests.java @@ -87,7 +87,7 @@ public void eachEdgeWithCenterLabelHasDirectEditTool() { // SuccessionAsUsage has a label but the grammar does not support the direct edit tool yet .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getSuccessionAsUsage()).negate()) .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getAllocationUsage()).negate()) - // Use a non editable fixed label + // Use a non-editable fixed label .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getIncludeUseCaseUsage()).negate()) // TransitionUsage has a label but the grammar does not support the direct edit tool yet .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getTransitionUsage()).negate()) @@ -95,6 +95,8 @@ public void eachEdgeWithCenterLabelHasDirectEditTool() { .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getFeatureValue()).negate()) // SatisfyRequirementUsage edge has a label (satisfy) but it is a constant and should not be modifiable .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getSatisfyRequirementUsage()).negate()) + // FramedConcernMembership edge has a label (frame) but it is a constant and should not be modifiable + .filter(this.diagramPredicates.hasDomainType(SysmlPackage.eINSTANCE.getFramedConcernMembership()).negate()) .toList(); new EdgeDescriptionHasDirectEditToolChecker().checkAll(edgeDescriptionCandidates); } diff --git a/doc/content/modules/user-manual/assets/images/release-notes-frame-edge-tool.png b/doc/content/modules/user-manual/assets/images/release-notes-frame-edge-tool.png new file mode 100644 index 000000000..34271a0f6 Binary files /dev/null and b/doc/content/modules/user-manual/assets/images/release-notes-frame-edge-tool.png differ diff --git a/doc/content/modules/user-manual/assets/images/release-notes-frame-edge.png b/doc/content/modules/user-manual/assets/images/release-notes-frame-edge.png new file mode 100644 index 000000000..799e320f8 Binary files /dev/null and b/doc/content/modules/user-manual/assets/images/release-notes-frame-edge.png differ diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.5.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.5.0.adoc index 7321c37a0..15e94ab81 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.5.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.5.0.adoc @@ -28,6 +28,13 @@ image::release-notes-stakeholder-menu.png[Tool to create a new Stakeholder, widt + image::release-notes-stakeholder-node.png[Default representation of Stakeholder connected to its corresponding `RequirementDefinition`, width=60%,height=60%] +** Add support for the _frame_ graphical edge. +A frame edge can be created between a `RequirementUsage` or a `RequirementDefinition` graphical node and a `ConcernUsage` graphical node. ++ +image::release-notes-frame-edge-tool.png[Edge tool to frame a ConcernUsage, width=60%,height=60%] ++ +image::release-notes-frame-edge.png[Frame edge between a RequirementUsage and a ConcernUsage, width=60%,height=60%] + == Bug fixes * In diagrams: