Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ::> ...; }}.
* <p>
* 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.
* </p>
*
* @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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
* <code>RequirementDerivation</code>.
*
* @param eObject
* an object to access to the library resources.
*
* @return the standard <code>DerivationMetadata</code> 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 <code>RequirementDerivation</code>.
*
* @param eObject
* an object to access to the library resources.
*
* @return the standard <code>OriginalRequirementMetadata</code> 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 <code>RequirementDerivation</code>.
*
* @param eObject
* an object to access to the library resources.
*
* @return the standard <code>DerivedRequirementMetadata</code> definition.
*/
public MetadataDefinition retrieveDerivedRequirementMetadata(Element eObject) {
return this.findByNameAndTypeInStandardLibraries(eObject, MetadataDefinition.class, "RequirementDerivation::DerivedRequirementMetadata");
}

private <T extends Element> T findByNameAndTypeInStandardLibraries(Element context, Class<T> klass, String qualifiedName) {
return context.eResource().getResourceSet().getResources().stream()
.flatMap(resource -> this.getLibraries(resource, true).stream())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
* <p>
* 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.
* </p>
* <p>
* The ends are annotated rather than relying on their declaration order, so that the direction of the derivation
* stays explicit.
* </p>
*
* @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<Feature> 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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
Loading
Loading