C4L1GraphParseTreeListener.java

package org.thewonderlemming.c4plantuml.graphml.parse;

import java.util.ArrayList;
import java.util.List;

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.C4L1ParserListenerDecorator;
import org.thewonderlemming.c4plantuml.grammars.SourceType;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1ParserBaseListener;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1ParserListener;
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 L1 grammar and feeds the JAXB model with the retrieved
 * information.
 *
 * @author thewonderlemming
 *
 */
public class C4L1GraphParseTreeListener implements C4L1ParserListenerDecorator {

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

    private final C4L1ParserBaseListener decorated = new C4L1ParserBaseListener();

    private final GraphModel graph;

    private final List<Throwable> unrecoverableErrors;


    /**
     * Default constructor.
     */
    public C4L1GraphParseTreeListener() {

        this.unrecoverableErrors = new ArrayList<>();
        this.graph = GraphModel
            .builder()
                .withId("c4l1")
                .withDefaultDirection()
                .withoutData()
                .withoutNodes()
                .withoutEdges()
                .build();
    }

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

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

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

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

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

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

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

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

        assert (ctx.Alias().size() == 2) : "Expected to find two aliases within C4L1 Relationship rule";

        final String sourceAlias = ctx.Alias(0).getText();
        final String targetAlias = ctx.Alias(1).getText();
        final String label = ctx.String().getText();


        try {

            final String edgeId = EdgeModel
                .generateIdFrom(this.graph.getId())
                    .withSource(sourceAlias)
                    .withTarget(targetAlias)
                    .withLabel(label)
                    .withoutProtocol()
                    .withC4L1Level();

            final EdgeModel relationshipEdge = EdgeModel
                .builder()
                    .withId(edgeId)
                    .withSource(this.graph.getId() + "::" + sourceAlias)
                    .withTarget(this.graph.getId() + "::" + targetAlias)
                    .withoutData()
                    .build();

            relationshipEdge.setC4Level(SourceType.C4_L1);
            relationshipEdge.setLabel(label);

            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 C4L1Parser.SystemContext ctx) {

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

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

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

    /**
     * {@inheritDoc}
     */
    @Override
    public void enterTitle(final C4L1Parser.TitleContext ctx) {

        graph.setTitle(ctx.String(0).getText());

        if (ctx.String().size() > 1) {
            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 C4L1Parser specific grammar
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public C4L1ParserListener getDecoratedC4L1ParserListener() {
        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 but logging a generic message.
     */
    @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 C4L1Parser specific grammar
    }

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

        return name -> description -> entityType -> {

            final NodeModel node = NodeModel
                .builder()
                    .withId(this.graph.getId() + "::" + alias)
                    .withoutData()
                    .build();

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

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