com.github.ferstl.depgraph.AbstractGraphMojo.java Source code

Java tutorial

Introduction

Here is the source code for com.github.ferstl.depgraph.AbstractGraphMojo.java

Source

/*
 * Copyright (c) 2014 - 2016 by Stefan Ferstl <st.ferstl@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.ferstl.depgraph;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.ScopeArtifactFilter;
import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
import org.apache.maven.shared.artifact.filter.StrictPatternIncludesArtifactFilter;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.CommandLineUtils.StringStreamConsumer;
import org.codehaus.plexus.util.cli.Commandline;
import com.github.ferstl.depgraph.graph.DependencyGraphException;
import com.github.ferstl.depgraph.graph.GraphFactory;
import com.github.ferstl.depgraph.graph.style.StyleConfiguration;
import com.github.ferstl.depgraph.graph.style.resource.BuiltInStyleResource;
import com.github.ferstl.depgraph.graph.style.resource.ClasspathStyleResource;
import com.github.ferstl.depgraph.graph.style.resource.FileSystemStyleResource;
import com.github.ferstl.depgraph.graph.style.resource.StyleResource;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;

/**
 * Abstract mojo to create all possible kinds of graphs in the dot format. Graphs are created with instances of the
 * {@link GraphFactory} interface. This class defines an abstract method to create such factories. In case Graphviz is
 * install on the system where this plugin is executed, it is also possible to run the dot program and create images
 * out of the generated dot files. Besides that, this class allows the configuration of several basic mojo parameters,
 * such as includes, excludes, etc.
 */
abstract class AbstractGraphMojo extends AbstractMojo {

    private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\r?\n");
    private static final String DOT_EXTENSION = ".dot";
    private static final String OUTPUT_DOT_FILE_NAME = "dependency-graph" + DOT_EXTENSION;

    /**
     * The scope of the artifacts that should be included in the graph. An empty string indicates all scopes (default).
     * The scopes being interpreted are the scopes as Maven sees them, not as specified in the pom. In summary:
     * <ul>
     * <li>{@code compile}: Shows compile, provided and system dependencies</li>
     * <li>{@code provided}: Shows provided dependencies</li>
     * <li>{@code runtime}: Shows compile and runtime dependencies</li>
     * <li>{@code system}: Shows system dependencies</li>
     * <li>{@code test} (default): Shows all dependencies</li>
     * </ul>
     *
     * @since 1.0.0
     */
    @Parameter(property = "scope")
    private String scope;

    /**
     * List of artifacts to be included in the form of {@code groupId:artifactId:type:classifier}.
     *
     * @since 1.0.0
     */
    @Parameter(property = "includes", defaultValue = "")
    private List<String> includes;

    /**
     * List of artifacts to be excluded in the form of {@code groupId:artifactId:type:classifier}.
     *
     * @since 1.0.0
     */
    @Parameter(property = "excludes", defaultValue = "")
    private List<String> excludes;

    /**
     * List of artifacts, in the form of {@code groupId:artifactId:type:classifier}, to restrict the dependency graph
     * only to artifacts that depend on them.
     *
     * @since 1.0.4
     */
    @Parameter(property = "targetIncludes", defaultValue = "")
    private List<String> targetIncludes;
    /**
     * The path to the generated dot file.
     *
     * @since 1.0.0
     */
    @Parameter(property = "outputFile", defaultValue = "${project.build.directory}/" + OUTPUT_DOT_FILE_NAME)
    private File outputFile;

    /**
     * If set to {@code true} and Graphviz is installed on the system where this plugin is executed, the dot file will be
     * converted to a graph image using Graphviz' dot executable.
     *
     * @see #imageFormat
     * @see #dotExecutable
     * @since 1.0.0
     */
    @Parameter(property = "createImage", defaultValue = "false")
    private boolean createImage;

    /**
     * The format for the graph image when {@link #createImage} is set to {@code true}.
     *
     * @since 1.0.0
     */
    @Parameter(property = "imageFormat", defaultValue = "png")
    private String imageFormat;

    /**
     * Path to the dot executable. Use this option in case {@link #createImage} is set to {@code true} and the dot
     * executable is not on the system {@code PATH}.
     *
     * @since 1.0.0
     */
    @Parameter(property = "dotExecutable")
    private File dotExecutable;

    /**
     * Path to a custom style configuration in JSON format.
     *
     * @since 2.0.0
     */
    @Parameter(property = "customStyleConfiguration", defaultValue = "")
    private String customStyleConfiguration;

    /**
     * If set to {@code true} the effective style configuration used to create this graph will be printed on the console.
     *
     * @since 2.0.0
     */
    @Parameter(property = "printStyleConfiguration", defaultValue = "false")
    private boolean printStyleConfiguration;

    /**
     * Local maven repository required by the {@link DependencyTreeBuilder}.
     */
    @Parameter(defaultValue = "${localRepository}", readonly = true)
    ArtifactRepository localRepository;

    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;

    @Component(hint = "default")
    DependencyGraphBuilder dependencyGraphBuilder;

    @Component
    DependencyTreeBuilder dependencyTreeBuilder;

    @Override
    public final void execute() throws MojoExecutionException, MojoFailureException {
        ArtifactFilter globalFilter = createGlobalArtifactFilter();
        ArtifactFilter targetFilter = createTargetArtifactFilter();
        StyleConfiguration styleConfiguration = loadStyleConfiguration();

        try {
            GraphFactory graphFactory = createGraphFactory(globalFilter, targetFilter, styleConfiguration);

            writeDotFile(graphFactory.createGraph(this.project));

            if (this.createImage) {
                createGraphImage();
            }

        } catch (DependencyGraphException e) {
            throw new MojoExecutionException("Unable to create dependency graph.", e.getCause());
        } catch (IOException e) {
            throw new MojoExecutionException("Unable to write graph file.", e);
        }
    }

    protected abstract GraphFactory createGraphFactory(ArtifactFilter globalFilter, ArtifactFilter targetFilter,
            StyleConfiguration styleConfiguration);

    /**
     * Override this method to configure additional style resources. It is recommendet to call
     * {@code super.getAdditionalStyleResources()} and add them to the set.
     *
     * @return A set of additional built-in style resources to use.
     */
    protected Set<BuiltInStyleResource> getAdditionalStyleResources() {
        // We need to preserve the order of style configurations
        return new LinkedHashSet<>();
    }

    private ArtifactFilter createGlobalArtifactFilter() {
        AndArtifactFilter filter = new AndArtifactFilter();

        if (this.scope != null) {
            filter.add(new ScopeArtifactFilter(this.scope));
        }

        if (!this.includes.isEmpty()) {
            filter.add(new StrictPatternIncludesArtifactFilter(this.includes));
        }

        if (!this.excludes.isEmpty()) {
            filter.add(new StrictPatternExcludesArtifactFilter(this.excludes));
        }

        return filter;
    }

    private ArtifactFilter createTargetArtifactFilter() {
        AndArtifactFilter filter = new AndArtifactFilter();

        if (!this.targetIncludes.isEmpty()) {
            filter.add(new StrictPatternIncludesArtifactFilter(this.targetIncludes));
        }

        return filter;
    }

    private StyleConfiguration loadStyleConfiguration() throws MojoFailureException {
        // default style resources
        ClasspathStyleResource defaultStyleResource = BuiltInStyleResource.DEFAULT_STYLE
                .createStyleResource(getClass().getClassLoader());

        // additional style resources from the mojo
        Set<StyleResource> styleResources = new LinkedHashSet<>();
        for (BuiltInStyleResource additionalResource : getAdditionalStyleResources()) {
            styleResources.add(additionalResource.createStyleResource(getClass().getClassLoader()));
        }

        // custom style resource
        if (StringUtils.isNotBlank(this.customStyleConfiguration)) {
            StyleResource customStyleResource = getCustomStyleResource();
            getLog().info("Using custom style configuration " + customStyleResource);
            styleResources.add(customStyleResource);
        }

        // load and print
        StyleConfiguration styleConfiguration = StyleConfiguration.load(defaultStyleResource,
                styleResources.toArray(new StyleResource[0]));
        if (this.printStyleConfiguration) {
            getLog().info("Using effective style configuration:\n" + styleConfiguration.toJson());
        }

        return styleConfiguration;
    }

    private StyleResource getCustomStyleResource() throws MojoFailureException {
        StyleResource customStyleResource;
        if (StringUtils.startsWith(this.customStyleConfiguration, "classpath:")) {
            String resourceName = StringUtils.substring(this.customStyleConfiguration, 10,
                    this.customStyleConfiguration.length());
            customStyleResource = new ClasspathStyleResource(resourceName, getClass().getClassLoader());
        } else {
            customStyleResource = new FileSystemStyleResource(Paths.get(this.customStyleConfiguration));
        }

        if (!customStyleResource.exists()) {
            throw new MojoFailureException(
                    "Custom configuration '" + this.customStyleConfiguration + "' does not exist.");
        }

        return customStyleResource;
    }

    private void writeDotFile(String dotGraph) throws IOException {
        Path outputFilePath = this.outputFile.toPath();
        Path parent = outputFilePath.getParent();
        if (parent != null) {
            Files.createDirectories(parent);
        }

        try (Writer writer = Files.newBufferedWriter(outputFilePath, StandardCharsets.UTF_8)) {
            writer.write(dotGraph);
        }
    }

    private void createGraphImage() throws IOException {
        String graphFileName = createGraphFileName();
        Path graphFile = this.outputFile.toPath().getParent().resolve(graphFileName);

        String dotExecutable = determineDotExecutable();
        String[] arguments = new String[] { "-T", this.imageFormat, "-o", graphFile.toAbsolutePath().toString(),
                this.outputFile.getAbsolutePath() };

        Commandline cmd = new Commandline();
        cmd.setExecutable(dotExecutable);
        cmd.addArguments(arguments);

        getLog().info("Running Graphviz: " + dotExecutable + " " + Joiner.on(" ").join(arguments));

        StringStreamConsumer systemOut = new StringStreamConsumer();
        StringStreamConsumer systemErr = new StringStreamConsumer();
        int exitCode;

        try {
            exitCode = CommandLineUtils.executeCommandLine(cmd, systemOut, systemErr);
        } catch (CommandLineException e) {
            throw new IOException("Unable to execute Graphviz", e);
        }

        Splitter lineSplitter = Splitter.on(LINE_SEPARATOR_PATTERN).omitEmptyStrings().trimResults();
        Iterable<String> output = Iterables.concat(lineSplitter.split(systemOut.getOutput()),
                lineSplitter.split(systemErr.getOutput()));

        for (String line : output) {
            getLog().info("  dot> " + line);
        }

        if (exitCode != 0) {
            throw new IOException("Graphviz terminated abnormally. Exit code: " + exitCode);
        }

        getLog().info("Graph image created on " + graphFile.toAbsolutePath());
    }

    private String createGraphFileName() {
        String dotFileName = this.outputFile.getName();

        String graphFileName;
        if (dotFileName.endsWith(DOT_EXTENSION)) {
            graphFileName = dotFileName.substring(0, dotFileName.lastIndexOf(".")) + "." + this.imageFormat;
        } else {
            graphFileName = dotFileName + this.imageFormat;
        }
        return graphFileName;
    }

    private String determineDotExecutable() throws IOException {
        if (this.dotExecutable == null) {
            return "dot";
        }

        Path dotExecutablePath = this.dotExecutable.toPath();
        if (!Files.exists(dotExecutablePath)) {
            throw new NoSuchFileException("The dot executable '" + this.dotExecutable + "' does not exist.");
        } else if (Files.isDirectory(dotExecutablePath) || !Files.isExecutable(dotExecutablePath)) {
            throw new IOException(
                    "The dot executable '" + this.dotExecutable + "' is not a file or cannot be executed.");
        }

        return dotExecutablePath.toAbsolutePath().toString();
    }
}