Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gh#4 graphml support #8

Merged
merged 6 commits into from
Jun 14, 2024
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Currently supported artifacts:
- SysML XMI/UML (based on prototype at https://github.com/kbss-cvut/xmi-parser-mwe)
- `.zip` archive containing a model file and relevant profile files
- `*.xmi`/`*.uml`/`*.xml` file. The file or the directory containing the file should also contain relevant profile data
- `.graphml` files (produced by [yEd](https://www.yworks.com/products/yed) and [yEd Live](https://www.yworks.com/products/yed-live))

Tested on XMI artifacts produced by [Modelio](https://www.modelio.org/index.htm) and [Enterprise Architect](https://sparxsystems.com/products/ea/).

Expand Down Expand Up @@ -38,6 +39,19 @@ Do not forget to add the AKAENE Maven repository:
</repositories>
```

## How to Use

`ControlStructureParsers` is able to find the correct parser and use it to parse the specified model file.

```java
import com.akaene.stpa.scs.parser.ControlStructureParsers;

final File input = // get input file
final Model model = ControlStructureParsers.parse(input);
```

But it is also possible to use the parser implementations directly.

## License

MIT
16 changes: 15 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.akaene.stpa</groupId>
<artifactId>control-structure-parser</artifactId>
<version>0.0.7</version>
<version>0.0.8</version>
<name>Control Structure Parser</name>
<description>Safety control structure parser for STPA</description>

Expand Down Expand Up @@ -99,18 +99,32 @@
<version>5.5.0.v20221116-1811</version>
</dependency>

<!-- XSLT -->
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId>
<version>12.4</version>
</dependency>

<!-- DOM -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.4</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/akaene/stpa/scs/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ public List<Stereotype> getStereotypes() {

@Override
public String toString() {
return name + " : " + type.getName();
return name + " : " + (type != null ? type.getName() : "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@ public interface ControlStructureParser {
* @return Parsed model
*/
Model parse(File input);

/**
* Checks whether this parser supports the specified input file.
*
* @param input Input file to potentially parse
* @return {@code true} if the file type is supported by this parser, {@code false} otherwise
*/
boolean supports(File input);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.akaene.stpa.scs.parser;

import com.akaene.stpa.scs.model.Model;
import com.akaene.stpa.scs.parser.sysml.SysMLXMIParser;

import java.io.File;

Expand All @@ -10,8 +9,6 @@
*/
public class ControlStructureParserRunner {

private final ControlStructureParser parser = new SysMLXMIParser();

public static void main(String[] args) {
if (args.length != 1) {
throw new IllegalArgumentException("Expected exactly one argument");
Expand All @@ -24,7 +21,7 @@ private void parseAndPrint(String path) {
if (!file.exists()) {
throw new IllegalArgumentException("Specified path does not exist.");
}
final Model model = parser.parse(file);
final Model model = ControlStructureParsers.parse(file);
System.out.println(model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.akaene.stpa.scs.parser;

import com.akaene.stpa.scs.model.Model;
import com.akaene.stpa.scs.parser.graphml.GraphMLParser;
import com.akaene.stpa.scs.parser.sysml.SysMLXMIParser;

import java.io.File;
import java.util.List;

/**
* Provides parsing of control structure for specified file.
* <p>
* It contains a list of supported parsers. When a file is provided, it finds the first parser that supports it and uses
* it to parse the system control structure.
*/
public class ControlStructureParsers {

private static final List<ControlStructureParser> parsers = List.of(
new SysMLXMIParser(),
new GraphMLParser()
);

/**
* Finds a suitable parser and uses it to parse system control structure from the specified file.
*
* @param input File containing system model
* @return Model of system control structure read from the specified file
*/
public static Model parse(File input) {
return parsers.stream().filter(p -> p.supports(input)).findFirst().map(p -> p.parse(input))
.orElseThrow(
() -> new IllegalArgumentException("No parser found that would support file " + input));
}
}
154 changes: 154 additions & 0 deletions src/main/java/com/akaene/stpa/scs/parser/graphml/GraphMLParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.akaene.stpa.scs.parser.graphml;

import com.akaene.stpa.scs.exception.ControlStructureParserException;
import com.akaene.stpa.scs.model.Component;
import com.akaene.stpa.scs.model.Connector;
import com.akaene.stpa.scs.model.ConnectorEnd;
import com.akaene.stpa.scs.model.Model;
import com.akaene.stpa.scs.model.Stereotype;
import com.akaene.stpa.scs.parser.ControlStructureParser;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* Control structure parser for GraphML files (produced by yEd).
* <p>
* It expects:
* <ul>
* <li>Components to be represented by shape of type {@literal rectangle}</li>
* <li>Control actions to be represented by edges of type {@literal line}</li>
* <li>Feedback to be represented by edges of type {@literal dashed}</li>
* <li>Additional control information to be represented by edges of type {@literal dotted}</li>
* </ul>
*/
public class GraphMLParser implements ControlStructureParser {

private static final Logger LOG = LoggerFactory.getLogger(GraphMLParser.class);

public static final String FILE_EXTENSION = ".graphml";

@Override
public Model parse(File input) {
LOG.debug("Parsing input using {}.", getClass().getSimpleName());
final Document document = readDocument(input);
final ParsingState state = new ParsingState();
readNodes(document).forEach(n -> state.nodes.put(n.id(), n));
readConnectors(state, document);
return state.result;
}

private Document readDocument(File file) {
try {
return Jsoup.parse(file, StandardCharsets.UTF_8.name(), "", Parser.xmlParser());
} catch (IOException e) {
throw new ControlStructureParserException("Unable to parse file " + file.getName(), e);
}
}

private List<Node> readNodes(Document document) {
final Elements nodeElements = document.select("node");
final List<Node> nodes = nodeElements.stream().filter(n -> !n.select("y|Shape[type=\"rectangle\"]").isEmpty())
.map(n -> {
final String id = n.id();
final String label = n.select("y|NodeLabel").stream()
.map(e -> e.text().trim())
.collect(Collectors.joining(" "))
.replace('\n', ' ');
return new Node(id, label, new Component(label, id, null));
}).toList();
LOG.trace("Found {} nodes.", nodes.size());
return nodes;
}

private void readConnectors(ParsingState state, Document document) {
final Elements edges = document.select("edge");
edges.forEach(e -> {
final String id = e.id();
final Node source = state.nodes.get(e.attr("source"));
final Node target = state.nodes.get(e.attr("target"));
if (source == null || target == null) {
LOG.error("Edge {} is missing resolved source or target node.", id);
return;
}
final Elements labels = e.select("y|EdgeLabel");
final String label = labels.stream().map(l -> l.wholeText().trim()).collect(Collectors.joining("\n"));
final Optional<EdgeStereotype> stereotype = edgeToStereotype(e);
final String[] items = label.split("\n");
for (String labelItem : items) {
final Connector connector = new Connector(labelItem, id,
new ConnectorEnd(source.component(), null, null, null),
new ConnectorEnd(target.component(), null, null, null));
stereotype.ifPresent(s -> connector.addStereotype(s.getStereotype()));
state.result.addConnector(connector);
}
});
}

private Optional<EdgeStereotype> edgeToStereotype(Element edge) {
final String lineType = edge.select("y|LineStyle").attr("type");
for (EdgeStereotype stereotype : EdgeStereotype.values()) {
if (stereotype.getEdgeType().equals(lineType)) {
return Optional.of(stereotype);
}
}
LOG.debug("Edge {} is of no matching stereotyped type.", edge);
return Optional.empty();
}

@Override
public boolean supports(File input) {
return input.exists() && input.getName().endsWith(FILE_EXTENSION);
}

private record Node(String id, String label, Component component) {
}

private enum EdgeStereotype {

ControlAction("line", "ControlAction"), Feedback("dashed", "Feedback"),
AdditionalInfo("dotted", "AdditionalControlInformation");

private final String edgeType;
private final Stereotype stereotype;

EdgeStereotype(String edgeType, String stereotype) {
this.edgeType = edgeType;
this.stereotype = new Stereotype(stereotype);
}

public String getEdgeType() {
return edgeType;
}

public Stereotype getStereotype() {
return stereotype;
}
}

private static class ParsingState {

private final Model result = new Model();

private final Map<String, Node> nodes = new HashMap<>();

private ParsingState() {
for (EdgeStereotype es : EdgeStereotype.values()) {
result.addStereotype(es.getStereotype());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class EMFSysMLXMIParser implements ControlStructureParser {

@Override
public Model parse(File input) {
LOG.debug("Parsing input using {}.", getClass().getSimpleName());
final XMI2UMLResource xmi = parseAsResource(input);
final org.eclipse.uml2.uml.Model emfModel = getModelElement(xmi);
final ParsingState state = initParsingState(xmi);
Expand All @@ -68,16 +69,16 @@ protected ParsingState initParsingState(XMI2UMLResource resource) {
}

public XMI2UMLResource parseAsResource(File input) {
LOG.debug("Parsing XMI file '{}'.", input);
LOG.debug("Parsing XMI file '{}'.", input.getName());
ResourceSet set = new ResourceSetImpl();
Stream.of(SysMLXMIParser.SUPPORTED_EXTENSIONS)
Stream.of(SysMLXMIParser.SUPPORTED_FILE_EXTENSIONS)
.forEach(ext -> set.getResourceFactoryRegistry().getExtensionToFactoryMap()
.put(ext, XMI2UMLResource.Factory.INSTANCE));
try {
final URI uri = URI.createFileURI(input.getAbsolutePath());
return (XMI2UMLResource) set.getResource(uri, true);
} catch (RuntimeException e) {
throw new ControlStructureParserException("Unable to parse file " + input, e);
throw new ControlStructureParserException("Unable to parse file " + input.getName(), e);
}
}

Expand Down Expand Up @@ -257,6 +258,11 @@ protected void extractAssociations(org.eclipse.uml2.uml.Model xmiModel, ParsingS
result.forEach(state.result::addAssociation);
}

@Override
public boolean supports(File input) {
return input.exists() && Stream.of(SysMLXMIParser.SUPPORTED_FILE_EXTENSIONS).anyMatch(ext -> input.getName().endsWith(ext));
}

protected static class ParsingState {

protected final Model result = new Model();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class EnterpriseArchitectSysMLXMIParser extends EMFSysMLXMIParser {

@Override
public Model parse(File input) {
LOG.debug("Parsing input using {}.", getClass().getSimpleName());
final File transformedInput = transformToEMFReadable(input);
try {
return super.parse(transformedInput);
Expand All @@ -55,6 +56,11 @@ public Model parse(File input) {
}
}

@Override
public boolean supports(File input) {
return isEnterpriseArchitectFile(input);
}

@Override
protected EnterpriseArchitectParsingState initParsingState(XMI2UMLResource resource) {
return new EnterpriseArchitectParsingState(resource);
Expand Down Expand Up @@ -95,7 +101,7 @@ protected Collection<Stereotype> getElementStereotypes(EObject element, ParsingS
}

public static boolean isEnterpriseArchitectFile(File input) {
LOG.trace("Checking if input file '{}' was produced by Enterprise Architect.", input);
LOG.trace("Checking if input file '{}' was produced by Enterprise Architect.", input.getName());
try {
final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
final XMLEventReader eventReader = inputFactory.createXMLEventReader(new FileInputStream(input));
Expand Down
Loading