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.xmlThis 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.javaAFAIK 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.javaAFAIK 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.javaAFAIK 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.javaSome 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.javaAgain, 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.javaLost 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.javaJava Generics are a real gotcha for HTML entitiespackage 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.jellyI 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}"> ${it.graphName} <st:include page=“largeGraph.jelly”/> </j:if> </l:main-panel> </l:layout></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractBuildAction/floatingBox.jellyThe <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}"> ${from.graphName} <st:include page=“normalGraph.jelly”/> enlarge </j:if></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractBuildAction/index.jellyI 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> ${it.displayName} <j:if test="${it.graphActive}"> ${it.graphName} <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.jellyThese 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.jellyThe <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}"> ${it.displayName} ${it.summary} </t:summary> <j:if test="${it.floatingBoxActive}"> <st:include page=“floatingBox.jelly”/> </j:if></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/enlargedGraph.jellyI 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}"> ${it.graphName} <st:include page=“largeGraph.jelly”/> </j:if> </l:main-panel> </l:layout></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/floatingBox.jellyThe <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}"> ${from.graphName} <st:include page=“normalGraph.jelly”/> enlarge </j:if></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/index.jellyI 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> ${it.displayName} <j:if test="${it.graphActive}"> ${it.graphName} <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.jellyThese 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>
/ Writing a Hudson plugin (Part 5½ - Typos corrected)
Created Sat, 01 Mar 2008 00:00:00 +0000
Modified Sat, 01 Mar 2008 00:00:00 +0000