SourceType.java

package org.thewonderlemming.c4plantuml.grammars;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.TokenStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1Lexer;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L1Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2Lexer;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L2Parser;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L3Lexer;
import org.thewonderlemming.c4plantuml.grammars.generated.C4L3Parser;

/**
 * An enumeration that binds C4 grammars to their specific {@link Lexer} and {@link Parser}, and provides convenience
 * methods such as parsing source files and identifying a source C4 level.
 *
 * @author thewonderlemming
 *
 */
public enum SourceType {

    /**
     * A C4 <a href="https://c4model.com/#SystemContextDiagram">System Context</a> grammar.
     */
    C4_L1(C4L1Lexer.class, C4L1Parser.class, "c4l1", "C4_L1"),

    /**
     * A C4 <a href="https://c4model.com/#ContainerDiagram">Container</a> grammar.
     */
    C4_L2(C4L2Lexer.class, C4L2Parser.class, "c4l2", "C4_L2"),

    /**
     * A C4 <a href="https://c4model.com/#ComponentDiagram">Component</a> grammar.
     */
    C4_L3(C4L3Lexer.class, C4L3Parser.class, "c4l3", "C4_L3");


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

    private static final Map<String, SourceType> SOURCE_TYPES_BY_FILENAME = new HashMap<>();

    private final String filenameStartsWith;

    private final String level;

    private final Class<? extends Lexer> lexerClass;

    private final Class<? extends Parser> parserClass;


    static {
        for (final SourceType sourceType : SourceType.values()) {
            SOURCE_TYPES_BY_FILENAME.put(sourceType.filenameStartsWith, sourceType);
        }
    }


    /**
     * Guesses the given source file C4 level and returns an {@link Optional} of the matching enumeration value if any, or
     * empty.
     *
     * @param fullyQualifiedName the C4 source file to guess the level from.
     * @return an {@link Optional} of the matching enumeration if any or empty.
     */
    public static Optional<SourceType> getSourceTypeFromFilename(final Path fullyQualifiedName) {

        final String filename = fullyQualifiedName.getFileName().toString();

        for (final Entry<String, SourceType> entry : SOURCE_TYPES_BY_FILENAME.entrySet()) {
            if (filename.toLowerCase().startsWith(entry.getKey())) {
                return Optional.of(entry.getValue());
            }
        }

        return Optional.empty();
    }

    private SourceType(final Class<? extends Lexer> lexerClass, final Class<? extends Parser> parserClass,
        final String filenameStartsWith, final String level) {

        this.lexerClass = lexerClass;
        this.parserClass = parserClass;
        this.filenameStartsWith = filenameStartsWith;
        this.level = level;
    }

    /**
     * Parses the given C4 source file and returns an {@link Optional} of the created {@link Parser} or empty if any
     * exception occurs. Parsing errors are reported to the given {@link BaseErrorListener} instance.
     *
     * @param sourcePath the C4 source file to parse.
     * @param charset the {@link Charset} of the source file.
     * @param errorListener the error listener to report syntax error to.
     * @return n {@link Optional} of the new {@link Parser} instance or empty on error.
     */
    public Optional<? extends Parser> createParser(final Path sourcePath, final Charset charset,
        final BaseErrorListener errorListener) {

        try {

            final CharStream stream = CharStreams.fromPath(sourcePath, charset);
            final Lexer lexer = this.lexerClass.getDeclaredConstructor(CharStream.class).newInstance(stream);
            lexer.removeErrorListeners();
            lexer.addErrorListener(errorListener);

            final CommonTokenStream tokens = new CommonTokenStream(lexer);
            final Parser parser = this.parserClass.getDeclaredConstructor(TokenStream.class).newInstance(tokens);
            parser.removeErrorListeners();
            parser.addErrorListener(errorListener);

            return Optional.ofNullable(parser);

        } catch (
            IOException | InstantiationException | IllegalAccessException | IllegalArgumentException
            | InvocationTargetException | NoSuchMethodException | SecurityException e) {

            LOGGER.error("Something went wrong as the lexer or the parser could not be instantiated", e);
        }

        return Optional.empty();
    }

    /**
     * Returns the associated C4 level of the current enumeration value.
     *
     * @return the current enumeration C4 level.
     */
    public String getC4Level() {
        return this.level;
    }

    /**
     * Returns the associated {@link Parser} type of the current enumeration value.
     *
     * @return the current enumeration {@link Parser} type.
     */
    @SuppressWarnings("unchecked")
    public Class<Parser> getParserType() {
        return (Class<Parser>) this.parserClass;
    }
}