Skip to content

Commit 1df7ae9

Browse files
committed
[1981] Escape special characters when exporting Expose elements
Bug: #1981 Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
1 parent df413f8 commit 1df7ae9

9 files changed

Lines changed: 152 additions & 88 deletions

File tree

CHANGELOG.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The property `SysONTestsProperties#ELASTICSEARCH` has been removed, tests that r
2727

2828
- [releng] Update to https://github.com/eclipse-sirius/sirius-web[Sirius Web 2026.1.2]
2929
- [releng] Update to https://github.com/spring-projects/spring-boot/releases/tag/v3.5.10[Spring Boot 3.5.10]
30+
- [releng] Update to archunit-junit5 1.3.0
3031

3132
=== Bug fixes
3233

@@ -36,6 +37,7 @@ The property `SysONTestsProperties#ELASTICSEARCH` has been removed, tests that r
3637
- https://github.com/eclipse-syson/syson/issues/1973[#1973] [import] Fix an error during textual import when resolving the name of an unnamed redefined `Feature`.
3738
- https://github.com/eclipse-syson/syson/issues/1860[#1860] [diagrams] Add a precondition for compartment item node descriptions in order to filter out unwanted types.
3839
For example `ViewUsage` elements are no longer rendered in _parts_ compartments.
40+
- https://github.com/eclipse-syson/syson/issues/1981[#1981] [export] Fix an error during textual export where `Expose` elements with apostrophes in their name were not properly escaped.
3941

4042
=== Improvements
4143

backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/projects/ProjectDataVersioningRestControllerIntegrationTests.java

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024, 2025 Obeo.
2+
* Copyright (c) 2024, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -16,11 +16,13 @@
1616

1717
import java.io.IOException;
1818
import java.nio.charset.StandardCharsets;
19+
import java.time.Duration;
1920
import java.util.UUID;
2021

2122
import org.apache.commons.io.FileUtils;
2223
import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState;
2324
import org.eclipse.syson.AbstractIntegrationTests;
25+
import org.eclipse.syson.GivenSysONServer;
2426
import org.eclipse.syson.application.data.SimpleProjectElementsTestProjectData;
2527
import org.junit.jupiter.api.BeforeEach;
2628
import org.junit.jupiter.api.DisplayName;
@@ -29,8 +31,6 @@
2931
import org.springframework.boot.test.context.SpringBootTest;
3032
import org.springframework.boot.test.web.server.LocalServerPort;
3133
import org.springframework.core.io.ClassPathResource;
32-
import org.springframework.test.context.jdbc.Sql;
33-
import org.springframework.test.context.jdbc.SqlConfig;
3434
import org.springframework.test.web.reactive.server.WebTestClient;
3535
import org.springframework.transaction.annotation.Transactional;
3636

@@ -61,14 +61,13 @@ public void beforeEach() {
6161
this.givenInitialServerState.initialize();
6262
}
6363

64-
@Test
6564
@DisplayName("GIVEN the SysON REST API, WHEN we ask for all changes, THEN all changes should be returned")
66-
@Sql(scripts = { SimpleProjectElementsTestProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
67-
config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
68-
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
65+
@GivenSysONServer({ SimpleProjectElementsTestProjectData.SCRIPT_PATH })
66+
@Test
6967
public void givenSysONRestAPIWhenWeAskForAllChangesThenAllChangesShouldBeReturned() {
7068
var webTestClient = WebTestClient.bindToServer()
7169
.baseUrl(this.getHTTPBaseUrl())
70+
.responseTimeout(Duration.ofSeconds(30))
7271
.build();
7372

7473
String expectedJSON = null;
@@ -89,14 +88,13 @@ public void givenSysONRestAPIWhenWeAskForAllChangesThenAllChangesShouldBeReturne
8988
.json(expectedJSON);
9089
}
9190

92-
@Test
9391
@DisplayName("GIVEN the SysON REST API, WHEN we ask for all changes in an unknown project, THEN it should return an error")
94-
@Sql(scripts = { SimpleProjectElementsTestProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
95-
config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
96-
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
92+
@GivenSysONServer({ SimpleProjectElementsTestProjectData.SCRIPT_PATH })
93+
@Test
9794
public void givenSysONRestAPIWhenWeAskForAllChangesInUnknownProjectThenItShouldReturnAnError() {
9895
var webTestClient = WebTestClient.bindToServer()
9996
.baseUrl(this.getHTTPBaseUrl())
97+
.responseTimeout(Duration.ofSeconds(30))
10098
.build();
10199

102100
var uri = String.format("/api/rest/projects/%s/commits/%s/changes", INVALID_PROJECT, INVALID_PROJECT);
@@ -107,14 +105,13 @@ public void givenSysONRestAPIWhenWeAskForAllChangesInUnknownProjectThenItShouldR
107105
.isNotFound();
108106
}
109107

110-
@Test
111108
@DisplayName("GIVEN the SysON REST API, WHEN we ask for changes of a specific element, THEN those changes should be returned")
112-
@Sql(scripts = { SimpleProjectElementsTestProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
113-
config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
114-
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
109+
@GivenSysONServer({ SimpleProjectElementsTestProjectData.SCRIPT_PATH })
110+
@Test
115111
public void givenSysONRestAPIWhenWeAskForChangesOfASpecificElementThenThoseChangesShouldBeReturned() {
116112
var webTestClient = WebTestClient.bindToServer()
117113
.baseUrl(this.getHTTPBaseUrl())
114+
.responseTimeout(Duration.ofSeconds(30))
118115
.build();
119116

120117
String expectedJSON = null;
@@ -136,14 +133,13 @@ public void givenSysONRestAPIWhenWeAskForChangesOfASpecificElementThenThoseChang
136133
.json(expectedJSON);
137134
}
138135

139-
@Test
140136
@DisplayName("GIVEN the SysON REST API, WHEN we ask for specific changes in an unknown project, THEN it should return an error")
141-
@Sql(scripts = { SimpleProjectElementsTestProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD,
142-
config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
143-
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
137+
@GivenSysONServer({ SimpleProjectElementsTestProjectData.SCRIPT_PATH })
138+
@Test
144139
public void givenSysONRestAPIWhenWeAskForSpecificChangesInUnknownProjectThenItShouldReturnAnError() {
145140
var webTestClient = WebTestClient.bindToServer()
146141
.baseUrl(this.getHTTPBaseUrl())
142+
.responseTimeout(Duration.ofSeconds(30))
147143
.build();
148144

149145
var computedChangeId = UUID.nameUUIDFromBytes((INVALID_PROJECT + SimpleProjectElementsTestProjectData.SemanticIds.PART_ID).getBytes()).toString();

backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/impl/ElementImpl.java

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2023, 2025 Obeo.
2+
* Copyright (c) 2023, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -547,36 +547,64 @@ public void setOwningRelationship(Relationship newOwningRelationship) {
547547
* according to the KerML textual concrete syntax for qualified names (including use of unrestricted name notation
548548
* and escaped characters, as necessary). The qualifiedName is null if this Element has no owningNamespace or if
549549
* there is not a complete ownership chain of named Namespaces from a root Namespace to this Element. <!--
550-
* end-user-doc -->
550+
* end-user-doc
551+
* <p>
552+
* qualifiedName = <br/>
553+
* if owningNamespace = null then null <br/>
554+
* else if name <> null and owningNamespace.ownedMember-> select(m | m.name = name).indexOf(self) <> 1 then null
555+
* else if owningNamespace.owner = null then escapedName() <br/>
556+
* else if owningNamespace.qualifiedName = null or escapedName() = null then null <br/>
557+
* else owningNamespace.qualifiedName + '::' + escapedName() <br/>
558+
* endif <br/>
559+
* endif <br/>
560+
* endif <br/>
561+
* endif
562+
* </p>
563+
* -->
551564
*
552565
* @generated NOT
553566
*/
554567
@Override
555568
public String getQualifiedName() {
556-
String selfName = NameHelper.toPrintableName(this.getName());
569+
// SysMLv2 specification compliant implementation
570+
// at some point we will have to use this implementation
571+
// String getQualifiedName = null;
572+
// if (this.getOwningNamespace() == null) {
573+
// getQualifiedName = null;
574+
// } else if (this.getName() != null && this.getOwningNamespace().getOwnedMember().stream().filter(m ->
575+
// m.getName() == this.getName()).toList().indexOf(this) != 0) {
576+
// getQualifiedName = null;
577+
// } else if (this.getOwningNamespace().getOwner() != null) {
578+
// getQualifiedName = this.escapedName();
579+
// } else if (this.getOwningNamespace().getQualifiedName() == null || this.escapedName() == null) {
580+
// getQualifiedName = null;
581+
// } else {
582+
// getQualifiedName = this.getOwningNamespace().getQualifiedName() + "::" + this.escapedName();
583+
// }
584+
// return getQualifiedName;
585+
586+
var selfName = NameHelper.toPrintableName(this.getName(), true);
557587
if (selfName.isBlank()) {
558588
return null;
559589
}
560-
561-
StringBuilder qualifiedNameBuilder = new StringBuilder();
562-
Element container = this.getOwner();
563-
if (container != null && container instanceof Membership membership) {
564-
Element membershipContainer = membership.getOwner();
565-
if (membershipContainer != null) {
566-
String elementQN = membershipContainer.getQualifiedName();
590+
var qualifiedNameBuilder = new StringBuilder();
591+
var container = this.getOwner();
592+
if (container instanceof Membership membership) {
593+
var membershipContainer = membership.getOwner();
594+
if (membershipContainer instanceof Element) {
595+
var elementQN = membershipContainer.getQualifiedName();
567596
if (elementQN != null && !elementQN.isBlank()) {
568597
qualifiedNameBuilder.append(elementQN);
569598
qualifiedNameBuilder.append("::");
570599
}
571600
}
572601
} else if (container != null) {
573-
String elementQN = container.getQualifiedName();
602+
var elementQN = container.getQualifiedName();
574603
if (elementQN != null && !elementQN.isBlank()) {
575604
qualifiedNameBuilder.append(elementQN);
576605
qualifiedNameBuilder.append("::");
577606
}
578607
}
579-
580608
qualifiedNameBuilder.append(selfName);
581609
return qualifiedNameBuilder.toString();
582610
}
@@ -630,17 +658,11 @@ public String effectiveShortName() {
630658
*/
631659
@Override
632660
public String escapedName() {
633-
String escapedName = null;
634-
String name = this.getName();
635-
if (name == null) {
661+
String escapedName = this.getName();
662+
if (escapedName == null) {
636663
escapedName = this.getShortName();
637-
} else {
638-
escapedName = this.getName();
639-
}
640-
if (escapedName != null && escapedName.contains("\\S+")) {
641-
escapedName = "'" + escapedName + "'";
642664
}
643-
return escapedName;
665+
return NameHelper.toPrintableName(escapedName, true);
644666
}
645667

646668
/**

backend/metamodel/syson-sysml-metamodel/src/main/java/org/eclipse/syson/sysml/textual/SysMLElementSerializer.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public String caseAllocationUsage(AllocationUsage allocationUsage) {
178178
builder.appendWithSpaceIfNeeded(SysMLv2Keywords.ALLOCATION);
179179
builder.appendWithSpaceIfNeeded(declarationBuilder);
180180
}
181-
181+
182182
builder.appendWithSpaceIfNeeded(SysMLv2Keywords.ALLOCATE);
183183
this.appendConnectorPart(builder, allocationUsage);
184184
this.appendChildrenContent(builder, allocationUsage, allocationUsage.getOwnedMembership());
@@ -866,6 +866,21 @@ public String casePortUsage(PortUsage portUsage) {
866866
return builder.toString();
867867
}
868868

869+
@Override
870+
public String caseReferenceUsage(ReferenceUsage reference) {
871+
Appender builder = new Appender(this.lineSeparator, this.indentation);
872+
if (!this.isImplicit(reference)) {
873+
this.appendBasicUsagePrefix(builder, reference);
874+
this.appendUsageDeclaration(builder, reference);
875+
this.appendUsageCompletion(builder, reference);
876+
}
877+
if (builder.toString().equals(";")) {
878+
// This an EmptyUsage rule
879+
return "";
880+
}
881+
return builder.toString();
882+
}
883+
869884
@Override
870885
public String caseRenderingUsage(RenderingUsage rendering) {
871886
Appender builder = new Appender(this.lineSeparator, this.indentation);
@@ -908,21 +923,6 @@ public String caseRequirementDefinition(RequirementDefinition requirement) {
908923
return builder.toString();
909924
}
910925

911-
@Override
912-
public String caseReferenceUsage(ReferenceUsage reference) {
913-
Appender builder = new Appender(this.lineSeparator, this.indentation);
914-
if (!this.isImplicit(reference)) {
915-
this.appendBasicUsagePrefix(builder, reference);
916-
this.appendUsageDeclaration(builder, reference);
917-
this.appendUsageCompletion(builder, reference);
918-
}
919-
if (builder.toString().equals(";")) {
920-
// This an EmptyUsage rule
921-
return "";
922-
}
923-
return builder.toString();
924-
}
925-
926926
@Override
927927
public String caseRequirementUsage(RequirementUsage usage) {
928928
Appender builder = this.newAppender();

backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/impl/ElementTest.java renamed to backend/metamodel/syson-sysml-metamodel/src/test/java/org/eclipse/syson/sysml/impl/ElementImplTest.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024, 2025 Obeo.
2+
* Copyright (c) 2024, 2026 Obeo.
33
* This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v2.0
55
* which accompanies this distribution, and is available at
@@ -34,7 +34,7 @@
3434
*
3535
* @author gescande
3636
*/
37-
public class ElementTest {
37+
public class ElementImplTest {
3838

3939
/**
4040
* Test model
@@ -51,9 +51,10 @@ public class ElementTest {
5151
* }
5252
* private part def 'private def1x1';
5353
* private part def 'def-10';
54-
* private part def 'éléphant;
54+
* private part def 'éléphant';
5555
* private part def def_11;
5656
* private part def _def_12;
57+
* part def 'with escaped\'apostrophe';
5758
* }
5859
* }
5960
* }
@@ -70,14 +71,11 @@ private class TestModel {
7071
private Package p1x1;
7172
private PartDefinition def1x1;
7273
private PartDefinition privatedef1x1;
73-
7474
private PartDefinition def10;
75-
7675
private PartDefinition defElephant;
77-
7876
private PartDefinition def11;
79-
8077
private PartDefinition def12;
78+
private PartDefinition withEscapedApostrophe;
8179

8280
TestModel() {
8381
this.build();
@@ -97,6 +95,7 @@ private void build() {
9795
this.defElephant = this.builder.createInWithName(PartDefinition.class, this.p1x1, "éléphant");
9896
this.def11 = this.builder.createInWithName(PartDefinition.class, this.p1x1, "def_11");
9997
this.def12 = this.builder.createInWithName(PartDefinition.class, this.p1x1, "_def_12");
98+
this.withEscapedApostrophe = this.builder.createInWithName(PartDefinition.class, this.p1x1, "with escaped'apostrophe");
10099
}
101100
}
102101

@@ -113,6 +112,7 @@ public void getQualifiedNameTest() {
113112
assertEquals("p1::'p1 x1'::'éléphant'", testModel.defElephant.getQualifiedName());
114113
assertEquals("p1::'p1 x1'::def_11", testModel.def11.getQualifiedName());
115114
assertEquals("p1::'p1 x1'::_def_12", testModel.def12.getQualifiedName());
115+
assertEquals("p1::'p1 x1'::'with escaped\\'apostrophe'", testModel.withEscapedApostrophe.getQualifiedName());
116116
}
117117

118118
@DisplayName("Check documentation feature")
@@ -140,4 +140,11 @@ public void isLibraryElementNotInsideLibraryPackageTest() {
140140
assertThat(testModel.def1.isIsLibraryElement()).isFalse();
141141
assertThat(testModel.def12.isIsLibraryElement()).isFalse();
142142
}
143+
144+
@DisplayName("Check espacedNamed derived operation")
145+
@Test
146+
public void escapedNameTest() {
147+
var testModel = new TestModel();
148+
assertEquals("'with escaped\\'apostrophe'", testModel.withEscapedApostrophe.escapedName());
149+
}
143150
}

backend/tests/syson-tests/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
<dependency>
4848
<groupId>com.tngtech.archunit</groupId>
4949
<artifactId>archunit-junit5</artifactId>
50-
<version>0.19.0</version>
50+
<version>1.3.0</version>
5151
</dependency>
5252
</dependencies>
5353
<build>

0 commit comments

Comments
 (0)