Life gets in the way... but we're back with our final installment! So where to start, let's start with a publisher for freestyle builds, then we'll add a publisher for Maven 2 builds... These will both require some reports to display results, and then finally we'll need the plugin entry point. But before we get into all that, perhaps I should briefly explain structured form submission support
DataBoundConstructors
Hudson uses Stapler as it's web framework. One of the things that Stapler provides is support for constructing objects from a JSON data model. Basically, if you have a class with a public constructor annotated with @DataBoundConstructor
, Stapler will bind fields from a JSON object by matching the field name to the constructor parameter name. If a parameter also has a @DataBoundConstructor
, then Stapler will recurse to construct this child object from the child JSON object.
Note: The only hole in this (at the moment) is if you want to inject a variable class, i.e. it does not support the case where there are three ChildImpl
classes all implementing Child
, and all with @DataBoundConstructor
and Parent
's constructor has a parameter which takes Child
... However, plans are afoot to fix this!
JavaNCSSPublisher
Publishers in Hudson must have a Descriptor
, this will be registered with Hudson and allows Hudson to create Publisher instances which have the details for the project they are publishing. Descriptors are normally implemented as an inner class called DescriptorImpl
and there is normally a static field of the publisher DESCRIPTOR
that holds the Descriptor
singleton. 99.995% of the time, you will want your publisher to have a @DataBoundConstructor
, so without further delay, here is the publisher:
package hudson.plugins.javancss;
import hudson.maven.MavenModule;
import hudson.maven.MavenModuleSet;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Descriptor;
import hudson.plugins.helpers.AbstractPublisherImpl;
import hudson.plugins.helpers.Ghostwriter;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Publisher;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
public class JavaNCSSPublisher extends AbstractPublisherImpl {
private String reportFilenamePattern;
@DataBoundConstructor
public JavaNCSSPublisher(String reportFilenamePattern) {
reportFilenamePattern.getClass();
this.reportFilenamePattern = reportFilenamePattern;
}
public String getReportFilenamePattern() {
return reportFilenamePattern;
}
public boolean needsToRunAfterFinalized() {
return false;
}
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
public Descriptor<Publisher> getDescriptor() {
return DESCRIPTOR;
}
public Action getProjectAction(AbstractProject<?, ?> project) {
return new JavaNCSSProjectIndividualReport(project);
}
protected Ghostwriter newGhostwriter() {
return new JavaNCSSGhostwriter(reportFilenamePattern);
}
public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
private DescriptorImpl() {
super(JavaNCSSPublisher.class);
}
public String getDisplayName() {
return "Publish " + PluginImpl.DISPLAY_NAME;
}
public boolean isApplicable(Class<? extends AbstractProject> aClass) {
return !MavenModuleSet.class.isAssignableFrom(aClass)
&& !MavenModule.class.isAssignableFrom(aClass);
}
}
}
By inheriting from AbstractPublisherImpl
we get a lot of the work done for us, all we really need to do is provide a Ghostwriter
and the project level report (JavaNCSSProjectIndividualReport
which we will see later
We need hudson/plugins/javancss/JavaNCSSPublisher/config.jelly
to allow the user to specify the report file name pattern... not much to this, so here it is:
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:bh="/lib/health">
<f:entry title="JavaNCSS xml report pattern"
description="
This is a file name pattern that can be used to locate the JavaNCSS xml report files
(for example with Maven2 use <b>**/target/javancss-raw-report.xml</b>).
The path is relative to <a href='ws/'>the module root</a> unless
you are using Subversion as SCM and have configured multiple modules, in which case it is
relative to the workspace root.<br/>
JavaNCSS must be configured to generate XML reports for this plugin to function.
">
<f:textbox name="javancss.reportFilenamePattern" value="${instance.reportFilenamePattern}"/>
</f:entry>
</j:jelly>
JavaNCSSMavenPublisher
This is fairly similar to the freestyle publisher, except that we do not need the user to configure everything for us, as we can grab some of the stuff from the pom.xml
.
We could, if necessary, tweak the pom.xml
to ensure that the report we are looking for is generated... an example of this is the cobertura maven plugin which does not generate an XML report by default. A Hudson plugin can modify the cobertura plugin's configuration before it executes in order to ensure that the xml report is generated. Note: some people regard this kind of thing as evil, as the pom.xml is no longer behaving the same as when run from the command line.
Ok, so here is the Maven publisher...
package hudson.plugins.javancss;
import hudson.maven.*;
import hudson.model.Action;
import hudson.plugins.helpers.AbstractMavenReporterImpl;
import hudson.plugins.helpers.Ghostwriter;
import net.sf.json.JSONObject;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
public class JavaNCSSMavenPublisher extends AbstractMavenReporterImpl {
@DataBoundConstructor
public JavaNCSSMavenPublisher() {
}
private static final String PLUGIN_GROUP_ID = "org.codehaus.mojo";
private static final String PLUGIN_ARTIFACT_ID = "javancss-maven-plugin";
private static final String PLUGIN_EXECUTE_GOAL = "report";
protected boolean isExecutingMojo(MojoInfo mojo) {
return mojo.pluginName.matches(PLUGIN_GROUP_ID, PLUGIN_ARTIFACT_ID)
&& PLUGIN_EXECUTE_GOAL.equals(mojo.getGoal());
}
protected Ghostwriter newGhostwriter(MavenProject pom, MojoInfo mojo) {
// get the name of the xml report
String tempFileName;
try {
tempFileName = mojo.getConfigurationValue("tempFileName", String.class);
} catch (ComponentConfigurationException e) {
tempFileName = null;
}
if (tempFileName == null) {
// the name was not overridden in the pom, so use the default
tempFileName = "javancss-raw-report.xml";
}
// get the xml output directory
File baseDir = pom.getBasedir().getAbsoluteFile();
File xmlOutputDirectory;
try {
xmlOutputDirectory = mojo.getConfigurationValue("xmlOutputDirector", File.class);
} catch (ComponentConfigurationException e) {
xmlOutputDirectory = null;
}
if (xmlOutputDirectory == null) {
// the directory was not overridden in the pom, so use the default
xmlOutputDirectory = new File(pom.getBuild().getDirectory());
}
String searchPath;
String targetPath = makeDirEndWithFileSeparator(fixFilePathSeparator(xmlOutputDirectory.getAbsolutePath()));
String baseDirPath = makeDirEndWithFileSeparator(fixFilePathSeparator(baseDir.getAbsolutePath()));
if (targetPath.startsWith(baseDirPath)) {
searchPath = targetPath.substring(baseDirPath.length()) + tempFileName;
} else {
// we have no clue where this is, so default to anywhere
searchPath = "**/" + tempFileName;
}
return new JavaNCSSGhostwriter(searchPath, targets);
}
private String makeDirEndWithFileSeparator(String baseDirPath) {
if (!baseDirPath.endsWith(File.separator)) {
baseDirPath += File.separator;
}
return baseDirPath;
}
private String fixFilePathSeparator(String path) {
return path.replace(File.separatorChar == '/' ? '\\' : '/', File.separatorChar);
}
public Action getProjectAction(MavenModule module) {
for (MavenBuild build : module.getBuilds()) {
if (build.getAction(JavaNCSSBuildIndividualReport.class) != null) {
return new JavaNCSSProjectIndividualReport(module);
}
}
return null;
}
public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
/**
* {@inheritDoc}
*/
public MavenReporterDescriptor getDescriptor() {
return DESCRIPTOR; //To change body of implemented methods use File | Settings | File Templates.
}
public static final class DescriptorImpl extends MavenReporterDescriptor {
/**
* Do not instantiate DescriptorImpl.
*/
private DescriptorImpl() {
super(JavaNCSSMavenPublisher.class);
}
/**
* {@inheritDoc}
*/
public String getDisplayName() {
return "Publish " + PluginImpl.DISPLAY_NAME;
}
public MavenReporter newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return req.bindJSON(JavaNCSSMavenPublisher.class, formData);
}
}
}
The only complexity is in the newGhostwriter
method, where we have to find out what the configuration for the maven plugin is in order to find the xml report.
We will need a hudson/plugins/javancss/JavaNCSSMavenPublisher/config.jelly
file for this publisher... not much to this one as we get what we need from the pom.xml
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:bh="/lib/health">
</j:jelly>
The reports
We have a total of four reports to generate:- Individual build report - used for freestyle and maven 2 modules
- Individual project report - used for freestyle and maven 2 modules
- Aggregated build report - used for maven 2 projects
- Aggregated project report - used for maven 2 projects
To keep to our DRY principals, we'll use some abstract classes to pull together the common code. First, AbstractBuildReport
which will form the basis of our build reports:
package hudson.plugins.javancss;
import hudson.model.AbstractBuild;
import hudson.plugins.helpers.AbstractBuildAction;
import hudson.plugins.javancss.parser.Statistic;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
import java.util.Collection;
public abstract class AbstractBuildReport<T extends AbstractBuild<?, ?>> extends AbstractBuildAction<T> {
private final Collection<Statistic> results;
private final Statistic totals;
public AbstractBuildReport(Collection<Statistic> results) {
this.results = results;
this.totals = Statistic.total(results);
}
public Collection<Statistic> getResults() {
return results;
}
public Statistic getTotals() {
return totals;
}
public String getSummary() {
AbstractBuild<?, ?> prevBuild = getBuild().getPreviousBuild();
while (prevBuild != null && prevBuild.getAction(getClass()) == null) {
prevBuild = prevBuild.getPreviousBuild();
}
if (prevBuild == null) {
return totals.toSummary();
} else {
AbstractBuildReport action = prevBuild.getAction(getClass());
return totals.toSummary(action.getTotals());
}
}
public String getIconFileName() {
return PluginImpl.ICON_FILE_NAME;
}
public String getDisplayName() {
return PluginImpl.DISPLAY_NAME;
}
public String getUrlName() {
return PluginImpl.URL;
}
public boolean isGraphActive() {
return false;
}
}
Similarly, we have AbstractProjectReport
which will be used for project reports:
package hudson.plugins.javancss;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.ProminentProjectAction;
import hudson.plugins.helpers.AbstractProjectAction;
import hudson.plugins.javancss.parser.Statistic;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
public abstract class AbstractProjectReport<T extends AbstractProject<?, ?>>
extends AbstractProjectAction<T>
implements ProminentProjectAction {
public AbstractProjectReport(T project) {
super(project);
}
public String getIconFileName() {
for (AbstractBuild<?, ?> build = getProject().getLastBuild();
build != null; build = build.getPreviousBuild()) {
final AbstractBuildReport action = build.getAction(getBuildActionClass());
if (action != null) {
return PluginImpl.ICON_FILE_NAME;
}
}
return null;
}
public String getDisplayName() {
for (AbstractBuild<?, ?> build = getProject().getLastBuild();
build != null; build = build.getPreviousBuild()) {
final AbstractBuildReport action = build.getAction(getBuildActionClass());
if (action != null) {
return PluginImpl.DISPLAY_NAME;
}
}
return null;
}
public String getUrlName() {
for (AbstractBuild<?, ?> build = getProject().getLastBuild(); build != null; build = build.getPreviousBuild()) {
final AbstractBuildReport action = build.getAction(getBuildActionClass());
if (action != null) {
return PluginImpl.URL;
}
}
return null;
}
public String getSearchUrl() {
return PluginImpl.URL;
}
public boolean isGraphActive() {
return false;
}
public Collection<Statistic> getResults() {
for (AbstractBuild<?, ?> build = getProject().getLastBuild();
build != null; build = build.getPreviousBuild()) {
final AbstractBuildReport action = build.getAction(getBuildActionClass());
if (action != null) {
return action.getResults();
}
}
return Collections.emptySet();
}
public Statistic getTotals() {
for (AbstractBuild<?, ?> build = getProject().getLastBuild();
build != null; build = build.getPreviousBuild()) {
final AbstractBuildReport action = build.getAction(getBuildActionClass());
if (action != null) {
return action.getTotals();
}
}
return null;
}
protected abstract Class<? extends AbstractBuildReport> getBuildActionClass();
}
Now that we have these abstract classes, we can roll out our concrete reports. First the individual build report:
package hudson.plugins.javancss;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import hudson.maven.AggregatableAction;
import hudson.maven.MavenAggregatedReport;
import hudson.maven.MavenBuild;
import hudson.maven.MavenModule;
import hudson.maven.MavenModuleSetBuild;
import hudson.model.AbstractBuild;
import hudson.plugins.javancss.parser.Statistic;
public class JavaNCSSBuildIndividualReport extends AbstractBuildReport<AbstractBuild<?, ?>>
implements AggregatableAction {
public JavaNCSSBuildIndividualReport(Collection<Statistic> results) {
super(results);
}
@Override
public synchronized void setBuild(AbstractBuild<?, ?> build) {
super.setBuild(build);
if (this.getBuild() != null) {
for (Statistic r : getResults()) {
r.setOwner(this.getBuild());
}
}
}
public MavenAggregatedReport createAggregatedAction(MavenModuleSetBuild build,
Map<MavenModule,
List<MavenBuild>> moduleBuilds) {
return new JavaNCSSBuildAggregatedReport(build, moduleBuilds);
}
}
That was fairly painless... Note that we interfaces for both the freestyle and maven2 project types, this is OK as the freestyle projects will ignore the Maven2 stuff and vice-versa while the common code is shared by both. Next we need the aggregated build report:
package hudson.plugins.javancss;
import hudson.maven.*;
import hudson.model.Action;
import hudson.plugins.javancss.parser.Statistic;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class JavaNCSSBuildAggregatedReport
extends AbstractBuildReport<MavenModuleSetBuild>
implements MavenAggregatedReport {
public JavaNCSSBuildAggregatedReport(MavenModuleSetBuild build,
Map<MavenModule, List<MavenBuild>> moduleBuilds) {
super(new ArrayList<Statistic>());
setBuild(build);
}
public synchronized void update(Map<MavenModule, List<MavenBuild>> moduleBuilds,
MavenBuild newBuild) {
JavaNCSSBuildIndividualReport report =
newBuild.getAction(JavaNCSSBuildIndividualReport.class);
if (report != null) {
Collection<Statistic> u = Statistic.merge(report.getResults(), getResults());
getResults().clear();
getResults().addAll(u);
getTotals().add(report.getTotals());
}
}
public Class<? extends AggregatableAction> getIndividualActionType() {
return JavaNCSSBuildIndividualReport.class;
}
public Action getProjectAction(MavenModuleSet moduleSet) {
for (MavenModuleSetBuild build : moduleSet.getBuilds()) {
if (build.getAction(JavaNCSSBuildAggregatedReport.class) != null) {
return new JavaNCSSProjectAggregatedReport(moduleSet);
}
}
return null;
}
}
This report is only used for the Maven2 project types. The two key methods are update
which is called as each module completes, and getProjectAction
which should return the project level aggregated report if there is a report to show. At this point we're ready for the individual project report:
package hudson.plugins.javancss;
import hudson.model.AbstractProject;
import hudson.model.Actionable;
import hudson.model.ProminentProjectAction;
import hudson.model.AbstractBuild;
import hudson.util.ChartUtil;
import hudson.util.DataSetBuilder;
import hudson.plugins.javancss.parser.Statistic;
import java.util.Collection;
public class JavaNCSSProjectIndividualReport
extends AbstractProjectReport<AbstractProject<?, ?>>
implements ProminentProjectAction {
public JavaNCSSProjectIndividualReport(AbstractProject<?, ?> project) {
super(project);
}
protected Class<? extends AbstractBuildReport> getBuildActionClass() {
return JavaNCSSBuildIndividualReport.class;
}
}
Don't repeat ourselves comes in handy here as essentially all the work has been done for us!. The project aggregated report:
package hudson.plugins.javancss;
import hudson.model.Actionable;
import hudson.model.ProminentProjectAction;
import hudson.model.AbstractBuild;
import hudson.model.Action;
import hudson.maven.MavenModuleSet;
import hudson.maven.MavenModuleSetBuild;
import hudson.plugins.javancss.parser.Statistic;
public class JavaNCSSProjectAggregatedReport
extends AbstractProjectReport<MavenModuleSet>
implements ProminentProjectAction {
public JavaNCSSProjectAggregatedReport(MavenModuleSet project) {
super(project);
}
protected Class<? extends AbstractBuildReport> getBuildActionClass() {
return JavaNCSSBuildAggregatedReport.class;
}
}
Again DRY to the rescue... At this point all that remains is to present the reports from these backing objects... so on with the jelly views. The helper classes and our inheritance makes this easy... all we need is two jelly files: hudson/plugins/javancss/AbstractBuildReport/reportDetail.jelly
and hudson/plugins/javancss/AbstractProjectReport/reportDetail.jelly
. Here they are:
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<h1>Results</h1>
<table border="1px" class="pane sortable">
<thead>
<tr>
<th>Package</th>
<th title="Class count">Classes</th>
<th title="Function count">Functions</th>
<th title="Javadoc count">Javadocs</th>
<th title="Non-commenting Source Statements">NCSS</th>
<th title="Javadoc line count">JLC</th>
<th title="Single-line comment line count">SLCLC</th>
<th title="Multi-line comment line count">MLCLC</th>
</tr>
</thead>
<tfoot>
<tr>
<th align="left">Totals</th>
<th align="right">${it.totals.classes}</th>
<th align="right">${it.totals.functions}</th>
<th align="right">${it.totals.javadocs}</th>
<th align="right">${it.totals.ncss}</th>
<th align="right">${it.totals.javadocLines}</th>
<th align="right">${it.totals.singleCommentLines}</th>
<th align="right">${it.totals.multiCommentLines}</th>
</tr>
</tfoot>
<tbody>
<j:forEach var="r" items="${it.results}">
<tr>
<th align="left">${r.name}</th>
<td align="right">${r.classes}</td>
<td align="right">${r.functions}</td>
<td align="right">${r.javadocs}</td>
<td align="right">${r.ncss}</td>
<td align="right">${r.javadocLines}</td>
<td align="right">${r.singleCommentLines}</td>
<td align="right">${r.multiCommentLines}</td>
</tr>
</j:forEach>
</tbody>
</table>
</j:jelly>
Yep, the two files are identical! Other plugins may not be quite so lucky... but in general the project level report should be the same as the report for the latest build
Making a plugin
Now we are ready to make our plugin.... for this we need a class that extends hudson.Plugin
and registers our publisher's descriptors with the appropriate lists... here it is:
package hudson.plugins.javancss;
import hudson.Plugin;
import hudson.maven.MavenReporters;
import hudson.tasks.BuildStep;
public class PluginImpl extends Plugin {
public void start() throws Exception {
BuildStep.PUBLISHERS.add(JavaNCSSPublisher.DESCRIPTOR);
MavenReporters.LIST.add(JavaNCSSMavenPublisher.DESCRIPTOR);
}
public static String DISPLAY_NAME = "Java NCSS Report";
public static String GRAPH_NAME = "Java NCSS Trend";
public static String URL = "javancss";
public static String ICON_FILE_NAME = "graph.gif";
}
And that's pretty much it... we should have a working plugin
Finishing touches
OK, so the plugin does not have health reports (i.e. the weather icons) and it does not show a trend graph... I think I'm going to need a part 8 :-(
View comments