package hudson.plugins.violations.render; import static com.google.common.base.Suppliers.memoize; import static com.google.common.collect.Lists.newArrayList; import static java.util.logging.Level.WARNING; import hudson.Functions; import hudson.model.AbstractBuild; import hudson.plugins.violations.generate.XMLUtil; import hudson.plugins.violations.model.FileModel; import hudson.plugins.violations.model.Severity; import hudson.plugins.violations.model.Violation; import hudson.plugins.violations.parse.FileModelParser; import hudson.plugins.violations.parse.ParseXML; import java.io.File; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.logging.Logger; import com.google.common.base.Supplier; /** * A proxy class for FileModel used to allow lazy loading of FileModel. This * class is also used to render the FileModel. */ public class FileModelProxy { private static final Logger LOG = Logger.getLogger(FileModelProxy.class.getName()); private final File xmlFile; private final Supplier<FileModel> fileModel = memoize(new Supplier<FileModel>() { @Override public FileModel get() { if (!xmlFile.exists()) { LOG.log(WARNING, "The file " + xmlFile + " does not exist"); return null; } try { FileModel t = new FileModel(); ParseXML.parse(xmlFile, new FileModelParser().fileModel(t)); return t; } catch (Exception ex) { LOG.log(WARNING, "Unable to parse " + xmlFile, ex); return null; } } }); private String contextPath; private AbstractBuild<?, ?> build; /** * Construct this proxy. * * @param xmlFile * the xmlfile to create the FileModel from. */ public FileModelProxy(File xmlFile) { this.xmlFile = xmlFile; } /** * Fluid setting of the build attribute. * * @param build * the owner build. * @return this object. */ public FileModelProxy build(AbstractBuild<?, ?> build) { this.build = build; return this; } /** * Fluid setting of context path. This is used for getting icons. * * @param contextPath * the current WEB context path. * @return this object. */ public FileModelProxy contextPath(String contextPath) { this.contextPath = contextPath; return this; } /** * get the current build. * * @return the current build. */ public AbstractBuild<?, ?> getBuild() { return build; } /** * Get the type line. * * @param type * the violation type. * @return a rendered line containing the number of violations and the * number of suppressed violations. */ public String typeLine(String type) { FileModel.LimitType l = fileModel.get().getLimitTypeMap().get(type); StringBuilder b = new StringBuilder(); if (l == null) { return type + " ?number?"; } b.append(type); b.append("   "); b.append(l.getNumber()); b.append(" violation"); if (l.getNumber() > 1) { b.append("s"); } if (l.getSuppressed() != 0) { b.append(" ("); b.append(l.getSuppressed()); b.append(" not shown)"); } return b.toString(); } /** * Wheter to show lines. * * @return true if the file model contains lines. */ public boolean getShowLines() { return getFileModel().getLines().size() != 0; } public Set<Map.Entry<String, TreeSet<Violation>>> getTypeMapEntries() { return getFileModel().getTypeMap().entrySet(); } public Set<Map.Entry<Integer, Set<Violation>>> getViolationEntries() { return getFileModel().getLineViolationMap().entrySet(); } public String getDisplayName() { return getFileModel().getDisplayName(); } /** * This gets called from the index.jelly script to render the marked up * contents of the file. * * @return a table of lines and associated violations in html. */ public List<BlockData> getFileContent() { List<BlockData> blockDataList = newArrayList(); StringBuilder b = new StringBuilder(); int previousLine = -1; int startLine = 0; int currentLine = -1; for (Map.Entry<Integer, String> e : fileModel.get().getLines().entrySet()) { currentLine = e.getKey(); String line = e.getValue(); // Check if at start of block if (currentLine != (previousLine + 1)) { // Start of block // Check if need to write previous block if (b.length() > 0) { blockDataList.add(new BlockData(startLine, previousLine, b.toString(), new File(fileModel.get() .getDisplayName()).getName())); } b = new StringBuilder(); startLine = currentLine; } previousLine = currentLine; Set<Violation> v = fileModel.get().getLineViolationMap().get(currentLine); // TR b.append("<tr " + (v != null ? "class='violation'" : "") + ">"); // Visual Studio if (v != null) { // Only first one needed for line for (Violation violation : v) { b.append("<td class='source icon'>"); b.append(this.getVisualStudioLink(violation)); b.append("</td>"); break; } } else { b.append("<td class='source icon'/>\n"); } // Icon if (v != null) { showIcon(b, v); } else { b.append("<td class='source icon'/>\n"); } // Line number b.append("<td class='source line' id='line" + currentLine + "'>"); if (v != null) { addVDiv(b); } b.append(currentLine); if (v != null) { showDiv(b, v); b.append("</div>"); } b.append("</td>"); b.append("<td class='source message'>"); b.append(XMLUtil.escapeHTMLContent(contextPath + "/plugin/violations/images/tab.png", line)); b.append("</td>\n"); b.append("</tr>\n"); } if (b.length() > 0) { blockDataList.add(new BlockData(startLine, previousLine, b.toString(), new File(fileModel.get() .getDisplayName()).getName())); } return blockDataList; } public String getVisualStudioLink(Violation v) { StringBuilder ret = new StringBuilder(); String uriBase = "devenv:?file=" + this.fileModel.get().getDisplayName(); ret.append("<a href=\""); String uri = String.valueOf(uriBase + "&line=" + v.getLine()); ret.append(uri); ret.append("\"><img src=\"/plugin/violations/images/16x16/vs2010-play.png\" alt=\"Visual Studio 2010\"></a>"); return ret.toString(); } public String getViolationsSummary() { StringBuilder ret = new StringBuilder(); for (Entry<String, TreeSet<Violation>> t : this.fileModel.get().getTypeMap().entrySet()) { ret.append("<table class=\"pane\">"); ret.append("<tbody>"); ret.append("<tr><td class=\"pane-header\" colspan=\"5\">"); ret.append(this.typeLine(t.getKey())); ret.append("</td></tr>"); for (Violation v : t.getValue()) { ret.append("<tr>"); if (v.getSource().toUpperCase().contains("Security".toUpperCase())) { ret.append("<td class=\"pane\">"); ret.append("<img src=\"/plugin/violations/images/16x16/security.png\" alt=\"Security violation\">"); ret.append("</td>"); } else { ret.append("<td class=\"pane\" />"); } ret.append("<td class=\"pane\">"); ret.append(getVisualStudioLink(v)); ret.append("</td>"); ret.append("<td class=\"pane\">"); if (this.getShowLines()) { ret.append("<a href=\"#line"); ret.append(v.getLine()); ret.append("\">"); ret.append(v.getLine()); ret.append("</a>"); } else { ret.append(v.getLine()); } ret.append("</td>"); ret.append("<td class=\"pane\">"); ret.append(this.severityColumn(v)); ret.append("</td>"); ret.append("<td class=\"pane\" width=\"99%\">"); ret.append(v.getMessage()); ret.append("</td>"); ret.append("</tr>"); } ret.append("</table>"); ret.append("<p></p>"); } return ret.toString(); } /** * Get the file model. If the file model is present, return it, otherwise * parse the xml file. * * @return the file model or null if unable to parse. */ public FileModel getFileModel() { return this.fileModel.get(); } private String getSeverityIcon(int level) { String color = null; switch (level) { case Severity.HIGH_VALUE: color = "red"; break; case Severity.LOW_VALUE: color = "yellow"; break; default: color = "violet"; // medium (low,-,high) } return "/plugin/violations/images/16x16/" + color + "-warning.png"; } /** * Get the URL for the icon, taking context into account * * @param v * the violation * @return URL */ public String getSeverityIconUrl(Violation v) { return contextPath + getSeverityIcon(v.getSeverityLevel()); } /** * Get the severity column for a violation. * * @param v * the violation. * @return a string to place in the severity column of the violation table. */ public String severityColumn(Violation v) { StringBuilder b = new StringBuilder(); addVDiv(b); b.append("<a class='healthReport'>"); b.append("<img src='" + getSeverityIconUrl(v) + "' alt='" + v.getSeverity() + "'/>"); b.append("</a>"); b.append("<div class='healthReportDetails'>\n"); b.append("<table class='violationPopup'>\n"); b.append("<tr>\n"); b.append("<th>Severity</th>\n"); b.append("<td>"); b.append(v.getSeverity()); b.append("</td>\n"); b.append("</tr>\n"); b.append("<tr>\n"); b.append("<th>Class</th>\n"); b.append("<td>"); b.append(Functions.escape(v.getSource())); b.append("</td>\n"); b.append("</tr>\n"); b.append("<tr>\n"); b.append("<th>Detail</th>\n"); b.append("<td class='message'>"); b.append(Functions.escape(v.getSourceDetail())); b.append("</td>\n"); b.append("</tr>\n"); b.append("</table>\n"); b.append("</div>\n"); b.append("</div>\n"); return b.toString(); } private void addVDiv(StringBuilder b) { b.append("<div class='healthReport'"); b.append("onmouseover=\"this.className='healthReport hover';return true;\""); b.append("onmouseout=\"this.className='healthReport';return true;\">"); } private void showIcon(StringBuilder b, Set<Violation> violations) { // Get the worst icon in the set int level = Severity.LOW_VALUE; for (Violation v : violations) { if (v.getSeverityLevel() < level) { level = v.getSeverityLevel(); } } b.append("<td class='source icon'>"); addVDiv(b); b.append("<a class='healthReport'>"); b.append("<img src='" + contextPath + getSeverityIcon(level) + "'/>"); b.append("</a>"); showDiv(b, violations); b.append("</div>"); b.append("</td>"); } private void showDiv(StringBuilder b, Set<Violation> violations) { b.append("<div class='healthReportDetails'>\n"); b.append(" <table class='violationPopup'>\n"); b.append(" <thead>\n"); b.append(" <tr>\n"); b.append(" <th> Type</th>\n"); b.append(" <th> Class</th>\n"); b.append(" <th> Description</th>\n"); b.append(" </tr>\n"); b.append(" </thead>\n"); b.append(" <tbody>\n"); for (Violation v : violations) { b.append(" <tr>\n"); b.append(" <td>"); b.append(v.getType()); b.append("</td>\n"); b.append(" <td>"); b.append(v.getSource()); b.append("</td>\n"); b.append(" <td width='100%' class='message'>"); b.append(XMLUtil.escapeContent(v.getPopupMessage())); b.append("</td>\n"); b.append(" </tr>\n"); } b.append(" </tbody>\n"); b.append(" </table>\n"); b.append("</div>\n"); } public String getFileNameAlt() { return new File(fileModel.get().getDisplayName()).getName(); } public String getSummaryTable() { StringBuilder gst = new StringBuilder(); int count = 0; gst.append(" <table class='violations' width='100%'>\n"); gst.append(" <tr>\n"); gst.append(" <td class='violations-header'> # </td>\n"); gst.append(" <td class='violations-header'> Type </td>\n"); gst.append(" <td class='violations-header'> Class</td>\n"); gst.append(" <td class='violations-header'> Message</td>\n"); gst.append(" <td class='violations-header'> Description</td>\n"); gst.append(" </tr>\n"); Set<Violation> violations = fileModel.get().getLineViolationMap().get(0); for (Violation v : violations) { ++count; gst.append(" <tr>\n"); gst.append(" <td class='violations'>"); gst.append(count); gst.append("</td>\n"); gst.append(" <td class='violations'>"); gst.append(v.getType()); gst.append("</td>\n"); gst.append(" <td class='violations'>"); gst.append(v.getSource()); gst.append("</td>\n"); gst.append(" <td class='violations'>"); gst.append(v.getMessage()); gst.append("</td>\n"); gst.append(" <td class='violations'>"); gst.append(v.getPopupMessage()); gst.append("</td>\n"); gst.append(" </tr>\n"); } gst.append(" </table>\n"); gst.append("<p><br>\n"); gst.append("<h3>Total Number of violations: \n"); gst.append(count); gst.append("</h3><p>\n"); return gst.toString(); } }