From 64eb015d74811214cbbc6e6c56b3f976b63016bd Mon Sep 17 00:00:00 2001 From: kkkk1258999 Date: Mon, 17 Aug 2026 17:52:47 +0900 Subject: [PATCH] [2428] Create a requirement derivation from the General View Requirement derivations were displayed in the General View since #2397, but could only be created from SysML v2 text, unlike satisfy which has the New Satisfy Requirement tool. Add the New Derived Requirement edge tool on RequirementUsage graphical nodes. The edge is drawn from the derived requirement to the original one, the direction the derivation is read in and the direction its edge is displayed in. Unlike satisfy, frame, assume and require, which all have a dedicated metaclass and are created with a plain SysmlFactory call, a derivation is identified by metadata coming from the RequirementDerivation standard library. Those three definitions are resolved with findByNameAndTypeInStandardLibraries, the way the standard start and done actions already are, and the import of the library is added to the owning namespace when missing, through the existing ImportService#handleImport. The connection and its ends are created by the existing createConnectionUsage, so what is added here is only what is specific to a derivation: the on its ends. Annotating the ends rather than relying on their declaration order keeps the direction explicit, which is what the display side uses to orient the edge. The ends are nevertheless created original first, so that the end order fallback of the display orients the edge the same way. The created derivation has the same shape as one written in text, so both can be mixed in a model and the export gives back a #derivation connection. Bug: https://github.com/eclipse-syson/syson/issues/2428 Signed-off-by: kkkk1258999 --- CHANGELOG.adoc | 4 + .../GVRequirementDerivationCreationTests.java | 342 ++++++++++++++++++ .../services/ModelMutationElementService.java | 39 ++ .../services/aql/ModelMutationAQLService.java | 7 + .../eclipse/syson/services/UtilService.java | 40 ++ .../MetamodelMutationElementService.java | 80 ++++ .../MetamodelMutationElementServiceTest.java | 131 +++++++ .../view/services/ViewEdgeToolService.java | 28 ++ .../view/services/ViewEdgeToolSwitch.java | 1 + ...ase-notes-new-derived-requirement-tool.png | Bin 0 -> 42226 bytes .../pages/release-notes/2026.9.0.adoc | 10 +- 11 files changed, 681 insertions(+), 1 deletion(-) create mode 100644 backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementDerivationCreationTests.java create mode 100644 backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementServiceTest.java create mode 100644 doc/content/modules/user-manual/assets/images/release-notes-new-derived-requirement-tool.png diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6e572cc616..918c67cc7f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -83,7 +83,11 @@ A requirement derivation is a `ConnectionUsage` annotated with the `#derivation` The edge goes from the derived requirement to the original one and is displayed as a dashed `«derive»` line, so it can be distinguished from a `«satisfy»` edge. - https://github.com/eclipse-syson/syson/issues/2403[#2403] [diagrams] Add support for the inheritance of `ItemUsage` border nodes on `ActionDefinition` and `ActionUsage` graphical nodes. - https://github.com/eclipse-syson/syson/issues/2404[#2404] [diagrams] Add support for the inheritance of `ItemUsage` border nodes on `PortDefinition` and `PortUsage` graphical nodes. +- https://github.com/eclipse-syson/syson/issues/2428[#2428] [diagrams] Add the _New Derived Requirement_ edge tool on `RequirementUsage` graphical nodes, creating a requirement derivation towards another `RequirementUsage`. +The edge is drawn from the derived requirement to the original one, and the created derivation has the same shape as one written in text: a `ConnectionUsage` annotated with `#derivation`, whose ends are annotated with `#original` and `#derive`. +The import of the `RequirementDerivation` library is added to the owning namespace when it is missing. +== v20 == v2026.7.0 === Breaking changes diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementDerivationCreationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementDerivationCreationTests.java new file mode 100644 index 0000000000..05c6bc8c39 --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVRequirementDerivationCreationTests.java @@ -0,0 +1,342 @@ +/******************************************************************************* + * 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.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat; +import static org.eclipse.sirius.components.diagrams.tests.assertions.DiagramInstanceOfAssertFactories.EDGE; + +import com.jayway.jsonpath.JsonPath; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +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.IEditingContext; +import org.eclipse.sirius.components.core.api.IIdentityService; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.ViewModifier; +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.emf.diagram.IDiagramIdProvider; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.eclipse.syson.AbstractIntegrationTests; +import org.eclipse.syson.application.controllers.diagrams.testers.DropFromExplorerTester; +import org.eclipse.syson.application.controllers.diagrams.testers.EdgeCreationTester; +import org.eclipse.syson.application.data.GeneralViewEmptyTestProjectData; +import org.eclipse.syson.application.imports.MutationInsertTextualSysMLv2DataRunner; +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.ConnectionUsage; +import org.eclipse.syson.sysml.Element; +import org.eclipse.syson.sysml.MetadataUsage; +import org.eclipse.syson.sysml.NamespaceImport; +import org.eclipse.syson.sysml.OwningMembership; +import org.eclipse.syson.sysml.Package; +import org.eclipse.syson.sysml.RequirementUsage; +import org.eclipse.syson.sysml.SysmlPackage; +import org.eclipse.syson.sysml.dto.InsertTextualSysMLv2Input; +import org.eclipse.syson.tests.api.GivenSysONServer; +import org.eclipse.syson.util.IDescriptionNameGenerator; +import org.eclipse.syson.util.SysONRepresentationDescriptionIdentifiers; +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.transaction.annotation.Transactional; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +/** + * Tests the creation of a requirement derivation from the General View. + * + * @author kabayama + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GVRequirementDerivationCreationTests extends AbstractIntegrationTests { + + private static final String ORIGINAL_REQUIREMENT_NAME = "article5Compliance"; + + private static final String DERIVED_REQUIREMENT_NAME = "paymentDelayPrevention"; + + private static final String CREATION_TOOL_NAME = "New Derived Requirement"; + + private static final String DERIVATION_METADATA = "RequirementDerivation::DerivationMetadata"; + + private static final String ORIGINAL_END_METADATA = "RequirementDerivation::OriginalRequirementMetadata"; + + private static final String DERIVED_END_METADATA = "RequirementDerivation::DerivedRequirementMetadata"; + + /** + * Two requirements without any derivation between them, and without importing the RequirementDerivation library, + * so that the tool has to add that import itself. + */ + private static final String TWO_REQUIREMENTS = """ + requirement %s; + requirement %s; + """.formatted(ORIGINAL_REQUIREMENT_NAME, DERIVED_REQUIREMENT_NAME); + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private IGivenDiagramSubscription givenDiagramSubscription; + + @Autowired + private IGivenDiagramDescription givenDiagramDescription; + + @Autowired + private IDiagramIdProvider diagramIdProvider; + + @Autowired + private DropFromExplorerTester dropFromExplorerTester; + + @Autowired + private EdgeCreationTester edgeCreationTester; + + @Autowired + private MutationInsertTextualSysMLv2DataRunner insertTextRunner; + + @Autowired + private ExecuteEditingContextFunctionRunner executeEditingContextFunctionRunner; + + @Autowired + private IObjectSearchService objectSearchService; + + @Autowired + private IIdentityService identityService; + + private final IDescriptionNameGenerator descriptionNameGenerator = new SDVDescriptionNameGenerator(); + + @BeforeEach + public void setUp() { + this.givenInitialServerState.initialize(); + } + + private Flux givenSubscriptionToDiagram() { + var diagramEventInput = new DiagramEventInput(UUID.randomUUID(), GeneralViewEmptyTestProjectData.EDITING_CONTEXT, + GeneralViewEmptyTestProjectData.GraphicalIds.DIAGRAM_ID); + return this.givenDiagramSubscription.subscribe(diagramEventInput); + } + + @DisplayName("GIVEN two requirements displayed in a General View, WHEN using the requirement derivation edge tool between them, THEN a derive edge is created from the derived requirement to the original one") + @GivenSysONServer({ GeneralViewEmptyTestProjectData.SCRIPT_PATH }) + @Test + public void createRequirementDerivationFromTheDiagram() { + this.insertText(GeneralViewEmptyTestProjectData.SemanticIds.PACKAGE_1_ID, TWO_REQUIREMENTS); + var originalRequirementId = this.getRequirementId(ORIGINAL_REQUIREMENT_NAME); + var derivedRequirementId = this.getRequirementId(DERIVED_REQUIREMENT_NAME); + + var diagramDescription = this.givenDiagramDescription.getDiagramDescription(GeneralViewEmptyTestProjectData.EDITING_CONTEXT, + SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID); + var diagramDescriptionIdProvider = new DiagramDescriptionIdProvider(diagramDescription, this.diagramIdProvider); + var creationToolId = diagramDescriptionIdProvider.getEdgeCreationToolId( + this.descriptionNameGenerator.getNodeName(SysmlPackage.eINSTANCE.getRequirementUsage()), CREATION_TOOL_NAME); + + var flux = this.givenSubscriptionToDiagram(); + + AtomicReference diagram = new AtomicReference<>(); + AtomicReference originalNodeId = new AtomicReference<>(); + + Consumer initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set); + + Runnable dropOriginalRequirement = () -> this.dropFromExplorerTester.dropFromExplorerOnDiagram( + GeneralViewEmptyTestProjectData.EDITING_CONTEXT, diagram, originalRequirementId); + + Consumer afterFirstDropConsumer = assertRefreshedDiagramThat(newDiagram -> { + originalNodeId.set(newDiagram.getNodes().get(0).getId()); + diagram.set(newDiagram); + }); + + Runnable dropDerivedRequirement = () -> this.dropFromExplorerTester.dropFromExplorerOnDiagram( + GeneralViewEmptyTestProjectData.EDITING_CONTEXT, diagram, derivedRequirementId); + + AtomicReference derivedNodeId = new AtomicReference<>(); + Consumer afterSecondDropConsumer = assertRefreshedDiagramThat(newDiagram -> { + assertThat(newDiagram.getNodes()).hasSize(2); + derivedNodeId.set(newDiagram.getNodes().stream() + .map(node -> node.getId()) + .filter(nodeId -> !nodeId.equals(originalNodeId.get())) + .findFirst() + .orElseThrow()); + // No derivation exists yet, the tool has not been used. + assertThat(this.visibleEdges(newDiagram)).isEmpty(); + diagram.set(newDiagram); + }); + + // The edge is drawn from the derived requirement to the original one, the direction it is displayed in. + Runnable creationToolRunnable = () -> this.edgeCreationTester.createEdgeUsingNodeId( + GeneralViewEmptyTestProjectData.EDITING_CONTEXT, diagram, derivedNodeId.get(), originalNodeId.get(), creationToolId); + + Consumer afterCreationConsumer = assertRefreshedDiagramThat(newDiagram -> { + var newVisibleEdges = this.visibleEdges(newDiagram); + assertThat(newVisibleEdges).hasSize(1).first(EDGE) + .hasSourceId(derivedNodeId.get()) + .hasTargetId(originalNodeId.get()); + }); + + StepVerifier.create(flux) + .consumeNextWith(initialDiagramContentConsumer) + .then(dropOriginalRequirement) + .consumeNextWith(afterFirstDropConsumer) + .then(dropDerivedRequirement) + .consumeNextWith(afterSecondDropConsumer) + .then(creationToolRunnable) + .consumeNextWith(afterCreationConsumer) + .thenCancel() + .verify(Duration.ofSeconds(30)); + + this.checkCreatedDerivation(); + } + + /** + * Checks the semantic model produced by the tool: a connection annotated with the derivation metadata, whose ends + * are annotated and reference the two requirements, and the import of the library the metadata come from. + */ + private void checkCreatedDerivation() { + BiFunction function = (editingContext, input) -> { + var report = new StringBuilder(); + this.objectSearchService.getObject(editingContext, GeneralViewEmptyTestProjectData.SemanticIds.PACKAGE_1_ID) + .filter(Package.class::isInstance) + .map(Package.class::cast) + .ifPresent(rootPackage -> { + rootPackage.getOwnedElement().stream() + .filter(ConnectionUsage.class::isInstance) + .map(ConnectionUsage.class::cast) + .forEach(connection -> { + report.append("connection=").append(this.appliedMetadataOf(connection)); + connection.getConnectorEnd().forEach(end -> report.append(" end=").append(this.appliedMetadataOf(end)) + .append("->").append(this.referencedNameOf(end))); + }); + report.append(" imports="); + rootPackage.getOwnedImport().stream() + .filter(NamespaceImport.class::isInstance) + .map(NamespaceImport.class::cast) + .map(NamespaceImport::getImportedNamespace) + .filter(Objects::nonNull) + .forEach(namespace -> report.append(namespace.getName()).append(",")); + }); + return new ExecuteEditingContextFunctionSuccessPayload(input.id(), report.toString()); + }; + + var mono = this.executeEditingContextFunctionRunner + .execute(new ExecuteEditingContextFunctionInput(UUID.randomUUID(), GeneralViewEmptyTestProjectData.EDITING_CONTEXT, function)); + var report = Optional.ofNullable(mono.block(Duration.ofSeconds(20))) + .filter(ExecuteEditingContextFunctionSuccessPayload.class::isInstance) + .map(ExecuteEditingContextFunctionSuccessPayload.class::cast) + .map(ExecuteEditingContextFunctionSuccessPayload::result) + .map(String::valueOf) + .orElse(""); + + assertThat(report) + .as("The created connection should be a requirement derivation") + .contains("connection=[" + DERIVATION_METADATA + "]"); + assertThat(report) + .as("The original end should be annotated and reference the original requirement") + .contains("end=[" + ORIGINAL_END_METADATA + "]->" + ORIGINAL_REQUIREMENT_NAME); + assertThat(report) + .as("The derived end should be annotated and reference the derived requirement") + .contains("end=[" + DERIVED_END_METADATA + "]->" + DERIVED_REQUIREMENT_NAME); + assertThat(report) + .as("The library owning the metadata should have been imported") + .contains("RequirementDerivation"); + } + + /** + * The name of the element referenced by a connection end. + */ + private String referencedNameOf(org.eclipse.syson.sysml.Feature end) { + var referenceSubsetting = end.getOwnedReferenceSubsetting(); + if (referenceSubsetting == null || referenceSubsetting.getSubsettedFeature() == null) { + return "null"; + } + return referenceSubsetting.getSubsettedFeature().getName(); + } + + private String appliedMetadataOf(Element element) { + var names = new StringBuilder("["); + element.getOwnedRelationship().stream() + .filter(OwningMembership.class::isInstance) + .map(OwningMembership.class::cast) + .flatMap(membership -> membership.getOwnedRelatedElement().stream()) + .filter(MetadataUsage.class::isInstance) + .map(MetadataUsage.class::cast) + .map(MetadataUsage::getMetadataDefinition) + .filter(Objects::nonNull) + .forEach(definition -> names.append(definition.getQualifiedName())); + return names.append("]").toString(); + } + + private List visibleEdges(Diagram diagram) { + return diagram.getEdges().stream() + .filter(edge -> edge.getState() != ViewModifier.Hidden) + .toList(); + } + + private void insertText(String parentElementId, String content) { + var input = new InsertTextualSysMLv2Input(UUID.randomUUID(), GeneralViewEmptyTestProjectData.EDITING_CONTEXT, parentElementId, content); + var result = this.insertTextRunner.run(input); + + Map parsed = JsonPath.read(result.data(), "$.data.insertTextualSysMLv2"); + assertThat(parsed.get("__typename")).isEqualTo(SuccessPayload.class.getSimpleName()); + assertThat((List) parsed.get("messages")).isEmpty(); + } + + private String getRequirementId(String requirementName) { + BiFunction function = (editingContext, input) -> { + var requirementId = this.objectSearchService.getObject(editingContext, GeneralViewEmptyTestProjectData.SemanticIds.PACKAGE_1_ID) + .filter(Package.class::isInstance) + .map(Package.class::cast) + .stream() + .flatMap(rootPackage -> rootPackage.getOwnedElement().stream()) + .filter(RequirementUsage.class::isInstance) + .filter(requirement -> requirementName.equals(requirement.getName())) + .map(this.identityService::getId) + .findFirst() + .orElse(null); + return new ExecuteEditingContextFunctionSuccessPayload(input.id(), requirementId); + }; + + var mono = this.executeEditingContextFunctionRunner + .execute(new ExecuteEditingContextFunctionInput(UUID.randomUUID(), GeneralViewEmptyTestProjectData.EDITING_CONTEXT, function)); + + var identifier = Optional.ofNullable(mono.block(Duration.ofSeconds(10))) + .filter(ExecuteEditingContextFunctionSuccessPayload.class::isInstance) + .map(ExecuteEditingContextFunctionSuccessPayload.class::cast) + .map(ExecuteEditingContextFunctionSuccessPayload::result) + .filter(String.class::isInstance) + .map(String.class::cast) + .orElse(null); + + assertThat(identifier).as("The requirement %s should have been created", requirementName).isNotNull(); + return identifier; + } +} 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 3aa953add5..ec88b380b8 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 @@ -36,6 +36,8 @@ import org.eclipse.syson.sysml.Type; import org.eclipse.syson.sysml.ViewDefinition; import org.eclipse.syson.sysml.ViewUsage; +import org.eclipse.syson.services.ImportService; +import org.eclipse.syson.services.UtilService; import org.eclipse.syson.sysml.metamodel.helper.EMFUtils; import org.eclipse.syson.sysml.metamodel.services.MetamodelMutationElementService; import org.eclipse.syson.sysml.metamodel.util.ElementUtil; @@ -56,9 +58,46 @@ public class ModelMutationElementService { private final MetamodelMutationElementService metamodelMutationElementService; + private final UtilService utilService; + + private final ImportService importService; + public ModelMutationElementService() { this.metamodelMutationElementService = new MetamodelMutationElementService(); this.elementUtil = new ElementUtil(); + this.utilService = new UtilService(); + this.importService = new ImportService(); + } + + /** + * Create a requirement derivation between two {@link RequirementUsage}, the way it is written in text with + * {@code #derivation connection { end #original ::> ...; end #derive ::> ...; }}. + *

+ * The metadata identifying the derivation and its ends come from the {@code RequirementDerivation} standard + * library, so the import of that library is added to the owning namespace when it is missing. + *

+ * + * @param derived + * the requirement derived from {@code original}, the source of the edge + * @param original + * the requirement {@code derived} is derived from, the target of the edge + * @return the new derivation, or {@code derived} when the standard library could not be reached + */ + public Element createRequirementDerivation(RequirementUsage derived, RequirementUsage original) { + var derivationMetadata = this.utilService.retrieveDerivationMetadata(derived); + var originalEndMetadata = this.utilService.retrieveOriginalRequirementMetadata(derived); + var derivedEndMetadata = this.utilService.retrieveDerivedRequirementMetadata(derived); + boolean metadataFound = derivationMetadata != null && originalEndMetadata != null && derivedEndMetadata != null; + + if (metadataFound && derived.getOwner() instanceof Namespace owningNamespace) { + var derivation = this.metamodelMutationElementService.createRequirementDerivation(derived, original, owningNamespace, derivationMetadata, originalEndMetadata, + derivedEndMetadata); + // The ends and the connection reference the metadata of the RequirementDerivation library, which has to be + // imported for the created derivation to be valid on its own. + this.importService.handleImport(derivation, derivationMetadata); + return derivation; + } + return derived; } /** 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 9b5bb04844..f105b4cd9c 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 @@ -102,6 +102,13 @@ public Element createSatisfyRequirement(Element self, Element selectedObject) { return this.modelMutationElementService.createSatisfyRequirement(self, selectedObject); } + /** + * {@link ModelMutationElementService#createRequirementDerivation(RequirementUsage, RequirementUsage)}. + */ + public Element createRequirementDerivation(RequirementUsage derived, RequirementUsage original) { + return this.modelMutationElementService.createRequirementDerivation(derived, original); + } + /** * {@link MetamodelMutationElementService#createOccurrenceInOccurrence(Type, EClass)}. */ diff --git a/backend/services/syson-services/src/main/java/org/eclipse/syson/services/UtilService.java b/backend/services/syson-services/src/main/java/org/eclipse/syson/services/UtilService.java index b00e89f27b..03cda9fe27 100644 --- a/backend/services/syson-services/src/main/java/org/eclipse/syson/services/UtilService.java +++ b/backend/services/syson-services/src/main/java/org/eclipse/syson/services/UtilService.java @@ -51,6 +51,7 @@ import org.eclipse.syson.sysml.InterfaceUsage; import org.eclipse.syson.sysml.LibraryPackage; import org.eclipse.syson.sysml.Membership; +import org.eclipse.syson.sysml.MetadataDefinition; import org.eclipse.syson.sysml.Namespace; import org.eclipse.syson.sysml.NamespaceImport; import org.eclipse.syson.sysml.ObjectiveMembership; @@ -655,6 +656,45 @@ public ActionUsage retrieveStandardDoneState(Element eObject) { return this.findByNameAndTypeInStandardLibraries(eObject, ActionUsage.class, "States::StateAction::done"); } + /** + * Retrieve the metadata definition marking a requirement derivation, defined inside the standard library + * RequirementDerivation. + * + * @param eObject + * an object to access to the library resources. + * + * @return the standard DerivationMetadata definition. + */ + public MetadataDefinition retrieveDerivationMetadata(Element eObject) { + return this.findByNameAndTypeInStandardLibraries(eObject, MetadataDefinition.class, "RequirementDerivation::DerivationMetadata"); + } + + /** + * Retrieve the metadata definition marking the original end of a requirement derivation, defined inside the + * standard library RequirementDerivation. + * + * @param eObject + * an object to access to the library resources. + * + * @return the standard OriginalRequirementMetadata definition. + */ + public MetadataDefinition retrieveOriginalRequirementMetadata(Element eObject) { + return this.findByNameAndTypeInStandardLibraries(eObject, MetadataDefinition.class, "RequirementDerivation::OriginalRequirementMetadata"); + } + + /** + * Retrieve the metadata definition marking the derived end of a requirement derivation, defined inside the standard + * library RequirementDerivation. + * + * @param eObject + * an object to access to the library resources. + * + * @return the standard DerivedRequirementMetadata definition. + */ + public MetadataDefinition retrieveDerivedRequirementMetadata(Element eObject) { + return this.findByNameAndTypeInStandardLibraries(eObject, MetadataDefinition.class, "RequirementDerivation::DerivedRequirementMetadata"); + } + private T findByNameAndTypeInStandardLibraries(Element context, Class klass, String qualifiedName) { return context.eResource().getResourceSet().getResources().stream() .flatMap(resource -> this.getLibraries(resource, true).stream()) diff --git a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementService.java b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementService.java index 54521e680a..3918ed73ae 100644 --- a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementService.java +++ b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementService.java @@ -26,14 +26,18 @@ import org.eclipse.syson.sysml.EndFeatureMembership; import org.eclipse.syson.sysml.Feature; import org.eclipse.syson.sysml.FeatureChaining; +import org.eclipse.syson.sysml.FeatureTyping; import org.eclipse.syson.sysml.Flow; import org.eclipse.syson.sysml.FlowEnd; import org.eclipse.syson.sysml.FlowUsage; import org.eclipse.syson.sysml.InterfaceUsage; import org.eclipse.syson.sysml.Membership; +import org.eclipse.syson.sysml.Metaclass; +import org.eclipse.syson.sysml.MetadataUsage; import org.eclipse.syson.sysml.Namespace; import org.eclipse.syson.sysml.OccurrenceDefinition; import org.eclipse.syson.sysml.OccurrenceUsage; +import org.eclipse.syson.sysml.OwningMembership; import org.eclipse.syson.sysml.Package; import org.eclipse.syson.sysml.PortUsage; import org.eclipse.syson.sysml.Redefinition; @@ -174,6 +178,82 @@ public ConnectionUsage createConnectionUsage(Feature source, Feature target, Ele return (ConnectionUsage) this.createConnector(source, target, sourceContainer, targetContainer, newConnectionContainer, SysmlFactory.eINSTANCE.createConnectionUsage()); } + /** + * Creates a new requirement derivation in the given container. + *

+ * SysML v2 has no dedicated metaclass for requirement derivation: it is a {@link ConnectionUsage} annotated with + * the {@code #derivation} metadata, whose ends are annotated with the {@code #original} and {@code #derive} + * metadata. Those three metadata definitions come from the {@code RequirementDerivation} standard library and are + * given as parameters, so that this service does not have to resolve them. + *

+ *

+ * The ends are annotated rather than relying on their declaration order, so that the direction of the derivation + * stays explicit. + *

+ * + * @param derived + * the requirement derived from {@code original} + * @param original + * the requirement {@code derived} is derived from + * @param newConnectionContainer + * the container of the new {@link ConnectionUsage} + * @param derivationMetadata + * the {@code DerivationMetadata} definition, applied on the connection + * @param originalEndMetadata + * the {@code OriginalRequirementMetadata} definition, applied on the end referencing {@code original} + * @param derivedEndMetadata + * the {@code DerivedRequirementMetadata} definition, applied on the end referencing {@code derived} + * @return the new requirement derivation + */ + public ConnectionUsage createRequirementDerivation(RequirementUsage derived, RequirementUsage original, Namespace newConnectionContainer, Metaclass derivationMetadata, + Metaclass originalEndMetadata, Metaclass derivedEndMetadata) { + // The original requirement is the first end, so that a derivation whose ends are not annotated, which is what + // the end order fallback of the display expects, is still oriented the same way. + ConnectionUsage derivation = this.createConnectionUsage(original, derived, original.getOwner(), derived.getOwner(), newConnectionContainer); + // A derivation is not referenced by its name, so the generated one is dropped, the way the generated name of a + // connector end is. It keeps the derivation anonymous, as it is when written in text. + derivation.setDeclaredName(null); + this.applyPrefixMetadata(derivation, derivationMetadata); + + List ends = derivation.getConnectorEnd(); + if (ends.size() == 2) { + this.applyPrefixMetadata(ends.get(0), originalEndMetadata); + this.applyPrefixMetadata(ends.get(1), derivedEndMetadata); + } + return derivation; + } + + /** + * Applies a prefix metadata, such as the {@code #derivation} of a requirement derivation, on the given element. + * + * @param annotatedElement + * the element to annotate + * @param metadataDefinition + * the definition of the applied metadata + * @return the new {@link MetadataUsage}, or {@code null} if no metadata definition was given + */ + public MetadataUsage applyPrefixMetadata(Element annotatedElement, Metaclass metadataDefinition) { + if (metadataDefinition == null) { + return null; + } + MetadataUsage metadataUsage = SysmlFactory.eINSTANCE.createMetadataUsage(); + OwningMembership owningMembership = SysmlFactory.eINSTANCE.createOwningMembership(); + owningMembership.getOwnedRelatedElement().add(metadataUsage); + annotatedElement.getOwnedRelationship().add(owningMembership); + + FeatureTyping featureTyping = SysmlFactory.eINSTANCE.createFeatureTyping(); + featureTyping.setType(metadataDefinition); + featureTyping.setTypedFeature(metadataUsage); + metadataUsage.getOwnedRelationship().add(featureTyping); + + this.elementInitializerSwitch.doSwitch(owningMembership); + this.elementInitializerSwitch.doSwitch(metadataUsage); + this.elementInitializerSwitch.doSwitch(featureTyping); + // A prefix metadata has no name of its own, it is identified by the definition it is typed by. + metadataUsage.setDeclaredName(null); + return metadataUsage; + } + /** * Creates a new {@link FlowUsage} in the given container. * diff --git a/backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementServiceTest.java b/backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementServiceTest.java new file mode 100644 index 0000000000..7a4deab6a4 --- /dev/null +++ b/backend/services/syson-sysml-metamodel-services/src/test/java/org/eclipse/syson/sysml/metamodel/services/MetamodelMutationElementServiceTest.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * 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.sysml.metamodel.services; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.syson.sysml.ConnectionUsage; +import org.eclipse.syson.sysml.Element; +import org.eclipse.syson.sysml.Metaclass; +import org.eclipse.syson.sysml.MetadataDefinition; +import org.eclipse.syson.sysml.MetadataUsage; +import org.eclipse.syson.sysml.OwningMembership; +import org.eclipse.syson.sysml.Package; +import org.eclipse.syson.sysml.RequirementUsage; +import org.eclipse.syson.sysml.SysmlFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link MetamodelMutationElementService}. + * + * @author kabayama + */ +public class MetamodelMutationElementServiceTest { + + private static final String DERIVATION_METADATA = "DerivationMetadata"; + + private static final String ORIGINAL_METADATA = "OriginalRequirementMetadata"; + + private static final String DERIVED_METADATA = "DerivedRequirementMetadata"; + + private MetamodelMutationElementService mutationService; + + private MetamodelQueryElementService queryService; + + private Package owningPackage; + + private RequirementUsage original; + + private RequirementUsage derived; + + @BeforeEach + public void setUp() { + this.mutationService = new MetamodelMutationElementService(); + this.queryService = new MetamodelQueryElementService(); + + this.owningPackage = SysmlFactory.eINSTANCE.createPackage(); + this.owningPackage.setDeclaredName("ReqTrace"); + this.original = this.createRequirement("article5Compliance"); + this.derived = this.createRequirement("paymentDelayPrevention"); + } + + @DisplayName("GIVEN two requirements, WHEN creating a requirement derivation between them, THEN the derivation is recognized as one and its ends are resolved") + @Test + public void createRequirementDerivation() { + ConnectionUsage derivation = this.mutationService.createRequirementDerivation(this.derived, this.original, this.owningPackage, + this.createMetadataDefinition(DERIVATION_METADATA), + this.createMetadataDefinition(ORIGINAL_METADATA), + this.createMetadataDefinition(DERIVED_METADATA)); + + assertThat(derivation).isNotNull(); + assertThat(derivation.getOwner()).isSameAs(this.owningPackage); + assertThat(this.appliedMetadataNamesOf(derivation)).containsExactly(DERIVATION_METADATA); + assertThat(derivation.getConnectorEnd()).hasSize(2); + // A derivation is anonymous, the way it is when written in text. + assertThat(derivation.getDeclaredName()).isNull(); + } + + @DisplayName("GIVEN a created requirement derivation, WHEN reading its ends, THEN the original and the derived requirements are the ones given at creation") + @Test + public void createdRequirementDerivationHasItsEndsAnnotated() { + ConnectionUsage derivation = this.mutationService.createRequirementDerivation(this.derived, this.original, this.owningPackage, + this.createMetadataDefinition(DERIVATION_METADATA), + this.createMetadataDefinition(ORIGINAL_METADATA), + this.createMetadataDefinition(DERIVED_METADATA)); + + assertThat(this.appliedMetadataNamesOf(derivation.getConnectorEnd().get(0))).containsExactly(ORIGINAL_METADATA); + assertThat(this.appliedMetadataNamesOf(derivation.getConnectorEnd().get(1))).containsExactly(DERIVED_METADATA); + assertThat(this.queryService.getDerivationOriginalEnd(derivation)).isSameAs(this.original); + assertThat(this.queryService.getDerivationDerivedEnd(derivation)).isSameAs(this.derived); + } + + @DisplayName("GIVEN no metadata definition, WHEN applying a prefix metadata, THEN nothing is applied") + @Test + public void applyPrefixMetadataWithoutDefinition() { + assertThat(this.mutationService.applyPrefixMetadata(this.original, null)).isNull(); + assertThat(this.appliedMetadataNamesOf(this.original)).isEmpty(); + } + + private RequirementUsage createRequirement(String name) { + RequirementUsage requirement = SysmlFactory.eINSTANCE.createRequirementUsage(); + requirement.setDeclaredName(name); + OwningMembership membership = SysmlFactory.eINSTANCE.createOwningMembership(); + membership.getOwnedRelatedElement().add(requirement); + this.owningPackage.getOwnedRelationship().add(membership); + return requirement; + } + + private MetadataDefinition createMetadataDefinition(String name) { + MetadataDefinition metadataDefinition = SysmlFactory.eINSTANCE.createMetadataDefinition(); + metadataDefinition.setDeclaredName(name); + return metadataDefinition; + } + + private List appliedMetadataNamesOf(Element element) { + return element.getOwnedRelationship().stream() + .filter(OwningMembership.class::isInstance) + .map(OwningMembership.class::cast) + .flatMap(membership -> membership.getOwnedRelatedElement().stream()) + .filter(MetadataUsage.class::isInstance) + .map(MetadataUsage.class::cast) + .map(MetadataUsage::getMetadataDefinition) + .filter(Objects::nonNull) + .map(Metaclass::getDeclaredName) + .toList(); + } +} 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 33defbde87..14b49f9ab9 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 @@ -664,6 +664,34 @@ public EdgeTool createSatisfyRequirementEdgeTool() { .build(); } + /** + * Creates the edge tool creating a requirement derivation between two {@link org.eclipse.syson.sysml.RequirementUsage}. + *

+ * The edge is drawn from the derived requirement to the original one, the direction the derivation is read in and + * the direction its edge is displayed in. + *

+ * + * @return the edge tool creating a requirement derivation + */ + public EdgeTool createRequirementDerivationEdgeTool() { + var builder = this.diagramBuilderHelper.newEdgeTool(); + var body = this.viewBuilderHelper.newChangeContext() + .expression(ServiceMethod.of1(ModelMutationAQLService::createRequirementDerivation).aql(EdgeDescription.SEMANTIC_EDGE_SOURCE, EdgeDescription.SEMANTIC_EDGE_TARGET)); + + var targetNodeDescriptions = new ArrayList(); + var requirementUsageNodeDescriptionName = this.nameGenerator.getNodeName(SysmlPackage.eINSTANCE.getRequirementUsage()); + this.allNodeDescriptions.stream() + .filter(nd -> requirementUsageNodeDescriptionName.equals(nd.getName())) + .findFirst() + .ifPresent(targetNodeDescriptions::add); + + return builder.name(this.nameGenerator.getCreationToolName("New Derived ", SysmlPackage.eINSTANCE.getRequirementUsage())) + .iconURLsExpression(METAMODEL_ICONS_PATH + SysmlPackage.eINSTANCE.getConnectionUsage().getName() + SVG) + .body(body.build()) + .targetElementDescriptions(targetNodeDescriptions.toArray(NodeDescription[]::new)) + .build(); + } + public EdgeTool createFramedConcernEdgeTool() { var builder = this.diagramBuilderHelper.newEdgeTool(); var body = this.viewBuilderHelper.newChangeContext() 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 a6e811e6b5..58f9c017ab 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 @@ -290,6 +290,7 @@ public List caseRequirementUsage(RequirementUsage object) { */ private List createRequirementUsageEdgeTools(EClass eClass, RequirementUsage requirementUsage) { var edgeTools = new ArrayList(); + edgeTools.add(this.edgeToolService.createRequirementDerivationEdgeTool()); var targetNodes = this.allNodeDescriptions.stream().filter(nodeDesc -> nodeDesc.getName().toLowerCase().endsWith(USAGE) || this.edgeToolService.isTheNodeDescriptionFor(nodeDesc, SysmlPackage.eINSTANCE.getRequirementDefinition())).collect(Collectors.toList()); targetNodes.removeIf(nodeDesc -> this.edgeToolService.isTheNodeDescriptionFor(nodeDesc, SysmlPackage.eINSTANCE.getPortUsage())); diff --git a/doc/content/modules/user-manual/assets/images/release-notes-new-derived-requirement-tool.png b/doc/content/modules/user-manual/assets/images/release-notes-new-derived-requirement-tool.png new file mode 100644 index 0000000000000000000000000000000000000000..75bf636c49cc94b501b8f380af50b7c87e2a67fd GIT binary patch literal 42226 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 z

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 literal 0 HcmV?d00001 diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc index 8d7d4b3bac..4dff55e80c 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc @@ -31,7 +31,6 @@ The subject inheritance in _subject_ compartment is also visible in the followin *** `ConcernUsage` *** `SatisfyRequirementUsage` ** Add support for the inheritance of the parameter border nodes of `ActionDefinition` and `ActionUsage` graphical nodes. - ** Add _requirements_ compartments to `PartUsage` and `PartDefinition` graphical nodes. + image::release-notes-requirements-compartment-parts.png[Requirements compartments in PartUsage and PartDefinition graphical nodes, width=60%,height=60%] @@ -80,6 +79,15 @@ image::release-notes-action-inherited-item-border-nodes.png[ItemUsage border nod + image::release-notes-port-inherited-item-border-nodes.png[ItemUsage border node inheritance on PortDefinition and PortUsage] +** Requirement derivations can now be created from a _General View_ diagram, with the _New Derived Requirement_ edge tool of `RequirementUsage` graphical nodes. ++ +The edge is drawn from the derived requirement to the original one, the direction the derivation is read in: _this requirement is derived from that one_. ++ +image::release-notes-new-derived-requirement-tool.png[New Derived Requirement edge tool on RequirementUsage graphical nodes, width=60%,height=60%] ++ +A derivation created this way is the same as one written in text, so both can be mixed in the same model, and exporting the model gives back the `#derivation` connection shown above. +The `RequirementDerivation` library is imported automatically when the model does not import it yet. + == Bug fixes * In diagrams: