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
027 package net.sourceforge.cobertura.reporting.html;
028
029 import java.io.BufferedReader;
030 import java.io.File;
031 import java.io.InputStream;
032 import java.io.FileNotFoundException;
033 import java.io.IOException;
034 import java.io.InputStreamReader;
035 import java.io.PrintWriter;
036 import java.io.UnsupportedEncodingException;
037 import java.text.DateFormat;
038 import java.text.DecimalFormat;
039 import java.text.NumberFormat;
040 import java.util.Collection;
041 import java.util.Collections;
042 import java.util.Date;
043 import java.util.Iterator;
044 import java.util.SortedSet;
045 import java.util.TreeSet;
046 import java.util.Vector;
047
048 import net.sourceforge.cobertura.coveragedata.ClassData;
049 import net.sourceforge.cobertura.coveragedata.CoverageData;
050 import net.sourceforge.cobertura.coveragedata.LineData;
051 import net.sourceforge.cobertura.coveragedata.PackageData;
052 import net.sourceforge.cobertura.coveragedata.ProjectData;
053 import net.sourceforge.cobertura.coveragedata.SourceFileData;
054 import net.sourceforge.cobertura.reporting.ComplexityCalculator;
055 import net.sourceforge.cobertura.reporting.html.files.CopyFiles;
056 import net.sourceforge.cobertura.util.FileFinder;
057 import net.sourceforge.cobertura.util.Header;
058 import net.sourceforge.cobertura.util.IOUtil;
059 import net.sourceforge.cobertura.util.Source;
060 import net.sourceforge.cobertura.util.StringUtil;
061
062 import org.apache.log4j.Logger;
063
064 public 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 }