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