Monday, January 16, 2006

Maven Part 4: Plugin Fun

****************************************************************
Maven 2 Notes, Part 4
****************************************************************

----------------------------------------------------------------
Section 1: Using a plugin in another project
----------------------------------------------------------------

* In part 3 of this series, we looked at how to write a plugin and
invoke it as a stand-alone application. This is useful for testing,
but now let's look at how to use our plugin in another project.

* Let's call our project "jobsubmit-portlet" for historical reasons. The
entire pom.xml is shown below. I removed the namespace definitions
from the <project> but these are standard.

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>xportlets.jobsubmit</groupId>
<artifactId>jobsubmit-portlet</artifactId>
<packaging>war</packaging>
<version>2.0</version>
<name>Maven Webapp Archetype</name>
<url>http://maven.apache.org</url>
<build>
<finalName>jobsubmit-portlet</finalName>
<plugins>
<!-- This is the Ant plugin -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>process-resources</phase>
<configuration>
<tasks>
<echo message="${pom.dependencies}"/>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- This is our test plugin -->
<plugin>
<artifactId>PortletAppPlugin</artifactId>
<groupId>test.plugin</groupId>
<executions>
<execution>
<!-- Specify the phase that needs to execute the plugin -->
<phase>process-resources</phase>
<configuration>
<!-- We are going to override this value -->
<!-- We saw earlier that ${settings} gives us access to maven's
"system" properties. -->
<sampleParam>
${settings.localRepository}
</sampleParam>
</configuration>
<goals>
<goal>portlet-war-deploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>provided</scope>
</dependency>
</dependencies>

</project>

* You should first note that the plugin applies to the "process-resources" phase of our
build cycle, so the minimal command to run is

[shell-prompt> mvn clean process-resources

I add the "clean" goal just to force cleanup. We get as output

[gateway@gridfarm002 jobsubmit-portlet]$ mvn clean process-resources
[INFO] Scanning for projects...
[INFO] ----------------------------------------------------------------------------
[INFO] Building Maven Webapp Archetype
[INFO] task-segment: [clean, process-resources]
[INFO] ----------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory /home/gateway/Maven2Tests/jobsubmit-portlet/target
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [antrun:run {execution: default}]
[INFO] Executing tasks
[echo] [Dependency {groupId=junit, artifactId=junit, version=3.8.1, type=jar}]
[INFO] Executed tasks
[INFO] [PortletAppPlugin:portlet-war-deploy {execution: default}]
[INFO] Hollow World!
Sample Param: /home/gateway/.m2/repository
Artifact ID: junit
groupId: junit
[INFO] ----------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ----------------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Mon Jan 16 13:51:02 EST 2006
[INFO] Final Memory: 2M/5M
[INFO] ----------------------------------------------------------------------------


* You should also, comparing this to the previous "standalone" test, note that the value of the
parameter <sampleParams> replaces the default value defined in the plugin code. That code is
shown again below. The default value is ${project.version}.

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

* You should also note that the dependencies are listed correctly after "Sample Params". The "Executing tasks" line is
a holdover from our experiments with Apache Ant.

----------------------------------------------------------------
Section 2: Dipping into the repository with a plugin.
----------------------------------------------------------------

* So far we have seen how to discover the local repository's path and how to
extract the project dependencies programmatically with a plugin. Now
let's look at how to construct a real path to a particular

* The following plugin code will do the trick. Note I had to
construct the actual file path to the repository file
using the private pathResolver() method. There may be an
official Maven 2 way of doing this, which would guard against
directory layout convention changes if it exists.

* Also note the useful deps.getType(), which returns ".war" or
".jar" or whatever.


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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.nio.channels.FileChannel;

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

String groupId="";
String artifactId="";
String version="";

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

for(int i=0; i<theDepends.size();i++) {
Dependency deps=(Dependency)theDepends.get(i);
String repositoryPath=pathResolver(deps);
File theDepFile=new File(repositoryPath);
File destFile=new File(destinationDir+
File.separator+theDepFile.getName());

System.out.println(repositoryPath);
System.out.println(destFile);

if(!isValidFilePath(theDepFile))
throw new MojoExecutionException("ouch");

try {
copyFile(theDepFile,destFile);
}
catch (Exception ex) {
System.err.println(ex.getMessage());
}

//Confirm the write succeeded.
if(!isValidFilePath(destFile))
throw new MojoExecutionException("ouch");

}
}

/**
* Copies one file to another. This uses NIO. I based this
* on examples from David Flanagan's "Java in a Nutshell"
* from O'Reilly.
*/
private void copyFile(File repo, File dest) throws Exception{
FileInputStream fin=new FileInputStream(repo);
FileOutputStream fout=new FileOutputStream(dest);

FileChannel fc_in=fin.getChannel();
FileChannel fc_out=fout.getChannel();

fc_in.transferTo(0,fc_in.size(),fc_out);
fc_in.close();
fc_out.close();
}

/**
* Verifies that the indicated file actually exists.
*/
private boolean isValidFilePath(File testFile) {
if(!testFile.exists()) {
return testFile.exists();
}
else {
return testFile.isFile();
}
}

/**
* Converts a dependency into a file path.
*/
private String pathResolver(Dependency deps) {
String repositoryPath="";
repositoryPath+=localRepo+File.separator;
repositoryPath+=resolveGroup(deps.getGroupId())+File.separator;
repositoryPath+=deps.getArtifactId()+File.separator;
repositoryPath+=deps.getVersion()+File.separator;
repositoryPath+=deps.getArtifactId()+"-"+deps.getVersion()+".";
repositoryPath+=deps.getType();

return repositoryPath;
}

private String resolveGroup(String groupId) {
String correctedGroupId="";
System.out.println(groupId);
if(groupId.indexOf(".")<0) {
correctedGroupId=groupId;
}
else {
//replaceAll() uses regexp to replace macthes,
//so we need to use a double escape to avoid
//interpration of "." as match all: "."-->"\\."
correctedGroupId=groupId.replaceAll("\\.",File.separator);
}
return correctedGroupId;
}

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

/**
* Repository path parameter setting.
* @parameter expression="${settings.localRepository}"
*/
private String localRepo;

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

/**
* Second sample parameter setting.
* @parameter expression="/tmp/"
*/
private String destinationDir;
}

* You can test this plugin as usual by running the command

[shell-prompt> mvn clean compile test.plugin:PortletAppPlugin:1.0-SNAPSHOT:portlet-war-deploy

in the plugin's project directory.

* But you really want to use this in another project, as discussed above. We
change the pom.xml of our project descriptor above to look like this:

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>xportlets.jobsubmit</groupId>
<artifactId>jobsubmit-portlet</artifactId>
<packaging>war</packaging>
<version>2.0</version>
<name>Maven Webapp Archetype</name>
<url>http://maven.apache.org</url>
<build>
<finalName>jobsubmit-portlet</finalName>
<plugins>
<plugin>
<artifactId>PortletAppPlugin</artifactId>
<groupId>test.plugin</groupId>
<executions>
<execution>
<phase>process-resources</phase>
<configuration>
<destinationDir>
/tmp/
</destinationDir>
</configuration>
<goals>
<goal>portlet-war-deploy</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

* This is used for copying all of the dependencies (the junit jar file in this
case) to the value of the parameter <destinationDir>. The plugin comes with
several other properties (localRepo, theDepends), but the default values are
almost always acceptable.

No comments: