/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt, Peter Hayes * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package htmlpublisher; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.matrix.MatrixConfiguration; import hudson.matrix.MatrixProject; import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.FormValidation; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import javax.servlet.ServletException; import java.io.*; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import jenkins.model.Jenkins; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Saves HTML reports for the project and publishes them. * * @author Kohsuke Kawaguchi * @author Mike Rooney */ public class HtmlPublisher extends Recorder { private final ArrayList<HtmlPublisherTarget> reportTargets; @DataBoundConstructor @Restricted(NoExternalUse.class) public HtmlPublisher(List<HtmlPublisherTarget> reportTargets) { this.reportTargets = reportTargets != null ? new ArrayList<HtmlPublisherTarget>(reportTargets) : new ArrayList<HtmlPublisherTarget>(); } public ArrayList<HtmlPublisherTarget> getReportTargets() { return this.reportTargets; } /** * * @return SHA checksum of the written file */ private static String writeFile(ArrayList<String> lines, File path) throws IOException, NoSuchAlgorithmException { MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); //TODO: consider using UTF-8 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(path), Charset.defaultCharset())); try { for (int i = 0; i < lines.size(); i++) { String line = lines.get(i) + "\n"; bw.write(line); sha1.update(line.getBytes("UTF-8")); } } finally { bw.close(); } return Util.toHexString(sha1.digest()); } public ArrayList<String> readFile(String filePath) throws java.io.FileNotFoundException, java.io.IOException { return readFile(filePath, this.getClass()); } public static ArrayList<String> readFile(String filePath, Class<?> publisherClass) throws java.io.FileNotFoundException, java.io.IOException { ArrayList<String> aList = new ArrayList<String>(); try { final InputStream is = publisherClass.getResourceAsStream(filePath); try { // We expect that files have been generated with the default system's charset final Reader r = new InputStreamReader(is, Charset.defaultCharset()); try { final BufferedReader br = new BufferedReader(r); try { String line = null; while ((line = br.readLine()) != null) { aList.add(line); } br.close(); r.close(); is.close(); } finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } finally { try { r.close(); } catch (IOException e) { e.printStackTrace(); } } } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (IOException e) { // failure e.printStackTrace(); } return aList; } protected static String resolveParametersInString(Run<?, ?> build, TaskListener listener, String input) { try { return build.getEnvironment(listener).expand(input); } catch (Exception e) { listener.getLogger().println("Failed to resolve parameters in string \""+ input+"\" due to following error:\n"+e.getMessage()); } return input; } protected static String resolveParametersInString(EnvVars envVars, TaskListener listener, String input) { try { return envVars.expand(input); } catch (Exception e) { listener.getLogger().println("Failed to resolve parameters in string \""+ input+"\" due to following error:\n"+e.getMessage()); } return input; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException { return publishReports(build, build.getWorkspace(), launcher, listener, reportTargets, this.getClass()); } /** * Runs HTML the publishing operation for specified {@link HtmlPublisherTarget}s. * @return False if the operation failed * @since TODO */ public static boolean publishReports(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener, List<HtmlPublisherTarget> reportTargets, Class<?> publisherClass) throws InterruptedException { listener.getLogger().println("[htmlpublisher] Archiving HTML reports..."); // Grab the contents of the header and footer as arrays ArrayList<String> headerLines; ArrayList<String> footerLines; try { headerLines = readFile("/htmlpublisher/HtmlPublisher/header.html", publisherClass); footerLines = readFile("/htmlpublisher/HtmlPublisher/footer.html", publisherClass); } catch (FileNotFoundException e1) { e1.printStackTrace(); return false; } catch (IOException e1) { e1.printStackTrace(); return false; } for (int i=0; i < reportTargets.size(); i++) { // Create an array of lines we will eventually write out, initially the header. ArrayList<String> reportLines = new ArrayList<String>(headerLines); HtmlPublisherTarget reportTarget = reportTargets.get(i); boolean keepAll = reportTarget.getKeepAll(); boolean allowMissing = reportTarget.getAllowMissing(); FilePath archiveDir = workspace.child(resolveParametersInString(build, listener, reportTarget.getReportDir())); FilePath targetDir = reportTarget.getArchiveTarget(build); String levelString = keepAll ? "BUILD" : "PROJECT"; listener.getLogger().println("[htmlpublisher] Archiving at " + levelString + " level " + archiveDir + " to " + targetDir); // The index name might be a comma separated list of names, so let's figure out all the pages we should index. String[] csvReports = resolveParametersInString(build, listener, reportTarget.getReportFiles()).split(","); String[] titles = null; if (reportTarget.getReportTitles() != null && reportTarget.getReportTitles().trim().length() > 0 ) { titles = reportTarget.getReportTitles().trim().split("\\s*,\\s*"); } ArrayList<String> reports = new ArrayList<String>(); for (int j=0; j < csvReports.length; j++) { String report = csvReports[j]; report = report.trim(); // Ignore blank report names caused by trailing or double commas. if (report.equals("")) {continue;} reports.add(report); String tabNo = "tab" + (j + 1); // Make the report name the filename without the extension. int end = report.lastIndexOf('.'); String reportName; if (end > 0) { reportName = report.substring(0, end); } else { reportName = report; } String tabItem = "<li id=\"" + tabNo + "\" class=\"unselected\" onclick=\"updateBody('" + tabNo + "');\" value=\"" + report + "\">" + getTitle(reportName, titles, j) + "</li>"; reportLines.add(tabItem); } // Add the JS to change the link as appropriate. String hudsonUrl = Jenkins.getActiveInstance().getRootUrl(); Job job = build.getParent(); reportLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").innerHTML=\"Back to " + job.getName() + "\";</script>"); // If the URL isn't configured in Hudson, the best we can do is attempt to go Back. if (hudsonUrl == null) { reportLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").onclick = function() { history.go(-1); return false; };</script>"); } else { String jobUrl = hudsonUrl + job.getUrl(); reportLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").href=\"" + jobUrl + "\";</script>"); } reportLines.add("<script type=\"text/javascript\">document.getElementById(\"zip_link\").href=\"*zip*/" + reportTarget.getSanitizedName() + ".zip\";</script>"); try { if (!archiveDir.exists() && !allowMissing) { listener.error("Specified HTML directory '" + archiveDir + "' does not exist."); build.setResult(Result.FAILURE); return true; } else if (!keepAll) { // We are only keeping one copy at the project level, so remove the old one. targetDir.deleteRecursive(); } if (archiveDir.copyRecursiveTo("**/*", targetDir) == 0 && !allowMissing) { listener.error("Directory '" + archiveDir + "' exists but failed copying to '" + targetDir + "'."); final Result buildResult = build.getResult(); if (buildResult != null && buildResult.isBetterOrEqualTo(Result.UNSTABLE)) { // If the build failed, don't complain that there was no coverage. // The build probably didn't even get to the point where it produces coverage. listener.error("This is especially strange since your build otherwise succeeded."); } build.setResult(Result.FAILURE); return true; } } catch (IOException e) { Util.displayIOException(e, listener); e.printStackTrace(listener.fatalError("HTML Publisher failure")); build.setResult(Result.FAILURE); return true; } // Now add the footer. reportLines.addAll(footerLines); // And write this as the index try { if(archiveDir.exists()) { String checksum = writeFile(reportLines, new File(targetDir.getRemote(), reportTarget.getWrapperName())); reportTarget.handleAction(build, checksum); } } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { // cannot happen because SHA-1 is guaranteed to exist e.printStackTrace(); } } return true; } private static String getTitle(String report, String[] titles, int j) { if (titles != null && titles.length > j) { return titles[j]; } return report; } @Override public Collection<? extends Action> getProjectActions(AbstractProject<?, ?> project) { if (this.reportTargets.isEmpty()) { return Collections.emptyList(); } else { ArrayList<Action> actions = new ArrayList<Action>(); for (HtmlPublisherTarget target : this.reportTargets) { actions.add(target.getProjectAction(project)); if (project instanceof MatrixProject && ((MatrixProject) project).getActiveConfigurations() != null){ for (MatrixConfiguration mc : ((MatrixProject) project).getActiveConfigurations()){ try { mc.onLoad(mc.getParent(), mc.getName()); } catch (IOException e){ //Could not reload the configuration. } } } } return actions; } } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { @Override public String getDisplayName() { // return Messages.JavadocArchiver_DisplayName(); return "Publish HTML reports"; } /** * Performs on-the-fly validation on the file mask wildcard. */ public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException, ServletException { FilePath ws = project.getSomeWorkspace(); return ws != null ? ws.validateRelativeDirectory(value) : FormValidation.ok(); } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } }