Stephen Connolly / Writing a Hudson plugin (Part 7 - Putting it all together)

Created Sun, 01 Jun 2008 00:00:00 +0000 Modified Sun, 01 Jun 2008 00:00:00 +0000
1141 Words

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 supportDataBoundConstructorsHudson 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!JavaNCSSPublisherPublishers 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 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 { private DescriptorImpl() { super(JavaNCSSPublisher.class); } public String getDisplayName() { return “Publish " + PluginImpl.DISPLAY_NAME; } public boolean isApplicable(Class> extends AbstractBuildAction { private final Collection results; private final Statistic totals; public AbstractBuildReport(Collection results) { this.results = results; this.totals = Statistic.total(results); } public Collection 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 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 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> implements AggregatableAction { public JavaNCSSBuildIndividualReport(Collection 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> 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 implements MavenAggregatedReport { public JavaNCSSBuildAggregatedReport(MavenModuleSetBuild build, Map<MavenModule, List> moduleBuilds) { super(new ArrayList()); setBuild(build); } public synchronized void update(Map<MavenModule, List> moduleBuilds, MavenBuild newBuild) { JavaNCSSBuildIndividualReport report = newBuild.getAction(JavaNCSSBuildIndividualReport.class); if (report != null) { Collection u = Statistic.merge(report.getResults(), getResults()); getResults().clear(); getResults().addAll(u); getTotals().add(report.getTotals()); } } public Class> 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 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"> Results Package Classes Functions Javadocs NCSS JLC SLCLC MLCLC Totals ${it.totals.classes} ${it.totals.functions} ${it.totals.javadocs} ${it.totals.ncss} ${it.totals.javadocLines} ${it.totals.singleCommentLines} ${it.totals.multiCommentLines} <j:forEach var=“r” items="${it.results}"> ${r.name} ${r.classes} ${r.functions} ${r.javadocs} ${r.ncss} ${r.javadocLines} ${r.singleCommentLines} ${r.multiCommentLines} </j:forEach> </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 buildMaking a pluginNow 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 pluginFinishing touchesOK, 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 :-(