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\">&nbsp;</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\">&nbsp;</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\">&nbsp;</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\">&nbsp;</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\">&nbsp;</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\">&nbsp;"
584                                                            + lineNumber + "</td>");
585                                            if ((lineData != null) && (lineData.isCovered()))
586                                            {
587                                                    ret.append("  <td class=\"nbHitsCovered\">" 
588                                                                    + generateBranchInfo(lineData, "&nbsp;" + ((lineData != null) ? lineData.getHits() : 0)) 
589                                                                    + "</td>");
590                                                    ret
591                                                            .append("  <td class=\"src\"><pre class=\"src\">&nbsp;"
592                                                                            + generateBranchInfo(lineData, javaToHtml.process(lineStr))
593                                                                            + "</pre></td>");
594                                            }
595                                            else
596                                            {
597                                                    ret.append("  <td class=\"nbHitsUncovered\">"
598                                                                    + generateBranchInfo(lineData, "&nbsp;" + ((lineData != null) ? lineData.getHits() : 0))
599                                                                    + "</td>");
600                                                    ret
601                                                            .append("  <td class=\"src\"><pre class=\"src\"><span class=\"srcUncovered\">&nbsp;"
602                                                                            + generateBranchInfo(lineData, javaToHtml.process(lineStr))
603                                                                            + "</span></pre></td>");
604                                            }
605                                    }
606                                    else
607                                    {
608                                            ret.append("  <td class=\"numLine\">&nbsp;" + lineNumber
609                                                            + "</td>");
610                                            ret.append("  <td class=\"nbHits\">&nbsp;</td>\n");
611                                            ret.append("  <td class=\"src\"><pre class=\"src\">&nbsp;"
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.getNumberOfClasses() + "</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    }