Thursday, December 28, 2006

Maven Plugin Writing Intro

(this post is an excerpt from the upcoming book Java Power Tools... no reproductions without permission)

Using Plugins to Customize the Build Process

If there has been one substantial improvement between Maven 1 and Maven 2, it is the simplicity and flexibility of extending the default execution set with custom plugins. One can even write plugins in other programming languages, such as Jruby, Groovy, or Ant; however we will turn our focus to the most heavily used and supported default language: Java.

A Maven plugin is a collection of goals and, as mentioned in previous articles, a goal is a unit of work in the Maven build lifecycle. Maven comes with tools allowing you to easily create and install your own goals. This allows you to extend the default build lifecycle in any way you can imagine - like Ant tasks, if you are so inclined to draw the comparison - but with the benefits of the well-defined lifecycle and network portability of Maven.

In order to create a simple plugin through the archetype, type the following in your command line:

mvn archetype:create -DgroupId=my.plugin \
    -DartifactId=maven-my-plugin \
    -DarchetypeGroupId=org.apache.maven.archetypes \
    -DarchetypeArtifactId=maven-archetype-mojo \
    -DarchetypeVersion=1.0

In Maven, the implementation of a goal is done in a Mojo – a play-on-words meaning Maven POJO (Plain Old Java Object) and, well, mojo. All Java Maven Mojos implement the org.apache.maven.plugins.Mojo interface. Without getting too detailed, it is good to understand that Maven is built upon the inversion of control (IoC) container / dependency injection (DI) framework called Plexus. If you are familiar with Spring, you are good way there to understanding Plexus. Plexus is built around the concept of components that each play a "role", and each role has an implementation. The role name tends to be the fully qualified name of the interface. Like Spring, Plexus components (think Spring beans) are defined in an XML file. In Plexus that XML file is named components.xml and lives in META-INF/plexus. The consumers of a component need not know the role's implementation, as that is managed by the Plexus DI framework. When you create your own Mojo implementation, you are effectively creating your own component that implements the org.apache.maven.plugins.Mojo role.

So – you may be thinking – what does this have to do with Maven goals? When you create a Mojo class, you annotate the class with certain values; those values are then used to generate a variant of the Plexus components.xml file named plugin.xml living under META-INF/maven. So what translates those annotations to a plugin.xml file? Maven goals, of course! Your maven-plugin packaging project's build lifecycle binds goals that generate the descriptor for you. In short, non-jargon speak: Maven does that work for you.

In the plugin you generated above, navigate to the maven-my-plugin/src/main/java/my/plugin/MyMojo.java file and set the contents to the following:

package my.plugin;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;

/**
* A simple Mojo.
* @goal my-goal
*/
public class MyMojo extends AbstractMojo
{
/**
* This populates the message to print.
* @parameter required default-value="No message set"
*/
private String message;

public void execute()
throws MojoExecutionException
{
getLog().info( message );
}
}

Now install the plugin vis the normal Maven method; type:

mvn install

The execute method is solely responsible for executing the goal - any other methods you encounter in a Mojo are just helper methods. Maven injects values into the Mojo object directly into the project’s fields. In the example above the "message" field is a valid Maven property and is printed out the logger returned by the getLog method. Remember that Plexus is a "dependency injection" framework. Since we annotated the message field as a parameter, that parameter can now be populated by Maven (via Plexus). You can populate your goal in the same way you do any goal, through the configuration element in the POM:

<project>
<build>
<plugins>
<plugin>
<groupid>my.plugin</groupid>
<artifactid>maven-my-plugin</artifactid>
<configuration>
<message>Hello World!</message>
<configuration>

This sets the configuration for all goals under maven-my-plugin. Remember, a plugin can contain multiple goals, one per Mojo in the project. If you wish to configure a specific goal, you can create an execution. An execution is a configured set of goals to be executed.

<project>
<build>
<plugins>
<plugin>
<groupId>my.plugin</groupId>
<artifactId>maven-my-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>my-goal</goal>
</goals>
<configuration>
<message>Hello World!</message>
<configuration>

In either case, you execute the goal in the same way:

mvn my.plugin:maven-my-plugin:my-goal

Hello World!

You may wonder why so much typing when the create goal we ran was only archetype:create? That’s because the archetype goal has the groupId org.apache.maven.plugins, which is tried as a possible prefix by default when none is provided. You can add more plugin groups to your system by added this to your .m2/settings.xml file:

<settings>
...
<plugingroups>
<plugingroup>my.plugin</plugingroup>
</plugingroups>
</settings>

Furthermore, if your plugin name is surrounded by maven-*-plugin, Maven will allow you to simply type the name in the middle. Since we have already done this, you can now just run the much simpler goal:

mvn my:my-goal

The final way to configure a goal is via a property. You can set an expression to populate the property rather than a direct value.

@parameter expression="${my.message}"

This gives you the flexibility to set the property within the POM, in the settings.xml, or even on the command line… anywhere you can set a property in Maven.

mvn my:my-goal -Dmy.message=Hello

Prints:

[INFO] Hello

Manipulating the Build Lifecycle

Creating goals is great, however that alone is hardly much better than just creating Ant tasks. In order to benefit from Maven’s well-defined build lifecycle, it often makes sense to put your goal into the lifecycle somehow. In rare cases, your plugin may need to create its own lifecycle definition.

There are two major ways in which to bind a goal to a lifecycle. The first is to just add the goal to an execution phase, defined in your running project’s POM.

<project>
<build>
<plugins>
<plugin>
<groupId>my.plugin</groupId>
<artifactId>maven-my-plugin</artifactId>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>my-goal</goal>
</goals>
<configuration>
<message>I am validating</message>
<configuration>

Running

mvn validate
will print the configured message. Oftentimes a goal will be created with a specific phase in mind. When creating your Mojo, you can define a phase that the goal will run in. Add the following annotation to the my-goal goal and install the plugin via "mvn install".

@phase validate

Now you need only add the plugin to your POM configuration, and the my:my-goal goal will be bound to the validate phase for you.

<project>
<build>
<plugins>
<plugin>
<groupid>my.plugin</groupid>
<artifactid>maven-my-plugin</artifactid>

Another way to manipulate the build lifecycle is to create your own forked lifecycle. You can tell the Mojo to execute the forked lifecycle up to a given phase. If you do not set the lifecycle, then the default is used, but if you do, you must provide a definition of that new lifecycle.

@execute phase="validate" lifecycle="mycycle"

You define the mycycle build lifecycle in a META-INF/maven/lifecycle.xml file. The following lifecycle executes the my-goal goal (only once, not recursively) in the validate phase.

<lifecycles>
<lifecycle>
<id>mycycle</id>
<phases>
<phase>
<id>validate</id>
<executions>
<execution>
<goals>
<goal>my-goal</goal>
</goals>
<configuration>
<message>I am forked</message>
</configuration>
</execution>
</executions>
</phase>
</phases>
</lifecycle>
</lifecycles>

When combined with the above POM configuration, it will execute two validate phases:

[INFO] I am forked
...
[INFO] No message set

Hooking into Maven

The simplest way to hook into the Maven runtime is to create parameters populated by Maven parameters. Some commonly used parameters are as follows:

The current project (the POM data):

/**
* @parameter expression="${project}"
*/
private org.apache.maven.project.MavenProject project;

The current version:

/**
* @parameter expression="${project.version}"
*/
private String version;

A similar method may be used for getting simple properties from the POM, such as project.groupId/project.artifactId/project.url. Another commonly required parameter is the project’s build directory.

/**
* @parameter expression="${project.build.directory}"
*/
private java.io.File outputDirectory;

More complex values can be acquired with the following parameter names of the following types.

${project.build} =>
org.apache.maven.model.Build
${project.ciManagement} =>
org.apache.maven.model.CiManagement
${project.dependency} =>
org.apache.maven.model.Dependency
${project.dependencyManagement} =>
org.apache.maven.model.DependencyManagement
${project.distributionManagement} =>
org.apache.maven.model.DistributionManagement
${project.issueManagement} =>
org.apache.maven.model.IssueManagement
${project.license} =>
org.apache.maven.model.License
${project.mailingList} =>
org.apache.maven.model.MailingList
${project.organization} =>
org.apache.maven.model.Organization
${project.reporting} =>
org.apache.maven.model.Reporting
${project.scm} =>
org.apache.maven.model.Scm

The local repository:

/**
* @parameter expression="${localRepository}"
*/
private org.apache.maven.artifact.repository.ArtifactRepository localRepository;

Using Plexus Components

As mentioned above, Maven is built upon Plexus, which is an IoC container that manages components. There are components in Plexus that you may wish to use in your Mojos, for example, the Plexus JarArchiver class:

/**
* @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#jar}"
* @required
*/
private org.codehaus.plexus.archiver.jar.JarArchiver jarArchiver;

Just like other Maven expressions, these values can be injected from components when the parameter is prefixed with "component." followed by the Plexus role name. If the role can play varying roles, you can pinpoint that role via the role-hint, specified by "#jar", or "#zip", or whatever that role-hint may be.

Many plexus components exist in the Maven repository, and can be used in a similar way, for example the i18n component:

/**
* @parameter expression="${component. org.codehaus.plexus.i18n.I18N}"
* @required
* @readonly
*/
private org.codehaus.plexus.i18n.I18N i18n;

See the list at http://repo1.maven.org/maven2/org/codehaus/plexus/

Obviously your goals can and probably will be far more complicated than the examples shown, but you have been given the basic tools to start writing your own plugins, utilizing full control of Maven.

3 comments:

Shari said...

Great post. A little fix :

mvn archetype:create -DgroupId=my.plugin \
-DartifactId=maven-my-plugin \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-mojo

is not working. You have to specify the maven-archetype-mojo Version with -DarchetypeVersion=1.0 (or later)

Eric said...

Thanks Shari, it's fixed!

Anda Clarke/www.andarusu.com said...

Very good article, it helped me a lot.
You explained everything in a concise, straightforward way. Not everybody does so :)

Thanks,
Anda