001/*
002 * Cobertura - http://cobertura.sourceforge.net/
003 *
004 * Copyright (C) 2005 Mark Doliner
005 * Copyright (C) 2005 Grzegorz Lukasik
006 * Copyright (C) 2005 Jeremy Thomerson
007 * Copyright (C) 2006 Naoki Iwami
008 * Copyright (C) 2009 Charlie Squires
009 * Copyright (C) 2009 John Lewis
010 *
011 * Cobertura is free software; you can redistribute it and/or modify
012 * it under the terms of the GNU General Public License as published
013 * by the Free Software Foundation; either version 2 of the License,
014 * or (at your option) any later version.
015 *
016 * Cobertura is distributed in the hope that it will be useful, but
017 * WITHOUT ANY WARRANTY; without even the implied warranty of
018 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019 * General Public License for more details.
020 *
021 * You should have received a copy of the GNU General Public License
022 * along with Cobertura; if not, write to the Free Software
023 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
024 * USA
025 */
026
027package net.sourceforge.cobertura.reporting.html;
028
029import java.io.BufferedReader;
030import java.io.File;
031import java.io.InputStream;
032import java.io.FileNotFoundException;
033import java.io.IOException;
034import java.io.InputStreamReader;
035import java.io.PrintWriter;
036import java.io.UnsupportedEncodingException;
037import java.text.DateFormat;
038import java.text.DecimalFormat;
039import java.text.NumberFormat;
040import java.util.Collection;
041import java.util.Collections;
042import java.util.Date;
043import java.util.Iterator;
044import java.util.SortedSet;
045import java.util.TreeSet;
046import java.util.Vector;
047
048import net.sourceforge.cobertura.coveragedata.ClassData;
049import net.sourceforge.cobertura.coveragedata.CoverageData;
050import net.sourceforge.cobertura.coveragedata.LineData;
051import net.sourceforge.cobertura.coveragedata.PackageData;
052import net.sourceforge.cobertura.coveragedata.ProjectData;
053import net.sourceforge.cobertura.coveragedata.SourceFileData;
054import net.sourceforge.cobertura.reporting.ComplexityCalculator;
055import net.sourceforge.cobertura.reporting.html.files.CopyFiles;
056import net.sourceforge.cobertura.util.FileFinder;
057import net.sourceforge.cobertura.util.Header;
058import net.sourceforge.cobertura.util.IOUtil;
059import net.sourceforge.cobertura.util.Source;
060import net.sourceforge.cobertura.util.StringUtil;
061
062import org.apache.log4j.Logger;
063
064public class HTMLReport
065{
066
067        private static final Logger LOGGER = Logger.getLogger(HTMLReport.class);
068
069        private File destinationDir;
070
071        private FileFinder finder;
072
073        private ComplexityCalculator complexity;
074
075        private ProjectData projectData;
076
077        private String encoding;
078
079        /**
080         * Create a coverage report
081         * @param encoding 
082         */
083        public HTMLReport(ProjectData projectData, File outputDir,
084                        FileFinder finder, ComplexityCalculator complexity, String encoding)
085                        throws Exception
086        {
087                this.destinationDir = outputDir;
088                this.finder = finder;
089                this.complexity = complexity;
090                this.projectData = projectData;
091                this.encoding = encoding;
092
093                CopyFiles.copy(outputDir);
094                generatePackageList();
095                generateSourceFileLists();
096                generateOverviews();
097                generateSourceFiles();
098        }
099
100        private String generatePackageName(PackageData packageData)
101        {
102                if (packageData.getName().equals(""))
103                        return "(default)";
104                return packageData.getName();
105        }
106
107        private void generatePackageList() throws IOException
108        {
109                File file = new File(destinationDir, "frame-packages.html");
110                PrintWriter out = null;
111
112                try
113                {
114                        out = IOUtil.getPrintWriter(file);
115
116                        out
117                                        .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
118                        out
119                                        .println("           \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
120
121                        out
122                                        .println("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">");
123                        out.println("<head>");
124                        out
125                                        .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />");
126                        out.println("<title>Coverage Report</title>");
127                        out
128                                        .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\" />");
129                        out.println("</head>");
130                        out.println("<body>");
131                        out.println("<h5>Packages</h5>");
132                        out.println("<table width=\"100%\">");
133                        out.println("<tr>");
134                        out
135                                        .println("<td nowrap=\"nowrap\"><a href=\"frame-summary.html\" onclick='parent.sourceFileList.location.href=\"frame-sourcefiles.html\"' target=\"summary\">All</a></td>");
136                        out.println("</tr>");
137
138                        Iterator iter = projectData.getPackages().iterator();
139                        while (iter.hasNext())
140                        {
141                                PackageData packageData = (PackageData)iter.next();
142                                String url1 = "frame-summary-" + packageData.getName()
143                                                + ".html";
144                                String url2 = "frame-sourcefiles-" + packageData.getName()
145                                                + ".html";
146                                out.println("<tr>");
147                                out.println("<td nowrap=\"nowrap\"><a href=\"" + url1
148                                                + "\" onclick='parent.sourceFileList.location.href=\""
149                                                + url2 + "\"' target=\"summary\">"
150                                                + generatePackageName(packageData) + "</a></td>");
151                                out.println("</tr>");
152                        }
153                        out.println("</table>");
154                        out.println("</body>");
155                        out.println("</html>");
156                }
157                finally
158                {
159                        if (out != null)
160                        {
161                                out.close();
162                        }
163                }
164        }
165
166        private void generateSourceFileLists() throws IOException
167        {
168                generateSourceFileList(null);
169                Iterator iter = projectData.getPackages().iterator();
170                while (iter.hasNext())
171                {
172                        PackageData packageData = (PackageData)iter.next();
173                        generateSourceFileList(packageData);
174                }
175        }
176
177        private void generateSourceFileList(PackageData packageData)
178                        throws IOException
179        {
180                String filename;
181                Collection sourceFiles;
182                if (packageData == null)
183                {
184                        filename = "frame-sourcefiles.html";
185                        sourceFiles = projectData.getSourceFiles();
186                }
187                else
188                {
189                        filename = "frame-sourcefiles-" + packageData.getName() + ".html";
190                        sourceFiles = packageData.getSourceFiles();
191                }
192
193                // sourceFiles may be sorted, but if so it's sorted by
194                // the full path to the file, and we only want to sort
195                // based on the file's basename.
196                Vector sortedSourceFiles = new Vector();
197                sortedSourceFiles.addAll(sourceFiles);
198                Collections.sort(sortedSourceFiles,
199                                new SourceFileDataBaseNameComparator());
200
201                File file = new File(destinationDir, filename);
202                PrintWriter out = null;
203                try
204                {
205                        out = IOUtil.getPrintWriter(file);
206
207                        out
208                                        .println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"");
209                        out
210                                        .println("           \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">");
211
212                        out.println("<html>");
213                        out.println("<head>");
214                        out
215                                        .println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>");
216                        out.println("<title>Coverage Report Classes</title>");
217                        out
218                                        .println("<link title=\"Style\" type=\"text/css\" rel=\"stylesheet\" href=\"css/main.css\"/>");
219                        out.println("</head>");
220                        out.println("<body>");
221                        out.println("<h5>");
222                        out.println(packageData == null ? "All Packages"
223                                        : generatePackageName(packageData));
224                        out.println("</h5>");
225                        out.println("<div class=\"separator\">&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}