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);
    }
}