Skip to content

Commit 78ace55

Browse files
adaussyAxelRICHARD
authored andcommitted
[2037] Backslash need to be escaped during textual export
Bug: #2037 Signed-off-by: Arthur Daussy <arthur.daussy@obeo.fr>
1 parent 5448bfc commit 78ace55

File tree

9 files changed

+225
-30
lines changed

9 files changed

+225
-30
lines changed

CHANGELOG.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ And the following methods have been added:
5151
** `getDependenciesForPublishedLibrary(IEMFEditingContext, Set<Resource>)`
5252
** `getDependenciesBasedOnResources(IEMFEditingContext, Set<Resource>)`
5353

54+
- https://github.com/eclipse-syson/syson/issues/2037[#2037] [export] `org.eclipse.syson.sysml.textual.SysMLElementSerializer` now takes a `org.eclipse.syson.sysml.textual.SysMLSerializingOptions` as parameter that gather all serializing options.
5455

5556
=== Dependency update
5657

@@ -76,6 +77,7 @@ For example `ViewUsage` elements are no longer rendered in _parts_ compartments.
7677
- https://github.com/eclipse-syson/syson/issues/2023[#2023] [diagrans] On diagrams, `ConnectionDefinition` graphical nodes are now correctly labelled as `«connection def»`.
7778
- https://github.com/eclipse-syson/syson/issues/2034[#2034] [import] Fix textual import to be able to annotate `Relationships`.
7879
- https://github.com/eclipse-syson/syson/issues/2032[#2032] [export] Fix NPE when exporting `LiteralString`.
80+
- https://github.com/eclipse-syson/syson/issues/2037[#2037] [export] Backslashes need to be escaped during textual export in `LiteralString`.
7981

8082
=== Improvements
8183

backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/services/DetailsViewService.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import org.eclipse.syson.sysml.ViewUsage;
7272
import org.eclipse.syson.sysml.metamodel.services.ElementInitializerSwitch;
7373
import org.eclipse.syson.sysml.textual.SysMLElementSerializer;
74+
import org.eclipse.syson.sysml.textual.SysMLSerializingOptions;
7475
import org.eclipse.syson.sysml.textual.utils.FileNameDeresolver;
7576

7677
/**
@@ -590,7 +591,13 @@ public String getValueExpressionTextualRepresentation(FeatureValue featureValue)
590591
Expression value = featureValue.getValue();
591592
String result = "";
592593
if (value != null) {
593-
String textualFormat = new SysMLElementSerializer("\n", "\t", new FileNameDeresolver(), s -> {
594+
SysMLSerializingOptions options = new SysMLSerializingOptions.Builder()
595+
.lineSeparator("\n")
596+
.nameDeresolver(new FileNameDeresolver())
597+
.indentation("\t")
598+
.needEscapeCharacter(false)
599+
.build();
600+
String textualFormat = new SysMLElementSerializer(options, s -> {
594601
// Do nothing for now
595602
}).doSwitch(value);
596603
if (textualFormat != null) {

backend/application/syson-application/src/test/java/org/eclipse/syson/sysml/textual/SysMLElementSerializerTest.java

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -871,17 +871,19 @@ private void addOwnedMembership(Element parent, Element ownedElement, Visibility
871871
}
872872
}
873873

874-
private String convertToText(Element source, Element context, int indent) {
875-
return new SysMLElementSerializer("\n", " ", new FileNameDeresolver(), this.status::add).doSwitch(source);
876-
}
877-
878874
private String convertToText(Element source) {
879-
return this.convertToText(source, (Element) source.eContainer(), 0);
875+
SysMLSerializingOptions options = new SysMLSerializingOptions.Builder()
876+
.lineSeparator("\n")
877+
.nameDeresolver(new FileNameDeresolver())
878+
.indentation(" ")
879+
.needEscapeCharacter(true)
880+
.build();
881+
return new SysMLElementSerializer(options, this.status::add).doSwitch(source);
880882
}
881883

882-
private void assertTextualFormEquals(String extexted, Element elementToTest) {
884+
private void assertTextualFormEquals(String expected, Element elementToTest) {
883885
String content = this.convertToText(elementToTest);
884-
assertEquals(extexted, content);
886+
assertEquals(expected, content);
885887
}
886888

887889
@Test
@@ -1270,6 +1272,49 @@ public void literalStringWithDoubleQuotes() {
12701272
this.assertTextualFormEquals("\"va\\\"lue\"", literalStr);
12711273
}
12721274

1275+
@Test
1276+
public void literalStringWithQuotes() {
1277+
LiteralString literalStr = SysmlFactory.eINSTANCE.createLiteralString();
1278+
literalStr.setValue("Unit is \"meter\"");
1279+
this.assertTextualFormEquals("\"Unit is \\\"meter\\\"\"", literalStr);
1280+
}
1281+
1282+
@Test
1283+
public void literalStringWithBackslash() {
1284+
LiteralString literalStr = SysmlFactory.eINSTANCE.createLiteralString();
1285+
literalStr.setValue("Folder\\SubFolder");
1286+
1287+
// Only \ is escaped
1288+
this.assertTextualFormEquals("\"Folder\\\\SubFolder\"", literalStr);
1289+
}
1290+
1291+
@Test
1292+
public void literalStringWithBoth() {
1293+
LiteralString literalStr = SysmlFactory.eINSTANCE.createLiteralString();
1294+
literalStr.setValue("Path: \"C:\\\"");
1295+
1296+
// Expected result : "Path: \"C:\\\""
1297+
this.assertTextualFormEquals("\"Path: \\\"C:\\\\\\\"\"", literalStr);
1298+
}
1299+
1300+
@Test
1301+
public void literalStringWithLineBreak() {
1302+
LiteralString literalStr = SysmlFactory.eINSTANCE.createLiteralString();
1303+
literalStr.setValue("Ligne1\nLigne2");
1304+
1305+
// \n should not be escaped
1306+
this.assertTextualFormEquals("\"Ligne1\nLigne2\"", literalStr);
1307+
}
1308+
1309+
@Test
1310+
public void literalStringWithTab() {
1311+
LiteralString literalStr = SysmlFactory.eINSTANCE.createLiteralString();
1312+
literalStr.setValue("Text\twith\ttab");
1313+
1314+
// Tabs should not be escaped
1315+
this.assertTextualFormEquals("\"Text\twith\ttab\"", literalStr);
1316+
}
1317+
12731318
@Test
12741319
public void literalRational() {
12751320
LiteralRational literalRat = SysmlFactory.eINSTANCE.createLiteralRational();

backend/application/syson-sysml-export/src/main/java/org/eclipse/syson/sysml/export/SysMLv2DocumentExporter.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.eclipse.syson.sysml.Element;
2323
import org.eclipse.syson.sysml.impl.MembershipCacheAdapter;
2424
import org.eclipse.syson.sysml.textual.SysMLElementSerializer;
25+
import org.eclipse.syson.sysml.textual.SysMLSerializingOptions;
2526
import org.eclipse.syson.sysml.textual.utils.FileNameDeresolver;
2627
import org.eclipse.syson.sysml.textual.utils.Status;
2728
import org.slf4j.Logger;
@@ -39,6 +40,13 @@ public class SysMLv2DocumentExporter implements IDocumentExporter {
3940

4041
private static final Logger LOGGER = LoggerFactory.getLogger(SysMLv2DocumentExporter.class);
4142

43+
private static final SysMLSerializingOptions SYSML_TEXTUAL_FORMAT_OPTIONS = new SysMLSerializingOptions.Builder()
44+
.lineSeparator(System.lineSeparator())
45+
.nameDeresolver(new FileNameDeresolver())
46+
.indentation("\t")
47+
.needEscapeCharacter(true)
48+
.build();
49+
4250
@Override
4351
public boolean canHandle(Resource resource, String mediaType) {
4452
boolean canHandle = false;
@@ -56,7 +64,8 @@ public Optional<byte[]> getBytes(Resource resource, String mediaType) {
5664
this.installCacheAdapter(resource, membershipCacheAdapter);
5765
try {
5866
List<Status> status = new ArrayList<>();
59-
String textualForm = new SysMLElementSerializer(System.lineSeparator(), "\t", new FileNameDeresolver(), status::add).doSwitch(element);
67+
String textualForm = new SysMLElementSerializer(SYSML_TEXTUAL_FORMAT_OPTIONS, status::add)
68+
.doSwitch(element);
6069
if (textualForm == null) {
6170
textualForm = "";
6271
}

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public class SysMLElementSerializer extends SysmlSwitch<String> {
6666

6767
private final Consumer<Status> reportConsumer;
6868

69+
private final boolean needEscapeCharacter;
70+
6971
/**
7072
* Collection of Memberships that should be skip when trying to handle the content of an object.
7173
* In most case, those membership has been handled with their parent content.
@@ -75,16 +77,16 @@ public class SysMLElementSerializer extends SysmlSwitch<String> {
7577
/**
7678
* Simple constructor.
7779
*
78-
* @param lineSeparator
79-
* the string used to separate line
80-
* @param indentation
81-
* the string used to indent the file
80+
* @param options
81+
* option to configure the serialization
82+
* @param reportConsumer used to report any error during serialization
8283
*/
83-
public SysMLElementSerializer(String lineSeparator, String indentation, INameDeresolver nameDeresolver, Consumer<Status> reportConsumer) {
84+
public SysMLElementSerializer(SysMLSerializingOptions options, Consumer<Status> reportConsumer) {
8485
super();
85-
this.lineSeparator = lineSeparator;
86-
this.indentation = indentation;
87-
this.nameDeresolver = nameDeresolver;
86+
this.lineSeparator = options.lineSeparator();
87+
this.indentation = options.indentation();
88+
this.nameDeresolver = options.nameDeresolver();
89+
this.needEscapeCharacter = options.needEscapeCharacter();
8890
if (reportConsumer == null) {
8991
this.reportConsumer = r -> {
9092
};
@@ -630,11 +632,8 @@ private String toPreciseReal(double value) {
630632
@Override
631633
public String caseLiteralString(LiteralString literal) {
632634
Appender builder = this.newAppender();
633-
builder.append("\"");
634-
if (literal.getValue() != null) {
635-
builder.append(literal.getValue().replace("\"", "\\\""));
636-
}
637-
builder.append("\"");
635+
final String value = literal.getValue();
636+
builder.append(this.toKerMLStringValue(value));
638637
return builder.toString();
639638
}
640639

@@ -3066,4 +3065,35 @@ private void appendAssignmentTargetMember(Appender builder, EList<Membership> me
30663065
}
30673066
}
30683067
}
3068+
3069+
private String toKerMLStringValue(String input) {
3070+
if (input == null) {
3071+
return "\"\"";
3072+
}
3073+
3074+
StringBuilder sb = new StringBuilder();
3075+
sb.append('"');
3076+
if (this.needEscapeCharacter) {
3077+
for (int i = 0; i < input.length(); i++) {
3078+
char c = input.charAt(i);
3079+
switch (c) {
3080+
case '\\':
3081+
sb.append("\\\\"); // Escape backslash
3082+
break;
3083+
case '\"':
3084+
sb.append("\\\""); // Escape "
3085+
break;
3086+
default:
3087+
sb.append(c);
3088+
break;
3089+
}
3090+
}
3091+
} else {
3092+
sb.append(input);
3093+
}
3094+
3095+
3096+
sb.append('"');
3097+
return sb.toString();
3098+
}
30693099
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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.sysml.textual;
14+
15+
import java.util.Objects;
16+
17+
import org.eclipse.syson.sysml.textual.utils.INameDeresolver;
18+
19+
/**
20+
* Option used in the SysmlSerializer.
21+
*
22+
* @param lineSeparator
23+
* the string used to separate line
24+
* @param indentation
25+
* the string used to indent the file
26+
* @param nameDeresolver
27+
* a {@link INameDeresolver}
28+
* @param needEscapeCharacter
29+
* (Optional) holds true if forbidden character in the textual format needs to be escaped
30+
*/
31+
public record SysMLSerializingOptions(String lineSeparator, String indentation, INameDeresolver nameDeresolver, boolean needEscapeCharacter) {
32+
33+
public SysMLSerializingOptions {
34+
Objects.requireNonNull(lineSeparator, "property :lineSeparator is required");
35+
Objects.requireNonNull(indentation, "property :indentation is required");
36+
Objects.requireNonNull(nameDeresolver, "property :nameDeresolver is required");
37+
}
38+
39+
/**
40+
* Builder for {@link SysMLSerializingOptions}.
41+
*
42+
* @author Arthur Daussy
43+
*/
44+
public static final class Builder {
45+
46+
private String lineSeparator;
47+
48+
private String indentation;
49+
50+
private INameDeresolver nameDeresolver;
51+
52+
private boolean needEscapeCharacter;
53+
54+
public Builder() {
55+
}
56+
57+
public Builder lineSeparator(String aLineSeparator) {
58+
this.lineSeparator = Objects.requireNonNull(aLineSeparator, "Null aLineSeparator");
59+
return this;
60+
}
61+
62+
public Builder indentation(String anIndentation) {
63+
this.indentation = Objects.requireNonNull(anIndentation, "Null anIndentation");
64+
return this;
65+
}
66+
67+
public Builder nameDeresolver(INameDeresolver aNameDeresolver) {
68+
this.nameDeresolver = Objects.requireNonNull(aNameDeresolver, "Null aNameDeresolver");
69+
return this;
70+
}
71+
72+
73+
public Builder needEscapeCharacter(boolean isNeedEscapeCharacter) {
74+
this.needEscapeCharacter = isNeedEscapeCharacter;
75+
return this;
76+
}
77+
78+
public SysMLSerializingOptions build() {
79+
return new SysMLSerializingOptions(this.lineSeparator, this.indentation, this.nameDeresolver, this.needEscapeCharacter);
80+
}
81+
}
82+
}

backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/DiagramQueryLabelService.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.eclipse.syson.sysml.VariantMembership;
5656
import org.eclipse.syson.sysml.helper.LabelConstants;
5757
import org.eclipse.syson.sysml.textual.SysMLElementSerializer;
58+
import org.eclipse.syson.sysml.textual.SysMLSerializingOptions;
5859
import org.eclipse.syson.sysml.textual.utils.Appender;
5960
import org.eclipse.syson.sysml.textual.utils.FileNameDeresolver;
6061
import org.eclipse.syson.sysml.textual.utils.INameDeresolver;
@@ -777,19 +778,30 @@ private void getReferenceUsagePrefix(Usage usage, StringBuilder label) {
777778
/**
778779
* Builds a SysMLSerializer.
779780
*
780-
* @param resolvableName
781-
* holds <code>true</code> to compute resolvable names for references, otherwise simple name are used to
782-
* reference an element.
781+
* @param sysmlTextualFormatCompliant
782+
* holds <code>true</code> to build a Serializer that would produce a valid text that is fully compliant with SysML textual format (resolvable name, escaped character), otherwise
783+
* holds <code>false /code> to build a serializer that provides simple String representation (use simple name, do not escape character in LiteralString etc...). The second mode is
784+
* used to print elements in diagrams or in the detail view.
783785
* @return a new {@link SysMLElementSerializer}.
784786
*/
785-
private SysMLElementSerializer buildSerializer(boolean resolvableName) {
787+
private SysMLElementSerializer buildSerializer(boolean sysmlTextualFormatCompliant) {
786788
final INameDeresolver nameDeresolver;
787-
if (resolvableName) {
789+
final boolean escapeChar;
790+
if (sysmlTextualFormatCompliant) {
788791
nameDeresolver = new FileNameDeresolver();
792+
escapeChar = true;
789793
} else {
790794
nameDeresolver = new SimpleNameDeresolver();
795+
escapeChar = false;
791796
}
792-
return new SysMLElementSerializer("\n", " ", nameDeresolver, s -> {
797+
798+
SysMLSerializingOptions options = new SysMLSerializingOptions.Builder()
799+
.lineSeparator("\n")
800+
.nameDeresolver(nameDeresolver)
801+
.indentation(" ")
802+
.needEscapeCharacter(escapeChar)
803+
.build();
804+
return new SysMLElementSerializer(options, s -> {
793805
this.logger.info(s.message());
794806
});
795807
}

backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/services/ViewLabelService.java

Lines changed: 9 additions & 2 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
@@ -21,6 +21,7 @@
2121
import org.eclipse.syson.services.SimpleNameDeresolver;
2222
import org.eclipse.syson.sysml.Element;
2323
import org.eclipse.syson.sysml.textual.SysMLElementSerializer;
24+
import org.eclipse.syson.sysml.textual.SysMLSerializingOptions;
2425
import org.slf4j.Logger;
2526
import org.slf4j.LoggerFactory;
2627

@@ -66,7 +67,13 @@ public boolean showIcon(Object object) {
6667
* @return the element itself
6768
*/
6869
public Element sendMessageWithTextualRepresentation(String msg, String level, Element element) {
69-
var serializer = new SysMLElementSerializer("\n", " ", new SimpleNameDeresolver(), s -> {
70+
SysMLSerializingOptions options = new SysMLSerializingOptions.Builder()
71+
.lineSeparator("\n")
72+
.nameDeresolver(new SimpleNameDeresolver())
73+
.indentation(" ")
74+
.needEscapeCharacter(false)
75+
.build();
76+
var serializer = new SysMLElementSerializer(options, s -> {
7077
this.logger.info(s.message());
7178
});
7279
this.feedbackMessageService.addFeedbackMessage(new Message(MessageFormat.format(msg, serializer.doSwitch(element)), MessageLevel.valueOf(level)));

doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ package Test {
8585
```
8686

8787
** Fix a textual export problem while exporting `LiteralString` with no value.
88+
** Fix the export of `LiteralString` with backslash characters.
8889

8990
* In _Explorer_ view:
9091

0 commit comments

Comments
 (0)