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