Monday, February 11, 2008

Writing a Hudson plug-in (Part 4 – Abstract Publishers and MavenReporters)

This is the fourth installment, which covers the two abstract classes that call the Ghostwriter for us. When we implement our plugin we will extend from these two classes. These classes perform the work of calling the Ghostwriter at the appropriate points in the build, the concrete classes that we will develop in Part 5, however have the responsibility of creating a configured Ghostwriter and providing it to the superclass for execution.

AbstractPublisherImpl

In a way, this is the simplest of the two classes:

package hudson.plugins.helpers;

import hudson.tasks.Publisher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.Launcher;

import java.io.IOException;

public abstract class AbstractPublisherImpl extends Publisher {

protected abstract Ghostwriter newGhostwriter();

public boolean perform(AbstractBuild build,
Launcher launcher,
final BuildListener listener)
throws InterruptedException, IOException {
return BuildProxy.doPerform(newGhostwriter(), build, listener);
}

public boolean prebuild(AbstractBuild build,
BuildListener listener) {
return true;
}
}

Fairly simple, we just ask the BuildProxy to invoke the new Ghostwriter created by the concrete class.

AbstractMavenReporterImpl

This class needs to do a bit more work. First I'll present the abstract methods and protected methods that can be overrided by implementing classes:

package hudson.plugins.helpers;

import hudson.maven.MavenReporter;
import hudson.maven.MojoInfo;
import hudson.maven.MavenBuildProxy;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.plugins.helpers.BuildProxy;
import hudson.plugins.javancss.PluginImpl;
import org.apache.maven.project.MavenProject;

import java.io.IOException;

public abstract class AbstractMavenReporterImpl extends MavenReporter {

protected abstract boolean isExecutingMojo(MojoInfo mojo);

protected abstract Ghostwriter newGhostwriter(MavenProject pom,
MojoInfo mojo);

protected boolean autoconfPom(MavenBuildProxy build,
MavenProject pom,
MojoInfo mojo,
BuildListener listener) {
return true;
}

protected boolean isAutoconfMojo(MojoInfo mojo) {
return false;
}

}

Maven project execution consists of invocations of various golas of the different mojos that are bound to the Maven lifecycle. Hudson inserts hooks that allow us to intercept mojos before they execute and after they have executed. Intercepting prior to execution can be useful to ensure that the plugin has been configured with the options we require (e.g. XML output enabled). Post execution is usually when we want to invoke our Ghostwriter. If the implementing class wants to be able to tweak the mojo configuration prior to execution it needs to override isAutoconfMojo in order to identify the mojo executions that need to be tweaked, and override autoconfPom to do the actual configuration.

The implementing class needs to provide:

  • a isExecutingMojo method to identify execution of the mojo that this publisher reports on.
  • a newGhostwriter method that constructs the Ghostwriter which will do the work for us. The new Ghostwriter can either be configured manually on the build page, and/or can be configured based on information in the pom.

The final thing that we need to decide is when to execute the publisher:

package hudson.plugins.helpers;

...

public abstract class AbstractMavenReporterImpl extends MavenReporter {

...

protected MojoExecutionReportingMode getExecutionMode() {
return MojoExecutionReportingMode.ONLY_REPORT_ON_SUCCESS;
}

...

protected enum MojoExecutionReportingMode {
ONLY_REPORT_ON_SUCCESS {
Boolean isOkToContinue(MavenReporter reporter,
MavenBuildProxy build,
BuildListener listener,
Throwable error) {
return error == null ? null : Boolean.TRUE;
}
},
ALWAYS_REPORT_STABLE {
Boolean isOkToContinue(MavenReporter reporter,
MavenBuildProxy build,
BuildListener listener,
Throwable error) {
return null;
}},
REPORT_UNSTABLE_ON_ERROR {
Boolean isOkToContinue(MavenReporter reporter,
MavenBuildProxy build,
BuildListener listener,
Throwable error) {
if (error != null) {
listener.getLogger().println("[HUDSON] "
+ reporter.getDescriptor().getDisplayName()
+ " setting build to UNSTABLE");
build.setResult(Result.UNSTABLE);
}
return null;
}
};

abstract Boolean isOkToContinue(MavenReporter reporter,
MavenBuildProxy build,
BuildListener listener,
Throwable error);
}
}

We define three different execution modes:

  • ONLY_REPORT_ON_SUCCESS - this ensures that we only run the publisher if the mojo executed without error.
  • ALWAYS_REPORT_STABLE - runs the publisher even if the mojo had an execution error, never marks the build as failed or unstable (note that Maven will most likely mark the build as failed or unstable)
  • REPORT_UNSTABLE_ON_ERROR - runs the publisher even if the mojo had an execution error. In the event of a mojo execution error the build will be marked UNSTABLE

All that's left is are the methods to tie these all together:

package hudson.plugins.helpers;

...

public abstract class AbstractMavenReporterImpl extends MavenReporter {

....

public boolean preExecute(MavenBuildProxy build,
MavenProject pom,
MojoInfo mojo,
BuildListener listener)
throws InterruptedException, IOException {
return !isAutoconfMojo(mojo)
|| autoconfPom(build, pom, mojo, listener);
}

public boolean postExecute(MavenBuildProxy build,
MavenProject pom,
MojoInfo mojo,
BuildListener listener,
Throwable error)
throws InterruptedException, IOException {
if (!isExecutingMojo(mojo)) {
// not a mojo who's result we are interested in
return true;
}

final Boolean okToContinue = getExecutionMode()
.isOkToContinue(this, build, listener, error);
if (okToContinue != null) {
return okToContinue;
}

build.registerAsProjectAction(this);

return BuildProxy.doPerform(newGhostwriter(pom, mojo),
build,
pom,
listener);
}

}

The preExecute method checks if this is an mojo execution that we want to tweak, executing the tweaks if necessary. The postExecute method checks if this the the mojo we want to report on. Then it checks if there were execution errors, following the execution mode reported by getExecutionMode. We register this MavenReporter as a project action (thus signalling Hudson that it should call the getProjectAction() method of our reporter. We will have this method return null if we actually don't want a project action. This is safer than trying to be smart and only calling register if we want a project action!). Finally we invoke the Ghostwriter!

Summary

Well that's it for Part 4. In the next installment, I will introduce a DRY set of classes for the reports that the publisher will generate

1 comment:

Stephen Connolly said...

Updated to add call to build.registerAsProjectAction(...) to AbstractMavenReporterImpl.postExecute