package net.sourceforge.cobertura.reporting.html;
import net.sourceforge.cobertura.coveragedata.*;
import net.sourceforge.cobertura.reporting.ComplexityCalculator;
import net.sourceforge.cobertura.reporting.html.files.CopyFiles;
import net.sourceforge.cobertura.util.*;
import java.io.*;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HTMLReport {
private static final Logger LOGGER = LoggerFactory
.getLogger(HTMLReport.class);
private File destinationDir;
private FileFinder finder;
private ComplexityCalculator complexity;
private ProjectData projectData;
private String encoding;
public HTMLReport(ProjectData projectData, File outputDir,
FileFinder finder, ComplexityCalculator complexity, String encoding)
throws Exception {
this.destinationDir = outputDir;
this.finder = finder;
this.complexity = complexity;
this.projectData = projectData;
this.encoding = encoding;
CopyFiles.copy(outputDir);
generatePackageList();
generateSourceFileLists();
generateOverviews();
generateSourceFiles();
}
private String generatePackageName(PackageData packageData) {
if (packageData.getName().equals(""))
return "(default)";
return packageData.getName();
}
private void generatePackageList() throws IOException {
File file = new File(destinationDir, "frame-packages.html");
PrintWriter out = null;
try {
out = IOUtil.getPrintWriter(file);
out
.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
out
.println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
out
.println("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">");
out.println("<head>");
out
.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />");
out.println("<title>Coverage Report</title>");
out
.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />");
out.println("</head>");
out.println("<body>");
out.println("<h5>Packages</h5>");
out.println("<table width=\"100%\">");
out.println("<tr>");
out
.println("<td nowrap=\"nowrap\"><a href=\"frame-summary.html\" onclick='parent.sourceFileList.location.href=\"frame-sourcefiles.html\"' target=\"summary\">All</a></td>");
out.println("</tr>");
Iterator iter = projectData.getPackages().iterator();
while (iter.hasNext()) {
PackageData packageData = (PackageData) iter.next();
String url1 = "frame-summary-" + packageData.getName()
+ ".html";
String url2 = "frame-sourcefiles-" + packageData.getName()
+ ".html";
out.println("<tr>");
out.println("<td nowrap=\"nowrap\"><a href=\"" + url1
+ "\" onclick='parent.sourceFileList.location.href=\""
+ url2 + "\"' target=\"summary\">"
+ generatePackageName(packageData) + "</a></td>");
out.println("</tr>");
}
out.println("</table>");
out.println("</body>");
out.println("</html>");
} finally {
if (out != null) {
out.close();
}
}
}
private void generateSourceFileLists() throws IOException {
generateSourceFileList(null);
Iterator iter = projectData.getPackages().iterator();
while (iter.hasNext()) {
PackageData packageData = (PackageData) iter.next();
generateSourceFileList(packageData);
}
}
private void generateSourceFileList(PackageData packageData)
throws IOException {
String filename;
Collection sourceFiles;
if (packageData == null) {
filename = "frame-sourcefiles.html";
sourceFiles = projectData.getSourceFiles();
} else {
filename = "frame-sourcefiles-" + packageData.getName() + ".html";
sourceFiles = packageData.getSourceFiles();
}
Vector sortedSourceFiles = new Vector();
sortedSourceFiles.addAll(sourceFiles);
Collections.sort(sortedSourceFiles,
new SourceFileDataBaseNameComparator());
File file = new File(destinationDir, filename);
PrintWriter out = null;
try {
out = IOUtil.getPrintWriter(file);
out
.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
out
.println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
out.println("<html>");
out.println("<head>");
out
.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
out.println("<title>Coverage Report Classes</title>");
out
.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
out.println("</head>");
out.println("<body>");
out.println("<h5>");
out.println(packageData == null
? "All Packages"
: generatePackageName(packageData));
out.println("</h5>");
out.println("<div class=\"separator\"> </div>");
out.println("<h5>Classes</h5>");
if (!sortedSourceFiles.isEmpty()) {
out.println("<table width=\"100%\">");
out.println("<tbody>");
for (Iterator iter = sortedSourceFiles.iterator(); iter
.hasNext();) {
SourceFileData sourceFileData = (SourceFileData) iter
.next();
out.println("<tr>");
String percentCovered;
if (sourceFileData.getNumberOfValidLines() > 0)
percentCovered = getPercentValue(sourceFileData
.getLineCoverageRate());
else
percentCovered = "N/A";
out
.println("<td nowrap=\"nowrap\"><a target=\"summary\" href=\""
+ sourceFileData.getNormalizedName()
+ ".html\">"
+ sourceFileData.getBaseName()
+ "</a> <i>("
+ percentCovered
+ ")</i></td>");
out.println("</tr>");
}
out.println("</tbody>");
out.println("</table>");
}
out.println("</body>");
out.println("</html>");
} finally {
if (out != null) {
out.close();
}
}
}
private void generateOverviews() throws IOException {
generateOverview(null);
Iterator iter = projectData.getPackages().iterator();
while (iter.hasNext()) {
PackageData packageData = (PackageData) iter.next();
generateOverview(packageData);
}
}
private void generateOverview(PackageData packageData) throws IOException {
Iterator iter;
String filename;
if (packageData == null) {
filename = "frame-summary.html";
} else {
filename = "frame-summary-" + packageData.getName() + ".html";
}
File file = new File(destinationDir, filename);
PrintWriter out = null;
try {
out = IOUtil.getPrintWriter(file);;
out
.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
out
.println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
out.println("<html>");
out.println("<head>");
out
.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
out.println("<title>Coverage Report</title>");
out
.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
out
.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/sortabletable.css\"/>");
out
.println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>");
out
.println("<script type=\"text/javascript\" src=\"js/sortabletable.js\"></script>");
out
.println("<script type=\"text/javascript\" src=\"js/customsorttypes.js\"></script>");
out.println("</head>");
out.println("<body>");
out.print("<h5>Coverage Report - ");
out.print(packageData == null
? "All Packages"
: generatePackageName(packageData));
out.println("</h5>");
out.println("<div class=\"separator\"> </div>");
out.println("<table class=\"report\" id=\"packageResults\">");
out.println(generateTableHeader("Package", true));
out.println("<tbody>");
SortedSet packages;
if (packageData == null) {
out.println(generateTableRowForTotal());
packages = projectData.getPackages();
} else {
packages = projectData.getSubPackages(packageData.getName());
}
iter = packages.iterator();
while (iter.hasNext()) {
PackageData subPackageData = (PackageData) iter.next();
out.println(generateTableRowForPackage(subPackageData));
}
out.println("</tbody>");
out.println("</table>");
out.println("<script type=\"text/javascript\">");
out
.println("var packageTable = new SortableTable(document.getElementById(\"packageResults\"),");
out
.println(" [\"String\", \"Number\", \"Percentage\", \"Percentage\", \"FormattedNumber\"]);");
out.println("packageTable.sort(0);");
out.println("</script>");
Collection sourceFiles;
if (packageData == null) {
PackageData defaultPackage = (PackageData) projectData
.getChild("");
if (defaultPackage != null) {
sourceFiles = defaultPackage.getSourceFiles();
} else {
sourceFiles = new TreeSet();
}
} else {
sourceFiles = packageData.getSourceFiles();
}
if (sourceFiles.size() > 0) {
out.println("<div class=\"separator\"> </div>");
out.println("<table class=\"report\" id=\"classResults\">");
out.println(generateTableHeader("Classes in this Package",
false));
out.println("<tbody>");
iter = sourceFiles.iterator();
while (iter.hasNext()) {
SourceFileData sourceFileData = (SourceFileData) iter
.next();
out.println(generateTableRowsForSourceFile(sourceFileData));
}
out.println("</tbody>");
out.println("</table>");
out.println("<script type=\"text/javascript\">");
out
.println("var classTable = new SortableTable(document.getElementById(\"classResults\"),");
out
.println(" [\"String\", \"Percentage\", \"Percentage\", \"FormattedNumber\"]);");
out.println("classTable.sort(0);");
out.println("</script>");
}
out.println(generateFooter());
out.println("</body>");
out.println("</html>");
} finally {
if (out != null) {
out.close();
}
}
}
private void generateSourceFiles() {
Iterator iter = projectData.getSourceFiles().iterator();
while (iter.hasNext()) {
SourceFileData sourceFileData = (SourceFileData) iter.next();
try {
generateSourceFile(sourceFileData);
} catch (IOException e) {
LOGGER.info("Could not generate HTML file for source file "
+ sourceFileData.getName() + ": "
+ e.getLocalizedMessage());
}
}
}
private void generateSourceFile(SourceFileData sourceFileData)
throws IOException {
if (!sourceFileData.containsInstrumentationInfo()) {
LOGGER.info("Data file does not contain instrumentation "
+ "information for the file " + sourceFileData.getName()
+ ". Ensure this class was instrumented, and this "
+ "data file contains the instrumentation information.");
}
String filename = sourceFileData.getNormalizedName() + ".html";
File file = new File(destinationDir, filename);
PrintWriter out = null;
try {
out = IOUtil.getPrintWriter(file);
out
.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
out
.println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
out.println("<html>");
out.println("<head>");
out
.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
out.println("<title>Coverage Report</title>");
out
.println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
out
.println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>");
out.println("</head>");
out.println("<body>");
out.print("<h5>Coverage Report - ");
String classPackageName = sourceFileData.getPackageName();
if ((classPackageName != null) && classPackageName.length() > 0) {
out.print(classPackageName + ".");
}
out.print(sourceFileData.getBaseName());
out.println("</h5>");
out.println("<div class=\"separator\"> </div>");
out.println("<table class=\"report\">");
out.println(generateTableHeader("Classes in this File", false));
out.println(generateTableRowsForSourceFile(sourceFileData));
out.println("</table>");
out.println("<div class=\"separator\"> </div>");
out.println(generateHtmlizedJavaSource(sourceFileData));
out.println(generateFooter());
out.println("</body>");
out.println("</html>");
} finally {
if (out != null) {
out.close();
}
}
}
private String generateBranchInfo(LineData lineData, String content) {
boolean hasBranch = (lineData != null) ? lineData.hasBranch() : false;
if (hasBranch) {
StringBuffer ret = new StringBuffer();
ret.append("<a title=\"Line ").append(lineData.getLineNumber())
.append(": Conditional coverage ").append(
lineData.getConditionCoverage());
if (lineData.getConditionSize() > 1) {
ret.append(" [each condition: ");
for (int i = 0; i < lineData.getConditionSize(); i++) {
if (i > 0)
ret.append(", ");
ret.append(lineData.getConditionCoverage(i));
}
ret.append("]");
}
ret.append(".\">").append(content).append("</a>");
return ret.toString();
} else {
return content;
}
}
private String generateHtmlizedJavaSource(SourceFileData sourceFileData) {
Source source = finder.getSource(sourceFileData.getName());
if (source == null) {
return "<p>Unable to locate " + sourceFileData.getName()
+ ". Have you specified the source directory?</p>";
}
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(source
.getInputStream(), encoding));
} catch (UnsupportedEncodingException e) {
return "<p>Unable to open " + source.getOriginDesc()
+ ": The encoding '" + encoding
+ "' is not supported by your JVM.</p>";
} catch (Throwable t) {
return "<p>Unable to open " + source.getOriginDesc() + ": "
+ t.getLocalizedMessage() + "</p>";
}
StringBuffer ret = new StringBuffer();
ret
.append("<table cellspacing=\"0\" cellpadding=\"0\" class=\"src\">\n");
try {
String lineStr;
JavaToHtml javaToHtml = new JavaToHtml();
int lineNumber = 1;
while ((lineStr = br.readLine()) != null) {
ret.append("<tr>");
if (sourceFileData.isValidSourceLineNumber(lineNumber)) {
LineData lineData = sourceFileData
.getLineCoverage(lineNumber);
ret.append(" <td class=\"numLineCover\"> "
+ lineNumber + "</td>");
if ((lineData != null) && (lineData.isCovered())) {
ret.append(" <td class=\"nbHitsCovered\">"
+ generateBranchInfo(lineData, " "
+ ((lineData != null) ? lineData
.getHits() : 0)) + "</td>");
ret
.append(" <td class=\"src\"><pre class=\"src\"> "
+ generateBranchInfo(lineData,
javaToHtml.process(lineStr))
+ "</pre></td>");
} else {
ret.append(" <td class=\"nbHitsUncovered\">"
+ generateBranchInfo(lineData, " "
+ ((lineData != null) ? lineData
.getHits() : 0)) + "</td>");
ret
.append(" <td class=\"src\"><pre class=\"src\"><span class=\"srcUncovered\"> "
+ generateBranchInfo(lineData,
javaToHtml.process(lineStr))
+ "</span></pre></td>");
}
} else {
ret.append(" <td class=\"numLine\"> " + lineNumber
+ "</td>");
ret.append(" <td class=\"nbHits\"> </td>\n");
ret.append(" <td class=\"src\"><pre class=\"src\"> "
+ javaToHtml.process(lineStr) + "</pre></td>");
}
ret.append("</tr>\n");
lineNumber++;
}
} catch (IOException e) {
ret.append("<tr><td>Error reading " + source.getOriginDesc() + ": "
+ e.getLocalizedMessage() + "</td></tr>\n");
} finally {
try {
br.close();
source.close();
} catch (IOException e) {
}
}
ret.append("</table>\n");
return ret.toString();
}
private static String () {
return "<div class=\"footer\">Report generated by "
+ "<a href=\"http://cobertura.sourceforge.net/\" target=\"_top\">Cobertura</a> "
+ Header.version() + " on "
+ DateFormat.getInstance().format(new Date()) + ".</div>";
}
private static String (String title,
boolean showColumnForNumberOfClasses) {
StringBuffer ret = new StringBuffer();
ret.append("<thead>");
ret.append("<tr>");
ret.append(" <td class=\"heading\">" + title + "</td>");
if (showColumnForNumberOfClasses) {
ret.append(" <td class=\"heading\"># Classes</td>");
}
ret.append(" <td class=\"heading\">"
+ generateHelpURL("Line Coverage",
"The percent of lines executed by this test run.")
+ "</td>");
ret.append(" <td class=\"heading\">"
+ generateHelpURL("Branch Coverage",
"The percent of branches executed by this test run.")
+ "</td>");
ret
.append(" <td class=\"heading\">"
+ generateHelpURL(
"Complexity",
"Average McCabe's cyclomatic code complexity for all methods. This is basically a count of the number of different code paths in a method (incremented by 1 for each if statement, while loop, etc.)")
+ "</td>");
ret.append("</tr>");
ret.append("</thead>");
return ret.toString();
}
private static String generateHelpURL(String text, String description) {
StringBuffer ret = new StringBuffer();
boolean popupTooltips = false;
if (popupTooltips) {
ret
.append("<a class=\"hastooltip\" href=\"help.html\" onclick=\"popupwindow('help.html'); return false;\">");
ret.append(text);
ret.append("<span>" + description + "</span>");
ret.append("</a>");
} else {
ret
.append("<a class=\"dfn\" href=\"help.html\" onclick=\"popupwindow('help.html'); return false;\">");
ret.append(text);
ret.append("</a>");
}
return ret.toString();
}
private String generateTableRowForTotal() {
StringBuffer ret = new StringBuffer();
double ccn = complexity.getCCNForProject(projectData);
ret.append(" <tr>");
ret.append("<td><b>All Packages</b></td>");
ret.append("<td class=\"value\">" + projectData.getNumberOfClasses()
+ "</td>");
ret.append(generateTableColumnsFromData(projectData, ccn));
ret.append("</tr>");
return ret.toString();
}
private String generateTableRowForPackage(PackageData packageData) {
StringBuffer ret = new StringBuffer();
String url1 = "frame-summary-" + packageData.getName() + ".html";
String url2 = "frame-sourcefiles-" + packageData.getName() + ".html";
double ccn = complexity.getCCNForPackage(packageData);
ret.append(" <tr>");
ret.append("<td><a href=\"" + url1
+ "\" onclick='parent.sourceFileList.location.href=\"" + url2
+ "\"'>" + generatePackageName(packageData) + "</a></td>");
ret.append("<td class=\"value\">" + packageData.getNumberOfChildren()
+ "</td>");
ret.append(generateTableColumnsFromData(packageData, ccn));
ret.append("</tr>");
return ret.toString();
}
private String generateTableRowsForSourceFile(SourceFileData sourceFileData) {
StringBuffer ret = new StringBuffer();
String sourceFileName = sourceFileData.getNormalizedName();
double ccn = complexity.getCCNForSourceFile(sourceFileData);
Iterator iter = sourceFileData.getClasses().iterator();
while (iter.hasNext()) {
ClassData classData = (ClassData) iter.next();
ret
.append(generateTableRowForClass(classData, sourceFileName,
ccn));
}
return ret.toString();
}
private String generateTableRowForClass(ClassData classData,
String sourceFileName, double ccn) {
StringBuffer ret = new StringBuffer();
ret.append(" <tr>");
ret.append("<td><a href=\"" + sourceFileName + ".html\">"
+ classData.getBaseName() + "</a></td>");
ret.append(generateTableColumnsFromData(classData, ccn));
ret.append("</tr>\n");
return ret.toString();
}
private static String generateTableColumnsFromData(
CoverageData coverageData, double ccn) {
int numLinesCovered = coverageData.getNumberOfCoveredLines();
int numLinesValid = coverageData.getNumberOfValidLines();
int numBranchesCovered = coverageData.getNumberOfCoveredBranches();
int numBranchesValid = coverageData.getNumberOfValidBranches();
return "<td>" + generatePercentResult(numLinesCovered, numLinesValid)
+ "</td><td>"
+ generatePercentResult(numBranchesCovered, numBranchesValid)
+ "</td><td class=\"value\"><span class=\"hidden\">" + ccn
+ ";</span>" + getDoubleValue(ccn) + "</td>";
}
private static String generatePercentResult(int dividend, int divisor) {
StringBuffer sb = new StringBuffer();
sb
.append("<table cellpadding=\"0px\" cellspacing=\"0px\" class=\"percentgraph\"><tr class=\"percentgraph\"><td align=\"right\" class=\"percentgraph\" width=\"40\">");
if (divisor > 0)
sb.append(getPercentValue((double) dividend / divisor));
else
sb
.append(generateHelpURL(
"N/A",
"Line coverage and branch coverage will appear as \"Not Applicable\" when Cobertura can not find line number information in the .class file. This happens for stub and skeleton classes, interfaces, or when the class was not compiled with \"debug=true.\""));
sb
.append("</td><td class=\"percentgraph\"><div class=\"percentgraph\">");
if (divisor > 0) {
sb.append("<div class=\"greenbar\" style=\"width:"
+ (dividend * 100 / divisor) + "px\">");
sb.append("<span class=\"text\">");
sb.append(dividend);
sb.append("/");
sb.append(divisor);
} else {
sb.append("<div class=\"na\" style=\"width:100px\">");
sb.append("<span class=\"text\">");
sb
.append(generateHelpURL(
"N/A",
"Line coverage and branch coverage will appear as \"Not Applicable\" when Cobertura can not find line number information in the .class file. This happens for stub and skeleton classes, interfaces, or when the class was not compiled with \"debug=true.\""));
}
sb.append("</span></div></div></td></tr></table>");
return sb.toString();
}
private static String getDoubleValue(double value) {
return new DecimalFormat().format(value);
}
private static String getPercentValue(double value) {
return StringUtil.getPercentValue(value);
}
}