Previous: Chapter
11 |
Up: Chapter 11 |
Next: Chapter
12 |
If you've heard this story before, don't stop me, because I'd like to hear it again. -- Groucho Marx
This chapter follows an example project. You may wish to download the example and follow along.
Introduction
In the previous chapter we touched upon the subject of writing plugins as they relate to the build lifecycle, but skimmed over a lot of the details that makes the Maven plugin model so powerful.
Inversion of Control and Dependency Injection
Maven is run on the powerful Plexus inversion of control (IoC) framework. The idea of IoC is most often implemented in Java with a mechanism called dependency injection. The basic idea of IoC is that the control of creating objects is removed from the code itself and placed into the hands of an IoC framework... a framework whose onus is management of object creation. The control is now no longer in the code, but has been (you guessed it) inverted to be framework controled. The most famous example of an IoC framework in Java is Spring, but there are many implementations of this basic idea, such as Pico or Nano. The [idea] is that you can create code that is not bound to specific implementations of a class or interface, but manage the actual implementing class (as well as values to properly populate that new object) externally, often with an XML file. In the case of Plexus, components (objects managed by the Plexus IoC container) are defined with files findable in the classpath under META-INF/plexus/components.xml.
Dependency injection is the specific mechanism implementing most Java IoC solutions. There are several methods for injecting dependant values into a component object: constructor, setter, or field injections. Plexus can handle all three of them.
Constructor injection is populating an object's values through its constructor when it is created. For example, if I used java.lang.Integer as a component, a new instance would be constructed by the IoC container and populated with some value (specified by the external definition), lets say 3, as "new Integer(3)" (although actually implemented using Java reflection, you get the basic idea).
Setter injection is using the setter method of a Java bean to populate the object - but accessible as a property. This is the method used by Spring. For example if we wanted to construct a java.util.Date object and set the hours through the setHours method, your external definition would likely be something akin to <hours>3</hours> or hours=3, where the container would create "Date d = new Date(); d.setHours(3);" by appending the property name "hours" to "set" to construct the method name.
Field injection is a way to directory populate a field based upon its name. It is similar than the method above, without the name construction... the name of the field is the name of the external property. Using the java.util.Date, we can set the time in milliseconds directly to the null date (Jan 1, 1970) internal field fastTime, even though it is a private field as "Date d = new Date(); d.fastTime = 0;".
Plugin Execution and The Plugin Descriptor
Although Plexus is capable of all three dependency injection techniques, Maven only uses two types: field and setter injection. A mojo is a Maven container - with parts of the underlying implementation managed by Plexus. Maven maps execution details by the use of annotations and implementation of the Mojo interface. Maven's plugin configuration definition is META-INF/maven/plugin.xml. The annotations generate the configuration, and the interface provides the hook. This is how mojos attach themselves into the Mavan runtime.
If you look back to the previous chapter when displaying the lifecycle goals bound to the "maven-plugin" packaging type, the plugin:descriptor goal was bound to the generate-resources phase. This goal is in charge of generating the plugin descriptor (the plugin.xml file). This is configuration that Maven uses to manage a particular plugin and set of mojos, as well as contains some configuration information used by Maven to inject values.
<plugin>
<description></description>
<groupId>com.training.plugins</groupId>
<artifactId>maven-zip-plugin</artifactId>
<version>1-SNAPSHOT</version>
<goalPrefix>zip</goalPrefix>
<isolatedRealm>false</isolatedRealm>
<inheritedByDefault>true</inheritedByDefault>
<mojos>
<mojo>
<goal>zip</goal>
<description>Zips up the output directory.</description>
<requiresDirectInvocation>false</requiresDirectInvocation>
<requiresProject>true</requiresProject>
<requiresReports>false</requiresReports>
<aggregator>false</aggregator>
<requiresOnline>false</requiresOnline>
<inheritedByDefault>true</inheritedByDefault>
<phase>package</phase>
<implementation>com.training.plugins.ZipMojo</implementation>
<language>java</language>
<instantiationStrategy>per-lookup</instantiationStrategy>
<executionStrategy>once-per-session</executionStrategy>
<parameters>
<parameter>
<name>baseDirectory</name>
<type>java.io.File</type>
<required>false</required>
<editable>true</editable>
<description>Base directory of the project.</description>
</parameter>
<parameter>
<name>buildDirectory</name>
<type>java.io.File</type>
<required>false</required>
<editable>true</editable>
<description>Directory containing the build files.</description>
</parameter>
</parameters>
<configuration>
<buildDirectory implementation="java.io.File">${project.build.directory}</buildDirectory>
<baseDirectory implementation="java.io.File">${basedir}</baseDirectory>
</configuration>
<requirements>
<requirement>
<role>org.codehaus.plexus.archiver.Archiver</role>
<role-hint>zip</role-hint>
<field-name>zipArchiver</field-name>
</requirement>
</requirements>
</mojo>
</mojos>
<dependencies/>
</plugin>Notice that the configuration elements are named the same as the fields in the ZipMojo class. The property values should look familiar - much like properties and filters. They will be replaced with values set by the given property's value, assuming the implementation can match.
This is a good time for a sidebar concerning more advanced properties. Properties in Maven are more than simply name=value string pairs. The values can be any sort of Java object. In the example above the given properties are java.io.File objects, but they could just as easily be java.lang.Integer (popualted by a number) or org.apache.maven.project.MavenProject (populated by ${project}).
There were three elements in the ZipMojo plugin. The third is under the "requirement" element. This is how the plugin definition injects Plexus components into the mojo. It detects Plexus components through the "component." property. Plexus components are not set by properties like normal values, but rather required, which is good. If you are adding them, chances are you want to use them.
The Mojo
It was mentioned earlier that Maven maps execution details by the use of annotations and implementation of the Mojo interface. The annotations are used to generate the plugin.xml file above - this file tells Maven how to execute the goal properly. The mojo does the actual execution. It was no exageration to say that Maven is largely a plugin execution framework. If you think back to the build lifecycle, all of the work is done by the goals bound to the phases - the rest of the work is merely managerial, and is the same reguardless of what is actually being executed. This gives Maven an extreme flexibility as a build framework. If you wish to create a goal that emails the developer list on execution it is entirely possible (please don't do this... its very annoying, especially once you start automating your builds with some continuous integration tools).
Sticking with the zip plugin example from the previous chapter, let us take a closer look at how to use the plugin/goal/lifecycle/configuration/property dynamic to your advantage.
Java Mojos
Since Maven is a Java project, it lends to reason that plugins may only be written in Java. Though only Java bytecode may ultimately be executed by Maven, there are several ways to get that bytecode a-churnin'. Plexus is capable of loading several JVM runnable programming languages such as Java, Ant, Groovy, Jython and JRuby. But our focus will be strictly on Java and Ant based mojos.
Stepping back from the mojo of the previous chapter, let us begin again more slowly, with a toy plugin.
/**
* Echos an object string to the output screen.
* @goal echo
*/
public class EchoMojo extends AbstractMojo
{
/**
* Any Object to print out.
* @parameter expression="${echo.message}" default-value="ECHO Echo echo..."
*/
private Object message;
public void execute()
throws MojoExecutionException, MojoFailureException
{
getLog().info( message.toString() );
}
}This mojo implements a simple goal that echos the message to the logger (the Maven logger defaults to the command-line) inherited from the AbstractMojo. The logger can output in different levels through the methods: debug, info, warn, and error. You should only communicate output via this logger in a mojo, and avoid other methods such as System.out.print(...).
- @goal goalName - This is the only required annotation which gives a name to this goal unique to this plugin.
- @requiresDependencyResolution requiredScope - Flags this mojo as requiring the dependencies in the specified scope (or an implied scope) to be resolved before it can execute. Supports compile, runtime, and test.
- @requiresProject - Marks that this goal must be run inside of a project, default is true. This is opposed to plugins like archetypes, which do not.
- @requiresReports ? false
- @aggregator ? false
- @requiresOnline ? false
- @requiresDirectInvocation ? false
- @phase phaseName - We saw this annotation in chapter 4, where we used it to set the default phase this goal will bind to if configured as a plugin execution in the POM.
- @execute [goal=goalName|phase=phaseName
[lifecycle=lifecycleId]] - This annotation was also touched upon in the
previous chapter. This annotation notes to the Maven runtime that some
alternate execution must first take place before this goal will execute. There
are three main examples of this in use:
- @execute phase="package" lifecycle="zip"
- @execute phase="compile"
- @execute goal="zip:zip"
If lifecycle is not specified, it defaults to "default".
Note that annotations are used solely to generate the plugin.xml descriptor. If you reeeeally want to go to all of the trouble writing the plugin.xml file by hand, you won't need these annotations, but its not recommended by sane people!
Now is a good time to run the goal in any project's basedir: "mvn com.training.plugins:echo-maven-plugin:echo -Decho.message=$project.version". It will print out something like:
[INFO] [echo:echo] [INFO] 1.0-SNAPSHOT
Cool, eh? The ${project.version} property sets the ${echo.message} property, which in turn populates the message field via Maven's dependency injection mechanism. How does Maven know what field to populate, and what property to relate to it? What if no value is set at all to echo.message? These are answered by the parameter annotations.
/**
* Any Object to print out.
* @parameter
* expression="${echo.message}"
* default-value="ECHO Echo echo..."
*/
private Object message;There are two methods for setting the above field. One is to set the property ${echo.message} in some way. This can be through the command line as shown above, or in the POM as a property, or in the system's settings.xml file.
<project>
...
<properties>
<echo.message>Print Me!</echo.message>
</properties>
</project>Whatever way implemented note that the priority is, from highest to lowest: command line (via the -D flag), profiles, system settings, POM. If none of those methods have set a property then Maven will inject the default-value into the field (but note, not the expression).
- @parameter [alias="someAlias"] [expression="$someExpression"] [default-value="value"] - Required to mark a private field as a paramter. (Note parameters may also be setter methods, but their utility is rarely necessary)
- @required - Whether this parameter is required for the Mojo to function. This is used to validate the configuration for a Mojo before it is injected, and before the Mojo is executed from some half-state. NOTE: Specification of this annotation flags the parameter as required; there is no true/false value.
- @readonly - Specifies that this parameter cannot be configured directly by the user (as in the case of POM-specified configuration). This is useful when you want to force the user to use common POM elements rather than plugin configurations, as in the case where you want to use the artifact's final name as a parameter. In this case, you want the user to modify <build><finalName/></build> rather than specifying a value for finalName directly in the plugin configuration section. It is also useful to ensure that - for example - a List-typed parameter which expects items of type Artifact doesn't get a List full of Strings. NOTE: Specification of this annotation flags the parameter as non-editable; there is no true/false value.
- @component - Indicates to lookup and populate the field with an implementation with a Plexus Component. You can enact the same behavior through the @parameter expression="${component.yourpackage.YourComponentClass}", however, @component is the prefered method. Component injection via expression may be deprecated in the future.
- @deprecated - Marks a parameter as deprecated. The rules on deprecation are the same as normal Java with language elements. This will trigger a warning when a user tries to configure a parameter marked as deprecated.
/**
* Zips up the output directory.
* @goal zip
* @phase package
*/
public class ZipMojo extends AbstractMojo
{
/**
* The Zip archiver.
* @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#zip}"
*/
private ZipArchiver zipArchiver;
/**
* Directory containing the build files.
* @parameter expression="${project.build.directory}"
*/
private File buildDirectory;
/**
* Base directory of the project.
* @parameter expression="${basedir}"
*/
private File baseDirectory;
/**
* A set of file patterns to include in the zip.
* @parameter property="includes"
*/
private String[] mIncludes;
/**
* A set of file patterns to exclude from the zip.
* @parameter property="excludes"
*/
private String[] mExcludes;
public void setExcludes( String[] excludes ) { mExcludes = excludes; }
public void setIncludes( String[] includes ) { mIncludes = includes; }
public void execute()
throws MojoExecutionException
{
try {
zipArchiver.addDirectory( buildDirectory, includes, excludes );
zipArchiver.setDestFile( new File( baseDirectory, "ouput.zip" ) );
zipArchiver.createArchive();
} catch( Exception e ) {
throw new MojoExecutionException( "Could not zip", e );
}
}
}When you install the maven-zip-plugin containing the above goal (notice the addition of the includes and excludes fields), you can run the zip goal in the maven-zip-plugin-test project as before
mvn zip:zip
The output.zip file generated will contain the README.txt from from src/main/resources. However, if you add the following configuration to the POM
<configuration>
<excludes>
<exclude>**/*.txt</exclude>
</excludes>
</configuration>and rerun "mvn zip:zip" the zip file will be empty.
The corrolation should be obvious. The default behavior of a field annotated with @parameter is to be configurable in the POM.
Common Tools
Since Maven exists to make your life easier, it is only right that it provides tools to make writing mojos easier as well with tools. Up until now we have yet to mention Plexus, but Plexus is really the lifeblood of Maven. It is a dependency injection framework, an IoC container like Spring or Nano.
Mojos in Other Languages
BeanShell
BeanShell plugins will be easily readable to anyone who knows their Java kin. BeanShell is a clean way to write Java-like plugins without a lot of the more verbose syntax. You can use beanshell for mojo-writing with the following structure (rather than the source residing under src/main/java, place it under src/main/scripts):
beanshell-maven-plugin
|-- pom.xml
`-- src
`-- main
`-- scripts
`-- echo.bshYou then create a BeanShell script, annotated in a similar method to a Java plugin using Javadoc.
/**
* @goal echo
*/
import org.apache.maven.plugin.Mojo;
import org.apache.maven.script.beanshell.BeanshellMojoAdapter;
execute()
{
logger.info( "Echo: " + message );
}
/**
* Outputs the message.toString()
*
* @parameter expression="${message}" type="java.lang.Object" default-value="ECHO Echo echo..."
*/
setMessage( msg )
{
message = msg;
}
return new BeanshellMojoAdapter( (Mojo) this, this.interpreter );Since BeanShell is built into the Maven core, installing and running this goal is similar to any Java plugin.
mvn install mvn com.mycompany:beanshell-maven-plugin:echo -Dmessage=Hi!
You can learn more about BeanShell from the website: http://www.beanshell.org/
Ant
Unlike BeanShell, Ant plugins are not built into the core of Maven, although it is easy to use them. Before creating an Ant plugin, let's take a quick dive into two important areas of the Maven plugin framework: descriptor extraction and component runtime.
As mentioned above, plugin descriptors describe to the Maven runtime how to execute a mojo in a plugin. Just because you create a project of type "maven-plugin" that contains some Java files named something akin to EchoMojo.java - does not mean that Maven knows what to do with it. The plugin descriptor (META-INF/maven/plugin.xml file) describes runtime details such as:
- What goal named X maps to mojo Y (eg. "echo" goal maps to "EchoMojo.class")
- What fields are injected with what values (eg. let $message populate the private Object message; field)
- and more...
"Descriptor extraction" is what happens when Maven (the maven-plugin-plugin) scans through your plugin project and tries to build a plugin descripor based off of available data. This can take many forms. In the case of Java and BeanShell, it is a Javadoc-like markup. In the case of Ant we will see it's own XML markup. Descriptor markup will always be in a form comfortable to the Mojo language - for example JRuby uses RDoc as it's descriptor form.
The other important area of mojo execution in the Maven plugin framework is the component runtime. In cases like Java based Mojos execution is no problem: just run the execute method in the Mojo object. In cases like BeanShell this is slightly different, since the script must be adapted to run as a normal Java Mojo (hence, the BeanshellMojoAdapter). Luckly, it is built in.
The reason we mention these two items now is because for non-built-in language extensions, they must be injected into the Maven runtime. Luckly for us, this is just a slight change to the plugin's POM. Your plugin consumers will never know the difference.
Back to writing an Ant plugin. Let us start with the POM:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.training.plugins</groupId>
<artifactId>maven-antbased-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>Ant-Based Plugin</name>
<dependencies>
<!-- -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-script-ant</artifactId>
<version>2.0.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<!-- Add the Ant plugin tools to the plugin -->
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-tools-ant</artifactId>
<version>2.0.5</version>
</dependency>
</dependencies>
<configuration>
<goalPrefix>antbased</goalPrefix>
</configuration>
</plugin>
</plugins>
</build>
</project>This makes the maven-script-ant artifact available at runtime (so it can execute the ant script goal for you). It also adds the maven-plugin-tools-ant as a dependency of the maven-plugin-plugin, so the plugin plugin can create the necessary plugin descriptor - the thing that makes a plugin a plugin.
With all that out of the way, let us get back to writing plugins. Unlike other mojo language extensions, Ant plugins do not use markup within the build.xml (Ant's version of a set of Mojos), but rather an external mojos.xml file. This was done to simplify turning existing Ant build.xml files into full-fledged Maven mojos without much internal alteration. The directory structure of our new antbased-maven-plugin will be as such:
antbased-maven-plugin
|-- pom.xml
`-- src
`-- main
`-- scripts
|-- echo.build.xml
`-- echo.mojos.xmlThe file's prefix does can be any name, as long as they match. Since the *.mojos.xml file stores the data extracted to create a plugin descriptor, it must exist alongside a *.build.xml file, or else the maven-plugin-plugin will pass it by. As you might have guessed, the echo.build.xml is just a normal Ant build.xml, and contains the logic of our mojo(s). echo.build.xml contains:
<project>
<target name="echotarget">
<echo>${message}</echo>
</target>
</project>There the echo.mojos.xml file contains the following:
<pluginMetadata>
<mojos>
<mojo>
<goal>echo</goal>
<call>echotarget</call>
<description>Echos a Message</description>
<parameters>
<parameter>
<name>message</name>
<property>message</property>
<required>false</required>
<expression>${message}</expression>
<type>java.lang.Object</type>
<description>Outputs the messag</description>
</parameter>
</parameter>
</parameters>
</mojo>
</mojos>
</pluginMetadata>The build file is fairly strightforward - it echoes the value of the ${message} property. The corrosponding mojos.xml gives Maven some information about how to interact with the build file. goal is the name of the goal, in our case echo. call specifies the name of the target to execute when the goal is called. The name of the target is echotarget>>. Finally, the <<<parameters element contains a list of properties that Maven should populate for Ant prior to executing the build script. Note the similarity of values to our previous "echo" incantations.
You can find more about Ant-based plugins at the Maven homepage: http://maven.apache.org/guides/plugin/guide-ant-plugin-development.html
JRuby
The JRuby plugin exists under the Codehaus "Mojo" project repository. Since the newest versions are not necessarily deployed to central repository at any given time if you wish to utilize the latest-and-greatest you must point Maven at the remote repository locations by adding the following to your pom.xml (or settings.xml) file. Learn more about this in their respective Appendices.
<repositories>
<repository>
<id>codehaus.org</id>
<url>http://repository.codehaus.org/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>codehaus.org</id>
<url>http://repository.codehaus.org/</url>
</pluginRepository>
</pluginRepositories>
</project>JRuby is yet another plugin language - but is covered here for a simple purpose: it is trivial to manipulate files in Ruby, and that is a large component of build systems, making it a convenient mechanism for writing Maven plugins.
Since JRuby can reflect on the JVM runtime, you can invoke and access Java class instances available in the classpath through JRuby plugins. You can find details at the JRuby site: http://jruby.codehaus.org/.
To create a JRuby plugin, first create a simple project. Rather than the source residing under src/main/java, place it under src/main/scripts.
jrubybased-maven-plugin
|-- pom.xml
`-- src
`-- main
`-- scripts
`-- my_mojo.rbThe Mojo class must extend Mojo (which you then extend "execute", or the mojo won't run). At the end of the file add "run_mojo" followed by the mojo's class name (not an instance of the class).
# This is a mojo description
# @goal "my-mojo"
# @phase "validate"
# @requiresDependencyResolution "compile"
class MyMojo < Mojo
# @parameter type="java.lang.String" default-value="nothing" alias="a_string"
def prop;;end
# @parameter type="org.apache.maven.project.MavenProject" expression="${project}"
# @required true
def project;;end
def execute
info "The following String was passed to prop: '#{$prop}'"
info "My project artifact is: #{$project.artifactId}"
end
end
run_mojo MyMojoThere are some important values above to note:
- The annotations are similar to the Java versions here.
- type must be provided as the fully-qualified name of the Java class, for example "org.apache.maven.project.MavenProject". The following are comma-seperated.
- Parameter values are accessed as global variables of the given name - i.e., the parameter named project can be used as ${project}. Java objects can be used as normal JRuby objects.
- You can output using the normal ruby methods (puts, print) or use the corrosponding mojo methods (info, error).
Finally, the POM resembles the following:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany</groupId>
<artifactId>jrubybased-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<dependencies>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jruby-maven-plugin</artifactId>
<version>1.0-beta-4</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>2.1</version>
<dependencies>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jruby-maven-plugin</artifactId>
<version>1.0-beta-4</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>You can find more about the jruby-maven-plugin at it's homepage: http://mojo.codehaus.org/jruby-maven-plugin/.
Summary
The ability to use and configure plugins is a powerful tool in the Maven world - but can only take you so far. If you ever require an action that the community has not yet considered, or with to add your own features to the ever-growing Maven toolset, the ability to write plugins is a necessity. Luckly for all of us, it is not a difficult feat to achive - made simpler still by Maven's support of alternate programming languages - for those uncomfortable with Java.
Previous: Chapter
11 |
Up: Chapter 11 |
Next: Chapter
12 |