# C4L3GraphParseTreeListener.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.C4L3ParserListenerDecorator;
    import org.thewonderlemming.c4plantuml.grammars.SourceType;
    import org.thewonderlemming.c4plantuml.grammars.generated.C4L3Parser;
    import org.thewonderlemming.c4plantuml.grammars.generated.C4L3ParserBaseListener;
    import org.thewonderlemming.c4plantuml.grammars.generated.C4L3ParserListener;
    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 L3 grammar and feeds the JAXB model with the retrieved
     * information.
     *
     * @author thewonderlemming
     *
     */
    public class C4L3GraphParseTreeListener implements C4L3ParserListenerDecorator {

        private static final Logger LOGGER = LoggerFactory.getLogger(C4L3GraphParseTreeListener.class);

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

        private GraphModel containerBoundaryGraph;

        private final C4L3ParserBaseListener decorated = new C4L3ParserBaseListener();

        private GraphModel graph;

        private final List<Throwable> unrecoverableErrors = new ArrayList<>();

        /**
         * Default constructor.
         */
        public C4L3GraphParseTreeListener() {
            reset(0);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterCloud(final C4L3Parser.CloudContext ctx) {

            assert (ctx.String().size() == 2) : "Expected to find two strings within C4L3 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.C3_CLOUD);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterComponent(final C4L3Parser.ComponentContext ctx) {

            assert (ctx.String().size() == 3) : "Expected to find two strings within C4L3 Component 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.COMPONENT() != null
                ? EntityType.C3_COMPONENT
                : EntityType.C3_COMPONENT_DB;

            addNewComponentNodeWithAlias(alias, technological_stack)
                .withName(name)
                    .withDescription(description)
                    .withEntityType(entityType);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterContainer(final C4L3Parser.ContainerContext ctx) {

            assert (ctx.String().size() == 3) : "Expected to find two strings within C4L3 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();

            addNewNodeWithAlias(alias, technological_stack)
                .withName(name)
                    .withDescription(description)
                    .withEntityType(EntityType.C3_CONTAINER);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterContainer_boundary(final C4L3Parser.Container_boundaryContext ctx) {

            final String alias = buildNodeAlias(ctx.Alias().getText());
            final String name = ctx.String().getText();

            final NodeModel containerBoundaryNode = NodeModel
                .builder()
                    .withId(alias)
                    .withoutData()
                    .build();

            this.containerBoundaryGraph.setId(alias + ":");
            this.containerBoundaryGraph.setTitle(name);

            containerBoundaryNode.setGraph(this.containerBoundaryGraph);
            this.graph.addOrReplaceNode(containerBoundaryNode);
        }

        /**
         * {@inheritDoc}
         * <p>
         * Does nothing.
         */
        @Override
        public void enterEveryRule(final ParserRuleContext ctx) {
            // This is too generic to be called in a C4L3Parser specific grammar
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterPerson(final C4L3Parser.PersonContext ctx) {

            assert (ctx.String().size() == 2) : "Expected to find two strings within C4L3 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.C3_PERSON);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterPerson_ext(final C4L3Parser.Person_extContext ctx) {

            assert (ctx.String().size() == 2) : "Expected to find two strings within C4L3 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.C3_PERSON_EXT);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterRelationship(final C4L3Parser.RelationshipContext ctx) {

            assert (ctx.Alias().size() == 2) : "Expected to find two aliases within C4L3 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_L3);
                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 C4L3Parser.SystemContext ctx) {

            assert (ctx.String().size() == 2) : "Expected to find two strings within C4L3 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.C3_SYSTEM);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterSystem_ext(final C4L3Parser.System_extContext ctx) {

            assert (ctx.String().size() == 2) : "Expected to find two strings within C4L3 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.C3_SYSTEM_EXT);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void enterTitle(final C4L3Parser.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 C4L3Parser specific grammar
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public C4L3ParserListener getDecoratedC4L3ParserListener() {
            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;
        }

        /**
         * Resets the current listener so that it can be reused for the next C4 L3 source file.
         *
         * @param parentNodeId the {@link NodeModel} ID the current graph should be tied to.
         */
        public void reset(final int parentNodeId) {

            this.unrecoverableErrors.clear();
            this.graph = GraphModel
                .builder()
                    .withId("c4l3::" + Integer.toString(parentNodeId) + ":")
                    .withDefaultDirection()
                    .withoutData()
                    .withoutNodes()
                    .withoutEdges()
                    .build();

            this.containerBoundaryGraph = GraphModel
                .builder()
                    .withId("containerBoundary")
                    .withDefaultDirection()
                    .withoutData()
                    .withoutNodes()
                    .withoutEdges()
                    .build();
        }

        /**
         * {@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 C4L3Parser specific grammar
        }

        private WithName<WithDescription<WithEntityType<Void>>> addNewComponentNodeWithAlias(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.containerBoundaryGraph.addOrReplaceNode(node);
                return null;
            };
        }

        private WithName<WithDescription<WithEntityType<Void>>> addNewNodeWithAlias(final String alias) {
            return addNewNodeWithAlias(alias, "");
        }

        private WithName<WithDescription<WithEntityType<Void>>> addNewNodeWithAlias(final String alias,
            final String technologicalStack) {

            return name -> description -> entityType -> {

                final NodeModel node = NodeModel
                    .builder()
                        .withId(buildNodeAlias(alias))
                        .withoutData()
                        .build();

                node.setName(name);
                node.setDescription(description);
                node.setEntityType(entityType);

                if (!technologicalStack.isEmpty()) {
                    node.setTechnologicalStack(technologicalStack);
                }

                this.graph.addOrReplaceNode(node);
                return null;
            };
        }

        private String buildNodeAlias(final String alias) {
            return buildNodeAlias(alias, false);
        }

        private String buildNodeAlias(final String alias, final boolean isComponent) {

            final String graphId = isComponent ? this.containerBoundaryGraph.getId() : this.graph.getId();

            final String fqAlias = graphId + ":" + alias;
            this.c4AliasToGraphAlias.put(alias, fqAlias);

            return fqAlias;
        }

        private String c4AliasToGraphAlias(final String c4Alias) {
            return this.c4AliasToGraphAlias.get(c4Alias);
        }
    }