Wednesday, February 20, 2008

Writing a Hudson plug-in (Part 5 – Reporting)

Until now we have been generating classes that collect the results we want to display. We have not hooked into Hudson's methods of displaying reports. There are essentially four places that we could want to generate reports:

  • In each individual build (we'll call this a Single Build Report),
  • In the project (we'll call this a Single Project Report),
  • In the summary of all the Maven 2 module builds associated with a multi-module Maven 2 build (we'll call this an Aggregate Build Report), and finally
  • In the Maven 2 project itself (we'll call this an Aggregate Project Report).

To make matters more confusing, each of these reports usually to implement different classes and need to implement different interfaces:

  • Project Reports usually inherit from Actionable
  • Build Reports usually inherit from HealthReportingAction
  • Single Build Reports in Maven 2 projects need to implement AggregatableAction

What we need is to put some common framework around these reports to ensure that we are not repeating ourselves all the time. In general, each of these four reports will be doing mostly the same things.

  • The Build Reports will be displaying the results for a specific build
  • The Project Reports will be displaying the results for the latest build (and possibly a trend chart if that makes sense)
  • The Aggregate Reports will be displaying the aggregate of all the Single Reports

Sound's like a job for multiple inheritance! Fortune would have it that Java does not support multiple inheritance, so we will have to use some form of composition to get the same effect, and generics can help reduce the repetition too. But enough! On to the solution.

AbstractBuildAction

We'll start with a parent class for all our Build Reports. We'll make this class generic, parameterised by the Build class that it operates on, so that we can use the same core code for all the Build Reports:

package hudson.plugins.helpers;

import hudson.model.AbstractBuild;
import hudson.model.HealthReportingAction;

public abstract class AbstractBuildAction<BUILD extends AbstractBuild<?, ?>>
implements HealthReportingAction {

private BUILD build = null;

protected AbstractBuildAction() {
}

public synchronized BUILD getBuild() {
return build;
}

public synchronized void setBuild(BUILD build) {
if (this.build == null && this.build != build) {
this.build = build;
}
}
}

We store a reference to the build in a local variable and provide a getter for the build. Ideally we would like to the build variable to be final and initialize it in the constructor. However, Hudson's remoting interface will get in the way of this for any actions created on the slave. The solution is to use a setter to set the build. Additionally, we have some logic that makes this setter write-once. This is just a safety net to stop us from accidentally confusing Hudson. Strictly speaking, if you are careful the setter can be justa a simple setter and not write once.

[Correction] We also need to modify BuildProxy so that it calls our setter for us for actions added from slave side executions:

package hudson.plugsin.helpers;

...

public final class BuildProxy implements Serializable {

...

private final List<AbstractBuildAction<AbstractBuild<?,?>>> actions =
new ArrayList<AbstractBuildAction<?,?>>>();

...

public void updateBuild(AbstractBuild<,?> build) {
for (AbstractBuildAction<AbstractBuild<?,?>> action: actions) {
if (!build.getActions().contains(action)) {
action.setBuild(build);
build.getActions().add(action);
}
}


if (result != null && result.isWorseThan(build.getResult())) {
build.setResult(result);
}
}

...

public List<AbstractBuildAction<AbstractBuild<?,?>>> getActions() {
return actions;
}

...

}

[/Correction]

In addition to these simple getters and setters, we want to provide a framework for displaying the report detail. Each different type of report has different ways of fitting into the Hudson UI. We are going to attempt to standardise these by using wrapper .jelly files to call a standard set which implementing classes can override. Jelly files are stored as resources in the hudson plugin, so with the Maven2 project structure for a Hudson plugin, the java source is /src/main/java/hudson/plugins/helpers/AbstractBuildAction.java and the jelly files for this java class are in /src/main/resources/hudson/plugins/helpers/AbstractBuildAction/. The basic things that all build reports want to do are as follows:

  • A main report detail page (e.g. package level summary of the number of lines of code)
  • A graph of some sort, with the option to enlarge it. (e.g. trend graph of the number of lines of code)
  • A simple summary of the report for the main page (i.e. "17,345 lines of code (+1,534)")

We will implement this functionality with some properties of the AbstractBuildAction, and some default .jelly files. First the additional properties:

...

public abstract class AbstractBuildAction<BUILD extends AbstractBuild<?, ?>> implements HealthReportingAction {

...

public boolean isFloatingBoxActive() {
return false;
}

public boolean isGraphActive() {
return false;
}

public String getGraphName() {
return getDisplayName();
}

public String getSummary() {
return "";
}
}

These four properties will be used to control how the action appears. isFloatingBoxActive allows us to enable the floating box on the build summary page. isGraphActive allows us to activate the graph inside the floating box. getGraphName should return the title that will be displayed above the graph. And finally, getSummary will control the summary text to display beside the build report icon on the build summary page.

Next we have some empty default place-holder jelly files. These will be the jelly files that we can override in our actual reports:

reportDetail.jelly

<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">
</j:jelly>

normalGraph.jelly

<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">
</j:jelly>

largeGraph.jelly

<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">
</j:jelly>

The reportDetail.jelly page will be used for the main report detail page, the normalGraph.jelly page will be used for the floating trend graph, while the largeGraph.jelly page will be used for the enlarged graph. To hook these pages into the Hudson framework, we need the following jelly pages:

index.jelly

<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">
<l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css">
<st:include it="${it.build}" page="sidepanel.jelly"/>
<l:main-panel>
<h1>${it.displayName}</h1>
<j:if test="${it.graphActive}">
<h2>${it.graphName}</h2>
<st:include page="normalGraph.jelly"/>
</j:if>
<st:include page="reportDetail.jelly"/>
</l:main-panel>
</l:layout>
</j:jelly>

This page will also include the trend graph if it is active

floatingBox.jelly

<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">
<j:if test="${it.graphActive}">
<div style="width:500px;">
<div class="test-trend-caption">
${from.graphName}
</div>

<st:include page="normalGraph.jelly"/>

<div style="text-align:right">
<a href="enlargedGraph">enlarge</a>
</div>
</div>
</j:if>
</j:jelly>

summary.jelly

<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:i="jelly:fmt">
<t:summary icon="${it.iconFileName}">
<a href="${it.urlName}">${it.displayName}</a>
${it.summary}
</t:summary>
<j:if test="${it.floatingBoxVisible}}">
<div style="float:right">
<st:include page="floatingBox.jelly"/>
</div>
</j:if>
</j:jelly>

Note that the build main page does not support floating boxes, so we have to hack it in via the summary.jelly page

enlargedGraph.jelly

<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">
<l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css">
<st:include it="${it.build}" page="sidepanel.jelly"/>
<l:main-panel>
<j:if test="${it.graphActive}">
<h1>${it.graphName}</h1>
<st:include page="largeGraph.jelly"/>
</j:if>
</l:main-panel>
</l:layout>
</j:jelly>

AbstractProjectAction

This is similar to AbstractBuildAction, however, we don't have to deal with the write-once setter problem.

package hudson.plugins.helpers;

import hudson.model.AbstractProject;
import hudson.model.Actionable;

abstract public class AbstractProjectAction<PROJECT extends AbstractProject<?, ?>> extends Actionable {
private final PROJECT project;

protected AbstractProjectAction(PROJECT project) {
this.project = project;
}

public PROJECT getProject() {
return project;
}

public boolean isFloatingBoxActive() {
return true;
}

public boolean isGraphActive() {
return false;
}

public String getGraphName() {
return getDisplayName();
}

}

Again we have a set of jelly files, we'll repeat the same placeholders:

reportDetail.jelly

<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">
</j:jelly>

normalGraph.jelly

<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">
</j:jelly>

largeGraph.jelly

<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">
</j:jelly>

The other link in pages are similar, only changing from a reference for ${it.build} to ${it.project}

index.jelly

<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">
<l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css">
<st:include it="${it.project}" page="sidepanel.jelly"/>
<l:main-panel>
<h1>${it.displayName}</h1>
<j:if test="${it.graphActive}">
<h2>${it.graphName}</h2>
<st:include page="normalGraph.jelly"/>
</j:if>
<st:include page="reportDetail.jelly"/>
</l:main-panel>
</l:layout>
</j:jelly>

floatingBox.jelly

<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:i="jelly:fmt" xmlns:local="local">
<j:if test="${it.graphActive}">
<div style="width:500px;">
<div class="test-trend-caption">
${from.graphName}
</div>

<st:include page="normalGraph.jelly"/>

<div style="text-align:right">
<a href="enlargedGraph">enlarge</a>
</div>
</div>
</j:if>
</j:jelly>

The project page automatically includes the floating box, so we don;t require the summary hack

enlargedGraph.jelly

<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">
<l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css">
<st:include it="${it.project}" page="sidepanel.jelly"/>
<l:main-panel>
<j:if test="${it.graphActive}">
<h1>${it.graphName}</h1>
<st:include page="largeGraph.jelly"/>
</j:if>
</l:main-panel>
</l:layout>
</j:jelly>

Summary

That's it for part 5, next time we will implement the javancss parser and get the results for a build. Part 7 will finish off the plugin with the reports based on these two base classes.

7 comments:

Roberto Franchini said...

The link to hudson is broken, there's a comma after hudson instead of a dot.

Stephen Connolly said...

Added correction to BuildProxy

PepeMax said...

oh god, this is sooo confusing...
looks like this stuff should be in a library, not in a tutorial, Don't think that a plugin developer should care (at least not much) about master/slave, most of this classes should be already in huds so we can use it when needed

Omar Tahboub said...

Is there a Git hub repo so that I can download the code?

Elizabeth Suku said...

How do you make sure that the right jelly file gets used? I am trying to write a plugin for Jenkins, with 3 functionalities - 1. To set a system configuration value. 2. To set a Post build step value and 3. To set a post build action value.

The first two functions are working, for the post build action i am unable to get the field values to show up based on the jelly file.
The first two functions are implemented with one java file and jelly filewhile the third one is implemented using another java and another jelly file.

Any help is appreciated.

Stephen Connolly said...

@Omar: nope... this tutorial is quite old by now... while still somewhat valid the exact details probably need tweaking... but time always escapes me!

Stephen Connolly said...

@Elizabeth: the .jelly file names correspond to the URLs that they get bound to (or to the st:include tags that pull them in)