Skip to content

Commit 9eaf007

Browse files
adaussyAxelRICHARD
authored andcommitted
[2182] All downstream applications to extend ISysMLMoveElementService
Bug: #2182 Signed-off-by: Arthur Daussy <arthur.daussy@obeo.fr>
1 parent 359fa78 commit 9eaf007

10 files changed

Lines changed: 680 additions & 117 deletions

File tree

CHANGELOG.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
- https://github.com/eclipse-syson/syson/issues/2198[#2198] [diagrams] Improve diagram-to-diagram drag and drop to support dropping multiple graphical nodes at once, leveraging Sirius Web's `droppedNodes` and `droppedElements` variables.
2222
- https://github.com/eclipse-syson/syson/issues/2194[#2194] [diagrams] Properly report feedback messages to user when using _ISysMLMoveElementService_.
23+
- https://github.com/eclipse-syson/syson/issues/2182[#2182] [services] Provide a way for downstream applications to extend _ISysMLMoveElementService_;
24+
2325

2426
=== New features
2527

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.syson;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat;
17+
18+
import com.jayway.jsonpath.JsonPath;
19+
20+
import java.text.MessageFormat;
21+
import java.time.Duration;
22+
import java.util.List;
23+
import java.util.Objects;
24+
import java.util.UUID;
25+
import java.util.concurrent.atomic.AtomicReference;
26+
import java.util.function.Consumer;
27+
28+
import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramEventInput;
29+
import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramRefreshedEventPayload;
30+
import org.eclipse.sirius.components.collaborative.diagrams.dto.DropNodesInput;
31+
import org.eclipse.sirius.components.core.api.IFeedbackMessageService;
32+
import org.eclipse.sirius.components.core.api.SuccessPayload;
33+
import org.eclipse.sirius.components.diagrams.Diagram;
34+
import org.eclipse.sirius.components.diagrams.layoutdata.Position;
35+
import org.eclipse.sirius.components.representations.Message;
36+
import org.eclipse.sirius.components.representations.MessageLevel;
37+
import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState;
38+
import org.eclipse.syson.application.controllers.diagrams.testers.DropNodesWithMessageMutationRunner;
39+
import org.eclipse.syson.application.data.GeneralViewFlowUsageProjectData;
40+
import org.eclipse.syson.services.api.IDefaultSysMLMoveElementService;
41+
import org.eclipse.syson.services.api.ISysMLMoveElementServiceDelegate;
42+
import org.eclipse.syson.services.api.MoveStatus;
43+
import org.eclipse.syson.services.diagrams.api.IGivenDiagramSubscription;
44+
import org.eclipse.syson.sysml.Element;
45+
import org.junit.jupiter.api.BeforeEach;
46+
import org.junit.jupiter.api.DisplayName;
47+
import org.junit.jupiter.api.Test;
48+
import org.springframework.beans.factory.annotation.Autowired;
49+
import org.springframework.boot.test.context.SpringBootTest;
50+
import org.springframework.boot.test.context.TestConfiguration;
51+
import org.springframework.context.annotation.Bean;
52+
import org.springframework.context.annotation.Import;
53+
import org.springframework.transaction.annotation.Transactional;
54+
55+
import reactor.core.publisher.Flux;
56+
import reactor.test.StepVerifier;
57+
58+
/**
59+
* Integration tests for {@link ISysMLMoveElementServiceDelegate}.
60+
*
61+
* @author Arthur Daussy
62+
*/
63+
@Transactional
64+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
65+
@Import(SysMLMoveElementServiceDelegateIntegrationTests.SysMLMoveElementServiceDelegateTestConfiguration.class)
66+
public class SysMLMoveElementServiceDelegateIntegrationTests extends AbstractIntegrationTests {
67+
68+
private static final String DROP_NODES_TYPENAME = "$.data.dropNodes.__typename";
69+
70+
private static final String DROP_NODES_MESSAGE_BODY = "$.data.dropNodes.messages[0].body";
71+
72+
private static final String DROP_NODES_MESSAGE_LEVEL = "$.data.dropNodes.messages[0].level";
73+
74+
private static final String EXPECTED_FEEDBACK_MESSAGE = "Moved screen to Package1";
75+
76+
@Autowired
77+
private IGivenInitialServerState givenInitialServerState;
78+
79+
@Autowired
80+
private IGivenDiagramSubscription givenDiagramSubscription;
81+
82+
@Autowired
83+
private DropNodesWithMessageMutationRunner dropNodeRunner;
84+
85+
private Flux<DiagramRefreshedEventPayload> givenSubscriptionToDiagram() {
86+
var diagramEventInput = new DiagramEventInput(UUID.randomUUID(),
87+
GeneralViewFlowUsageProjectData.EDITING_CONTEXT_ID,
88+
GeneralViewFlowUsageProjectData.GraphicalIds.DIAGRAM_ID);
89+
return this.givenDiagramSubscription.subscribe(diagramEventInput);
90+
}
91+
92+
@BeforeEach
93+
public void beforeEach() {
94+
this.givenInitialServerState.initialize();
95+
}
96+
97+
@DisplayName("GIVEN a move delegate, WHEN Screen is dropped into Package 1, THEN the delegate moves it and returns a feedback message")
98+
@GivenSysONServer({ GeneralViewFlowUsageProjectData.SCRIPT_PATH })
99+
@Test
100+
public void moveScreenPartWithDelegate() {
101+
var flux = this.givenSubscriptionToDiagram();
102+
103+
AtomicReference<Diagram> diagram = new AtomicReference<>();
104+
105+
Consumer<Object> initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set);
106+
107+
Runnable dropScreenPartFromDiagramToPackage = () -> {
108+
var input = new DropNodesInput(
109+
UUID.randomUUID(),
110+
GeneralViewFlowUsageProjectData.EDITING_CONTEXT_ID,
111+
GeneralViewFlowUsageProjectData.GraphicalIds.DIAGRAM_ID,
112+
List.of(GeneralViewFlowUsageProjectData.GraphicalIds.SCREEN_PART_ID),
113+
null,
114+
List.of(new Position(0, 0)));
115+
var result = this.dropNodeRunner.run(input);
116+
String typename = JsonPath.read(result.data(), DROP_NODES_TYPENAME);
117+
assertThat(typename).isEqualTo(SuccessPayload.class.getSimpleName());
118+
String messageBody = JsonPath.read(result.data(), DROP_NODES_MESSAGE_BODY);
119+
assertThat(messageBody).isEqualTo(EXPECTED_FEEDBACK_MESSAGE);
120+
String messageLevel = JsonPath.read(result.data(), DROP_NODES_MESSAGE_LEVEL);
121+
assertThat(messageLevel).isEqualTo(MessageLevel.INFO.toString());
122+
};
123+
124+
Consumer<Object> updatedDiagramContentConsumer = assertRefreshedDiagramThat(newDiagram -> {
125+
assertThat(newDiagram.getNodes())
126+
.extracting(org.eclipse.sirius.components.diagrams.Node::getTargetObjectId)
127+
.contains(GeneralViewFlowUsageProjectData.SemanticIds.SCREEN_PART);
128+
});
129+
130+
StepVerifier.create(flux)
131+
.consumeNextWith(initialDiagramContentConsumer)
132+
.then(dropScreenPartFromDiagramToPackage)
133+
.consumeNextWith(updatedDiagramContentConsumer)
134+
.thenCancel()
135+
.verify(Duration.ofSeconds(10));
136+
}
137+
138+
/**
139+
* Test configuration used to activate the move delegate only for this integration test.
140+
*/
141+
@TestConfiguration
142+
static class SysMLMoveElementServiceDelegateTestConfiguration {
143+
144+
@Bean
145+
ISysMLMoveElementServiceDelegate feedbackMoveElementServiceDelegate(IDefaultSysMLMoveElementService defaultMoveElementService, IFeedbackMessageService feedbackMessageService) {
146+
return new FeedbackMoveElementServiceDelegate(defaultMoveElementService, feedbackMessageService);
147+
}
148+
}
149+
150+
/**
151+
* Test move delegate which behaves like the default implementation and emits a feedback message.
152+
*/
153+
private static final class FeedbackMoveElementServiceDelegate implements ISysMLMoveElementServiceDelegate {
154+
155+
private final IDefaultSysMLMoveElementService defaultMoveElementService;
156+
157+
private final IFeedbackMessageService feedbackMessageService;
158+
159+
FeedbackMoveElementServiceDelegate(IDefaultSysMLMoveElementService defaultMoveElementService, IFeedbackMessageService feedbackMessageService) {
160+
this.defaultMoveElementService = Objects.requireNonNull(defaultMoveElementService);
161+
this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService);
162+
}
163+
164+
@Override
165+
public boolean canHandle(Element element, Element newParent) {
166+
return true;
167+
}
168+
169+
@Override
170+
public MoveStatus moveSemanticElement(Element element, Element newParent) {
171+
MoveStatus moveStatus = this.defaultMoveElementService.moveSemanticElement(element, newParent);
172+
if (moveStatus.isSuccess() && element != newParent) {
173+
String elementLabel = element.getDeclaredName();
174+
String parentLabel = newParent.getDeclaredName();
175+
this.feedbackMessageService.addFeedbackMessage(new Message(MessageFormat.format("Moved {0} to {1}", elementLabel, parentLabel), MessageLevel.INFO));
176+
}
177+
return moveStatus;
178+
}
179+
}
180+
}

backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/GeneralViewFlowUsageProjectData.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ public class GeneralViewFlowUsageProjectData {
2323
public static final String SCRIPT_PATH = "/scripts/database-content/GeneralView-FlowUsage.sql";
2424

2525
/**
26-
* Ids of the graphical elements elements.
26+
* Ids of the graphical elements.
2727
*/
2828
public static final class GraphicalIds {
2929
public static final String DIAGRAM_ID = "2a1b62cf-36ea-41d2-8433-8ff781b3f4e5";
3030

3131
public static final String CONNECTION_EDGE_ID = "7f3367d5-a200-339b-820a-2408e5b82c84";
32+
33+
public static final String SCREEN_PART_ID = "946dc191-c8df-3f97-96a9-02cce98768f4";
3234
}
3335

3436
/**
@@ -41,5 +43,9 @@ public static final class SemanticIds {
4143

4244
public static final String VIDEO_SIGNAL_ID = "d9b2dd49-4bae-44a7-8137-73bcaf0a2d5e";
4345

46+
public static final String SCREEN_PART = "7776b0d9-287c-441f-b203-36b7ba41a7c6";
47+
48+
public static final String PACKAGE_1 = "8cb4376f-fe0d-4501-abcb-05eac69e766c";
49+
4450
}
4551
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025, 2026 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.syson.services;
14+
15+
import java.util.Objects;
16+
17+
import org.eclipse.emf.ecore.EObject;
18+
import org.eclipse.syson.services.api.IDefaultSysMLMoveElementService;
19+
import org.eclipse.syson.services.api.ISysMLMoveElementCheckerService;
20+
import org.eclipse.syson.services.api.MoveStatus;
21+
import org.eclipse.syson.sysml.Element;
22+
import org.eclipse.syson.sysml.FeatureMembership;
23+
import org.eclipse.syson.sysml.Import;
24+
import org.eclipse.syson.sysml.Membership;
25+
import org.eclipse.syson.sysml.MembershipExpose;
26+
import org.eclipse.syson.sysml.OwningMembership;
27+
import org.eclipse.syson.sysml.Package;
28+
import org.eclipse.syson.sysml.SysmlFactory;
29+
import org.eclipse.syson.sysml.SysmlPackage;
30+
import org.springframework.stereotype.Service;
31+
32+
/**
33+
* Default {@link IDefaultSysMLMoveElementService} for SysML elements.
34+
*
35+
* @author Arthur Daussy
36+
*/
37+
@Service
38+
public class DefaultSysMLMoveElementService implements IDefaultSysMLMoveElementService {
39+
40+
private final DeleteService deleteService;
41+
42+
private final UtilService utilService;
43+
44+
private final ISysMLMoveElementCheckerService moveElementCheckerService;
45+
46+
public DefaultSysMLMoveElementService(ISysMLMoveElementCheckerService moveElementCheckerService) {
47+
this.moveElementCheckerService = Objects.requireNonNull(moveElementCheckerService);
48+
this.deleteService = new DeleteService();
49+
this.utilService = new UtilService();
50+
}
51+
52+
@Override
53+
public MoveStatus moveSemanticElement(Element element, Element newParent) {
54+
final MoveStatus moveStatus;
55+
MoveStatus canMoveStatus = this.moveElementCheckerService.canMove(element, newParent);
56+
if (!canMoveStatus.isSuccess()) {
57+
moveStatus = canMoveStatus;
58+
} else if (element == newParent) {
59+
moveStatus = MoveStatus.buildSuccess();
60+
} else if (newParent instanceof Membership) {
61+
moveStatus = MoveStatus.buildFailure("Membership can't be used as a target of a move");
62+
} else if (element instanceof Import imprt) {
63+
newParent.getOwnedRelationship().add(0, imprt);
64+
moveStatus = MoveStatus.buildSuccess();
65+
} else {
66+
moveStatus = this.moveWithMembership(element, newParent);
67+
}
68+
return moveStatus;
69+
}
70+
71+
/**
72+
* Moves the owning membership of {@code element} to {@code parent}.
73+
* <p>
74+
* This method may create a new membership in {@code parent}, potentially with a different type than
75+
* {@code element.getOwningMembership()}. For example, an element moved into a package will have an
76+
* {@link OwningMembership} instance as its parent, regardless of its original containing membership.
77+
* </p>
78+
*
79+
* @param element
80+
* the element that has been dropped
81+
* @param parent
82+
* the element inside which the drop has been performed
83+
* @return true if the given element has been moved
84+
*/
85+
private MoveStatus moveWithMembership(Element element, Element parent) {
86+
final MoveStatus moveStatus;
87+
88+
if (element != null && element.eContainer() instanceof Membership currentMembership) {
89+
if (parent instanceof Package) {
90+
// the expected membership should be an OwningMembership
91+
if (currentMembership instanceof FeatureMembership) {
92+
var owningMembership = SysmlFactory.eINSTANCE.createOwningMembership();
93+
// Add the new membership to its container first to make sure its content stays in the same
94+
// resource. Otherwise the cross-referencer will delete all the references pointing to its
95+
// related element, which will have unexpected results on the model.
96+
parent.getOwnedRelationship().add(owningMembership);
97+
moveStatus = MoveStatus.buildSuccess();
98+
owningMembership.getOwnedRelatedElement().add(element);
99+
// If the currentMembership is exposed, we need to add new links between the MembershipExposes and
100+
// the new owningMembership
101+
var eInverseRelatedElements = this.utilService.getEInverseRelatedElements(currentMembership, SysmlPackage.eINSTANCE.getMembershipImport_ImportedMembership());
102+
for (EObject eObject : eInverseRelatedElements) {
103+
if (eObject instanceof MembershipExpose membershipExpose) {
104+
membershipExpose.setImportedMembership(owningMembership);
105+
}
106+
}
107+
this.deleteService.deleteFromModel(currentMembership);
108+
} else {
109+
parent.getOwnedRelationship().add(currentMembership);
110+
moveStatus = MoveStatus.buildSuccess();
111+
}
112+
} else {
113+
// the expected membership should be a FeatureMembership
114+
if (currentMembership instanceof FeatureMembership) {
115+
parent.getOwnedRelationship().add(currentMembership);
116+
moveStatus = MoveStatus.buildSuccess();
117+
} else {
118+
var featureMembership = SysmlFactory.eINSTANCE.createFeatureMembership();
119+
parent.getOwnedRelationship().add(featureMembership);
120+
moveStatus = MoveStatus.buildSuccess();
121+
featureMembership.getOwnedRelatedElement().add(element);
122+
// If the currentMembership is exposed, we need to add new links between the MembershipExposes and
123+
// the new featureMembership
124+
var eInverseRelatedElements = this.utilService.getEInverseRelatedElements(currentMembership, SysmlPackage.eINSTANCE.getMembershipImport_ImportedMembership());
125+
for (EObject eObject : eInverseRelatedElements) {
126+
if (eObject instanceof MembershipExpose membershipExpose) {
127+
membershipExpose.setImportedMembership(featureMembership);
128+
}
129+
}
130+
this.deleteService.deleteFromModel(currentMembership);
131+
}
132+
}
133+
} else {
134+
moveStatus = MoveStatus.buildFailure("Element is not contained in a membership");
135+
}
136+
return moveStatus;
137+
}
138+
}

0 commit comments

Comments
 (0)