Wednesday, June 03, 2009

Executing a Maven plugin from a Maven plugin

I've searched for a way to call a Maven plugin from inside another plugin but couldn't find any. So I've put on my hacker hat and made it happen. It might not be the best way to do it or even orthodox but here goes.

For example, Terracotta has a Maven plugin, namely "tc" and I want to add a 'help' goal to it which works exactly like the help plugin

With the help plugin, you can do this:
mvn help:describe org.terracotta.maven.plugins:tc-maven-plugin -Ddetail


But it's a little verbose. I want to do
 
mvn tc:help


which would accomplish the same thing. To be able to do that, I have my "tc" plugin depended on "help" by declaring it as a dependency in the pom.

    <dependency>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-help-plugin</artifactId>
     <version>2.1</version>
    </dependency> 


Then I looked in the source of the "help" plugin, specifically DescribedMojo.java which handles to goal "help:describe" as shown above. So basically, if I can construct a DescribeMojo instance and fill in the needed info, you'll be able to execute it just like your own mojo. The trick is, DescribeMojo has all of it fields "private" and no setters.
So I hacked the hell out of it and dug into its privates by using reflection. This is a no-no in so many books but I feel I'm working on a known version (2.1) of the "help" plugin so the chance of its API change is slim.

/**
 * Print help for all known goals
 * 
 * @author hhuynh
 * 
 * @goal help
 */
public class HelpMojo extends AbstractMojo {
  .....

  /**
   * @parameter expression="${project.remoteArtifactRepositories}"
   * @required
   * @readonly
   */
  private List remoteRepositories;

  /**
   * The goal you want to see help. By default help prints for all goals
   * 
   * @parameter expression="${goal}"
   */
  private String goal;

  public void execute() throws MojoExecutionException, MojoFailureException {
    DescribeMojo describeMojo = new DescribeMojo();
    setValue(describeMojo, "artifactFactory", artifactFactory);
    setValue(describeMojo, "pluginManager", pluginManager);
    setValue(describeMojo, "projectBuilder", projectBuilder);
    setValue(describeMojo, "project", project);
    setValue(describeMojo, "settings", settings);
    setValue(describeMojo, "session", session);
    setValue(describeMojo, "localRepository", localRepository);
    setValue(describeMojo, "remoteRepositories", remoteRepositories);

    setValue(describeMojo, "plugin", "org.terracotta.maven.plugins:tc-maven-plugin");
    setValue(describeMojo, "detail", true);
    setValue(describeMojo, "goal", goal);
    
    describeMojo.execute();
  }

  private void setValue(Object o, String field, Object value) throws MojoFailureException {
    Class c = o.getClass();
    Field _field;
    try {
      _field = c.getDeclaredField(field);
      _field.setAccessible(true);
      _field.set(o, value);
    } catch (Exception e) {
      throw new MojoFailureException(e.getMessage());
    }
  }
}


The 3 important fields I had to fill out are:

  1. plugin: full name of the plugin you want to print help

  2. detail: I want full description by default

  3. goal: If specified, it only prints help for that goal


There are other component fields that Maven will normally inject them into your own mojo automatically. For example, the field "remoteRepositories" is needed by DescribeMojo and since we construct it by hand, we have to inject the value of this field ourselves. But you can get the value of it for free by Maven so all I need to do is just declaring it and passing it along.

Voila, you got a fully functional DescribeMojo instance and all that left is to call "execute()" on it.

So that's how the hack is done.
Post a Comment