package htmlpublisher; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.FilePath; import hudson.model.AbstractItem; import hudson.model.AbstractDescribableImpl; import hudson.model.Action; import hudson.model.DirectoryBrowserSupport; import hudson.model.ProminentProjectAction; import hudson.model.Run; import hudson.model.Descriptor; import hudson.Extension; import hudson.model.AbstractBuild; import hudson.model.InvisibleAction; import hudson.model.Job; import java.io.File; import java.io.IOException; import javax.annotation.Nonnull; import javax.servlet.ServletException; import hudson.util.HttpResponses; import jenkins.model.RunAction2; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.DataBoundConstructor; /** * A representation of an HTML directory to archive and publish. * * @author Mike Rooney * */ public class HtmlPublisherTarget extends AbstractDescribableImpl<HtmlPublisherTarget> { /** * The name of the report to display for the build/project, such as "Code Coverage" */ private final String reportName; /** * The path to the HTML report directory relative to the workspace. */ private final String reportDir; /** * The file[s] to provide links inside the report directory. */ private final String reportFiles; /** * If this is true and keepAll is true, publish the link on project level even if build failed. */ private final boolean alwaysLinkToLastBuild; private String reportTitles; /** * If true, archive reports for all successful builds, otherwise only the most recent. */ private final boolean keepAll; /** * If true, will allow report to be missing and build will not fail on missing report. */ private final boolean allowMissing; /** * Do not use, but keep to maintain compatibility with older releases. See JENKINS-31366. */ @Deprecated private transient String wrapperName; /** * The name of the file which will be used as the wrapper index. */ private static final String WRAPPER_NAME = "htmlpublisher-wrapper.html"; /** * @deprecated Use {@link #HtmlPublisherTarget(java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean, boolean, boolean)}. */ @Deprecated public HtmlPublisherTarget(String reportName, String reportDir, String reportFiles, boolean keepAll, boolean allowMissing) { this(reportName, reportDir, reportFiles, "", keepAll, false, allowMissing); } public String getReportTitles() { return reportTitles; } /** * Constructor. * @param reportName Report name * @param reportDir Source directory in the job workspace * @param reportFiles Files to be published * @param reportTitles Files Title to be published * @param keepAll True if the report should be stored for all builds * @param alwaysLinkToLastBuild If true, the job action will refer the latest build. * Otherwise, the latest successful one will be referenced * @param allowMissing If true, blocks the build failure if the report is missing * @since 1.4 */ @DataBoundConstructor public HtmlPublisherTarget(String reportName, String reportDir, String reportFiles,String reportTitles, boolean keepAll, boolean alwaysLinkToLastBuild, boolean allowMissing) { this.reportName = reportName; this.reportDir = reportDir; this.reportFiles = reportFiles; this.reportTitles = reportTitles; this.keepAll = keepAll; this.alwaysLinkToLastBuild = alwaysLinkToLastBuild; this.allowMissing = allowMissing; } /** * Constructor. * @param reportName Report name * @param reportDir Source directory in the job workspace * @param reportFiles Files to be published * @param keepAll True if the report should be stored for all builds * @param alwaysLinkToLastBuild If true, the job action will refer the latest build. * Otherwise, the latest successful one will be referenced * @param allowMissing If true, blocks the build failure if the report is missing * @since 1.4 */ public HtmlPublisherTarget(String reportName, String reportDir, String reportFiles, boolean keepAll, boolean alwaysLinkToLastBuild, boolean allowMissing) { this.reportName = reportName; this.reportDir = reportDir; this.reportFiles = reportFiles; this.keepAll = keepAll; this.alwaysLinkToLastBuild = alwaysLinkToLastBuild; this.allowMissing = allowMissing; } public String getReportName() { return this.reportName; } public String getReportDir() { return this.reportDir; } public String getReportFiles() { return this.reportFiles; } public boolean getAlwaysLinkToLastBuild() { return this.alwaysLinkToLastBuild; } public boolean getKeepAll() { return this.keepAll; } public boolean getAllowMissing() { return this.allowMissing; } public String getSanitizedName() { String safeName = this.reportName; safeName = safeName.replace(" ", "_"); return safeName; } public String getWrapperName() { return WRAPPER_NAME; } public FilePath getArchiveTarget(Run build) { return new FilePath(this.keepAll ? getBuildArchiveDir(build) : getProjectArchiveDir(build.getParent())); } /** * Gets the directory where the HTML report is stored for the given project. */ private File getProjectArchiveDir(AbstractItem project) { return new File(new File(project.getRootDir(), "htmlreports"), this.getSanitizedName()); } /** * Gets the directory where the HTML report is stored for the given build. */ private File getBuildArchiveDir(Run run) { return new File(new File(run.getRootDir(), "htmlreports"), this.getSanitizedName()); } protected abstract class BaseHTMLAction implements Action { private HtmlPublisherTarget actualHtmlPublisherTarget; protected transient AbstractItem project; public BaseHTMLAction(HtmlPublisherTarget actualHtmlPublisherTarget) { this.actualHtmlPublisherTarget = actualHtmlPublisherTarget; } public String getUrlName() { return actualHtmlPublisherTarget.getSanitizedName(); } public String getDisplayName() { String action = actualHtmlPublisherTarget.reportName; return dir().exists() ? action : null; } public String getIconFileName() { return dir().exists() ? "graph.gif" : null; } public boolean shouldLinkToLastBuild() { return actualHtmlPublisherTarget.getAlwaysLinkToLastBuild(); } /** * Serves HTML reports. */ public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { DirectoryBrowserSupport dbs = new DirectoryBrowserSupport(this, new FilePath(this.dir()), this.getTitle(), "graph.gif", false); if (req.getRestOfPath().equals("")) { throw HttpResponses.forwardToView(this, "index.jelly"); } dbs.generateResponse(req, rsp, this); } protected abstract String getTitle(); protected abstract File dir(); } public class HTMLAction extends BaseHTMLAction implements ProminentProjectAction { private transient HTMLBuildAction actualBuildAction; public HTMLAction(AbstractItem project, HtmlPublisherTarget actualHtmlPublisherTarget) { super(actualHtmlPublisherTarget); this.project = project; } @Override protected File dir() { if (this.project instanceof Job) { final Job job = (Job) this.project; Run run = getArchiveBuild(job); if (run != null) { File javadocDir = getBuildArchiveDir(run); if (javadocDir.exists()) { for (HTMLBuildAction a : run.getActions(HTMLBuildAction.class)) { if (a.getHTMLTarget().getReportName().equals(getHTMLTarget().getReportName())) { actualBuildAction = a; } } return javadocDir; } } } return getProjectArchiveDir(this.project); } private Run getArchiveBuild(@Nonnull Job job) { if (shouldLinkToLastBuild()) { return job.getLastBuild(); } else { return job.getLastSuccessfulBuild(); } } @Override protected String getTitle() { return this.project.getDisplayName() + " html2"; } /** * Gets {@link HtmlPublisherTarget}, for which the action has been created. * @return HTML Report description * @since TODO */ public @Nonnull HtmlPublisherTarget getHTMLTarget() { return HtmlPublisherTarget.this; } @Restricted(NoExternalUse.class) // read by Groovy view public HTMLBuildAction getActualBuildAction() { return actualBuildAction; } } /** * Hidden action, which indicates the build has been published on the project level. * This action is not an instance of {@link BaseHTMLAction} , because we want to * avoid confusions with actions referring to the data. * @since TODO */ public static class HTMLPublishedForProjectMarkerAction extends InvisibleAction implements RunAction2 { private transient Run<?, ?> build; private final HtmlPublisherTarget actualHtmlPublisherTarget; public HTMLPublishedForProjectMarkerAction(Run<?, ?> build, HtmlPublisherTarget actualHtmlPublisherTarget) { this.actualHtmlPublisherTarget = actualHtmlPublisherTarget; this.build = build; } @WithBridgeMethods(value = AbstractBuild.class, adapterMethod = "getAbstractBuildOwner") public final Run<?,?> getOwner() { return build; } @Deprecated private final Object getAbstractBuildOwner(Run build, Class targetClass) { return build instanceof AbstractBuild ? (AbstractBuild) build : null; } @Override public void onAttached(Run<?, ?> r) { this.build = r; } @Override public void onLoad(Run<?, ?> r) { this.build = r; } public HtmlPublisherTarget getHTMLTarget() { return actualHtmlPublisherTarget; } } public class HTMLBuildAction extends BaseHTMLAction implements RunAction2 { private transient Run<?, ?> build; private String wrapperChecksum; public HTMLBuildAction(Run<?, ?> build, HtmlPublisherTarget actualHtmlPublisherTarget) { super(actualHtmlPublisherTarget); this.build = build; } @WithBridgeMethods(value = AbstractBuild.class, castRequired = true) public final Run<?,?> getOwner() { return build; } @Override protected String getTitle() { return this.build.getDisplayName() + " html3"; } @Override protected File dir() { return getBuildArchiveDir(this.build); } /** * Gets {@link HtmlPublisherTarget}, for which the action has been created. * @return HTML Report description * @since TODO */ public @Nonnull HtmlPublisherTarget getHTMLTarget() { return HtmlPublisherTarget.this; } @Override public void onAttached(Run<?, ?> r) { build = r; this.project = r.getParent(); } @Override public void onLoad(Run<?, ?> r) { build = r; this.project = r.getParent(); } public String getWrapperChecksum() { return wrapperChecksum; } private void setWrapperChecksum(String wrapperChecksum) { this.wrapperChecksum = wrapperChecksum; } } @Deprecated public void handleAction(Run<?, ?> build) { handleAction(build, null); } /* package */ void handleAction(Run<?, ?> build, String checksum) { // Add build action, if coverage is recorded for each build if (this.keepAll) { HTMLBuildAction a = new HTMLBuildAction(build, this); a.setWrapperChecksum(checksum); build.addAction(a); } else { // Othwewise we add a hidden marker build.addAction(new HTMLPublishedForProjectMarkerAction(build, this)); } } public Action getProjectAction(AbstractItem item) { return new HTMLAction(item, this); } @Override public int hashCode() { int hash = 5; hash = 97 * hash + (this.reportName != null ? this.reportName.hashCode() : 0); hash = 97 * hash + (this.reportDir != null ? this.reportDir.hashCode() : 0); hash = 97 * hash + (this.reportFiles != null ? this.reportFiles.hashCode() : 0); hash = 97 * hash + (this.alwaysLinkToLastBuild ? 1 : 0); hash = 97 * hash + (this.keepAll ? 1 : 0); hash = 97 * hash + (this.allowMissing ? 1 : 0); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final HtmlPublisherTarget other = (HtmlPublisherTarget) obj; if ((this.reportName == null) ? (other.reportName != null) : !this.reportName.equals(other.reportName)) { return false; } if ((this.reportDir == null) ? (other.reportDir != null) : !this.reportDir.equals(other.reportDir)) { return false; } if ((this.reportFiles == null) ? (other.reportFiles != null) : !this.reportFiles.equals(other.reportFiles)) { return false; } if (this.alwaysLinkToLastBuild != other.alwaysLinkToLastBuild) { return false; } if (this.keepAll != other.keepAll) { return false; } if (this.allowMissing != other.allowMissing) { return false; } return true; } @Extension public static class DescriptorImpl extends Descriptor<HtmlPublisherTarget> { public String getDisplayName() { return ""; } } }