001/* 002 * Cobertura - http://cobertura.sourceforge.net/ 003 * 004 * Copyright (C) 2005 Mark Doliner 005 * Copyright (C) 2005 Grzegorz Lukasik 006 * Copyright (C) 2005 Jeremy Thomerson 007 * Copyright (C) 2006 Naoki Iwami 008 * Copyright (C) 2009 Charlie Squires 009 * Copyright (C) 2009 John Lewis 010 * 011 * Cobertura is free software; you can redistribute it and/or modify 012 * it under the terms of the GNU General Public License as published 013 * by the Free Software Foundation; either version 2 of the License, 014 * or (at your option) any later version. 015 * 016 * Cobertura is distributed in the hope that it will be useful, but 017 * WITHOUT ANY WARRANTY; without even the implied warranty of 018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 019 * General Public License for more details. 020 * 021 * You should have received a copy of the GNU General Public License 022 * along with Cobertura; if not, write to the Free Software 023 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 024 * USA 025 */ 026 027package net.sourceforge.cobertura.reporting.html; 028 029import java.io.BufferedReader; 030import java.io.File; 031import java.io.InputStream; 032import java.io.FileNotFoundException; 033import java.io.IOException; 034import java.io.InputStreamReader; 035import java.io.PrintWriter; 036import java.io.UnsupportedEncodingException; 037import java.text.DateFormat; 038import java.text.DecimalFormat; 039import java.text.NumberFormat; 040import java.util.Collection; 041import java.util.Collections; 042import java.util.Date; 043import java.util.Iterator; 044import java.util.SortedSet; 045import java.util.TreeSet; 046import java.util.Vector; 047 048import net.sourceforge.cobertura.coveragedata.ClassData; 049import net.sourceforge.cobertura.coveragedata.CoverageData; 050import net.sourceforge.cobertura.coveragedata.LineData; 051import net.sourceforge.cobertura.coveragedata.PackageData; 052import net.sourceforge.cobertura.coveragedata.ProjectData; 053import net.sourceforge.cobertura.coveragedata.SourceFileData; 054import net.sourceforge.cobertura.reporting.ComplexityCalculator; 055import net.sourceforge.cobertura.reporting.html.files.CopyFiles; 056import net.sourceforge.cobertura.util.FileFinder; 057import net.sourceforge.cobertura.util.Header; 058import net.sourceforge.cobertura.util.IOUtil; 059import net.sourceforge.cobertura.util.Source; 060import net.sourceforge.cobertura.util.StringUtil; 061 062import org.apache.log4j.Logger; 063 064public class HTMLReport 065{ 066 067 private static final Logger LOGGER = Logger.getLogger(HTMLReport.class); 068 069 private File destinationDir; 070 071 private FileFinder finder; 072 073 private ComplexityCalculator complexity; 074 075 private ProjectData projectData; 076 077 private String encoding; 078 079 /** 080 * Create a coverage report 081 * @param encoding 082 */ 083 public HTMLReport(ProjectData projectData, File outputDir, 084 FileFinder finder, ComplexityCalculator complexity, String encoding) 085 throws Exception 086 { 087 this.destinationDir = outputDir; 088 this.finder = finder; 089 this.complexity = complexity; 090 this.projectData = projectData; 091 this.encoding = encoding; 092 093 CopyFiles.copy(outputDir); 094 generatePackageList(); 095 generateSourceFileLists(); 096 generateOverviews(); 097 generateSourceFiles(); 098 } 099 100 private String generatePackageName(PackageData packageData) 101 { 102 if (packageData.getName().equals("")) 103 return "(default)"; 104 return packageData.getName(); 105 } 106 107 private void generatePackageList() throws IOException 108 { 109 File file = new File(destinationDir, "frame-packages.html"); 110 PrintWriter out = null; 111 112 try 113 { 114 out = IOUtil.getPrintWriter(file); 115 116 out 117 .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""); 118 out 119 .println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); 120 121 out 122 .println("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">"); 123 out.println("<head>"); 124 out 125 .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />"); 126 out.println("<title>Coverage Report</title>"); 127 out 128 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />"); 129 out.println("</head>"); 130 out.println("<body>"); 131 out.println("<h5>Packages</h5>"); 132 out.println("<table width=\"100%\">"); 133 out.println("<tr>"); 134 out 135 .println("<td nowrap=\"nowrap\"><a href=\"frame-summary.html\" onclick='parent.sourceFileList.location.href=\"frame-sourcefiles.html\"' target=\"summary\">All</a></td>"); 136 out.println("</tr>"); 137 138 Iterator iter = projectData.getPackages().iterator(); 139 while (iter.hasNext()) 140 { 141 PackageData packageData = (PackageData)iter.next(); 142 String url1 = "frame-summary-" + packageData.getName() 143 + ".html"; 144 String url2 = "frame-sourcefiles-" + packageData.getName() 145 + ".html"; 146 out.println("<tr>"); 147 out.println("<td nowrap=\"nowrap\"><a href=\"" + url1 148 + "\" onclick='parent.sourceFileList.location.href=\"" 149 + url2 + "\"' target=\"summary\">" 150 + generatePackageName(packageData) + "</a></td>"); 151 out.println("</tr>"); 152 } 153 out.println("</table>"); 154 out.println("</body>"); 155 out.println("</html>"); 156 } 157 finally 158 { 159 if (out != null) 160 { 161 out.close(); 162 } 163 } 164 } 165 166 private void generateSourceFileLists() throws IOException 167 { 168 generateSourceFileList(null); 169 Iterator iter = projectData.getPackages().iterator(); 170 while (iter.hasNext()) 171 { 172 PackageData packageData = (PackageData)iter.next(); 173 generateSourceFileList(packageData); 174 } 175 } 176 177 private void generateSourceFileList(PackageData packageData) 178 throws IOException 179 { 180 String filename; 181 Collection sourceFiles; 182 if (packageData == null) 183 { 184 filename = "frame-sourcefiles.html"; 185 sourceFiles = projectData.getSourceFiles(); 186 } 187 else 188 { 189 filename = "frame-sourcefiles-" + packageData.getName() + ".html"; 190 sourceFiles = packageData.getSourceFiles(); 191 } 192 193 // sourceFiles may be sorted, but if so it's sorted by 194 // the full path to the file, and we only want to sort 195 // based on the file's basename. 196 Vector sortedSourceFiles = new Vector(); 197 sortedSourceFiles.addAll(sourceFiles); 198 Collections.sort(sortedSourceFiles, 199 new SourceFileDataBaseNameComparator()); 200 201 File file = new File(destinationDir, filename); 202 PrintWriter out = null; 203 try 204 { 205 out = IOUtil.getPrintWriter(file); 206 207 out 208 .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""); 209 out 210 .println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); 211 212 out.println("<html>"); 213 out.println("<head>"); 214 out 215 .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>"); 216 out.println("<title>Coverage Report Classes</title>"); 217 out 218 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>"); 219 out.println("</head>"); 220 out.println("<body>"); 221 out.println("<h5>"); 222 out.println(packageData == null ? "All Packages" 223 : generatePackageName(packageData)); 224 out.println("</h5>"); 225 out.println("<div class=\"separator\"> </div>"); 226 out.println("<h5>Classes</h5>"); 227 if (!sortedSourceFiles.isEmpty()) 228 { 229 out.println("<table width=\"100%\">"); 230 out.println("<tbody>"); 231 232 for (Iterator iter = sortedSourceFiles.iterator(); iter 233 .hasNext();) 234 { 235 SourceFileData sourceFileData = (SourceFileData)iter.next(); 236 out.println("<tr>"); 237 String percentCovered; 238 if (sourceFileData.getNumberOfValidLines() > 0) 239 percentCovered = getPercentValue(sourceFileData 240 .getLineCoverageRate()); 241 else 242 percentCovered = "N/A"; 243 out 244 .println("<td nowrap=\"nowrap\"><a target=\"summary\" href=\"" 245 + sourceFileData.getNormalizedName() 246 + ".html\">" 247 + sourceFileData.getBaseName() 248 + "</a> <i>(" 249 + percentCovered 250 + ")</i></td>"); 251 out.println("</tr>"); 252 } 253 out.println("</tbody>"); 254 out.println("</table>"); 255 } 256 257 out.println("</body>"); 258 out.println("</html>"); 259 } 260 finally 261 { 262 if (out != null) 263 { 264 out.close(); 265 } 266 } 267 } 268 269 private void generateOverviews() throws IOException 270 { 271 generateOverview(null); 272 Iterator iter = projectData.getPackages().iterator(); 273 while (iter.hasNext()) 274 { 275 PackageData packageData = (PackageData)iter.next(); 276 generateOverview(packageData); 277 } 278 } 279 280 private void generateOverview(PackageData packageData) throws IOException 281 { 282 Iterator iter; 283 284 String filename; 285 if (packageData == null) 286 { 287 filename = "frame-summary.html"; 288 } 289 else 290 { 291 filename = "frame-summary-" + packageData.getName() + ".html"; 292 } 293 File file = new File(destinationDir, filename); 294 PrintWriter out = null; 295 296 try 297 { 298 out = IOUtil.getPrintWriter(file);; 299 300 out 301 .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""); 302 out 303 .println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); 304 305 out.println("<html>"); 306 out.println("<head>"); 307 out 308 .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>"); 309 out.println("<title>Coverage Report</title>"); 310 out 311 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>"); 312 out 313 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/sortabletable.css\"/>"); 314 out 315 .println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>"); 316 out 317 .println("<script type=\"text/javascript\" src=\"js/sortabletable.js\"></script>"); 318 out 319 .println("<script type=\"text/javascript\" src=\"js/customsorttypes.js\"></script>"); 320 out.println("</head>"); 321 out.println("<body>"); 322 323 out.print("<h5>Coverage Report - "); 324 out.print(packageData == null ? "All Packages" 325 : generatePackageName(packageData)); 326 out.println("</h5>"); 327 out.println("<div class=\"separator\"> </div>"); 328 out.println("<table class=\"report\" id=\"packageResults\">"); 329 out.println(generateTableHeader("Package", true)); 330 out.println("<tbody>"); 331 332 SortedSet packages; 333 if (packageData == null) 334 { 335 // Output a summary line for all packages 336 out.println(generateTableRowForTotal()); 337 338 // Get packages 339 packages = projectData.getPackages(); 340 } 341 else 342 { 343 // Get subpackages 344 packages = projectData.getSubPackages(packageData.getName()); 345 } 346 347 // Output a line for each package or subpackage 348 iter = packages.iterator(); 349 while (iter.hasNext()) 350 { 351 PackageData subPackageData = (PackageData)iter.next(); 352 out.println(generateTableRowForPackage(subPackageData)); 353 } 354 355 out.println("</tbody>"); 356 out.println("</table>"); 357 out.println("<script type=\"text/javascript\">"); 358 out 359 .println("var packageTable = new SortableTable(document.getElementById(\"packageResults\"),"); 360 out 361 .println(" [\"String\", \"Number\", \"Percentage\", \"Percentage\", \"FormattedNumber\"]);"); 362 out.println("packageTable.sort(0);"); 363 out.println("</script>"); 364 365 // Get the list of source files in this package 366 Collection sourceFiles; 367 if (packageData == null) 368 { 369 PackageData defaultPackage = (PackageData)projectData 370 .getChild(""); 371 if (defaultPackage != null) 372 { 373 sourceFiles = defaultPackage.getSourceFiles(); 374 } 375 else 376 { 377 sourceFiles = new TreeSet(); 378 } 379 } 380 else 381 { 382 sourceFiles = packageData.getSourceFiles(); 383 } 384 385 // Output a line for each source file 386 if (sourceFiles.size() > 0) 387 { 388 out.println("<div class=\"separator\"> </div>"); 389 out.println("<table class=\"report\" id=\"classResults\">"); 390 out.println(generateTableHeader("Classes in this Package", 391 false)); 392 out.println("<tbody>"); 393 394 iter = sourceFiles.iterator(); 395 while (iter.hasNext()) 396 { 397 SourceFileData sourceFileData = (SourceFileData)iter.next(); 398 out.println(generateTableRowsForSourceFile(sourceFileData)); 399 } 400 401 out.println("</tbody>"); 402 out.println("</table>"); 403 out.println("<script type=\"text/javascript\">"); 404 out 405 .println("var classTable = new SortableTable(document.getElementById(\"classResults\"),"); 406 out 407 .println(" [\"String\", \"Percentage\", \"Percentage\", \"FormattedNumber\"]);"); 408 out.println("classTable.sort(0);"); 409 out.println("</script>"); 410 } 411 412 out.println(generateFooter()); 413 414 out.println("</body>"); 415 out.println("</html>"); 416 } 417 finally 418 { 419 if (out != null) 420 { 421 out.close(); 422 } 423 } 424 } 425 426 private void generateSourceFiles() 427 { 428 Iterator iter = projectData.getSourceFiles().iterator(); 429 while (iter.hasNext()) 430 { 431 SourceFileData sourceFileData = (SourceFileData)iter.next(); 432 try 433 { 434 generateSourceFile(sourceFileData); 435 } 436 catch (IOException e) 437 { 438 LOGGER.info("Could not generate HTML file for source file " 439 + sourceFileData.getName() + ": " 440 + e.getLocalizedMessage()); 441 } 442 } 443 } 444 445 private void generateSourceFile(SourceFileData sourceFileData) 446 throws IOException 447 { 448 if (!sourceFileData.containsInstrumentationInfo()) 449 { 450 LOGGER.info("Data file does not contain instrumentation " 451 + "information for the file " + sourceFileData.getName() 452 + ". Ensure this class was instrumented, and this " 453 + "data file contains the instrumentation information."); 454 } 455 456 String filename = sourceFileData.getNormalizedName() + ".html"; 457 File file = new File(destinationDir, filename); 458 PrintWriter out = null; 459 460 try 461 { 462 out = IOUtil.getPrintWriter(file); 463 464 out 465 .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""); 466 out 467 .println(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"); 468 469 out.println("<html>"); 470 out.println("<head>"); 471 out 472 .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>"); 473 out.println("<title>Coverage Report</title>"); 474 out 475 .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>"); 476 out 477 .println("<script type=\"text/javascript\" src=\"js/popup.js\"></script>"); 478 out.println("</head>"); 479 out.println("<body>"); 480 out.print("<h5>Coverage Report - "); 481 String classPackageName = sourceFileData.getPackageName(); 482 if ((classPackageName != null) && classPackageName.length() > 0) 483 { 484 out.print(classPackageName + "."); 485 } 486 out.print(sourceFileData.getBaseName()); 487 out.println("</h5>"); 488 489 // Output the coverage summary for this class 490 out.println("<div class=\"separator\"> </div>"); 491 out.println("<table class=\"report\">"); 492 out.println(generateTableHeader("Classes in this File", false)); 493 out.println(generateTableRowsForSourceFile(sourceFileData)); 494 out.println("</table>"); 495 496 // Output the coverage summary for methods in this class 497 // TODO 498 499 // Output this class's source code with syntax and coverage highlighting 500 out.println("<div class=\"separator\"> </div>"); 501 out.println(generateHtmlizedJavaSource(sourceFileData)); 502 503 out.println(generateFooter()); 504 505 out.println("</body>"); 506 out.println("</html>"); 507 } 508 finally 509 { 510 if (out != null) 511 { 512 out.close(); 513 } 514 } 515 } 516 517 private String generateBranchInfo(LineData lineData, String content) { 518 boolean hasBranch = (lineData != null) ? lineData.hasBranch() : false; 519 if (hasBranch) 520 { 521 StringBuffer ret = new StringBuffer(); 522 ret.append("<a title=\"Line ").append(lineData.getLineNumber()).append(": Conditional coverage ") 523 .append(lineData.getConditionCoverage()); 524 if (lineData.getConditionSize() > 1) 525 { 526 ret.append(" [each condition: "); 527 for (int i = 0; i < lineData.getConditionSize(); i++) 528 { 529 if (i > 0) 530 ret.append(", "); 531 ret.append(lineData.getConditionCoverage(i)); 532 } 533 ret.append("]"); 534 } 535 ret.append(".\">").append(content).append("</a>"); 536 return ret.toString(); 537 } 538 else 539 { 540 return content; 541 } 542 } 543 544 private String generateHtmlizedJavaSource(SourceFileData sourceFileData) 545 { 546 Source source = finder.getSource(sourceFileData.getName()); 547 548 if (source == null) 549 { 550 return "<p>Unable to locate " + sourceFileData.getName() 551 + ". Have you specified the source directory?</p>"; 552 } 553 554 BufferedReader br = null; 555 try 556 { 557 br = new BufferedReader(new InputStreamReader(source.getInputStream(), encoding)); 558 } 559 catch (UnsupportedEncodingException e) 560 { 561 return "<p>Unable to open " + source.getOriginDesc() 562 + ": The encoding '" + encoding +"' is not supported by your JVM.</p>"; 563 } 564 catch (Throwable t) 565 { 566 return "<p>Unable to open " + source.getOriginDesc() + ": " + t.getLocalizedMessage() + "</p>"; 567 } 568 569 StringBuffer ret = new StringBuffer(); 570 ret 571 .append("<table cellspacing=\"0\" cellpadding=\"0\" class=\"src\">\n"); 572 try 573 { 574 String lineStr; 575 JavaToHtml javaToHtml = new JavaToHtml(); 576 int lineNumber = 1; 577 while ((lineStr = br.readLine()) != null) 578 { 579 ret.append("<tr>"); 580 if (sourceFileData.isValidSourceLineNumber(lineNumber)) 581 { 582 LineData lineData = sourceFileData.getLineCoverage(lineNumber); 583 ret.append(" <td class=\"numLineCover\"> " 584 + lineNumber + "</td>"); 585 if ((lineData != null) && (lineData.isCovered())) 586 { 587 ret.append(" <td class=\"nbHitsCovered\">" 588 + generateBranchInfo(lineData, " " + ((lineData != null) ? lineData.getHits() : 0)) 589 + "</td>"); 590 ret 591 .append(" <td class=\"src\"><pre class=\"src\"> " 592 + generateBranchInfo(lineData, javaToHtml.process(lineStr)) 593 + "</pre></td>"); 594 } 595 else 596 { 597 ret.append(" <td class=\"nbHitsUncovered\">" 598 + generateBranchInfo(lineData, " " + ((lineData != null) ? lineData.getHits() : 0)) 599 + "</td>"); 600 ret 601 .append(" <td class=\"src\"><pre class=\"src\"><span class=\"srcUncovered\"> " 602 + generateBranchInfo(lineData, javaToHtml.process(lineStr)) 603 + "</span></pre></td>"); 604 } 605 } 606 else 607 { 608 ret.append(" <td class=\"numLine\"> " + lineNumber 609 + "</td>"); 610 ret.append(" <td class=\"nbHits\"> </td>\n"); 611 ret.append(" <td class=\"src\"><pre class=\"src\"> " 612 + javaToHtml.process(lineStr) + "</pre></td>"); 613 } 614 ret.append("</tr>\n"); 615 lineNumber++; 616 } 617 } 618 catch (IOException e) 619 { 620 ret.append("<tr><td>Error reading " 621 + source.getOriginDesc() + ": " 622 + e.getLocalizedMessage() + "</td></tr>\n"); 623 } 624 finally 625 { 626 try 627 { 628 br.close(); 629 source.close(); 630 } 631 catch (IOException e) 632 { 633 } 634 } 635 636 ret.append("</table>\n"); 637 638 return ret.toString(); 639 } 640 641 private static String generateFooter() 642 { 643 return "<div class=\"footer\">Report generated by " 644 + "<a href=\"http://cobertura.sourceforge.net/\" target=\"_top\">Cobertura</a> " 645 + Header.version() + " on " 646 + DateFormat.getInstance().format(new Date()) + ".</div>"; 647 } 648 649 private static String generateTableHeader(String title, 650 boolean showColumnForNumberOfClasses) 651 { 652 StringBuffer ret = new StringBuffer(); 653 ret.append("<thead>"); 654 ret.append("<tr>"); 655 ret.append(" <td class=\"heading\">" + title + "</td>"); 656 if (showColumnForNumberOfClasses) 657 { 658 ret.append(" <td class=\"heading\"># Classes</td>"); 659 } 660 ret.append(" <td class=\"heading\">" 661 + generateHelpURL("Line Coverage", 662 "The percent of lines executed by this test run.") 663 + "</td>"); 664 ret.append(" <td class=\"heading\">" 665 + generateHelpURL("Branch Coverage", 666 "The percent of branches executed by this test run.") 667 + "</td>"); 668 ret 669 .append(" <td class=\"heading\">" 670 + generateHelpURL( 671 "Complexity", 672 "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.)") 673 + "</td>"); 674 ret.append("</tr>"); 675 ret.append("</thead>"); 676 return ret.toString(); 677 } 678 679 private static String generateHelpURL(String text, String description) 680 { 681 StringBuffer ret = new StringBuffer(); 682 boolean popupTooltips = false; 683 if (popupTooltips) 684 { 685 ret 686 .append("<a class=\"hastooltip\" href=\"help.html\" onclick=\"popupwindow('help.html'); return false;\">"); 687 ret.append(text); 688 ret.append("<span>" + description + "</span>"); 689 ret.append("</a>"); 690 } 691 else 692 { 693 ret 694 .append("<a class=\"dfn\" href=\"help.html\" onclick=\"popupwindow('help.html'); return false;\">"); 695 ret.append(text); 696 ret.append("</a>"); 697 } 698 return ret.toString(); 699 } 700 701 private String generateTableRowForTotal() 702 { 703 StringBuffer ret = new StringBuffer(); 704 double ccn = complexity.getCCNForProject(projectData); 705 706 ret.append(" <tr>"); 707 ret.append("<td><b>All Packages</b></td>"); 708 ret.append("<td class=\"value\">" 709 + projectData.getNumberOfSourceFiles() + "</td>"); 710 ret.append(generateTableColumnsFromData(projectData, ccn)); 711 ret.append("</tr>"); 712 return ret.toString(); 713 } 714 715 private String generateTableRowForPackage(PackageData packageData) 716 { 717 StringBuffer ret = new StringBuffer(); 718 String url1 = "frame-summary-" + packageData.getName() + ".html"; 719 String url2 = "frame-sourcefiles-" + packageData.getName() + ".html"; 720 double ccn = complexity.getCCNForPackage(packageData); 721 722 ret.append(" <tr>"); 723 ret.append("<td><a href=\"" + url1 724 + "\" onclick='parent.sourceFileList.location.href=\"" + url2 725 + "\"'>" + generatePackageName(packageData) + "</a></td>"); 726 ret.append("<td class=\"value\">" + packageData.getNumberOfChildren() 727 + "</td>"); 728 ret.append(generateTableColumnsFromData(packageData, ccn)); 729 ret.append("</tr>"); 730 return ret.toString(); 731 } 732 733 private String generateTableRowsForSourceFile(SourceFileData sourceFileData) 734 { 735 StringBuffer ret = new StringBuffer(); 736 String sourceFileName = sourceFileData.getNormalizedName(); 737 // TODO: ccn should be calculated per-class, not per-file 738 double ccn = complexity.getCCNForSourceFile(sourceFileData); 739 740 Iterator iter = sourceFileData.getClasses().iterator(); 741 while (iter.hasNext()) 742 { 743 ClassData classData = (ClassData)iter.next(); 744 ret 745 .append(generateTableRowForClass(classData, sourceFileName, 746 ccn)); 747 } 748 749 return ret.toString(); 750 } 751 752 private String generateTableRowForClass(ClassData classData, 753 String sourceFileName, double ccn) 754 { 755 StringBuffer ret = new StringBuffer(); 756 757 ret.append(" <tr>"); 758 // TODO: URL should jump straight to the class (only for inner classes?) 759 ret.append("<td><a href=\"" + sourceFileName 760 + ".html\">" + classData.getBaseName() + "</a></td>"); 761 ret.append(generateTableColumnsFromData(classData, ccn)); 762 ret.append("</tr>\n"); 763 return ret.toString(); 764 } 765 766 /** 767 * Return a string containing three HTML table cells. The first 768 * cell contains a graph showing the line coverage, the second 769 * cell contains a graph showing the branch coverage, and the 770 * third cell contains the code complexity. 771 * 772 * @param ccn The code complexity to display. This should be greater 773 * than 1. 774 * @return A string containing the HTML for three table cells. 775 */ 776 private static String generateTableColumnsFromData(CoverageData coverageData, 777 double ccn) 778 { 779 int numLinesCovered = coverageData.getNumberOfCoveredLines(); 780 int numLinesValid = coverageData.getNumberOfValidLines(); 781 int numBranchesCovered = coverageData.getNumberOfCoveredBranches(); 782 int numBranchesValid = coverageData.getNumberOfValidBranches(); 783 784 // The "hidden" CSS class is used below to write the ccn without 785 // any formatting so that the table column can be sorted correctly 786 return "<td>" + generatePercentResult(numLinesCovered, numLinesValid) 787 +"</td><td>" 788 + generatePercentResult(numBranchesCovered, numBranchesValid) 789 + "</td><td class=\"value\"><span class=\"hidden\">" 790 + ccn + ";</span>" + getDoubleValue(ccn) + "</td>"; 791 } 792 793 /** 794 * This is crazy complicated, and took me a while to figure out, 795 * but it works. It creates a dandy little percentage meter, from 796 * 0 to 100. 797 * @param dividend The number of covered lines or branches. 798 * @param divisor The number of valid lines or branches. 799 * @return A percentage meter. 800 */ 801 private static String generatePercentResult(int dividend, int divisor) 802 { 803 StringBuffer sb = new StringBuffer(); 804 805 sb.append("<table cellpadding=\"0px\" cellspacing=\"0px\" class=\"percentgraph\"><tr class=\"percentgraph\"><td align=\"right\" class=\"percentgraph\" width=\"40\">"); 806 if (divisor > 0) 807 sb.append(getPercentValue((double)dividend / divisor)); 808 else 809 sb.append(generateHelpURL( 810 "N/A", 811 "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.\"")); 812 sb.append("</td><td class=\"percentgraph\"><div class=\"percentgraph\">"); 813 if (divisor > 0) 814 { 815 sb.append("<div class=\"greenbar\" style=\"width:" 816 + (dividend * 100 / divisor) + "px\">"); 817 sb.append("<span class=\"text\">"); 818 sb.append(dividend); 819 sb.append("/"); 820 sb.append(divisor); 821 } 822 else 823 { 824 sb.append("<div class=\"na\" style=\"width:100px\">"); 825 sb.append("<span class=\"text\">"); 826 sb.append(generateHelpURL( 827 "N/A", 828 "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.\"")); 829 } 830 sb.append("</span></div></div></td></tr></table>"); 831 832 return sb.toString(); 833 } 834 835 private static String getDoubleValue(double value) 836 { 837 return new DecimalFormat().format(value); 838 } 839 840 private static String getPercentValue(double value) 841 { 842 return StringUtil.getPercentValue(value); 843 } 844 845}