Saturday, December 10, 2005

Using Maven 2, Part 3

****************************************************************
Maven 2 Notes, Part 3
****************************************************************

----------------------------------------------------------------
Section 1: Writing a plugin intro
----------------------------------------------------------------
* Previously we looked at how to use Maven 2 to deploy WAR files (and portlet applications) from a remote
repository into a local Tomcat directory. This wasn't too successful, so now we're going to look at
writing a plugin to do this.

* In quick summary, the plugin should do the following:
1. Grab a war file from the local repository.
2. Put it in the directory of our choice.
3. That's it.
Note the remote download isn't necessary, since this is done through the existing <dependency/> mechanism.

* There is a Tomcat plugin under development from Codehaus that can perform tasks such as load and install
war files into a running tomcat, so we'll look at that later, maybe, if I can get it to compile.

* Before we go on, here is a quick summary of the important parts:
1. Plugins (or MOJOs) use a simple POJO/javabean structure with directives included
in the comments for some reason.
2. The plugin's execute() method is where you do the thing (ie useful code goes here).
3. You can pass all kinds of variables (not just strings) into the plugin from the POM.

----------------------------------------------------------------
Section 2: Writing a do-nothing plugin
----------------------------------------------------------------
* The real documentation for writing a plugin is here:

http://maven.apache.org/guides/plugin/guide-java-plugin-development.html

Maven plugins are called "Mojos", a play on "POJOs" for reasons that will become clear later.

* First, let me say I was disappointed that there is no plugin archetype. Recall archetypes are Maven 2's way
for generating boilerplate code. So I used the the standard way, discussed in Part 1:

mvn archetype:create -DgroupId=test.plugin -DartifactId=PortletAppPlugin

* The resulting POM requires a few modifications: we specify the packaging to be a maven-plugin, and
we add a dependency on the maven plugin api. This is detailed in the link above, but here is mine:

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>test.plugin</groupId>
<artifactId>PortletAppPlugin</artifactId>
<!-- This specifies the plugin packaging -->
<packaging>maven-plugin</packaging>

<version>1.0-SNAPSHOT</version>
<name>Portlet Webapp Plugin</name>
<url>http://maven.apache.org</url>
<dependencies>
<!-- This is needed for the plugin classes. -->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
</project>

I omitted the junit dependencies for brevity. You need to either include this or else eliminate the src/test directory.

* Write the code. The starter plugin looks like this:

package test.plugin;

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

/**
* @goal portlet-war-deploy
* @description Copy a war to a tomcat directory.
*/
public class PortletAppPlugin extends AbstractMojo {

public void execute() throws MojoExecutionException
{
getLog().info("Hollow World!");
}
}

* One really intereting feature of the plugin is that is REQUIRES two comment fields:
@goal
@description
These are used later, and we will also see this when setting parameters.

* To now install, we follow the usual path: "mvn compile" to make sure we have made no mistakes in the java code, and then
"mvn install" to put it in the local repo.

* We can now run our plugin on the command line like so:

mvn test.plugin:PortletAppPlugin:1.0-SNAPSHOT:portlet-war-deploy

Note that the "porlet-war-deploy" at the end of this is taken from "@goal" in the comment field. When we run this
command, the getLog() message will be printed to the screen.

* Note for clarity that we will eventually want to integrate this plugin into other POMs. The above command
is a useful way of testing things in stand-alone mode.

----------------------------------------------------------------
Section 3: Writing a do-something plugin
----------------------------------------------------------------

* To be useful, our plugin needs to have some parameter settings. These can then be used in the execute
statement. This also reveals a bit of POJOing combined with a little Too Much Magic with bean creation.

* Let's modify our code a bit to use a simple string parameter.

public class PortletAppPlugin extends AbstractMojo {

public void execute() throws MojoExecutionException
{
getLog().info("Hollow World!");
System.out.println("Sample Param:" + sampleParam);
}

/**
* Sample parameter setting.
* @parameter expression="Hollow"
*/
private String sampleParam;

}

Re-install this ("mvn install") and then run the plugin.

* The thing to notice is that we have again used comments and annotations: the @parameter is used to not only describe
the sampleParam string, but to provide it with a default value. When this MOJO is executed, you will see that
the sampleParam value has been correctly populated.

* We can provide alternative values for the sampleParam in the plugin configuration description section of the
POM using the plugin. This is described in detail at

http://maven.apache.org/guides/mini/guide-configuring-plugins.html


* Note also that the sampleParam string is private. The container that creates this and other MOJOs must be
generating get and set methods.

* The expression stuff really starts to become useful when we insert Maven parameter expressions. Try the following:

/**
* Sample parameter setting.
* @parameter expression="${pom.version}"
*/
private String sampleParam;

This will correctly print out (in the execute() method) the version in the pom.xml. But what happens when I put this
plugin in another project's pom.xml? Will it print the plugin's version or the container project's version? My guess
from Maven's inheritance behavior is that it will use the parent project's ${pom} information, rather than the plugin's.

* So let's test this out and in the process show how to add our plugin to another project. Take the "jobsubmit-portlet"
project from part 1 of this series and add the following (just put it below the Ant plugin).
<project>
...
<build>
...
<plugins>
<plugin>
<artifactId>PortletAppPlugin</artifactId>
<groupId>test.plugin</groupId>
<executions>
<execution>
<phase>process-resources</phase>
<configuration>
<sampleParam>
${pom.version}
</sampleParam>
</configuration>
<goals>
<goal>portlet-war-deploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
</project>

There are several things to note. First, Maven will find the Plugin in my local plugin repository because, where
I previously installed it. Second, <sampleParam></sampleParam> is used to set the plugin value--this is passed to
my code above. Third, I must finally specify the plugin's goal. Recall that this was specified using @goal at the top
of the MOJO java code.

* When we run this, we will in fact see that the value of sampleParam that gets printed is the version of the
jobsubmit-portlet project, not the PortletAppPlugin, as expected. Further, we can remove the <configuration> entirely
so that we use default values. We will again see that the value of the sampleParam is the version of the jobsubmit-portlet,
NOT the plugin's version, again as expected.

----------------------------------------------------------------
Section 4: Using more complicated parameters.
----------------------------------------------------------------
* One of the primary reasons for reviewing plugins is that we can no longer write simple Jelly scripts for processing
the POM. For example, recall that the ${pom.dependencies} gives you access to all the dependencies as one big
string. Echoing this value using the Ant plugin gives something like

[Dependency {groupId=junit, artifactId=junit, version=3.8.1, type=jar}]

* The problem is that the POM is actually returning an object (org.apache.maven.model.DependencyManager), and we are
actually seeing the output of the toString() method.

But now with plugins, we can actually get access to this stuff.

* However, we immediately run into a problem: Maven 2 has no javadoc laying around for some reason. So we instead
have to either browse the SVN repo on line or else check out the code and see for ourselves. The source actually has a
non-intuitive layout. The real code for doing stuff seems to be under bootstrap:



http://svn.apache.org/viewcvs.cgi/maven/components/trunk/bootstrap/bootstrap-mini/src/main/java/org/apache/maven/bootstrap/


* By downloading the Maven 2 source code and by snaking around in the on-line SVN repo, I found the following works.
Here is the required dependency in the POM for our plugin:


<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>2.0</version>
</dependency>

Add this to the pom.xml for the plugin. You need this to compile the plugin with the additional Dependency class below.


* And here is the Plugin Code:

package test.plugin;

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

import org.apache.maven.model.Dependency;
import java.util.ArrayList;

/**
* @goal portlet-war-deploy
* @description Copy a war to a tomcat directory.
*/
public class PortletAppPlugin extends AbstractMojo {

public void execute() throws MojoExecutionException
{
getLog().info("Hollow World!");
System.out.println("Sample Param: "+sampleParam);

for(int i=0; i<theDepends.size();i++) {
Dependency deps=(Dependency)theDepends.get(i);
System.out.println(deps.toString());
}
}

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

/**
* Second sample parameter setting.
* @parameter expression="${project.dependencies}"
*/
private ArrayList theDepends;
}

* To test this, clean, compile, install and then run the plugin.

mvn clean install test.plugin:PortletAppPlugin:1.0-SNAPSHOT:portlet-war-deploy

The output is

[INFO] Hollow World!
Sample Param: 1.0-SNAPSHOT
Dependency {groupId=junit, artifactId=junit, version=3.8.1, type=jar}
Dependency {groupId=org.apache.maven, artifactId=maven-plugin-api, version=2.0, type=jar}
Dependency {groupId=org.apache.maven, artifactId=maven-model, version=2.0, type=jar}

* That is, it worked as expected. We can now use the Dependency class to get/set various properties since
it is really just a JavaBean:

for(int i=0; i<theDepends.size();i++) {
Dependency deps=(Dependency)theDepends.get(i);
System.out.println("Artifact ID: "+deps.getArtifactId());
System.out.println("groupId: "+deps.getGroupId());
}

* Thus we can see that the execute() method can be used to do most of the work. Our next step will
be to use more meaningful parameters:
o The path to the Tomcat directory.
o The path to the specified dependency's WAR file.

No comments: