LintingMojo.java

package org.thewonderlemming.c4plantuml.mojo.linting;

import java.io.IOException;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.thewonderlemming.c4plantuml.linter.Linter;
import org.thewonderlemming.c4plantuml.linter.rules.AbstractLintingRule;
import org.thewonderlemming.c4plantuml.mojo.AbstractParentMojo;
import org.thewonderlemming.c4plantuml.mojo.MojoReporter;
import org.thewonderlemming.c4plantuml.mojo.linting.rules.builtin.BuiltInLintingRulesFactory;
import org.thewonderlemming.c4plantuml.mojo.linting.rules.custom.AbstractCustomLintingRule;
import org.thewonderlemming.c4plantuml.mojo.linting.rules.custom.AbstractCustomLintingRuleFactory;

/**
 * A MOJO that parses the C4 PlantUML files that are contained in the output directory and applies a set of linting
 * rules to find violations. Breaks the build on violations if said so.
 *
 * @author thewonderlemming
 *
 */
@Mojo(name = "lint", defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class LintingMojo extends AbstractParentMojo {

    private Set<AbstractLintingRule> builtInRules;

    @Parameter(property = "rules", alias = "rules")
    private BuiltInRules builtInRulesConfiguration;

    private Set<AbstractLintingRule> customRules;

    @Parameter(property = "customRules", alias = "customRules")
    private Properties customRulesConfiguration;

    @Parameter(property = "failOnLintErrors", defaultValue = "true", alias = "failOnLintErrors")
    private boolean failOnLintErrors;

    private MojoReporter reporter;


    private static boolean isCustomRule(final Class<? extends AbstractLintingRule> rule) {
        return AbstractCustomLintingRule.class.isAssignableFrom(rule);
    }

    private static boolean isNotCustomRule(final Class<? extends AbstractLintingRule> rule) {
        return !isCustomRule(rule);
    }

    /**
     * Parses the C4 PlantUML files to find linting violations, and breaks the build if the {@code failOnLintErrors} is
     * set to {@code true}.
     * <p>
     * {@inheritDoc}
     */
    @Override
    protected void doExecute() throws MojoExecutionException, MojoFailureException {

        validateBuiltInRulesConfiguration();
        validateCustomRulesConfiguration();
        setReporter();
        retrieveAndSetRules();

        lintOutputDirectory();
        reportsAndBreakBuildIfNecessary();
    }

    private Optional<AbstractLintingRule> buildAndConfigureBuiltInRule(
        final Class<? extends AbstractLintingRule> ruleType) {

        final Optional<String> ruleName = BuiltInLintingRulesFactory.getRuleNameForType(ruleType);
        assert (ruleName.isPresent()) : "Expected rule " + ruleType.getName() + " to be found in "
            + BuiltInLintingRulesFactory.class.getName();

        return ruleName.isPresent()
            ? BuiltInLintingRulesFactory.createInstanceForName(ruleName.get(), this.builtInRulesConfiguration)
            : Optional.empty();
    }

    private Optional<? extends AbstractCustomLintingRule> buildAndConfigureCustomRules(
        final Class<? extends AbstractCustomLintingRuleFactory<? extends AbstractCustomLintingRule>> factoryClass) {

        try {

            return factoryClass.newInstance().createCustomRule(customRulesConfiguration);

        } catch (InstantiationException | IllegalAccessException e) {

            final String errMsg = String
                .format("Error while trying to build custom rule from factory %s: %s",
                    factoryClass.getName(),
                    e.getMessage());

            getLog().error(errMsg);
            return Optional.empty();
        }
    }

    private void lintOutputDirectory() throws MojoFailureException {

        assert (getOutputDirectory() != null) : "Expected outputDirectory to be set";
        assert (this.reporter != null) : "Expected reporter to be set";
        assert ((this.builtInRules != null && !this.builtInRules.isEmpty())
            || (this.customRules != null && !this.customRules.isEmpty())) : "Expected rules to be set";

        try {

            final Linter linter = Linter
                .builder()
                    .newLinter()
                    .withReporter(this.reporter)
                    .addLintingRules(this.builtInRules)
                    .addLintingRules(this.customRules)
                    .build();

            processOutputDirectoryFiles(path -> linter.lint(path, getCurrentCharset()));

        } catch (final IOException e) {

            final String errMsg = "Cannot process output directory: \"" + getOutputDirectory() + "\" for linting";
            throw new MojoFailureException(errMsg);
        }
    }

    private AbstractLintingRule logFoundRule(final AbstractLintingRule lintingRule) {

        final String message = "Linting rule <" + lintingRule.getClass().getName()
            + "> was detected in the classpath and will be used.";

        getLog().debug(message);

        return lintingRule;
    }

    private void reportsAndBreakBuildIfNecessary() throws MojoFailureException {

        assert (this.reporter != null) : "Expected reporter to be set";

        if (this.reporter.containsReports()) {

            getLog().debug("Lint errors found.");

            this.reporter
                .getReports()
                    .forEach(report -> getLog().error(report));

            if (this.failOnLintErrors) {
                throw new MojoFailureException(
                    "The build contains lint errors. Please fix them then restart the build.");
            }

        } else {
            getLog().debug("No lint errors found.");
        }
    }

    private void retrieveAndSetRules() throws MojoExecutionException {

        this.builtInRules = LintingRulesFinder
            .findBuiltInLintingRules()
                .stream()
                .filter(LintingMojo::isNotCustomRule)
                .map(this::buildAndConfigureBuiltInRule)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .map(this::logFoundRule)
                .collect(Collectors.toSet());

        this.customRules = LintingRulesFinder
            .findCustomLintingRuleFactories()
                .stream()
                .map(this::buildAndConfigureCustomRules)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .map(this::logFoundRule)
                .collect(Collectors.toSet());

        if (this.builtInRules.isEmpty() && this.customRules.isEmpty()) {
            throw new MojoExecutionException("No active rules to be found. Skipping.");
        }
    }

    private void setReporter() {
        this.reporter = new MojoReporter();
    }

    private void validateBuiltInRulesConfiguration() {

        if (this.builtInRulesConfiguration == null) {

            getLog().info("No configuration found for built-in rules. Falling back to defaults.");
            this.builtInRulesConfiguration = new BuiltInRules();
        }
    }

    private void validateCustomRulesConfiguration() {

        if (this.customRulesConfiguration == null) {

            getLog().debug("No configuration found for custom rules. Falling back to defaults.");
            this.customRulesConfiguration = new Properties();
        }
    }
}