Ok, life has got in the way and I have not been able to update this series as quickly as I originally intended. In any case, there were a number of bugs in Hudson that I discovered and are now fixed, and there was a good deal of tidy-up needed in order for me to figure out what to do for parts 6 and 7. The good news is that I am getting closer to finishing writing these final two parts. The bad news is that there are a number of corrections to the previous posts (I have put some corrections in-line for parts 4 and 5). This post aims to ensure that, for parts 6 and 7, everyone is able to follow from the same code!
pom.xml
This needs to be updated to reference a Hudson version of at least 1.200 in order to have the bugs I identified fixed.
src/main/java/hudson/plugins/helpers/AbstractBuildAction.java
AFAIK I escaped all the HTML entities and this file is the same as constructed from the previous posts.
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;
}
}
public boolean isFloatingBoxActive() {
return false;
}
public boolean isGraphActive() {
return false;
}
public String getGraphName() {
return getDisplayName();
}
public String getSummary() {
return "";
}
}
src/main/java/hudson/plugins/helpers/AbstractMavenReporterImpl.java
AFAIK I escaped all the HTML entities and this file is the same as constructed from the previous posts.
package hudson.plugins.helpers;
import hudson.maven.MavenBuildProxy;
import hudson.maven.MavenReporter;
import hudson.maven.MojoInfo;
import hudson.model.BuildListener;
import hudson.model.Result;
import org.apache.maven.project.MavenProject;
import java.io.IOException;
public abstract class AbstractMavenReporterImpl extends MavenReporter {
protected MojoExecutionReportingMode getExecutionMode() {
return MojoExecutionReportingMode.ONLY_REPORT_ON_SUCCESS;
}
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);
}
protected abstract boolean isExecutingMojo(MojoInfo mojo);
protected abstract Ghostwriter newGhostwriter(MavenProject pom, MojoInfo mojo);
public boolean preExecute(MavenBuildProxy build,
MavenProject pom,
MojoInfo mojo,
BuildListener listener)
throws InterruptedException, IOException {
return !isAutoconfMojo(mojo) || autoconfPom(build, pom, mojo, listener);
}
protected boolean autoconfPom(MavenBuildProxy build,
MavenProject pom,
MojoInfo mojo,
BuildListener listener) {
return true;
}
protected boolean isAutoconfMojo(MojoInfo mojo) {
return false;
}
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);
}
}
src/main/java/hudson/plugins/helpers/AbstractProjectAction.java
AFAIK I escaped all the HTML entities and this file is the same as constructed from the previous posts.
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();
}
}
src/main/java/hudson/plugins/helpers/AbstractPublisherImpl.java
Some of the HTML entities were not properly escaped.
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;
}
}
src/main/java/hudson/plugins/helpers/BuildProxy.java
Again, some of the HTML entities were not properly escaped. Additionally, this is the file that has had the most updates during this tutorial, so here it is in full:
package hudson.plugins.helpers;
import hudson.FilePath;
import hudson.maven.MavenBuildProxy;
import hudson.util.IOException2;
import hudson.model.Action;
import hudson.model.Result;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import java.util.Calendar;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.io.Serializable;
import org.apache.maven.project.MavenProject;
public final class BuildProxy implements Serializable {
private final FilePath artifactsDir;
private final FilePath projectRootDir;
private final FilePath buildRootDir;
private final FilePath executionRootDir;
private final Calendar timestamp;
private final List<AbstractBuildAction<AbstractBuild<?, ?>>> actions =
new ArrayList<AbstractBuildAction<AbstractBuild<?, ?>>>();
private Result result = null;
private boolean continueBuild = true;
public static boolean doPerform(Ghostwriter ghostwriter,
AbstractBuild<?, ?> build,
BuildListener listener)
throws IOException, InterruptedException {
// first, do we need to do anything on the slave
if (ghostwriter instanceof Ghostwriter.SlaveGhostwriter) {
// construct the BuildProxy instance that we will use
BuildProxy buildProxy = new BuildProxy(
new FilePath(build.getArtifactsDir()),
new FilePath(build.getProject().getRootDir()),
new FilePath(build.getRootDir()),
build.getProject().getModuleRoot(),
build.getTimestamp());
BuildProxyCallableHelper callableHelper = new BuildProxyCallableHelper(
buildProxy, ghostwriter, listener);
try {
buildProxy = buildProxy.getExecutionRootDir().act(callableHelper);
buildProxy.updateBuild(build);
// terminate the build if necessary
if (!buildProxy.isContinueBuild()) {
return false;
}
} catch (Exception e) {
throw unwrapException(e, listener);
}
}
// finally, on to the master
final Ghostwriter.MasterGhostwriter masterGhostwriter =
Ghostwriter.MasterGhostwriter.class.cast(ghostwriter);
return masterGhostwriter == null
|| masterGhostwriter.performFromMaster(build,
build.getProject().getModuleRoot(), listener);
}
private static RuntimeException unwrapException(Exception e,
BuildListener listener)
throws IOException, InterruptedException {
if (e.getCause() instanceof IOException) {
throw new IOException2(e.getCause().getMessage(), e);
}
if (e.getCause() instanceof InterruptedException) {
e.getCause().printStackTrace(listener.getLogger());
throw new InterruptedException(e.getCause().getMessage());
}
if (e.getCause() instanceof RuntimeException) {
throw new RuntimeException(e.getCause());
}
// How on earth do we get this far down the branch
e.printStackTrace(listener.getLogger());
throw new RuntimeException("Unexpected exception", e);
}
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 static boolean doPerform(Ghostwriter ghostwriter,
MavenBuildProxy mavenBuildProxy,
MavenProject pom,
final BuildListener listener)
throws InterruptedException, IOException {
// first, construct the BuildProxy instance that we will use
BuildProxy buildProxy = new BuildProxy(
mavenBuildProxy.getArtifactsDir(),
mavenBuildProxy.getProjectRootDir(),
mavenBuildProxy.getRootDir(),
new FilePath(pom.getBasedir()),
mavenBuildProxy.getTimestamp());
// do we need to do anything on the slave
if (ghostwriter instanceof Ghostwriter.SlaveGhostwriter) {
final Ghostwriter.SlaveGhostwriter slaveGhostwriter =
(Ghostwriter.SlaveGhostwriter) ghostwriter;
// terminate the build if necessary
if (!slaveGhostwriter.performFromSlave(buildProxy, listener)) {
return false;
}
}
// finally, on to the master
try {
return mavenBuildProxy.execute(new BuildProxyCallableHelper(
buildProxy, ghostwriter, listener));
} catch (Exception e) {
throw unwrapException(e, listener);
}
}
private BuildProxy(FilePath artifactsDir,
FilePath projectRootDir,
FilePath buildRootDir,
FilePath executionRootDir,
Calendar timestamp) {
this.artifactsDir = artifactsDir;
this.projectRootDir = projectRootDir;
this.buildRootDir = buildRootDir;
this.executionRootDir = executionRootDir;
this.timestamp = timestamp;
}
public List<AbstractBuildAction<AbstractBuild<?, ?>>> getActions() {
return actions;
}
public FilePath getArtifactsDir() {
return artifactsDir;
}
public FilePath getBuildRootDir() {
return buildRootDir;
}
public FilePath getExecutionRootDir() {
return executionRootDir;
}
public FilePath getProjectRootDir() {
return projectRootDir;
}
public Calendar getTimestamp() {
return timestamp;
}
public Result getResult() {
return result;
}
public void setResult(Result result) {
this.result = result;
}
public boolean isContinueBuild() {
return continueBuild;
}
public void setContinueBuild(boolean continueBuild) {
this.continueBuild = continueBuild;
}
}
src/main/java/hudson/plugins/helpers/BuildProxyCallableHelper.java
Lost some HTML entities (again!)
package hudson.plugins.helpers;
import hudson.remoting.Callable;
import hudson.maven.MavenBuildProxy;
import hudson.maven.MavenBuild;
import hudson.model.BuildListener;
import java.io.IOException;
class BuildProxyCallableHelper implements Callable<BuildProxy, Exception>,
MavenBuildProxy.BuildCallable<Boolean, Exception> {
private final BuildProxy buildProxy;
private final Ghostwriter ghostwriter;
private final BuildListener listener;
BuildProxyCallableHelper(BuildProxy buildProxy,
Ghostwriter ghostwriter,
BuildListener listener) {
this.buildProxy = buildProxy;
this.ghostwriter = ghostwriter;
this.listener = listener;
}
public Boolean call(MavenBuild mavenBuild) throws Exception {
buildProxy.updateBuild(mavenBuild);
if (ghostwriter instanceof Ghostwriter.MasterGhostwriter) {
final Ghostwriter.MasterGhostwriter masterBuildStep =
(Ghostwriter.MasterGhostwriter) ghostwriter;
return masterBuildStep.performFromMaster(mavenBuild,
buildProxy.getExecutionRootDir(), listener);
}
return true;
}
public BuildProxy call() throws Exception {
if (ghostwriter instanceof Ghostwriter.SlaveGhostwriter) {
final Ghostwriter.SlaveGhostwriter slaveBuildStep =
(Ghostwriter.SlaveGhostwriter) ghostwriter;
try {
buildProxy.setContinueBuild(
slaveBuildStep.performFromSlave(buildProxy, listener));
return buildProxy;
} catch (IOException e) {
throw new Exception(e);
} catch (InterruptedException e) {
throw new Exception(e);
}
}
return buildProxy;
}
}
src/main/java/hudson/plugins/helpers/Ghostwriter.java
Java Generics are a real gotcha for HTML entities
package hudson.plugins.helpers;
import hudson.model.BuildListener;
import hudson.model.AbstractBuild;
import hudson.FilePath;
import java.io.Serializable;
import java.io.IOException;
public interface Ghostwriter extends Serializable {
public static interface SlaveGhostwriter extends Ghostwriter {
boolean performFromSlave(BuildProxy build, BuildListener listener)
throws InterruptedException, IOException;
}
public static interface MasterGhostwriter extends Ghostwriter {
boolean performFromMaster(AbstractBuild<?, ?> build,
FilePath executionRoot, BuildListener listener)
throws InterruptedException, IOException;
}
}
src/main/resources/hudson/plugins/helpers/AbstractBuildAction/enlargedGraph.jelly
I left a "css
" attribute in the <l:layout>
tag.
<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">
<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>
src/main/resources/hudson/plugins/helpers/AbstractBuildAction/floatingBox.jelly
The <j:if>
should be based on the from
variable and not it
<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="${from.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>
src/main/resources/hudson/plugins/helpers/AbstractBuildAction/index.jelly
I left a "css
" attribute in the <l:layout>
tag.
<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">
<st:include it="${it.build}" page="sidepanel.jelly"/>
<l:main-panel>
<h1>${it.displayName}</h1>
<j:if test="${it.graphActive}">
<h4>${it.graphName}</h4>
<st:include page="normalGraph.jelly"/>
</j:if>
<st:include page="reportDetail.jelly"/>
</l:main-panel>
</l:layout>
</j:jelly>
src/main/resources/hudson/plugins/helpers/AbstractBuildAction/largeGraph.jelly, src/main/resources/hudson/plugins/helpers/AbstractBuildAction/normalGraph.jelly, and src/main/resources/hudson/plugins/helpers/AbstractBuildAction/reportDetail.jelly
These are all the same content, and are basically empty placeholders to be overrided in classes that extend AbstractBuildAction. I do not think there are any corrections.
<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>
src/main/resources/hudson/plugins/helpers/AbstractBuildAction/summary.jelly
The <j:if>
had an extra }
in the expression
<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.floatingBoxActive}">
<div style="float:right">
<st:include page="floatingBox.jelly"/>
</div>
</j:if>
</j:jelly>
src/main/resources/hudson/plugins/helpers/AbstractProjectAction/enlargedGraph.jelly
I left a "css
" attribute in the <l:layout>
tag.
<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">
<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>
src/main/resources/hudson/plugins/helpers/AbstractProjectAction/floatingBox.jelly
The <j:if>
should be based on the from
variable and not it
<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="${from.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>
src/main/resources/hudson/plugins/helpers/AbstractProjectAction/index.jelly
I left a "css
" attribute in the <l:layout>
tag.
<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">
<st:include it="${it.project}" page="sidepanel.jelly"/>
<l:main-panel>
<h1>${it.displayName}</h1>
<j:if test="${it.graphActive}">
<h4>${it.graphName}</h4>
<st:include page="normalGraph.jelly"/>
</j:if>
<st:include page="reportDetail.jelly"/>
</l:main-panel>
</l:layout>
</j:jelly>
src/main/resources/hudson/plugins/helpers/AbstractProjectAction/largeGraph.jelly, src/main/resources/hudson/plugins/helpers/AbstractProjectAction/normalGraph.jelly, and src/main/resources/hudson/plugins/helpers/AbstractProjectAction/reportDetail.jelly
These are all the same content, and are basically empty placeholders to be overrided in classes that extend AbstractProjectAction. I do not think there are any corrections.
<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>
Hi Stephen,
ReplyDeleteFirst of all, thanks for this excellent tutorial series, it was greatly needed.
Any news on when part 7 will be available?
Thx,
Jos