package hudson.plugins.violations; import java.lang.ref.WeakReference; import java.io.IOException; import java.io.File; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import hudson.model.HealthReport; import hudson.model.AbstractBuild; import hudson.model.Result; import hudson.model.AbstractProject; import hudson.model.Project; /** cannot be used in a slave import hudson.maven.MavenModule; /**/ import hudson.plugins.violations.parse.ParseXML; import hudson.plugins.violations.parse.BuildModelParser; import hudson.plugins.violations.render.FileModelProxy; import hudson.plugins.violations.render.NoViolationsFile; import hudson.plugins.violations.model.BuildModel; import hudson.plugins.violations.model.FileModel; import hudson.plugins.violations.model.Suppression; import hudson.plugins.violations.util.RecurDynamic; import hudson.plugins.violations.util.HelpHudson; import hudson.plugins.violations.hudson.AbstractViolationsBuildAction; /** cannot be used in a slave import hudson.plugins.violations.hudson.maven.*; /**/ /** * This contains the report for the violations * of a particular build. */ public class ViolationsReport implements Serializable { private static final Logger LOG = Logger.getLogger(ViolationsReport.class.getName()); private AbstractBuild<?, ?> build; private ViolationsConfig config; private Map<String, Integer> violations = new TreeMap<String, Integer>(); private Map<String, TypeSummary> typeSummaries = new TreeMap<String, TypeSummary>(); private transient WeakReference<BuildModel> modelReference; /** * Set the build. * @param build the current build. */ public void setBuild(AbstractBuild<?, ?> build) { this.build = build; } /** * Get the build. * @return the build. */ public AbstractBuild<?, ?> getBuild() { return build; } /** * Set the config. * @param config the config. */ public void setConfig(ViolationsConfig config) { this.config = config; } /** * Get the config. * @return the config. */ public ViolationsConfig getConfig() { return config; } /** * Get the violation counts for the build. * @return a map of type to count. */ public Map<String, Integer> getViolations() { return violations; } /** * Get the overall health for the build. * @return the health report, null if there are no counts. */ public HealthReport getBuildHealth() { List<HealthReport> reports = getBuildHealths(); HealthReport ret = null; for (HealthReport report: reports) { ret = HealthReport.min(ret, report); } return ret; } /** * Get a health report for each type. * @return a list of health reports. */ public List<HealthReport> getBuildHealths() { List<HealthReport> ret = new ArrayList<HealthReport>(); for (String type: config.getTypeConfigs().keySet()) { HealthReport health = getHealthReportFor(type); if (health != null) { ret.add(health); } } return ret; } /** * Get the health for a particulat type. * @param type the type to get the health for. * @return the health report. */ public HealthReport getHealthReportFor(String type) { Integer count = violations.get(type); if (count == null || config.getTypeConfigs() == null) { return null; } int h = config.getTypeConfigs().get(type).getHealthFor(count); if (h < 0) { return new HealthReport( 0, Messages._ViolationsReport_NoReport(type)); } else { return new HealthReport( h, Messages._ViolationsReport_ViolationsCount(type, count)); } } /** * Get the detailed model for the build. * This is lazily build from an xml created during publisher action. * @return the build model. */ public BuildModel getModel() { BuildModel model = null; if (modelReference != null) { model = modelReference.get(); if (model != null) { return model; } } File xmlFile = new File( build.getRootDir(), MagicNames.VIOLATIONS + "/" + MagicNames.VIOLATIONS + ".xml"); try { model = new BuildModel(xmlFile); ParseXML.parse( xmlFile, new BuildModelParser().buildModel(model)); } catch (Exception ex) { LOG.log(Level.WARNING, "Unable to parse " + xmlFile, ex); return null; } modelReference = new WeakReference<BuildModel>(model); return model; } /** * Get the file model proxt for a file name. * @param name the name to use. * @return the file model proxy. */ public FileModelProxy getFileModelProxy( String name) { BuildModel model = getModel(); if (model == null) { return null; } return model.getFileModelMap().get(name); } /** * This gets called to display a particular violation file report. * @param token the current token in the path being parsed. * @param req the http/stapler request. * @param rsp the http/stapler response. * @return an object to handle the token. */ public Object getDynamic( String token, StaplerRequest req, StaplerResponse rsp) { //System.out.println("LOOKING for " + req.getRestOfPath()); String name = req.getRestOfPath(); if (name.equals("")) { return null; } if (name.startsWith("/")) { name = name.substring(1); } FileModelProxy proxy = getFileModelProxy(name); if (proxy != null) { return new RecurDynamic( "", name, proxy.build(build).contextPath(req.getContextPath())); } else { return new RecurDynamic( "", name, new NoViolationsFile(name, build)); } } /** * get the configuration for this job. * @return the configuration of the job. */ public ViolationsConfig getLiveConfig() { AbstractProject<?, ?> abstractProject = build.getProject(); AbstractProject notQuestion = (AbstractProject) abstractProject; if (abstractProject instanceof Project) { Project project = (Project) abstractProject; ViolationsPublisher publisher = (ViolationsPublisher) project.getPublisher( ViolationsPublisher.DESCRIPTOR); return publisher == null ? null : publisher.getConfig(); /** Remove the following as it cannot be uesed in a slave } else if (notQuestion instanceof MavenModule) { MavenModule mavenModule = (MavenModule) notQuestion; ViolationsMavenReporter reporter = (ViolationsMavenReporter) mavenModule.getReporters(). get(ViolationsMavenReporter.DESCRIPTOR); return reporter == null ? null : reporter.getConfig(); /**/ } return null; } /** * Add a suppression to the set of suppressions. * @param suppression the suppression to add. */ public void addSuppression(Suppression suppression) throws IOException { ViolationsConfig config = getLiveConfig(); if (config != null) { config.getSuppressions().add(suppression); ((AbstractProject)build.getParent()).save(); } } /** * Remove a suppression to the set of suppressions. * @param suppression the suppression to remove. */ public void removeSuppression(Suppression suppression) throws IOException { ViolationsConfig config = getLiveConfig(); if (config != null) { config.getSuppressions().remove(suppression); ((AbstractProject)build.getParent()).save(); } } /** * Get a map of type to type summary report. * @return a map. */ public Map<String, TypeSummary> getTypeSummaries() { return typeSummaries; } /** * Get a type summary for a particular type. * @param type the violation type. * @return the type summary. */ public TypeSummary getTypeSummary(String type) { TypeSummary ret = typeSummaries.get(type); if (ret == null) { ret = new TypeSummary(); typeSummaries.put(type, ret); } return ret; } /** * Get a map of type to type reports. * @return a map of type to type reports. */ public Map<String, TypeReport> getTypeReports() { Map<String, TypeReport> ret = new TreeMap<String, TypeReport>(); for (String t: violations.keySet()) { int c = violations.get(t); HealthReport health = getHealthReportFor(t); ret.put( t, new TypeReport(t, health.getIconUrl(), c)); } return ret; } /** * Graph this report. * Note that for some reason, yet unknown, hudson seems * to pick an in memory ViolationsReport object and * not the report for the build. * (Reason may be related to the fact that serialized builds may not be * the same as in-memory builds). * Need to find the correct build from the URI. * @param req the request paramters * @param rsp the response. * @throws IOException if there is an error writing the graph. */ public void doGraph(StaplerRequest req, StaplerResponse rsp) throws IOException { AbstractBuild<?, ?> tBuild = build; int buildNumber = HelpHudson.findBuildNumber(req); if (buildNumber != 0) { tBuild = (AbstractBuild<?, ?>) build.getParent().getBuildByNumber(buildNumber); if (tBuild == null) { tBuild = build; } } AbstractViolationsBuildAction r = tBuild.getAction(AbstractViolationsBuildAction.class); if (r == null) { return; } r.doGraph(req, rsp); } /** * Get the string number for a particular type. * @param t the type * @return the string - a number for a value type, * "" for not found and "No reports" for * < 0. */ public String getNumberString(String t) { Integer v = violations.get(t); if (v == null) { return ""; } if (v < 0) { return "<span style='color:red'>No reports</span>"; } return "" + v; } /** * Get the icon for a type. * @param t the type * @return the icon name. */ public String getIcon(String t) { Integer v = violations.get(t); HealthReport h = getHealthReportFor(t); if (h == null) { return null; } return h.getIconUrl(); } /** * Report class for a particular type. */ public class TypeReport { private final String type; private final String icon; private final int number; /** * Create the report class for a type. * @param type the violation type. * @param icon the health icon to display. * @param number the number of violations. */ public TypeReport(String type, String icon, int number) { this.type = type; this.icon = icon; this.number = number; } /** * Get the violation type. * @return the violation type. */ public String getType() { return type; } /** * Get the health icon to display. * @return the health icon. */ public String getIcon() { return icon; } /** * Get the number of violations. * @return the number. */ public int getNumber() { return number; } /** * Get the number of violations as a string. * @return the number if >= 0 othersise an error string. */ public String getNumberString() { if (number >= 0) { return "" + number; } else { return "<span style='color:red'>No reports</span>"; } } } /** * Get the previous ViolationsReport * @return the previous report if present, null otherwise. */ public ViolationsReport previous() { return findViolationsReport(build.getPreviousBuild()); } /** * Get the number of violations for a particular type. * @param type the violation type. * @return the number of violations. */ public int typeCount(String type) { if (getModel() == null) { return 0; } return getModel().getTypeCountMap().get(type).getCount(); } /** * Get the number of files in violation for a particular type. * @param type the violation type. * @return the number of files. */ public int fileCount(String type) { if (getModel() == null) { return 0; } return getModel().getTypeCountMap().get(type).getNumberFiles(); } /** * Get the number of violations of a type for a file. * @param type the type in question. * @param filename the name of the file. * @return the number found (0 for none and for failures). */ public int violationCount(String type, String filename) { FileModelProxy proxy = getFileModelProxy(filename); if (proxy == null) { return 0; } FileModel fileModel = proxy.getFileModel(); if (fileModel == null) { return 0; } FileModel.LimitType limit = fileModel.getLimitTypeMap().get(type); if (limit == null) { return 0; } return limit.getNumber(); } /** * Get the unstable status for this report. * @return true if one of the violations equals or exceed the * unstable threshold for that violations type. */ private boolean isUnstable() { for (String t: violations.keySet()) { int count = violations.get(t); Integer unstableLimit = config.getTypeConfigs().get(t).getUnstable(); if (unstableLimit == null) { continue; } if (count >= unstableLimit) { return true; } } return false; } /** * Get the failed status for this report. * @return true if one of the violations equals or exceed the * failed threshold of that violations type. */ private boolean isFailed() { for (String t: violations.keySet()) { int count = violations.get(t); Integer failCount = config.getTypeConfigs().get(t).getFail(); if (failCount == null) { continue; } if (count >= failCount) { return true; } } return false; } /** * Set the unstable/failed status of a build based * on this violations report. */ public void setBuildResult() { if (isFailed()) { build.setResult(Result.FAILURE); return; } if (isUnstable()) { build.setResult(Result.UNSTABLE); } } private static final long serialVersionUID = 1L; public static ViolationsReport findViolationsReport( AbstractBuild<?, ?> b) { for (; b != null; b = b.getPreviousBuild()) { if (b.getResult().isWorseOrEqualTo(Result.FAILURE)) { continue; } AbstractViolationsBuildAction action = b.getAction(AbstractViolationsBuildAction.class); if (action == null || action.getReport() == null) { continue; } ViolationsReport ret = action.getReport(); ret.setBuild(b); return ret; } return null; } public static ViolationsReportIterator iteration( AbstractBuild<?, ?> build) { return new ViolationsReportIterator(build); } public static class ViolationsReportIterator implements Iterator<ViolationsReport>, Iterable<ViolationsReport> { private AbstractBuild<?, ?> curr; public ViolationsReportIterator(AbstractBuild<?, ?> curr) { this.curr = curr; } @Override public Iterator<ViolationsReport> iterator() { return this; } public boolean hasNext() { return findViolationsReport(curr) != null; } public ViolationsReport next() { ViolationsReport ret = findViolationsReport(curr); if (ret != null) { curr = ret.getBuild().getPreviousBuild(); } return ret; } public void remove() { throw new UnsupportedOperationException(); } } }