C4Graph.java

package org.thewonderlemming.c4plantuml.graphml;

import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;

import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.tree.ParseTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thewonderlemming.c4plantuml.grammars.SourceType;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L3Parser;
import org.thewonderlemming.c4plantuml.graphml.export.GraphMLModelExporter;
import org.thewonderlemming.c4plantuml.graphml.model.C4Keys;
import org.thewonderlemming.c4plantuml.graphml.model.GraphMLModel;

/**
 * The C4 to GraphML transformation engine.
 * <p>
 * It relies on JAXB for the XML marshalling.
 *
 * @author thewonderlemming
 *
 */
public class C4Graph {

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

    private final Charset charset;

    private final boolean formatOutput;

    private final GraphMLModel model;

    private final C4GraphParseTreeListener parserTreeListener;

    private final boolean strictValidation;

    private final boolean validateOutput;


    private static Optional<ParseTree> createParseTree(final Parser parser) {

        final ParseTree tree;

        if (parser instanceof C4L1Parser) {
            tree = ((C4L1Parser) parser).diagram();

        } else if (parser instanceof C4L2Parser) {
            tree = ((C4L2Parser) parser).diagram();

        } else if (parser instanceof C4L3Parser) {
            tree = ((C4L3Parser) parser).diagram();

        } else {
            return Optional.empty();
        }

        return Optional.of(tree);
    }

    private static int sortPathAlphabetically(final Path left, final Path right) {
        return String.CASE_INSENSITIVE_ORDER.compare(left.toString(), right.toString());
    }

    /**
     * A constructor that turns on a strict validation of the XML output and that turns off the C4 parsing errors.
     *
     * @param charset the {@link Charset} to use to read the C4 files.
     * @param formatOutput a flag to tell whether the output XML should fit on a single line or not.
     */
    public C4Graph(final Charset charset, final boolean formatOutput) {
        this(charset, formatOutput, true, true, false);
    }

    /**
     * A constructor that turns on a strict validation of the XML output.
     *
     * @param charset the {@link Charset} to use to read the C4 files.
     * @param formatOutput a flag to tell whether the output XML should fit on a single line or not.
     * @param thrownOnParseError a flag to tell whether a parsing error should fail the transformation.
     */
    public C4Graph(final Charset charset, final boolean formatOutput, final boolean thrownOnParseError) {
        this(charset, formatOutput, true, true, thrownOnParseError);
    }

    /**
     * Default constructor.
     *
     * @param charset the {@link Charset} to use to read the C4 files.
     * @param formatOutput a flag to tell whether the output XML should fit on a single line or not.
     * @param validateOutput a flag to tell whether the output XML should be validated against the GraphML XSDs.
     * @param strictValidation a flag to tell whether a validation error should fail the transformation.
     * @param thrownOnParseError a flag to tell whether a parsing error should fail the transformation.
     */
    public C4Graph(final Charset charset, final boolean formatOutput, final boolean validateOutput,
        final boolean strictValidation, final boolean thrownOnParseError) {

        this.charset = charset;
        this.formatOutput = formatOutput;
        this.validateOutput = validateOutput;
        this.strictValidation = strictValidation;
        this.model = GraphMLModel
            .builder()
                .withKeys(C4Keys.getC4Keys())
                .withoutGraphs()
                .build();

        this.parserTreeListener = new C4GraphParseTreeListener(this.model, thrownOnParseError);
    }

    /**
     * Adds a {@link List} of C4 files to parse and returns the current instance to allow method chaining.
     * <p>
     * It is better to call that method with every C4 file to parse at once, because they will be sorted alphabetically,
     * while if you call that method more than once per generation, you should take responsibility of the ordering of
     * these source files.
     *
     * @param c4Files the {@link List} of C4 files to parse.
     * @return the current instance of {@link C4Graph} to allow method chaining.
     */
    public C4Graph addAndProcessC4Files(final List<Path> c4Files) {

        c4Files
            .stream()
                .sorted(C4Graph::sortPathAlphabetically)
                .forEach(this::processC4File);

        return this;
    }

    /**
     * Transforms the parsed C4 files to a single GraphML output and returns the result as a string.
     *
     * @return the XML output GraphML that represents the parsed C4 files. Returns empty if any silent error occurred.
     * @throws Throwable on parsing errors and/or validation errors, depending on the values of the flags in the
     * constructor.
     */
    public Optional<String> export() throws Throwable {

        this.parserTreeListener.wrapUp();

        return GraphMLModelExporter
            .export(this.model, this.charset, this.formatOutput, this.validateOutput, this.strictValidation);
    }

    private void processC4File(final Path c4File) {

        final Optional<SourceType> optionalSourceType = SourceType.getSourceTypeFromFilename(c4File);

        if (!optionalSourceType.isPresent()) {
            LOGGER.error("Cannot determine the C4 level from filename: {}. Skipping", c4File);
        }

        optionalSourceType
            .ifPresent(sourceType -> sourceType
                .createParser(c4File, charset, C4ErrorListener.getInstance())
                    .ifPresent(parser -> createParseTree(parser)
                        .ifPresent(tree -> this.parserTreeListener.parseTree(tree, c4File))));
    }
}