C4L2GraphParseTreeListener.java
package org.thewonderlemming.c4plantuml.graphml.parse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.ParseTreeListener;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thewonderlemming.c4plantuml.grammars.C4L2ParserListenerDecorator;
import org.thewonderlemming.c4plantuml.grammars.SourceType;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2ParserBaseListener;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2ParserListener;
import org.thewonderlemming.c4plantuml.graphml.model.CannotComputeEdgeModelId;
import org.thewonderlemming.c4plantuml.graphml.model.EdgeModel;
import org.thewonderlemming.c4plantuml.graphml.model.EntityType;
import org.thewonderlemming.c4plantuml.graphml.model.GraphModel;
import org.thewonderlemming.c4plantuml.graphml.model.NodeModel;
/**
* An ANTLR 4 {@link ParseTreeListener} that parses C4 L2 grammar and feeds the JAXB model with the retrieved
* information.
*
* @author thewonderlemming
*
*/
public class C4L2GraphParseTreeListener implements C4L2ParserListenerDecorator {
private static final Logger LOGGER = LoggerFactory.getLogger(C4L2GraphParseTreeListener.class);
private final Map<String, String> c4AliasToGraphAlias = new HashMap<>();
private final C4L2ParserBaseListener decorated = new C4L2ParserBaseListener();
private final GraphModel graph;
private final GraphModel systemBoundaryGraph;
private final List<Throwable> unrecoverableErrors;
/**
* Default constructor.
*/
public C4L2GraphParseTreeListener() {
this.unrecoverableErrors = new ArrayList<>();
this.graph = GraphModel
.builder()
.withId("c4l2")
.withDefaultDirection()
.withoutData()
.withoutNodes()
.withoutEdges()
.build();
this.systemBoundaryGraph = GraphModel
.builder()
.withId("systemBoundary")
.withDefaultDirection()
.withoutData()
.withoutNodes()
.withoutEdges()
.build();
}
/**
* {@inheritDoc}
*/
@Override
public void enterCloud(final C4L2Parser.CloudContext ctx) {
assert (ctx.String().size() == 2) : "Expected to find two strings within C4L2 Cloud rule";
final String alias = ctx.Alias().getText();
final String name = ctx.String(0).getText();
final String description = ctx.String(1).getText();
addNewNodeWithAlias(alias)
.withName(name)
.withDescription(description)
.withEntityType(EntityType.C2_CLOUD);
}
/**
* {@inheritDoc}
*/
@Override
public void enterContainer(final C4L2Parser.ContainerContext ctx) {
assert (ctx.String().size() == 3) : "Expected to find two strings within C4L2 Container rule";
final String alias = ctx.Alias().getText();
final String name = ctx.String(0).getText();
final String technological_stack = ctx.String(1).getText();
final String description = ctx.String(2).getText();
final EntityType entityType = ctx.CONTAINER() != null
? EntityType.C2_CONTAINER
: EntityType.C2_CONTAINER_DB;
addNewContainerNodeWithAlias(alias, technological_stack)
.withName(name)
.withDescription(description)
.withEntityType(entityType);
}
/**
* {@inheritDoc}
* <p>
* Does nothing.
*/
@Override
public void enterEveryRule(final ParserRuleContext ctx) {
// This is too generic to be called in a C4L2Parser specific grammar
}
/**
* {@inheritDoc}
*/
@Override
public void enterPerson(final C4L2Parser.PersonContext ctx) {
assert (ctx.String().size() == 2) : "Expected to find two strings within C4L2 Person rule";
final String alias = ctx.Alias().getText();
final String name = ctx.String(0).getText();
final String description = ctx.String(1).getText();
addNewNodeWithAlias(alias)
.withName(name)
.withDescription(description)
.withEntityType(EntityType.C2_PERSON);
}
/**
* {@inheritDoc}
*/
@Override
public void enterPerson_ext(final C4L2Parser.Person_extContext ctx) {
assert (ctx.String().size() == 2) : "Expected to find two strings within C4L2 PersonExt rule";
final String alias = ctx.Alias().getText();
final String name = ctx.String(0).getText();
final String description = ctx.String(1).getText();
addNewNodeWithAlias(alias)
.withName(name)
.withDescription(description)
.withEntityType(EntityType.C2_PERSON_EXT);
}
/**
* {@inheritDoc}
*/
@Override
public void enterRelationship(final C4L2Parser.RelationshipContext ctx) {
assert (ctx.Alias().size() == 2) : "Expected to find two aliases within C4L2 Relationship rule";
final String sourceAlias = ctx.Alias(0).getText();
final String targetAlias = ctx.Alias(1).getText();
final String label = ctx.String(0).getText();
final String protocol = ctx.String().size() > 1 ? ctx.String(1).getText() : "";
try {
final String edgeId = EdgeModel
.generateIdFrom(this.graph.getId())
.withSource(c4AliasToGraphAlias(sourceAlias))
.withTarget(c4AliasToGraphAlias(targetAlias))
.withLabel(label)
.withoutProtocol()
.withC4L2Level();
final EdgeModel relationshipEdge = EdgeModel
.builder()
.withId(edgeId)
.withSource(c4AliasToGraphAlias(sourceAlias))
.withTarget(c4AliasToGraphAlias(targetAlias))
.withoutData()
.build();
relationshipEdge.setC4Level(SourceType.C4_L2);
relationshipEdge.setLabel(label);
if (!protocol.isEmpty()) {
relationshipEdge.setProtocol(protocol);
}
this.graph.addOrReplaceEdge(relationshipEdge);
} catch (final CannotComputeEdgeModelId e) {
final String errMsg = String
.format("Cannot compute id for relationship '%s' -> '%s' = '%s' because of the following: %s",
sourceAlias,
targetAlias,
label,
e.getMessage());
LOGGER.error(errMsg, e);
this.unrecoverableErrors.add(new Exception(errMsg, e));
}
}
/**
* {@inheritDoc}
*/
@Override
public void enterSystem(final C4L2Parser.SystemContext ctx) {
assert (ctx.String().size() == 2) : "Expected to find two strings within C4L2 System rule";
final String alias = ctx.Alias().getText();
final String name = ctx.String(0).getText();
final String description = ctx.String(1).getText();
addNewNodeWithAlias(alias)
.withName(name)
.withDescription(description)
.withEntityType(EntityType.C2_SYSTEM);
}
/**
* {@inheritDoc}
*/
@Override
public void enterSystem_boundary(final C4L2Parser.System_boundaryContext ctx) {
final String alias = buildNodeAlias(ctx.Alias().getText());
final String name = ctx.String().getText();
final NodeModel systemBoundaryNode = NodeModel
.builder()
.withId(alias)
.withoutData()
.build();
this.systemBoundaryGraph.setId(alias + ":");
this.systemBoundaryGraph.setTitle(name);
systemBoundaryNode.setGraph(this.systemBoundaryGraph);
this.graph.addOrReplaceNode(systemBoundaryNode);
}
/**
* {@inheritDoc}
*/
@Override
public void enterSystem_ext(final C4L2Parser.System_extContext ctx) {
assert (ctx.String().size() == 2) : "Expected to find two strings within C4L2 SystemExt rule";
final String alias = ctx.Alias().getText();
final String name = ctx.String(0).getText();
final String description = ctx.String(1).getText();
addNewNodeWithAlias(alias)
.withName(name)
.withDescription(description)
.withEntityType(EntityType.C2_SYSTEM_EXT);
}
/**
* {@inheritDoc}
*/
@Override
public void enterTitle(final C4L2Parser.TitleContext ctx) {
this.graph.setTitle(ctx.String(0).getText());
if (ctx.String().size() > 1) {
this.graph.setAspect(ctx.String(1).getText());
}
}
/**
* {@inheritDoc}
* <p>
* Does nothing.
*/
@Override
public void exitEveryRule(final ParserRuleContext ctx) {
// This is too generic to be called in a C4L2Parser specific grammar
}
/**
* {@inheritDoc}
*/
@Override
public C4L2ParserListener getDecoratedC4L2ParserListener() {
return this.decorated;
}
/**
* Returns the result of the parsing as a graph.
*
* @return the graph result.
*/
public GraphModel getGraph() {
return this.graph;
}
/**
* Returns any unrecoverable exceptions that could have occurred during the parsing process.
*
* @return the unrecoverable exceptions that happened during the parsing.
*/
public List<Throwable> getUnrecoverableErrors() {
return unrecoverableErrors;
}
/**
* {@inheritDoc}
* <p>
* Does nothing.
*/
@Override
public void visitErrorNode(final ErrorNode node) {
LOGGER.debug("Parse errors are not handled there at this stage");
}
/**
* {@inheritDoc}
* <p>
* Does nothing.
*/
@Override
public void visitTerminal(final TerminalNode node) {
// This is too generic to be called in a C4L2Parser specific grammar
}
private WithName<WithDescription<WithEntityType<Void>>> addNewContainerNodeWithAlias(final String alias,
final String technologicalStack) {
return name -> description -> entityType -> {
final NodeModel node = NodeModel
.builder()
.withId(buildNodeAlias(alias, true))
.withoutData()
.build();
node.setName(name);
node.setDescription(description);
node.setEntityType(entityType);
if (!technologicalStack.isEmpty()) {
node.setTechnologicalStack(technologicalStack);
}
this.systemBoundaryGraph.addOrReplaceNode(node);
return null;
};
}
private WithName<WithDescription<WithEntityType<Void>>> addNewNodeWithAlias(final String alias) {
return name -> description -> entityType -> {
final NodeModel node = NodeModel
.builder()
.withId(buildNodeAlias(alias))
.withoutData()
.build();
node.setName(name);
node.setDescription(description);
node.setEntityType(entityType);
this.graph.addOrReplaceNode(node);
return null;
};
}
private String buildNodeAlias(final String alias) {
return buildNodeAlias(alias, false);
}
private String buildNodeAlias(final String alias, final boolean isContainer) {
final String fqAliasDelimiter = isContainer ? ":" : "::";
final String graphId = isContainer ? this.systemBoundaryGraph.getId() : this.graph.getId();
final String fqAlias = graphId + fqAliasDelimiter + alias;
this.c4AliasToGraphAlias.put(alias, fqAlias);
return fqAlias;
}
private String c4AliasToGraphAlias(final String c4Alias) {
return this.c4AliasToGraphAlias.get(c4Alias);
}
}