Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Add a few simple unit tests to help maintain this enumeration.
Add possibility for validation rules to specify their severity ; the SysML rules from the specification remain as WARNINGs for now.
Tweak the diagnostic messages to help users understand where the erroneous element is in their editing context.
Make sure diagnostics are sorted according to their validated element and its containing document.
- https://github.com/eclipse-syson/syson/issues/2042[#2042] [import] Improve error reporting while uploading a document.

=== New features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,22 @@ public void testReferentialUsages() throws IOException {
});
}

@Test
@DisplayName("GIVEN a really big SysML model, WHEN importing that model, THEN the import fails but a error message is returned")
public void checkBigSysmlFileFailure() throws IOException {

final StringBuilder builder = this.generateInDepthSysmlStructure(3000, 20);

String input = builder.toString();
this.checker.addExpectedReportMessage("[ERROR] Error: File size exceeds limit : The selected SysML file is too large to be processed by SysON." +
" Please optimize the model or split it into smaller sub-packages and try again.")
.checkImportedModel(resource -> {
assertThat(resource.getContents()).isEmpty();
})
.check(input);
}


@Test
@DisplayName("GIVEN of model with redefinition depending on inherited memberships computation, WHEN importing the model, THEN redefined feature should resolve properly using inherited " +
"memberships")
Expand Down Expand Up @@ -1331,4 +1347,22 @@ private void assertLiteralStringValue(String expected, Feature f) {
.extracting(e -> ((LiteralString) e).getValue())
.isEqualTo(expected);
}

private void generateInDepthSysmlStructure(StringBuilder builder, int prefix, int level, int maxDepth) {
builder.append("\t".repeat(level)).append("package pack_").append(prefix).append("_").append(level).append("{").append(System.lineSeparator());
builder.append("\t".repeat(level)).append(" part part_").append(prefix).append("_").append(level).append(";").append(System.lineSeparator());

if (level < maxDepth) {
this.generateInDepthSysmlStructure(builder, prefix, level + 1, maxDepth);
}
builder.append("\t".repeat(level)).append("}").append(System.lineSeparator());
}

private StringBuilder generateInDepthSysmlStructure(int width, int depth) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < width; i++) {
this.generateInDepthSysmlStructure(builder, i, 0, depth);
}
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*******************************************************************************/
package org.eclipse.syson.application.imports;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
Expand All @@ -25,6 +26,7 @@

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.sirius.web.application.document.services.LoadingReport;
import org.eclipse.sirius.web.application.document.services.api.ExternalResourceLoadingResult;
import org.eclipse.sirius.web.application.editingcontext.EditingContext;
import org.eclipse.syson.sysml.upload.SysMLExternalResourceLoaderService;
Expand All @@ -40,6 +42,8 @@ public class SysMLv2SemanticImportChecker {

private final List<Consumer<Resource>> semanticCheckers = new ArrayList<>();

private final List<String> expectedErrorMessages = new ArrayList<>();

private final EditingContext editingContext;


Expand All @@ -58,12 +62,22 @@ public void check(String importedText) throws IOException {
assertTrue(optExternalResourceLoadingResult.isPresent());

ExternalResourceLoadingResult externalResourceLoadingResult = optExternalResourceLoadingResult.get();
if (!this.expectedErrorMessages.isEmpty()) {
LoadingReport report = (LoadingReport) externalResourceLoadingResult.loadingReport();
assertThat(report.content()).isEqualTo(this.expectedErrorMessages);
}

for (Consumer<Resource> checker : this.semanticCheckers) {
checker.accept(externalResourceLoadingResult.resource());
}
}
}

public SysMLv2SemanticImportChecker addExpectedReportMessage(String report) {
this.expectedErrorMessages.add(report);
return this;
}

public SysMLv2SemanticImportChecker checkImportedModel(Consumer<Resource> checker) {
this.semanticCheckers.add(checker);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
import org.eclipse.emf.ecore.xmi.XMIResource;
import org.eclipse.emf.ecore.xmi.impl.XMIResourceImpl;
import org.eclipse.syson.sysml.ASTTransformer;
import org.eclipse.syson.sysml.AstParsingResult;
import org.eclipse.syson.sysml.SysmlToAst;
import org.eclipse.syson.sysml.textual.utils.Status;

/**
* Converts a SyML file to its XMI representation using SysON import.
Expand Down Expand Up @@ -89,23 +91,33 @@ private void convert(String targetFilePath, File sysmlFile, ResourceSet resource
SysmlToAst sysmlToAst = new SysmlToAst(null);
ASTTransformer astTransformer = new ASTTransformer();
try (InputStream inputStream = new FileInputStream(sysmlFile)) {
InputStream astStream = sysmlToAst.convert(inputStream, "sysml");
Resource resource = astTransformer.convertResource(astStream, resourceSet);

if (resource != null && !resource.getContents().isEmpty()) {
System.out.println("Model parsed successfully.");
XMIResource resourceToSave = new XMIResourceImpl(URI.createFileURI(targetFilePath));
resourceToSave.getContents().addAll(resource.getContents());
resourceToSave.save(Collections.emptyMap());
if (!astTransformer.getTransformationMessages().isEmpty()) {
final String errorMessage = astTransformer.getTransformationMessages().stream()
.map(message -> message.level().toString() + " - " + message.body())
.collect(Collectors.joining("\n", "\n", "\n"));
System.err.println("Error while parsing input file : " + errorMessage);
AstParsingResult astResult = sysmlToAst.convert(inputStream, "sysml");

if (!astResult.reports().isEmpty()) {
final String errorMessage = astResult.reports().stream()
.map(Status::toString)
.collect(Collectors.joining(System.lineSeparator(), System.lineSeparator(), System.lineSeparator()));
System.err.println("[AST] while parsing input file : " + errorMessage);
}

if (astResult.ast().isPresent()) {
Resource resource = astTransformer.convertResource(astResult.ast().get(), resourceSet);

if (resource != null && !resource.getContents().isEmpty()) {
System.out.println("Model parsed successfully.");
XMIResource resourceToSave = new XMIResourceImpl(URI.createFileURI(targetFilePath));
resourceToSave.getContents().addAll(resource.getContents());
resourceToSave.save(Collections.emptyMap());
} else {
System.err.println("Failed to parse resource or resource is empty.");
}
}

} else {
System.err.println("Failed to parse resource or resource is empty.");
if (!astTransformer.getTransformationMessages().isEmpty()) {
final String errorMessage = astTransformer.getTransformationMessages().stream()
.map(message -> message.level().toString() + " - " + message.body())
.collect(Collectors.joining(System.lineSeparator(), System.lineSeparator(), System.lineSeparator()));
System.err.println("Error while parsing input file : " + errorMessage);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*******************************************************************************
* 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;

import java.io.InputStream;
import java.util.List;
import java.util.Optional;

import org.eclipse.syson.sysml.textual.utils.Status;

/**
* Result of the parsing of the AST.
*
* @param ast an optional {@link InputStream} of the AST
* @param reports parsing messages to be reported to the user
*
* @author Arthur Daussy
*/
public record AstParsingResult(Optional<InputStream> ast, List<Status> reports) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2024 Obeo.
* Copyright (c) 2024, 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
Expand All @@ -14,16 +14,23 @@

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.eclipse.syson.sysml.textual.utils.Severity;
import org.eclipse.syson.sysml.textual.utils.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -48,13 +55,16 @@ public SysmlToAst(@Value("${org.eclipse.syson.syside.path:#{null}}") final Strin
this.cliPath = cliPath;
}

public InputStream convert(final InputStream input, final String fileExtension) {
InputStream output = null;
public AstParsingResult convert(final InputStream input, final String fileExtension) {
Path sysmlInputPath = null;
Path sysIdeInputPath = null;
Optional<InputStream> astInputStream = Optional.empty();
List<Status> reports = new ArrayList<>();

try {
final Path sysmlInputPath = this.createTempFile(input, "syson", fileExtension);
sysmlInputPath = this.createTempFile(input, "syson", fileExtension);


Path sysIdeInputPath = null;
if (this.cliPath != null) {
sysIdeInputPath = Path.of(this.cliPath);
} else {
Expand All @@ -64,47 +74,115 @@ public InputStream convert(final InputStream input, final String fileExtension)
sysIdeInputPath = this.createTempFile(sysIdeInputStream, "syside-cli", "js");
}

this.logger.info("Call syside application : node " + sysIdeInputPath.toString() + " dump " + sysmlInputPath.toString());
this.logger.info("Call syside application : node " + sysIdeInputPath + " dump " + sysmlInputPath);
final String[] args = { "node", sysIdeInputPath.toString(), "dump", sysmlInputPath.toString() };
ProcessBuilder pb = new ProcessBuilder(args);
pb = pb.redirectErrorStream(false);
final Process sysIdeProcess = pb.start();
final InputStream is = sysIdeProcess.getInputStream();
final InputStreamReader isr = new InputStreamReader(is);
final BufferedReader br = new BufferedReader(isr);
final StringBuilder builder = new StringBuilder();

String line = br.readLine();
if (line != null) {
while (!line.contains("{")) {
line = br.readLine();
}
builder.append(line);
while ((line = br.readLine()) != null) {
builder.append(line);

CompletableFuture<String> stdoutFuture = this.readStdOut(sysIdeProcess, reports);
CompletableFuture<String> stderrFuture = this.readStdErr(sysIdeProcess, reports);

this.handleStdError(stderrFuture.join(), reports);
boolean finished = sysIdeProcess.waitFor(60, TimeUnit.SECONDS);

if (finished) {
String stdout = stdoutFuture.join();
int exitCode = sysIdeProcess.exitValue();
if (exitCode == 0) {
astInputStream = Optional.of(new ByteArrayInputStream(stdout.getBytes()));
} else {
this.logger.error("The process that parse the SysML file ended with an error core : {}", exitCode);
}
} else {
final InputStream er = sysIdeProcess.getErrorStream();
final InputStreamReader err = new InputStreamReader(er);
final BufferedReader erbr = new BufferedReader(err);
this.logger.error("Fail to call syside application : \n " + erbr.lines().collect(Collectors.joining("\n")));
reports.add(new Status(Severity.ERROR, "Process timed out : The upload process was canceled."));
sysIdeProcess.destroyForcibly();
}

output = new ByteArrayInputStream(builder.toString().getBytes());

sysmlInputPath.toFile().delete();
} catch (final IOException | InterruptedException e) {
this.logger.error(e.getMessage());
} finally {
if (sysmlInputPath != null) {
sysmlInputPath.toFile().delete();
}
if (this.cliPath == null) {
sysIdeInputPath.toFile().delete();
}
}
return new AstParsingResult(astInputStream, reports);

} catch (final IOException e) {
this.logger.error(e.getMessage());

}

private CompletableFuture<String> readStdErr(Process sysIdeProcess, List<Status> messages) {
return CompletableFuture.supplyAsync(
() -> {
try {
return this.readStream(sysIdeProcess.getErrorStream());
} catch (IOException e) {
this.logger.error("Error while reading AST : " + e.getMessage(), e);
messages.add(new Status(Severity.ERROR, "Error while building AST on stdErr."));
return "";
}
});
}

private CompletableFuture<String> readStdOut(Process sysIdeProcess, List<Status> messages) {
return CompletableFuture.supplyAsync(
() -> {
try {
return this.readAstStream(sysIdeProcess.getInputStream());
} catch (IOException e) {
this.logger.error("Error while reading AST : " + e.getMessage(), e);
messages.add(new Status(Severity.ERROR, "Error while building AST on stdOut."));
return "";
}
});
}

private void handleStdError(String stderr, List<Status> reports) {
if (stderr != null && !stderr.isBlank()) {
this.logger.error("AST parsing errors :" + System.lineSeparator() + stderr);

if (stderr.contains("JSON.stringify")) {
// This case occurs when the provided JSon file is too big
reports.add(new Status(Severity.ERROR, "Error: File size exceeds limit : The selected SysML file is too large to be processed by SysON." +
" Please optimize the model or split it into smaller sub-packages and try again."));
} else {
reports.add(new Status(Severity.ERROR, "An unhandled exception has occurred during file parsing. Contact your administrator."));
}
}
}

private String readStream(InputStream stream) throws IOException {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
return reader.lines()
.collect(Collectors.joining(System.lineSeparator()));
}
}

return output;
private String readAstStream(InputStream stream) throws IOException {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
StringBuilder builder = new StringBuilder();
String line = reader.readLine();
if (line != null) {
while (line != null && !line.contains("{")) {
line = reader.readLine();
}
if (line != null) {
builder.append(line);
while ((line = reader.readLine()) != null) {
builder.append(line);
}
}
}
return builder.toString();
}
}

private Path createTempFile(final InputStream input, final String fileName, final String fileExtension) throws IOException, FileNotFoundException {
private Path createTempFile(final InputStream input, final String fileName, final String fileExtension) throws IOException {
final Path inputPath = Files.createTempFile(fileName, "." + fileExtension);
final OutputStream outStream = new FileOutputStream(inputPath.toString());
final byte[] buffer = new byte[8 * 1024];
Expand Down
Loading
Loading