Example how to create custom Maven Plugin

Maven has lots of plugins to assist you in project construction, testing, packaging and deployment. For example if you want to compile C++ code instead of Java, you can use native-maven-plugin . But what if you need something more specific? Then you can create a custom Maven plugin.

I will explain how to create a simple custom maven plugin to generate static blog site from Markdown files. I know we can already do that with maven-site-plugin since version 3.3, I will just use it for learning purposes.

You can find whole source code example at https://github.com/carlosvin/blog-maven-plugin.

Maven plugin concepts

Mojo
An executable goal in Maven, e.g: mvn your-plugin:your-mojo will execute a maven goal your-mojo declared as part of your-plugin.
Goal
It is equivalent to Mojo execution
Lifecycle

It is a well-defined sequence of phases. Each phase consists of a sequence of goals. Let's see an example of lifecycle, e.g: FooLifecycle has clean, prepare and assemble phases. Each of those phases has one of more goals. FooLifecycle:

  • clean
    • rmSources: a goal to remove source files
    • rmBuild: a goal to remove files in cache directory
  • prepare
    • installDependencies: a goal to download dependencies for the project
  • assemble
    • build: a goal to compile source files

To define a custom lifecycle as previous we will use src/main/resources/META-INF/plexus/components.xml, we will speak about that file in following sections. Normally is enough to override predefined lifecycles, in this example, we will override site lifecycle.

Hint

You can find an introduction to Maven lifecycles at https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

Create your custom plugin

The plugin we are about to explain will override site lifecycle, which has only 2 default phases, so when we run mvn site using our new custom plugin it will execute the goals we are about to create.

Our plugin will work with md file bindings: It will build the project and deploy it using maven deployment plugin.

Project structure

src/main/java
Where Java source code is
src/main/resources/META-INF/plexus/components.xml
file to create/override maven lifecycles and artifact types. Here we can specify which goals will be executed when for an artifact type, for example, we can say that for an artifact of type whatever when we run mvn foo it will verify the files, run tests, run linter, compile and zip al generated files.
src/test/java
Unit tests folder
src/it
Folder with all integration tests. Those integration tests are running actual projects and checking that outputs are as expected.
pom.xml
File to with Maven project description (Project Object Model)

Dependency Injection

Maven has finally chosen JSR-330 as dependency injection standard (previously it was Plexus Annotations API).

To use dependency injection with Maven we have to:

  1. Add javax.inject dependency to pom.xml so we can use @Inject, @Named, and @Singleton annotations in plugin implementation Java code.
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>
  1. Set up the sisu-maven-plugin to index the JSR-330 components you want made available to Maven.
<plugin>
    <groupId>org.eclipse.sisu</groupId>
    <artifactId>sisu-maven-plugin</artifactId>
    <version>0.3.3</version>
    <executions>
        <execution>
            <id>generate-index</id>
            <goals>
                <goal>main-index</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  1. Add annotations to your Mojo, e.g:
// This annotation is not a dependency injection one, we will explain later what it is for
@Mojo(name = "build", defaultPhase = LifecyclePhase.COMPILE)
public class BuildMojo extends AbstractMojo {

    private final FileSetManager fileSetManager;
    private final MdToHtml mdToHtml;

    // It will inject an instance of FileSetManager and MdToHtml
    @Inject
    public BuildMojo(FileSetManager fileSetManager, MdToHtml mdToHtml) {
        this.fileSetManager = fileSetManager;
        this.mdToHtml = mdToHtml;

Write a custom Mojo

It is straightforward to implement a Mojo class, we have to:

1. Implement Mojo interface

Your Mojo class has to implement org.apache.maven.plugin.Mojo, although it is more convenient to extend org.apache.maven.plugin.AbstractMojo, an abstract class to provide most of the infrastructure required to implement a Mojo except for execute method. That interface and class are described at Mojo API.

public class BuildMojo extends AbstractMojo {

2. Configure Mojo with Java 5 annotations

Annotate Mojo class with @Mojo and input parameters with @Parameter. Those annotations belong to another set of annotations to configure Mojos, Plugin Tools Java5 Annotations.

/**
* Generate HTML files from Markdown files
*/
@Mojo(name = "build", defaultPhase = LifecyclePhase.COMPILE)
public class BuildMojo extends AbstractMojo {

    /**
    * Output directory path where HTML files are generated
    */
    @Parameter(defaultValue = "${project.reporting.outputDirectory}", property = "siteOutputDirectory", required = true)
    private File outputDirectory;

    /**
    * A specific <code>fileSet</code> rule to select files and directories.
    * Fileset spec: https://maven.apache.org/shared/file-management/fileset.html
    */
    @Parameter
    private FileSet inputFiles;
@Mojo
Configures Mojo name and default lifecycle phase. To execute the Mojo in example we will use mvn site:build: site is the plugin name and build is name parameter.
@Parameter

We use it to pass configuration parameters to Mojo. @Parameter annotation accepts extra arguments

  • defaultValue: You can use properties placeholder or any String. If the parameter type is not a String, then Maven will try to cast it, e.g:
// If intParameter is not set in pom file, then "2" will be converted to 2 and assigned to intParameter.
@Parameter(defaultValue="2")
Integer intParameter;
  • property: It allows configuration of the Mojo parameter from the command line by referencing a system property that the user sets via the -D option.
# To assign "/var/www/html" value to  outputDirectory:

mvn site:build -DsiteOutputDirectory=/var/www/html

3. Implement execute method

As I have explained before at 1. Implement Mojo interface, our Mojo class extends org.apache.maven.plugin.AbstractMojo which has one unimplemented method from org.apache.maven.plugin.Mojo interface. In that method we are going to implement the Maven goal logic.

Mojo class instance is called from Maven execution lifecycle by invoking execute() method. Before calling execute(), Maven has performed some other tasks related with the Mojo:

  1. Maven instantiates Mojo and injects dependencies (Dependency Injection).
Mojo mojo = new BuildMojo(fileSetManager, mdToHtml);
  1. Maven configures the Mojo by assigning values to parameters.
  2. Maven calls execute method: mojo.execute();.

I will simplify execute method implementation, the sample project in github is more complicated and not good for learning.

// If there is any error during execution, it should throw MojoExecutionException
public void execute() throws MojoExecutionException {
    if (inputFiles == null) {
        setDefaultInput();
    }
    inputDirPath = Paths.get(inputFiles.getDirectory());

    // A way to get all selected files from FileSet
    // https://maven.apache.org/shared/file-management/fileset.html
    String[] includedFiles = fileSetManager.getIncludedFiles(inputFiles);

    outputDirPath = outputDirectory.toPath();
    if (includedFiles == null || includedFiles.length == 0) {
        // AbstractMojo supplies logger functionality
        getLog().warn("SKIP: There are no input files. " + getInputFilesToString());
    } else {
        // If output directory doesn't exist, it will be created
        if (!outputDirectory.exists()) {
            outputDirectory.mkdirs();
        }
        try {
            for (String f : includedFiles) {
                // it converts each file Markdown to HTML
                convertToHtml(Paths.get(f), outputDirectory);
            }
        } catch (InterruptedException e) {
            // Convert thrown exception to MojoExecutionException
            throw new MojoExecutionException(e.getLocalizedMessage(), e);
        }
    }
}

Unit tests

In the example we use JUnit 4, but you can use any other testing framework.

Firtsly, you have to add the unit test library dependency to pom.xml.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>

Then you just have to write your unit tests under src/test/java folder, for example: src/test/java/com/maven/plugins/blog/PathsTest.java.

To run the unit tests you just need to execute mvn test.

Integration tests

The 2 most popular ways to perform integration tests on custom maven plugins are using maven-failsafe-plugin or maven-invoker-plugin.

I've chosen maven-invoker-plugin because for me it is more straightforward. There is an answer at stackoverflow where they explain thoroughly the differences between them

How does Invoker Plugin work?

We create projects to use our custom plugin under src/it folder, so our plugin will be applied to test projects. After that, a validation script will be executed so we can check if our plugin outputs are as expected. For example, if our plugin is supposed to generate a file named foo.file, verification plugin will check if that file exists, if it doesn't, integration test will fail.

Configure Invoker Plugin

<plugin>
    <artifactId>maven-invoker-plugin</artifactId>
    <version>3.0.1</version>
    <configuration>
        <postBuildHookScript>verify</postBuildHookScript>
        <showVersion>true</showVersion>
        <streamLogs>true</streamLogs>
        <noLog>false</noLog>
        <showErrors>true</showErrors>
    </configuration>
    <executions>
        <execution>
            <id>integration-test</id>
            <goals>
                <goal>install</goal>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

In executions section we execute following goals:

  1. invoker:install will be executed during the phase pre-integration-test and will install the main project artifact into target/local-repo.
  2. invoker:run will be executed during the integration-test phase and it will execute all defined integration tests under src/it folder.

In configuration section:

<postBuildHookScript>verify</postBuildHookScript> configures invoker plugin to execute validation script after integration test project execution. This script may be written with either BeanShell or Groovy (verify.groovy or verify.bsh).

We have used other properties to show errors, show maven log and save it to a file.

You can check all invoker:run configuration properties at https://maven.apache.org/plugins/maven-invoker-plugin/run-mojo.html.

Create an Integration Test Project

It is a project we use to execute custom plugin goals, so we can validate if it produces the expected output.

There are 3 important files that matche with AAA phases ("Arrange-Act-Assert"):

pom.xml (Arrange)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.maven.plugins.it</groupId>
    <artifactId>simple-it</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>@project.groupId@</groupId>
                <artifactId>@project.artifactId@</artifactId>
                <version>@project.version@</version>
            </plugin>
        </plugins>
    </build>
</project>

It is a very simple pom file where we use placeholders to reference to our plugin under test. When invoker plugin executes following pom file, firstly will replace those placeholders to reference to the latest version sof our custom plugin which was recently installed in the local repository:

<plugin>
    <groupId>com.maven.plugins</groupId>
        <artifactId>blog</artifactId>
        <version>0.0.1-SNAPSHOT</version>
</plugin>

In that way invoker plugin ensures it is testing the latest version of current project.

invoker.properties (Act)

invoker.goals = blog:build
invoker.name = Test build MD

It will execute mvn blog:build, a goal defined in our custom plugin under example or what is the same, it will execute BuildMojo described in section "Write a custom Mojo".

verify.groovy (Assert)

File generated = new File( basedir, "target/site/README.html" );

assert generated.isFile()

It is checking if target/site/README.html file was generated by the plugin. fExecute validation script

Comentarios

Comments powered by Disqus